1

系列文章

React系列(一)-- 2013起源 OSCON - React Architecture by vjeux

React系列(二)-- React基本语法实现思路

React系列(三)-- Jsx, 合成事件与Refs

React系列(四)--- virtualdom diff算法实现分析

React系列(五)--- 从Mixin到HOC

React系列(六)--- 从HOC再到HOOKS

JSX的诞生

React 使用 JSX 来替代常规的 JavaScript。

JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。

我们不需要一定使用 JSX,但它有以下优点:

  • JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。
  • 它是类型安全的,在编译过程中就能发现错误。
  • 使用 JSX 编写模板更加简单快速。

编译

本质上来讲,JSX 只是为 React.createElement(component, props, ...children) 方法提供的语法糖

<div className="num" index={1}>
  <span>123456</span>
</div>
"use strict";

React.createElement("div", {
  className: "num",
  index: 1
}, React.createElement("span", null, "123456"));

具体效果可以在此体验

这就是为什么尽管你看不到里面使用过React,但是如果你不引入模块的话JSX会报错.

JSX原理

从上面的编译代码来看,JSX最终包含的信息其实分别是: 元素标签, 元素属性, 子元素.如果用Javascript对象来表示的话:

// 省略掉部分属性
{
  type: 'div'
  props: {
    className: 'num',
    index: 1,
    children: {
      type: 'span',
      props: { children: '123456' }
    },
  }
}

可以通过在线编译页面查看 codesandbox

所以整个过程大概如下
\105748fhcfjgijkjryctcf.png)

至于为什么会有中间编译成JS对象那一步而不直接编译成Dom元素.

  • 除了普通页面还可能渲染到canvas或者原生App(React Native了解一下)
  • 后面的diff比较需要用到

原生DOM API渲染流程

// 首先创建父标签
const parent = document.createElement('div')
parent.className = 'num'
parent.index = 1

// 创建子元素
const child = document.createElement('span')

// 创建文本节点
const text = document.createTextNode("")
text.nodeValue = '123456'

child.appendChild(text)
parent.appendChild(child)
document.body.appendChild(parent);

创建JSX对象

我们根据上面的结构可以大概模拟出实现函数

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children
    }
  }
}

但是有一个需要注意的点是像文本元素是不同结构的,所以需要特别区分,考虑到原生流程,我们也要给一个对应结构

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object'
          ? child
          : createTextElement(child)
      )
    }
  }
}

function createTextElement(text) {
  return {
    type: "TEXT",
    props: {
      nodeValue: text,
      children: []
    }
  }
}

尝试调用

createElement(
  "div",
  { className: "num", index: 1 },
  createElement("span", null, "123456")
)

最终运行得出结果

{
  type: 'div',
  props: {
    className: 'num',
    index: 1,
    children: [{
      type: 'span',
      props: [{
        children: [{
          type: 'TEXT',
          props: [{
            nodeValue: '123456'
          }]
        }]
      }]
    }],
  }
}

整个JSX语法解析成JSX对象的实现是由babel实现的,有兴趣自行了解

渲染实现

我们已经知道原生渲染方式和JSX对象结果,剩下的就是枚举方式生成元素

function render(component, wrapper) {
  // 区分标签
  const dom = component.type === 'TEXT' ? document.createTextNode("") : document.createElement(component.type)

  // 遍历props
  Object.keys(component.props).forEach(key => {
    if (key !== 'children') {
      dom[key] = component.props[key]
    }
  })

  // 是否需要渲染子元素
  component.props.children.length && component.props.children.forEach(child => render(child, dom))

  // 最终挂载方法
  wrapper.appendChild(dom)
}

事件处理

SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()

如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。合成事件与浏览器的原生事件不同,也不会直接映射到原生事件。例如,在 onMouseLeave 事件中 event.nativeEvent 将指向 mouseout 事件。每个 SyntheticEvent 对象都包含以下属性:

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type
从 v17 开始,e.persist() 将不再生效,因为 SyntheticEvent 不再放入事件池中。

支持的事件

React 通过将事件 normalize 以让他们在不同浏览器中拥有一致的属性。

以下的事件处理函数在冒泡阶段被触发。如需注册捕获阶段的事件处理函数,则应为事件名添加 Capture。例如,处理捕获阶段的点击事件请使用 onClickCapture,而不是 onClick

  • Clipboard Events
  • Composition Events
  • Keyboard Events
  • Focus Events
  • Form Events
  • Generic Events
  • Mouse Events
  • Pointer Events
  • Selection Events
  • Touch Events
  • UI Events
  • Wheel Events
  • Media Events
  • Image Events
  • Animation Events
  • Transition Events
  • Other Events

