3. fiber

128 阅读7分钟

实现虚拟dom

  • jsx其实是一种特殊语法,在webpack打包的时候 babel编译会编译成js jsx=>createElement方法
  • 虚拟dom就是一个js对象,以js对象的方式描述界面上dom的样子
  • React.createElement就是生成虚拟dom的方法

createElement方法

  • 创建了一个React对象,里面有个方法叫做createElement const React = { createElement }
  • 参数: 元素的类型,属性,儿子们
  • 返回一个对象 就是react元素,即虚拟dom。 包含type,props(children也是props的一部分)
  • 这样虚拟dom就构建完成 image.png

实现初次渲染

  • 定义元素类型

  • render是要把一个元素渲染到一个容器内部

  • 每一个fiber对象包含

    • tag 标识此元素类型
    • stateNode 一般情况下如果这个元素是一个原生节点的话,stateNode指向真实dom元素
    • props.children 是一个数组 里面放的是react元素,虚拟dom。后面会根据里面的每一个react元素创建对应的fiber
  • 从根节点开始渲染和调度, 分为两个阶段

    • render阶段
      • 此阶段有两个任务:1.根据虚拟dom生成fiber树; 2.收集effectlist
      • 对比新旧虚拟dom,逆行增量更新或创建
      • 成果是effect list 知道哪些节点删除了 哪些节点增加了
      • 这个阶段可能比较花时间,可以对任务进行拆分,拆分的维度是虚拟dom,此阶段可以暂停
    • commit阶段
      • 进行dom的更新创建阶段,此阶段不能暂停,要一气呵成

render阶段

  • performUnitOfWork
    • 对当前fiber节点调用beginWork方法构建fiber树
    • 返回下一个fiber节点:有儿子返回儿子;
    • 如果没有儿子,则当前节点完成 调用completeUnitOfWork
    • 没有儿子 找弟弟
    • 如果没有弟弟 先通过return返回父节点 让父亲完成(调用completeUnitOfWork)。再通过sibling找弟弟 (也就是找叔叔)
    • 直到找到根节点fiber为止
  • beginWork
    • 1.先处理自己,创建真实的dom元素,挂在当前节点fiber的stateNode属性上
    • 2.创建子fiber 拿出当前节点的children属性(react元素), 遍历 依次创建子元素的fiber,并通过retrun和父节点产生关联;通过sibling和遍历的这些兄弟节点产生关联,形成小fiber树
  • beginWork 全部执行完成 fiber树就创建完成了
  • completeUnitOfWork
    • 收集有副作用的fiber 然后组成effectlist单向链表
    • 先找到当前节点fiber的父fiber(currentFiber.return)
    • 每个fiber有两个属性 firstEffect指向第一个有副作用的子fiber ; lastEffect指向最后一个有副作用的子fiber; 中间的用nextEffect做成一个单链表

commit阶段

  • 循环effectlist单向链表,将dom依次进行挂载

实现元素的更新

  • 第一次渲染结束之后,将当前渲染成功的根fiber(workInProgressRoot)赋值给currentRoot(currentRoot = workInProgressRoot);然后清空workInProgressRoot(workInProgressRoot = null)
  • workInProgressRoot 渲染的时候有 渲染结束就没了
  • currentRoot是一直都有的,代表当前页面上看到的状态

更新的原理

  • 在系统中会有两棵fiber树:currentRoot 当前根fiber workInProgressRoot 正在工作的根fiber
  • 每个节点都是fiber节点,workInProgressRoot的根fiber和每一个子fiber都有一个指针指向对应的老节点,通过alternate属性。

image.png

  • 每次更新的时候都是拿到新的虚拟dom跟老的fiber节点进行比较,原因是在上次更新的时候,将虚拟dom节点转成了fiber节点,转完之后虚拟dom节点就没有了,只保留了fiber节点,每次重新渲染得到的都是虚拟dom节点,要把虚拟dom节点跟上一次的fiber节点进行对比,能更新则更新,不能则创建
  • 通过currentRoot有没有值来判断是初次渲染还是后面的更新渲染
  • 要想实现更新,还需要增加一个新的数组,因为删除的节点并不放在effectlist里,需要单独记录

image.png

  • 在commitRoot方法中,在执行effectlist之前先把该删除的元素删除掉,把deletions清空
  • 在commitWork方法中,增加删除节点和更新文本节点的逻辑

image.png

