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

系列文章

React系列(一)-- 2013起源 OSCON - React Architecture by vjeux
React系列(二)-- React基本语法实现思路
React系列(三)-- Jsx, 合成事件与Refs

JSX的诞生

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

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

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

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

编译

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

123456
"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)
}

事件处理

React的事件是基于SyntheticEvent的实例实现模拟跨浏览器原生事件一样的接口,包括stopPropagation()preventDefault(),期望事件的行为跨浏览器是相同的.甚至兼容直达IE8.每个SyntheicEvent对象都有如下属性:

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

基础科普

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

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

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

React是怎么管理事件系统的?

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

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values.
  this.setState({clickEvent: event});

  // You can still export event properties.
  this.setState({eventType: event.type});
}

比较常见的例子就是setState方法.

解决方案

  1. event.persist()

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

  1. 缓存属性

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

  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 
} }

访问 Refs

const node = this.myRef.current;
  • 如果用于一个普通HTMl元素时,React.createRef() 将接收底层 DOM 元素作为它的 current 属性以创建 ref
  • ref 属性被用于一个自定义类组件时,ref 对象将接收该组件已挂载的实例作为它的 current
  • 你不能在函数式组件上使用 ref 属性,因为它们没有实例。

回调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 (
      
); } }

如果是组件间传递回调形式的 refs如下:

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

无状态组件中使用

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

function CustomTextInput(props) {
  let inputRef;
  return (
    
inputRef = node} />
); }

你可能感兴趣的