React原理篇

152 阅读4分钟

React原理篇

函数式编程

  • 函数是"第一等公民"
  • 不可变值的重要性

vdom和diff算法

这里可以参考前面vue原理篇,因为react和vue底层都是基于diff算法实现,不过两个框架的具体实现和函数命名会有一些差异。

  • h函数(render函数)
  • vnode数据结构
  • patch函数

jsx本质

因为jsx可以直接被babel编译,我们来到babel官网编译jsx模板看看结果:

这是一段jsx代码:

const imgElem = <div id="div1">
  <p>AirHua</p>
  <img src={imgUrl}/>
</div>

编译后的结果:

const imgElem = /*#__PURE__*/React.createElement("div", {
  id: "div1"
}, /*#__PURE__*/React.createElement("p", null, "AirHua"), /*#__PURE__*/React.createElement("img", {
  src: imgUrl
}));

我们可以联系前面讲到的vue模板编译来理解jsx模板编译了:

  • React.createElement返回vnode
  • React.createElement第一个参数可能是组件或者hrml tag,就取决于首字母大写来解析了
  • 第二个参数是属性,没有的话为null
  • 第三个参数可以是几个单子元素,也可以子元素数组

合成事件

前面我们也提到过React里面的事件Event的不同,还是以这个例子来详细看看:

import React, { Component } from 'react';

export default class EventDemo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: 'airhua',
      list: [
        {
          id: '1-1',
          title: '标题1'
        },
        {
          id: '1-2',
          title: '标题2'
        },
        {
          id: '1-3',
          title: '标题3'
        }
      ]
    }
  }
  clickHander = (event) => {
    event.preventDefault();
    event.stopPropagation();

    console.log('target', event.target);
    console.log('current target', event.currentTarget);

    console.log('event', event);
    // console.log('event.__proto__.constructor', event.__proto__.constructor);

    console.log('nativeEvent', event.nativeEvent);
    console.log('nativeEvent target', event.nativeEvent.target)  // 指向当前元素,即当前元素触发
    console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 root !!!
    // console.log('nativeEvent.__proto__.constructor', event.nativeEvent.__proto__.constructor);
  }
  render() {
    return <div>
      <h1>{ this.state.name }</h1>
      <ul>
        {
          this.state.list.map((item, index) => (
            <li key={index} onClick={this.clickHander}>{ item.title }</li>
          ))
        }
      </ul>
    </div>;
  }
}

看看打印结果:

1644898939024.png

现象

  • react的event是SyntheticBaseEvent ,模拟出来 DOM 事件所有能力
  • event.nativeEvent 是原生事件对象,其 _proto_.constructor 是 PointerEvent
  • 所有事件都被挂载到了root节点上(react17前是document,后面为了不污染document)

原理

react内部实现了SyntheticBaseEvent,由root实例event去分发事件。

1644899756897.png

为何要合成事件机制

  • 更好的兼容性和跨平台
  • 绑定到root,减少内存消耗,避免频繁绑定
  • 方便事件的统一管理(如事务机制)

setState和batchUpdate

前面React基础篇我们说过setState的三个特性

  • 不可变值
  • 可能是异步,可能是同步
  • 同步时:传入对象会被合并,传入函数不被合并;异步时:不被合并

实际上这和batchUpadte机制分不开关系。

setState执行流程

  • setState接收一个新状态不会立即执行,而是先放到pending(等待队列)中
  • 判断isBatchingUpdates(是否批量更新模式)
    • true: 将接收到的新状态保存到dirtyCompents中
    • false: 遍历所有dirtyComponents,并且调用updateCompent更新state,执行完毕将isBatchUpdates置为true

1644907614155.png

接下来我们可以通过一个场景来解释一下:

componentDidMount() {
  this.setState({
    count: this.state.count + 1
  })
  console.log('1', this.state.count) // 0
  this.setState({
    count: this.state.count + 1
  })
  console.log('2', this.state.count) // 0
  setTimeout(() => {
    this.setState({
      count: this.state.count + 1
    })
    console.log('3', this.state.count) // 2
  })
  setTimeout(() => {
    this.setState({
      count: this.state.count + 1
    })
    console.log('4', this.state.count) // 3
  })
}

打印结果为0 0 2 3,解析一下执行过程就是:

  • 前面两次修改为同步代码,此时isBatchingUpdates:true,所以前两次执行会被缓存到dirtyComponents中,合并修改,所以打印为0。
  • 等同步代码执行完毕后,isBatchingUpdates:false,所以执行setTimeout时,要先遍历dirComponents,按顺序执行,就可以获取到值了
  • 打印结果为0 0 2 3

通过这些我们基本可以理解为什么setState会有这些特性了,最后我们再来看看batchUpdate机制实际是基于transaction机制来的

transaction

在React源码中transaction部分有一段字符是解释transaction的作用的:

*                       wrappers (injected at creation time)
*                                      +        +
*                                      |        |
*                    +-----------------|--------|--------------+
*                    |                 v        |              |
*                    |      +---------------+   |              |
*                    |   +--|    wrapper1   |---|----+         |
*                    |   |  +---------------+   v    |         |
*                    |   |          +-------------+  |         |
*                    |   |     +----|   wrapper2  |--------+   |
*                    |   |     |    +-------------+  |     |   |
*                    |   |     |                     |     |   |
*                    |   v     v                     v     v   | wrapper
*                    | +---+ +---+   +---------+   +---+ +---+ | invariants
* perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | +---+ +---+   +---------+   +---+ +---+ |
*                    |  initialize                    close    |
*                    +-----------------------------------------+

我们可能写一段类似伪代码来解释

handler = () => {
  // 开始initialize阶段 设置处于batchUpdate
  // isBatchUpdates = true

  // 逻辑操作
  // anyMethod

  // 结束close阶段
  // isBatchUpdates = false
}

实质上就是React处理时给在自己管控之下的函数都加上了isBatchingUpdates包括:生命周期(和它调用的函数)、React中注册的事件(和它调用的函数)

组件渲染过程

组件初始渲染

  • 获取生成props state
  • render()生成vnode(这里指的是React.createElement)
  • patch(elem, vnode)

[源码里不一定是这个patch函数,patch只是之前snabbdom里的实现,但是功能基本一样]

更新过程

  • setState(newState)

  • render()生成newVnode

  • patch(vnode, newVnode)[这个阶段拆分成了两个阶段]

    • reconciliation阶段-执行diff算法,纯js计算
    • commit阶段 - 将diff结果渲染DOM
    • 不拆分可能会导致性能问题,当组件复杂,计算和渲染压力都很大,还有DOM操作可能会卡顿

React fiber

  • 将reconciliation(协调算法)阶段进行任务拆分
  • DOM需要渲染时暂停计算,空闲时恢复
  • window.requestIdleCallback