Skip to content

React 原理

仲灏约 1 分钟

React 原理

回顾 vue 渲染和更新过程

  • 要点
    • 响应式
    • 模板解析
    • vdom
  • 过程
    • 初次渲染时
    • 更新时
    • 示意图

React 原理的要点

  • JSX 渲染出页面(JSX 的本质是什么)
  • 事件如何被绑定(之前说过,事件都挂载到了 document 上)
  • setState 过程(为何有时同步,有时异步?)
  • 组件更新和 diff 算法

JSX 的本质

首先,回顾一下 vodm 的知识

  • vnode 结构
  • h 函数
  • patch 函数

https://www.babeljs.cn/ 做一个测试,将以下 jsx 编译为 js

js
// JSX 基本用法
const imgElem = <div>
    <p>some text</p>
    <img src={imgUrl}/>
</div>

// JSX style
const styleData = { fontSize: '30px',  color: 'blue' }
const styleElem = <p style={styleData}>设置 style</p>

// JSX 加载组件
const app = <div>
    <Input submitTitle={onSubmitTitle}/>
    <List list={list}/>
</div>

// JSX 事件
const eventList = <p onClick={this.clickHandler}>
    some text
</p>

// JSX list
const listElem = <ul>{this.state.list.map((item, index) => {
    return <li key={item.id}>index {index}; title {item.title}</li>
})}</ul>

以上可见,React.createElement 就相当于 h 函数,执行则会返回一个 vnode 结构。

js
{
    tag: 'div', // 或是一个组件名 Input List 等
    props: {...},
    children: [...] // 或 '...' 其中只有文本
}

看 React.createElement 的第一个参数

  • 渲染 html 标签时,就是一个字符串, div p img 等
  • 渲染组件时,就是一个组件,Input List 等
  • React 规定,所有的组件必须大写字母开头,因为 html 标签都是小写字母开头 —— 这样就很好识别 jsx 标签是一个 html tag 还是自定义组件。

如果第一个参数是组件,那就继续去寻找该组件的 render 函数中的 jsx 结构,直到找到最底层,即 html tag 。

js
// 第一个参数是 List 组件
React.createElement(List, {
  list: list
})

// 找到 List 组件 jsx 结构,继续拆分
React.createElement("ul", null, list.map(
        function (item, index) {
            return React.createElement("li", {
                key: item.id
            }, "title ", item.title)
        }
    )
)

总结一下

  • JSX 是 React.createElement 的语法糖
  • React.createElement 最终返回 vnode
  • 其中遇到自定义组件,会继续找其 jsx 结构,继续渲染

最后,将 vnode 结构渲染为 elem ,之前已经讲过,React 这里的过程也是一样的。 React 将组件分为四个类型,分别进行渲染。

  • ReactEmptyComponent - 空组件 null undefined
  • ReactDOMComponent - html 节点
  • ReactTextComponent - 文本组件
  • ReactCompositeComponent - 自定义组件

事件机制

js
clickHandler = (event) => {
    event.preventDefault()
    event.stopPropagation()

    console.log('target', event.target) // 指向当前元素,即当前元素触发
    console.log('current target', event.currentTarget) // 指向当前元素,假象!!!

    // 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent
    console.log('event', event)

    // 原生 event 如下。其 __proto__.constructor 是 MouseEvent
    console.log('nativeEvent', event.nativeEvent)
    // 指向当前元素,即当前元素触发
    console.log('nativeEvent target', event.nativeEvent.target)
    // 指向 document !!!
    console.log('nativeEvent current target', event.nativeEvent.currentTarget)
}

通过之前的使用,可以得出

  • React 将所有时间都挂载到 document 上
  • event 不是原生的,是 SyntheticEvent 合成事件

React 自己实现了一套事件机制 —— 合成事件机制

  • 事件的绑定和销毁
  • 事件的触发和冒泡
  • 和 DOM 事件不一样

React 事件机制的实现流程

  • 示意图
  • diapatchEvent
  • JSX 渲染时,能知道每个事件和组件的关系,也就能触发到该组件的事件函数

为什么要这样做?

  • 为了兼容性和跨平台(仅仅是将事件挂载到 document ,其他都不依赖于 DOM 事件,很独立)
  • 挂载到 document ,减少内存消耗,方便事件绑定和解绑(不用再用 removeListener)
  • 方便对事件的统一管理,如事务机制 —— 下文介绍

setState 和 batchUpdate

根据之前对 setState 的应用,发现

  • 有时异步,有时同步(不受 React 控制的函数中,是同步)
  • 连续执行多次 setState ,只会更新一次(异步情况下)
  • 传入对象时,会被合并

这部分过程非常复杂,如果从源码入手,反而不太容易讲清楚。另外,由于细节过于复杂,所以再难的面试也不可能考察到那么细致的源码。所以,我们只需要了解这部分的流程和重点,应对面试应该没有问题

这部分的重要知识点:

  • batchUpdate 机制
  • transaction 机制

(解释什么是“事务”,例如数据库的事务操作。即将一组操作当做一个原子操作,例如银行转账)

js
transaction.initialize = function () {
    console.log('initialize')
}
transaction.close = function () {
    console.log('close')
}

function method(){
    console.log('abc')
}
transaction.perform(method)

// 输出 'initialize'
// 输出 'abc'
// 输出 'close'

哪些场景会命中 batchUpdate 机制?

  • 生命周期
  • React 事件

哪些不会命中 batchUpdate 机制?

  • setTimeout 等全局函数
  • 自己定义的 DOM 事件

总结 setState 的特点

  • 单独看 setState 本身,无所谓同步和异步
  • 同步还是异步,取决于是否命中 batchUpdate 机制
  • 通过事务机制,控制 isBatchingUpdates

组件更新和 diff 算法

回顾 vdom 的重点知识。

遍历 dirtyComponents 进行更新,组件更新依赖于 vdom 和 diff 算法。

  • 根据 newProps 和 newState 执行 render 函数
  • 生成 newVnode
  • patch(vnode, newVnode) —— 这里并不是一步完成,React 会有更详细的优化,但最终结果和 patch 一样。

更新是分位两个阶段

  • reconciliation 阶段。对 dirtyComponent 以及子组件进行 diff ,找出变化部分。这个阶段可以拆分为多个子任务,可以随时暂停和恢复。—— 至于为何要拆分,继续往下看。
  • commit 阶段。对当前 diff 获取的变化部分,进行 DOM 操作。一次性执行完成,不能拆分。

暴露性能问题 —— 注意,是在某些复杂的情况下,你不一定能遇到!

  • JS 是单线程,而且和 DOM 渲染共用一个线程
  • 当前项目复杂、组件数量多时,组件更新将占据大量 JS 计算
  • 此时,如果再有其他的 DOM 渲染需求(如动画、频繁的鼠标键盘操作),将会导致卡顿

解决方案 —— fiber (React 16 之后引入 fiber 架构)

注意,fiber 是 React 内部的运行机制,作为使用者体会不到,甚至有些情况都触发不到 fiber 机制。但是我们得知道它的基本情况:what why how

上次更新:

讨论区

欢迎留下想法与补充