dom-diff 比较

  • 在 reconcileChildren方法中

  • 在初次渲染时,reconcileChildren方法完成的是:根据当前节点的chidlren属性,也就是新的虚拟dom数组创建子fiber树

  • 通过当前节点的alternate属性找到对应的老fiber,从而找到老fiber的儿子fiber => oldFiber

  • while循环中加入 || oldFiber 因为新老节点 可能是删除或者增加操作 保证都能循环到

  • 通过老的fiber节点和新的虚拟dom进行比较来生成新的fiber节点,根据不同的情况标记不同的effectTag同时将需要删除的节点加入到deletions数组当中

  • if (oldFiber.alternate) { 说明至少已经更新了一次,可以复用fiber对象了 image.png

双缓冲机制

  • 每次更新需要生成一个新的fiber树,fiber节点通过alternate指针指向老节点。不停的更新就不停的创建fiber树,每个fiber树的每个节点都是新的,更新的越来越多,性能会越来越差(因为对象创建的越来越多)
  • 所以出现了一个优化方案叫做 双缓冲
    • 第一次渲染创建一棵fiber树,赋值给currentRoot
    • 第二次渲染再创建一棵fiber树,并通过alternate进行关联,赋值给currentRoot
    • 到第三次渲染时,是将第一次渲染出来的fiber树拿过来作为这一次的workInProgressRoot,让他的alternate指向第二次的渲染出来的fiber树
    • 后续就一直复用上上一次渲染的那棵树,这样就永远只有两棵树在来回复用
  • if (currentRoot && currentRoot.alternate) { // 表示第二次更新以及之后的更新
  • } else if (currentRoot) {// 表示第一次更新 image.png

image.png

实现类组件

Component类

updateQueue.js

  • 更新队列,每次调用satState的时候会创建一个更新,放到更新队列里去
  • 更新队列实际上是一个单链表
export class Update {
    constructor(payload) {
        this.payload = payload;
    }
}
// 单向链表
export class UpdateQueue {
    constructor() {
        this.firstUpdate = null;
        this.lastUpdate = null;
    }
    enqueueUpdate(update) { // 加入更新队列
        if (this.lastUpdate === null) {
            this.firstUpdate = this.lastUpdate = update;
        } else {
            this.lastUpdate.nextUpdate = update;
            this.lastUpdate = update;
        }
    }
    forceUpdate(state) { // 更新
        let currentUpdate = this.firstUpdate;
        while (currentUpdate) {
            state = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
            currentUpdate = currentUpdate.nextUpdate;
        }
        this.firstUpdate = this.lastUpdate = null;
        return state;
    }
}
class Component { 
    constructor(props) { 
        this.props = props;
        this.updateQueue = new UpdateQueue(); 
    } 
    setState(payload) { 
        // updateQueue是放在类组件对应的fiber节点上 internalFiber
        this.internalFiber.updateQueue.enqueueUpdate(new Update(payload)); 
        scheduleRoot();  // 从根节点开始调度
    } 
} 
Component.prototype.isReactComponent = true; 
let React = { 
    createElement, 
    Component 
}
  • beginWork
    • 类组件的stateNode是组件的实例

    • currentFiber.type 是类本身(如 class Counter extends React.Component中的Counter)

    • 双向指针:

      • currentFiber.stateNode.internalFiber = currentFiber; 类组件的实例的internalFiber属性指向fiber
      • currentFiber.stateNode = new currentFiber.type(currentFiber.props);fiber的stateNode指向组件的实例
    • 初始化更新队列

    • 给组件实例的state赋值

    • 老fiber的儿子和新虚拟dom进行对比 image.png

image.png

  • 类组件本身也会创建一个fiber节点, stateNode是类的实例,并不是dom元素,所以实例里的div要挂到这个实例fiber对应的父fiber对应的真实节点上去。所以commit也要做出更改
  • commitWork中:需要处理非dom节点的情况
    • 如果不是dom节点fiber,通过return向上查找,知道找到真实dom节点fiber为止
    • 新增节点时,通过children向下查找真实dom节点
    • 删除也需要判断

image.png

函数组件的实现

  • beginWork
    • 没有实例,所以处理起来比较简单
    • 将执行之后返回的虚拟dom 来对比创建fiber树即可

image.png

  • 在reconcileChildren中加入函数组件类型 其他逻辑不变 image.png

实现hooks

  • useState是一个语法糖,是基于useReducer实现的
  • hooks的使用
import React from './react';
import ReactDOM from './react-dom';

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return { count: state.count + 1 };
    default:
      return state;
  }
}
function FunctionCounter() {
  const [numberState, setNumberState] = React.useState({ number: 0 });
  const [countState, dispatch] = React.useReducer(reducer, { count: 0 });
  return (
    <div>
      <h1 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}>
        Count: {numberState.number}
      </h1 >
      <hr />
      <h1 onClick={() => dispatch({ type: 'ADD' })}>
        Count: {countState.count}
      </h1 >
    </div>
  )
}
ReactDOM.render(
  <FunctionCounter />,
  document.getElementById('root')
);
  • 为了实现hooks,首先需要加两个相关的变量
    • let workInProgressFiber = null; 正在工作中的fiber
    • let hookIndex = 0; hook索引
    • 因为一个函数里面可能会有多个hooks,需要使用索引来指向每一个hook
  • 实现useReducer
    • 当执行useReducer的时候,首先需要知道是在哪一个函数组件中执行的useReducer
    • 然后要知道当前的hook是第几个hook
    • workInProgressFiber是在真正执行函数组件的时候创建的
    • 在执行currentFiber.type(currentFiber.props)之前, 现将下面三个变量赋值

image.png

  • 所以在执行函数组件之前:
    • workInProgressFiber = currentFiber; 当前的function component对应的fiber
    • hookIndex = 0;
    • workInProgressFiber.hooks = [];

循环链表

  • 环状链表
  • queue.pending永远指向最后一个更新
  • queue.pending.next 永远指向第一个更新
  • 更新的顺序是不变的 image.png
  • 循环链表的构建和遍历

image.png

  • 初次渲染流程图 image.png

  • 更新流程图 image.png

  • currentlyRenderingFiber 当前正在工作的fiber

  • workInProgressHook 函数组件中当前正在工作的hook 执行到哪一个hook workInProgressHook就指的是谁

  • 所有的hook通过next连接成一个单向链表