基础科普

在JavaScript中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡.

  • stopPropagation(): 停止事件冒泡
  • preventDefault(): 阻止默认行为
  • return false: 实际上使用这个的时候会做三件事

    • event.preventDefault();
    • event.stopPropagation();
    • 停止回调函数执行并立即返回。

React是怎么管理事件系统的?(仅适用于 React 16 及更早版本、React Native)

出于性能原因.React会通过池方式复用SyntheicEvent对象,这意味着事件调用完成之后event.target上所有的属性都会失效.意思就是当我们尝试异步方式调用React事件,因为复用的原因,在事件回调执行完之后SyntheicEvent对象将不再存在,所以我们无法访问其属性.

function handleChange(e) {
  // This won't work because the event object gets reused.
  setTimeout(() => {
    console.log(e.target.value); // Too late!
  }, 100);
}

比较常见的例子

解决方案
  1. event.persist()

    事件回调中调用event.persist()方法,这样会在池中删除合成事件,并且允许用户代码保留对事件的引用。

    function handleChange(e) {
      // Prevents React from resetting its properties:
      e.persist();
    
      setTimeout(() => {
        console.log(e.target.value); // Works
      }, 100);
    }
  1. 缓存属性

    我们可以将事件属性存储在事件函数并且传递给异步回调函数而不是直接在异步回调里访问它们.

    <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
  1. Debouncing a synthetic event handler(不知道怎么翻译)

    // Correct
    this.setState((prevState, props) => ({
      counter: prevState.counter + props.increment
    }));
合成事件注册

源码注释

/**
 * Summary of `ReactBrowserEventEmitter` event handling:
 *
 *  - Top-level delegation is used to trap most native browser events. This
 *    may only occur in the main thread and is the responsibility of
 *    ReactDOMEventListener, which is injected and can therefore support
 *    pluggable event sources. This is the only work that occurs in the main
 *    thread.
 *
 *  - We normalize and de-duplicate events to account for browser quirks. This
 *    may be done in the worker thread.
 *
 *  - Forward these native events (with the associated top-level type used to
 *    trap it) to `EventPluginHub`, which in turn will ask plugins if they want
 *    to extract any synthetic events.
 *
 *  - The `EventPluginHub` will then process each event by annotating them with
 *    "dispatches", a sequence of listeners and IDs that care about that event.
 *
 *  - The `EventPluginHub` then dispatches the events.
 *
 * Overview of React and the event system:
 *
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 *    React Core     .  General Purpose Event Plugin System
 */

DOM将事件传给 ReactEventListener 注册到document

然后分发到具体节点.EventPluginHub 负责事件的存储,合成事件以及池方式的实现创建和销毁

后面是各种类型的合成事件模拟,交互通过 ReactEventEmitter 将原生的DOM事件转化成合成的事件,触发将对应操作推入队列批量执行.因为浏览器会为每个事件的每个 listener 创建一个事件对象,上面提到的池方式复用就是为了解决高额内存分配的问题.

event

其中事件都会被自动传入一个event对象,是由React将浏览器原生的event对象封装一下对外提供统一的API和属性.

this

因为React里调用传入方法的时候并不是通过对象方法方式,而是直接通过函数调用,所以里面指向的this是null或者undefined.

一般传入的时候需要手动用bind或者箭头函数显性绑定this指向

Refs & DOM

这是一种用于访问render方法中创建的DOM节点或React元素的方式.一般用于

  • 处理表单,媒体控制
  • 触发强制动画
  • 集成第三方DOM库

创建Refs

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }
  render() {
    return <div ref={this.myRef} />
  }
}

访问 Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

const node = this.myRef.current;

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。

将 DOM Refs 暴露给父组件

在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。

虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。

如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。

如果你对子组件的实现没有控制权的话,你剩下的选择是使用 findDOMNode(),但在严格模式 下已被废弃且不推荐使用

回调Refs

不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。这个函数接受 React 组件的实例或 HTML DOM 元素作为参数,以存储它们并使它们能被其他地方访问。

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // 直接使用原生 API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 渲染后文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调将 text 输入框的 DOM 节点存储到 React
    // 实例上(比如 this.textInput)
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 refs 一定是最新的。

你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef() 创建的对象 refs 一样。

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

在上面的例子中,Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 <input>。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点。

无状态组件中使用

因为无状态组件是不会被实例化的,但是我们可以用一个变量访问其中的组件或者dom元素组件的实例引用

function CustomTextInput(props) {
  let inputRef;
  return (
    <div>
      <input ref={(node) => inputRef = node} />
    </div>
  );
}

Afterward
621 声望62 粉丝

努力去做,对的坚持,静待结果