React 16 架构研究记录(文末有彩蛋

React 16 架构研究记录(文末有彩蛋

要看懂 React 16 的代码不易,但是其实也并没有想象中的那么困难,只要恒心一有,你总会能搞懂。就像 16 的作者所言:

Some of this may not make sense on first reading. That's okay, it will make more sense with time.
可能第一次阅读的时候不会懂,没关系,随着时间慢慢推移,总会明白的。

一路看过来,花了我4天多的时间,才搞明白了堆栈关系,然而还有很多不懂的,这部分不懂就留着之后亲自实现一个 React fiber ,相信也用不了多久。



Fiber 架构的核心理论


首先,因为之前 React 对 DOM 的遍历采用的是递归的形式进行,递归的好处就是代码简单,容易理解,可是也有着致命的缺陷:难以中断和恢复。因此,我们在 Mount 、render 巨大节点的时候,总是那么卡:

当运算量( React 递归)巨大的时候,会卡住整个 UI

注意,React 并不是不能像 Vue 那样采用依赖检查机制去玩,我们常见的 Mobx 这个库就是让玩家们体验了一把类 Vue 的操作。但是深度使用 React 的人来说,Mobx 的机制并不适合多人开发,这也就是为什么 Redux 能那么火的原因:

  • React 函数式,Redux 也是函数式
  • Redux 的强制约束对于多人开发项目的意义

React 为了保持这种函数式编程的优势,选择了实现「用户态调度」的方式去重新实现 React,也就是我们说的 Fiber,在 Fiber 架构下,React 的性能可以达到前所未有的高度:

理论上来说超越目前所有框架

要做到这一点,解决的办法是:

  1. 使用性能优化神器:requestIdleCallback,将每一帧中浪费的 CPU 时间,利用起来
  2. 重新实现 React 的树构成栈、更新栈,抛弃递归的方式,采用循环 ( while )、出栈入栈的形式对树进行遍历。树的深度遍历有很多种,递归是最好理解、代码最少的,但是也是最无法控制的,while + 栈 循环实现的深度遍历,能够很好的 pause 和恢复,相当轻松,在新的 React 源码中,明确使用了 while 循环。
  3. 每个、每种虚拟 DOM 节点都得到了升级,添加了 Alternate、child、sibling、return 的属性,这些个属性为的就是在更新阶段,能够停止、恢复以达到将 diff 和 patch 全部拆分成每个小任务,整棵树,以链表的形式组织。
react 15 和 react 16 的树构成

以上就是全部 React 16 的核心理论,在明白这些核心理论以后,我们自己造轮子就已经降低了。


React 堆栈的一次梳理

虽然理论懂了,手已经很痒了,但是为了之后能够自己写出 React fiber 的代码,因此对整个 React 调用栈进行了初步的梳理,梳理完毕,就可以开始撸代码了,一下代码组织形式调用栈顺序执行:

  • ReactDOM.render:不多说
  • legacyCreateRootFromDOMContainer:这个函数会给 root container 创建一个 Root.fiber 和是一个特殊的对象,这里的 Container 就是我们要 Mount 去的点,这函数的末尾:
function legacyCreateRootFromDOMContainer(container, forceHydrate) {

  // Legacy roots are not async by default.
  var isAsync = false;
  return new ReactRoot(container, isAsync, shouldHydrate);
}

我们可以看到,这里的 Async 模式是写死的,不能进行,如果想玩,直接手动修改为 True,

  • ReactRoot 其实也就是制造一个 ReactRoot Fiber 节点,这是一个特殊的节点,目前研究还没搞明白为什么要这么做。
  • updateContainer:拿到了 context ,其实没什么用
  • scheduleRootUpdate:这个函数就开始比较重要,当我们调用 render 和 setState 以后,我们都会让 React 进入 「更新」的流程,无论你是首次渲染,还是第二次更新, React 将首次渲染和之后的更新流程统一了起来,其实这一点一直是以前诟病的点...
//伴随着以下数据结构生成,用于 update/mount
 var update = {
      expirationTime: expirationTime,
    // partialState 可能是因为为了让首次渲染和之后的更新流程统一起来
      partialState: { element: element },
      callback: callback,
      isReplace: false,
      isForced: false,
      capturedValue: null,
      next: null
    };
  • insertUpdateIntoFiber:这个函数做了一件事情,setState 和 render 的时候,都会创建一个 update,这个方法就是把这个 update 放入队列中,这个队列是一个链表
  • scheduleWorkImpl:是执行虚拟DOM(fiber树)的更新,scheduleWork是他的包囊方法,其实没啥用
  • requestWork(root, expirationTime):请求任务,其实这个函数在同步模式下并没有什么卵用,在异步模式下,这个函数才会最终调用 rIC 调度函数
  • createWorkInProgress:因为目前是同步模式,因此我们直接往下走,看到createWorkInProgress。WorkInProgress 指的是新的树,这棵树用于更新,更新完毕以后代替旧的树成为 Current tree.
  • workloop:字体加粗,因为我们终于来到了一个重要函数,这个函数就是我说的大循环,异步与否就在这里走分支
while(next !== null){
   next =performUnitOfWork(next)
}
  • performUnitOfWork:这个函数会循环的返回树中的下一个节点,其中的逻辑比较复杂,其做的就是拿到当前的节点,初始化,构建 Fiber 信息(sibling return 等),然后返回节点的字元素出去,字元素作为 next 继续进来找子元素,层层递进,遍历,直到树走完为止
  • beginWork:在上看那个函数中,这个函数其实是一个 switch 函数其源码如下:
function beginWork(current, workInProgress, renderExpirationTime) 
// 这个函数是一个 switch tag.
  switch (workInProgress.tag) {
      case IndeterminateComponent:
        return mountIndeterminateComponent(current, workInProgress, renderExpirationTime);
      case FunctionalComponent:
        return updateFunctionalComponent(current, workInProgress);
      case ClassComponent:
        return updateClassComponent(current, workInProgress, renderExpirationTime);
      case HostRoot:
        return updateHostRoot(current, workInProgress, renderExpirationTime);
      case HostComponent:
        return updateHostComponent(current, workInProgress, renderExpirationTime);
      case HostText:
        return updateHostText(current, workInProgress);
      case CallHandlerPhase:
        // This is a restart. Reset the tag to the initial phase.
        workInProgress.tag = CallComponent;
      // Intentionally fall through since this is now the same.
      case CallComponent:
        return updateCallComponent(current, workInProgress, renderExpirationTime);
      case ReturnComponent:
        // A return component is just a placeholder, we can just run through the
        // next one immediately.
        return null;
      case HostPortal:
        return updatePortalComponent(current, workInProgress, renderExpirationTime);
      case ForwardRef:
        return updateForwardRef(current, workInProgress);
      case Fragment:
        return updateFragment(current, workInProgress);
      case Mode:
        return updateMode(current, workInProgress);
      case ContextProvider:
        return updateContextProvider(current, workInProgress, renderExpirationTime);
      case ContextConsumer:
        return updateContextConsumer(current, workInProgress, renderExpirationTime);
      default:
        invariant(false, 'Unknown unit of work tag. This error is likely caused by a bug in React. Please file an issue.');
    }
  }

Shit,这个逻辑看着也是蛋疼,但是已经知道了,其实就是 React 15 做的根据节点 Type 选择不同的策略进行 Mount/update 操作。

里面的 workInProgress.tag 是什么?这个比较关键:代码


  • reconcileChildren:终于来到了第二个核心方法,在这里就是处理 React child 的核心逻辑了


其实逻辑已经很清晰了

  1. 统一首次渲染和更新阶段:将首次渲染同样当成更新来处理,因为其实从没有节点,到有节点实际上就是一个更新过程。
  2. 构建流程从递归改为大循环,每一次循环只对本层的节点进行操作,将孩子返回给顶层,让顶层来做循环,在 Async 模式下可以中断等操作
  3. 构建的同时,生成 Fiber 信息,这些 Fiber 信息实际上就是为了之后在更新时,能够在任意的地方使得树遍历都可以用大循环来搞

具体的 Fiber 结构如下:


Alternate:一个极其重要的属性。在新的 Fiber 架构中,我们同样是有两颗 Fiber 树,一颗是旧的,一颗是新的(当调用 setState)以后。当我们的更新完毕以后,新的 Alternate 树就会变成我们的老树,以此进行新旧交替。



@司徒正美

把它叫做替身,确实,正妹大大很会起名字,这是一个新的属性,根据他的研究,Alternate 是用来探雷的,主要用于 ErrorBoundary 组件,试图要解决 「ErrorBoundary」不能自救的问题,因为现在 ErrorBoundary 如果出错了,会自爆,把错误往上传递,给其他的 ErrorBoundary。



child:因为就像我之前说的,要解决用户态调度问题,那么就要把每一个节点的 diff patch 过程都变成可控制的,因此我们需要将原来的递归,变成一个循环,以链表为链接,控制每一次的 diff 和 patch。因此,一个 Fiber 都会有一个 child 、sibling、return 三大属性作为链接树前后的指针


effectTag:一个很有意思的标记,用于记录 effect 的类型,effect 指的就是对 DOM 操作的方式,比如修改,删除等操作,用于到后面进行 Commit(类似数据库)


firstEffect 、lastEffect 等玩意是用来保存中断前后 effect 的状态,用户中断后恢复之前的操作。这个意思还是很迷糊的,因为 Fiber 使用了可中断的架构。


tag:根据 react 的源码,tag 以下的含义 ,用于标记,这个 Fiber 是什么鬼


export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

export const IndeterminateComponent = 0; // 尚不知是类组件还是函数式组件
export const FunctionalComponent = 1; // 函数式组件
export const ClassComponent = 2; // Class类组件
export const HostRoot = 3; // 组件树根组件,可以嵌套
export const HostPortal = 4; // 子树. Could be an entry point to a different renderer.
export const HostComponent = 5; // 标准组件,如地div, span等
export const HostText = 6; // 文本
export const CallComponent = 7; // 组件调用
export const CallHandlerPhase = 8; // 调用组件方法
export const ReturnComponent = 9; // placeholder(占位符)
export const Fragment = 10; // 片段

目前我掌握的信息大概就是这么多,你可以看到,我说的这些点都是和以前 15 是完全不同的, React 的 fiber 设计已经超乎了大多数前端开发的知识点,只能硬着头皮去肛了。


目前掌握的资料

  1. 毕竟发表在知乎,对于看不懂的人,建议大家稍微去看一下 React fiber 的知识点,回来以后很快就能懂
  2. 目前所有找到的资料我都已经放在了 Luy 的仓库

感慨一下,说 React 复杂嘛,其实真没有 Node.js 那么复杂和琐碎,Node.js 只要有操作系统和网络的一些知识思想还是很好懂的。


但是 React 的这个递归转大循环来控制调用栈的做法,我是真没想到了。我还记得以前在刷 leetcode(当然我也没刷过几题) 的时候,有一道题就是树的遍历,我当时用的栈来实现而不是递归的,然后去看了大家的答案以后发现全是两行代码的递归,深受打击,发誓要学会递归。后面碰上树结构,就是递归去搞,因为方便,看起来也很高端,所以就忘了用栈+循环来实现了。


如今一看,能够想明白这一层,也是当时刷那题所赐。。。。。真的是没有算法是最好的,什么场景用什么算法,解决什么问题,这才是最重要的!!!!


最后我放一段代码,也是一个极其经典的深度遍历的例子,看看谁能一下子看出来:

const mid = []
mid.push(fun1)
mid.push(fun2)

 let i = -1

dispatch(0)
function dispatch(index) {
   if (index <= i) return Promise.reject(new Error('next() called multiple times'))
  i = index
  let fn = mid[index]
  if (!fn) return Promise.resolve()
  try {
    Promise.resolve(
      fn('ddd', function next() {
        return dispatch(index + 1)
      })
    )
  } catch (e) {
    return Promise.reject(e)
  }
}

编辑于 2018-05-16

文章被以下专栏收录

    这是一个新手的专栏!至于为什么是哑铃呢?因为我还是一个健身减脂小能手,我的梦想是做最强壮的程序员。