「React 16」为 Luy 实现 React Fiber 架构

前言

Facebook 的研发能力真是惊人, Fiber 架构给 React 带来了新视野的同时,将调度一词介绍给了前端,然而这个架构实在不好懂,比起以前的 Vdom 树,新的 Fiber 树就麻烦太多。

可以说,React 16 和 React 15 已经是技巧上的分水岭,但是得益于 React 16 的 Fiber 架构,使得 React 即使在没有开启异步的情况下,性能依旧是得到了提高。

经过两个星期的痛苦研究,终于将 React 16 的渲染脉络摸得比较清晰,可以写文章来记录、回顾一下。

如果你已经稍微理解了 Fiber 架构,可以直接看代码:仓库地址

什么是 React Fiber ?

React Fiber 并不是所谓的纤程(微线程、协程),而是一种基于浏览器的单线程调度算法,背后的支持 API 是大名鼎鼎的: requestIdleCallback ,得到了这个 API 的支持,我们便可以将 React 中最耗时的部分放入其中。

回顾 React 历年来的算法都知道,reconcilation 算法实际上是一个大递归,大递归一旦进行,想要中断还是比较不好操作的,加上头大尾大的 React 15 代码已经膨胀到了不可思议的地步,在重重压力之下,React 使用了大循环来代替之前的大递归,虽然代码变得比递归难懂了几个梯度,但是实际上,代码量比原来少了非常多(开发版本 3W 行压缩到了 1.3W 行)

那问题就来了,什么是 Fiber :一种将 recocilation (递归 diff ),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧( 16ms )内,还有没有足够的时间允许计算。

React 16 前后的大小图

React 异步渲染流程图

image
  1. 用户调用 ReactDOM.render 方法,传入例如<App />组件,React 开始运作<App />
  2. <App /> 在内部会被转换成 RootFiber 节点,一个特殊的节点,并记录在一个全局变量中,TopTree
  3. 拿到 <App />RootFiber ,首先创建一个 <App /> 对应的 Fiber ,然后加上 Fiber 信息,以便之后回溯。随后,赋值给之前的全局变量 TopTree
  4. 使用 requestIdleCallback 重复第三个步骤,直到循环到树的所有节点
  5. 最后完成了 diff 阶段,一次性将变化更新到真实 DOM 中,以防止 UI 展示的不连续性

其中,重点就是 34 阶段,这两个阶段将创建真实 DOM 和组件渲染 ( render )拆分为无数的小碎块,使用 requestIdleCallback 连续进行。在 React 15 的时候,渲染、创建、插入、删除等操作是最费时的,在 React 16 中将渲染、创建抽离出来分片,这样性能就得到了极大的提升。

那为什么更新到真实 DOM 中不能拆分呢?理论上来说,是可以拆分的,但是这会造成 UI 的不连续性,极大的影响体验。

递归变成了循环

image

以简单的组件为例子:

  1. 从顶端的 div#root 向下走,先走左子树
  2. div 有两个孩子 span ,继续走左边的
  3. 来到 span ,之下只有一个 hello ,到此,不再继续往下,而是往上回到 span
  4. 因为 span 有一个兄弟,因此往兄弟 span 走去
  5. 兄弟 span 有孩子 luy ,到此,不继续往下,而是回到 luy 的老爹 span
  6. luy 的老爹 span 右边没有兄弟了,因此回到其老爹 div
  7. div 没有任何的兄弟,因此回到顶端的 div#root

每经过一个 Fiber 节点,执行 render 或者 document.createElement (或者更新 DOM )的操作

Fiber 数据结构

一个 Fiber 数据结构比较复杂

const Fiber = {
  tag: HOST_COMPONENT,
  type: 'div',
  return: parentFiber,
  child: childFiber,
  sibling: null,
  alternate: currentFiber,
  stateNode: document.createElement('div') | instance,
  props: { children: [], className: 'foo' },
  partialState: null,
  effectTag: PLACEMENT,
  effects: []
}

这是一个比较完整的 Fiber object,他复杂的原因是因为一个 Fiber 就代表了一个「正在执行或者执行完毕」的操作单元。这个概念不是那么好理解,如果要说得简单一点就是:以前的 VDOM 树节点的升级版。让我们介绍几个关键属性:

  • 由「 递归改循环 」我们可以得知,当我们循环的遍历树到达底部时,需要回到其父节点,那么对应的就是 Fiber 中的 return 属性(以前叫 parent )。 childsibling 类似,代表这个 Fiber 的子 Fiber 和兄弟 Fiber
  • stateNode 这个属性比较特殊,用于记录当前 Fiber 所对应的真实 DOM 节点 或者 当前虚拟组件的实例,这么做的原因第一是为了实现 Ref ,第二是为了实现 DOM 的跟踪
  • tag 属性在新版的 React 中一共有 14 种值,分别代表了不同的 JSX 类型。
  • effectTageffects 这两个属性为的是记录每个节点 Diff 后需要变更的状态,比如删除,移动,插入,替换,更新等...

alternate 属性我想拿出来单独说一下,这个属性是 Fiber 架构新加入的属性。我们都知道,VDOM 算法是在更新的时候生成一颗新的 VDOM 树,去和旧的进行对比。在 Fiber 架构中,当我们调用 ReactDOM.render 或者 setState 之后,会生成一颗树叫做:work-in-progress tree,这一颗树就是我们所谓的新树用来与我们的旧树进行对比,新的树和旧的树的 Fiber 是完全不一样的,此时,我们就需要 alternate 属性去链接新树和旧树。

司徒正美的研究中,一个 Fiber 和它的 alternate 属性构成了一个联婴体,他们有共同的 tagtypestateNode 属性,这些属性在错误边界自爆时,用于恢复当前节点。

开始写代码:Component 构造函数

讲了那么多的理论,大家一定是晕了,但是没办法,Fiber 架构已经比之前的简单 React 要复杂太多了,因此不可能指望一次性把 Fiber 的内容全部理解,需要反复多看。

当然,结合代码来梳理,思路旧更加清晰了。我们在构建新的架构时,老的 Luy 代码大部分都要进行重构了,先来看看几个主要重构的地方:

export class Component {
  constructor(props, context) {
    this.props = props
    this.context = context
    this.state = this.state || {}
    this.refs = {}
    this.updater = {}
  }

  setState(updater) {
    scheduleWork(this, updater)
  }

  render() {
    throw 'should implement `render()` function'
  }
}

Component.prototype.isReactComponent = true
  • 这就是 React.Component 的代码
  • 构造函数中,我们都进两个参数,一个是外部的 props ,一个是 context
  • 内部有 staterefsupdaterupdater 用于收集 setState 的信息,便于之后更新用。当然,在这个版本之中,我并没有使用。
  • setState 函数也并没有做队列处理,只是调用了 scheduleWork 这个函数
  • Component.prototype.isReactComponent = true ,这段代码表饰着,如果一个组件的类型为 function 且拥有 isReactComponent ,那么他就是一个有状态组件,在创建实例时需要用 new ,而无状态组件只需要 fn(props,context) 调用
const tag = {
  HostComponent: 'host',
  ClassComponent: 'class',
  HostRoot: 'root',
  HostText: 6,
  FunctionalComponent: 1
}

const updateQueue = []

export function render(Vnode, Container, callback) {
  updateQueue.push({
    fromTag: tag.HostRoot,
    stateNode: Container,
    props: { children: Vnode }
  })

  requestIdleCallback(performWork) //开始干活
}

export function scheduleWork(instance, partialState) {
  updateQueue.push({
    fromTag: tag.ClassComponent,
    stateNode: instance,
    partialState: partialState
  })
  requestIdleCallback(performWork) //开始干活
}

我们定义了一个全局变量 updateQueue 来记录我们所有的更新操作,每当 renderscheduleWork (setState) 触发时,我们都会往 updateQueuepush 一个状态,然后,进而调用大名鼎鼎的 requestIdleCallback 进行更新。在这里与之前的 react 15 最大不同是,更新阶段和首次渲染阶段得到了统一,都是使用了 updateQueue 进行更新。

实际上这里还有优化的空间,就是多次 setState 的时候,应该合并成一次再进行 requestIdleCallback 的调用,不过这并不是我们的目标,我们的目标是搞懂 Fiber 架构。requestIdleCallback 调用的是 performWork 函数,我们接下来看看

performWork 函数

const EXPIRATION_TIME = 1 // ms async 逾期时间
let nextUnitOfWork = null
let pendingCommit = null

function performWork(deadline) {
  workLoop(deadline)
  if (nextUnitOfWork || updateQueue.length > 0) {
    requestIdleCallback(performWork) //继续干
  }
}

function workLoop(deadline) {
  if (!nextUnitOfWork) {
    //一个周期内只创建一次
    nextUnitOfWork = createWorkInProgress(updateQueue)
  }

  while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }

  if (pendingCommit) {
    //当全局 pendingCommit 变量被负值
    commitAllwork(pendingCommit)
  }
}

熟悉 requestIdleCallback 的同学一定对这两个函数并不陌生,这两个函数其实做的就是所谓的异步调度。

requestIdleCallback 用法

performWork 函数主要做了两件事,第一件事就是拿到 deadline 进入我们之前所谓的大循环,也就是正式进入处理新旧 FiberDiff 阶段,这个阶段比较的奇妙,我们叫他 workLoop 阶段。workLoop 会一次处理 1 个或者多个 Fiber ,具体处理多少个,要看每一帧具体还剩下多少时间,如果一个 Fiber 消耗太多时间,那么就会等到下一帧再处理下一个 Fiber ,如此循环,遍历整个 VDOM 树。

在这里我们注意到,如果一个 Fiber 消耗太多时间,可能会导致一帧时间的逾期,不过其实没什么问题啦,也仅仅是一帧逾期而已,对于我们视觉上并没有多大的影响。

workLoop 函数主要是三部曲:

  1. createWorkInProgress 这个函数会构建一颗树的顶端,赋值给全局变量 nextUnitOfWork ,通过迭代的方式,不断更新 nextUnitOfWork 直到遍历完所有树的节点。
  2. performUnitOfWork 函数是第二步,不断的检测当前帧是否还剩余时间,进行 WorkInProgress tree 的迭代
  3. WorkInProgress tree 迭代完毕以后,调用 commitAllWork ,将所有的变更全部一次性的更新到 DOM 中,以保证 UI 的连续性

所有的 Diff 和创建真实 DOM 的操作,都在 performUnitOfWork 之中,但是插入和删除是在 commitAllWork 之中。接下来,我们逐一分析三部曲的内部操作。

第一步:createWorkInProgress

export function createWorkInProgress(updateQueue) {
  const updateTask = updateQueue.shift()
  if (!updateTask) return

  if (updateTask.partialState) {
    // 证明这是一个setState操作
    updateTask.stateNode._internalfiber.partialState = updateTask.partialState
  }

  const rootFiber =
    updateTask.fromTag === tag.HostRoot
      ? updateTask.stateNode._rootContainerFiber
      : getRoot(updateTask.stateNode._internalfiber)

  return {
    tag: tag.HostRoot,
    stateNode: updateTask.stateNode,
    props: updateTask.props || rootFiber.props,
    alternate: rootFiber // 用于链接新旧的 VDOM
  }
}

function getRoot(fiber) {
  let _fiber = fiber
  while (_fiber.return) {
    _fiber = _fiber.return
  }
  return _fiber

这个函数的主要作用就是构建 workInProgress 树的顶端并赋值给全局变量 nextUnitOfWork。

首先,我们先从 updateQueue 中获取一个任务对象 updateTask 。随后,进行判断是否是更新阶段。然后获取 workInProgress 树的顶端。如果是第一次渲染, RootFiber 的值是空的,因为我们并没有构建任何的树。

最后,我们将返回一个 Fiber 对象,这个 Fiber 对象的标识符( tag )是 HostRoot

第二步:performUnitOfWork

// 开始遍历
function performUnitOfWork(workInProgress) {
  const nextChild = beginWork(workInProgress)
  if (nextChild) return nextChild

  // 没有 nextChild, 我们看看这个节点有没有 sibling
  let current = workInProgress
  while (current) {
    //收集当前节点的effect,然后向上传递
    completeWork(current)
    if (current.sibling) return current.sibling
    //没有 sibling,回到这个节点的父亲,看看有没有sibling
    current = current.return
  }
}

我们调用 performUnitOfWork 处理我们的 workInProgress

整个函数做的事情其实就是一个左遍历树的过程。首先,我们调用 beginWork ,获得一个当前 Fiber 下的第一个孩子,如果有直接返回出去给 nextUnitOfWork ,当作下一个处理的节点;如果没有找到任何孩子,证明我们已经到达了树的底部,通过下面的 while 循环,回到当前节点的父节点,将当前 Fiber 下拥有 Effect 的孩子全部记录下来,以便于之后更新 DOM

然后查找当前节点的父亲节点,是否有兄弟,有就返回,当成下一个处理的节点,如果没有,就继续回溯。

整个过程用图来表示,就是:

image

在讨论第三部之前,我们仍然有两个迷惑的地方:

  1. beginWork 是如何创建孩子的
  2. completeWork 是如何收集 effect 的接下来,我们就来一起看看

beginWork

function beginWork(currentFiber) {
  switch (currentFiber.tag) {
    case tag.ClassComponent: {
      return updateClassComponent(currentFiber)
    }
    case tag.FunctionalComponent: {
      return updateFunctionalComponent(currentFiber)
    }
    default: {
      return updateHostComponent(currentFiber)
    }
  }
}

function updateHostComponent(currentFiber) {
  // 当一个 fiber 对应的 stateNode 是原生节点,那么他的 children 就放在 props 里
  if (!currentFiber.stateNode) {
    if (currentFiber.type === null) {
      //代表这是文字节点
      currentFiber.stateNode = document.createTextNode(currentFiber.props)
    } else {
      //代表这是真实原生 DOM 节点
      currentFiber.stateNode = document.createElement(currentFiber.type)
    }
  }
  const newChildren = currentFiber.props.children
  return reconcileChildrenArray(currentFiber, newChildren)
}

function updateFunctionalComponent(currentFiber) {
  let type = currentFiber.type
  let props = currentFiber.props
  const newChildren = currentFiber.type(props)

  return reconcileChildrenArray(currentFiber, newChildren)
}

function updateClassComponent(currentFiber) {
  let instance = currentFiber.stateNode
  if (!instance) {
    // 如果是 mount 阶段,构建一个 instance
    instance = currentFiber.stateNode = createInstance(currentFiber)
  }

  // 将新的state,props刷给当前的instance
  instance.props = currentFiber.props
  instance.state = { ...instance.state, ...currentFiber.partialState }

  // 清空 partialState
  currentFiber.partialState = null
  const newChildren = currentFiber.stateNode.render()

  // currentFiber 代表老的,newChildren代表新的
  // 这个函数会返回孩子队列的第一个
  return reconcileChildrenArray(currentFiber, newChildren)
}

beginWork 其实是一个判断分支的函数,整个函数的意思是:

  • 判断当前的 Fiber 是什么类型,是 class 的走 class 分支,是 stateless 的走 stateless,是原生节点的走原生分支
  • 如果没有 stateNode ,则创建一个 stateNode
  • 如果是 class ,则创建实例,调用 render 函数,渲染其儿子;如果是原生节点,调用 DOM API 创建原生节点;如果是 stateless ,就执行它,渲染出 VDOM 节点
  • 最后,走到最重要的函数, recocileChildrenArray 函数,将其每一个孩子进行链表的链接,进行 diff ,然后返回当前 Fiber 之下的第一个孩子

我们来看看比较重要的 classComponent 的构建流程

function updateClassComponent(currentFiber) {
  let instance = currentFiber.stateNode
  if (!instance) {
    // 如果是 mount 阶段,构建一个 instance
    instance = currentFiber.stateNode = createInstance(currentFiber)
  }

  // 将新的state,props刷给当前的instance
  instance.props = currentFiber.props
  instance.state = { ...instance.state, ...currentFiber.partialState }

  // 清空 partialState
  currentFiber.partialState = null
  const newChildren = currentFiber.stateNode.render()

  // currentFiber 代表老的,newChildren代表新的
  // 这个函数会返回孩子队列的第一个
  return reconcileChildrenArray(currentFiber, newChildren)
}

function createInstance(fiber) {
  const instance = new fiber.type(fiber.props)
  instance._internalfiber = fiber
  return instance
}

如果是首次渲染,那么组件并没有被实例话,此时我们调用 createInstance 实例化组件,然后将当前的 propsstate 赋值给 props 、state ,随后我们调用 render 函数,获得了新儿子 newChildren

渲染出新儿子之后,来到了新架构下最重要的核心函数 reconcileChildrenArray .

reconcileChildrenArray

const PLACEMENT = 1
const DELETION = 2
const UPDATE = 3

function placeChild(currentFiber, newChild) {
  const type = newChild.type

  if (typeof newChild === 'string' || typeof newChild === 'number') {
    // 如果这个节点没有 type ,这个节点就可能是 number 或者 string
    return createFiber(tag.HostText, null, newChild, currentFiber, PLACEMENT)
  }

  if (typeof type === 'string') {
    // 原生节点
    return createFiber(tag.HOST_COMPONENT, newChild.type, newChild.props, currentFiber, PLACEMENT)
  }

  if (typeof type === 'function') {
    const _tag = type.prototype.isReactComponent ? tag.CLASS_COMPONENT : tag.FunctionalComponent

    return {
      type: newChild.type,
      tag: _tag,
      props: newChild.props,
      return: currentFiber,
      effectTag: PLACEMENT
    }
  }
}

function reconcileChildrenArray(currentFiber, newChildren) {
  // 对比节点,相同的标记更新
  // 不同的标记 替换
  // 多余的标记删除,并且记录下来
  const arrayfiyChildren = arrayfiy(newChildren)

  let index = 0
  let oldFiber = currentFiber.alternate ? currentFiber.alternate.child : null
  let newFiber = null

  while (index < arrayfiyChildren.length || oldFiber !== null) {
    const prevFiber = newFiber
    const newChild = arrayfiyChildren[index]
    const isSameFiber = oldFiber && newChild && newChild.type === oldFiber.type

    if (isSameFiber) {
      newFiber = {
        type: oldFiber.type,
        tag: oldFiber.tag,
        stateNode: oldFiber.stateNode,
        props: newChild.props,
        return: currentFiber,
        alternate: oldFiber,
        partialState: oldFiber.partialState,
        effectTag: UPDATE
      }
    }

    if (!isSameFiber && newChild) {
      newFiber = placeChild(currentFiber, newChild)
    }

    if (!isSameFiber && oldFiber) {
      // 这个情况的意思是新的节点比旧的节点少
      // 这时候,我们要将变更的 effect 放在本节点的 list 里
      oldFiber.effectTag = DELETION
      currentFiber.effects = currentFiber.effects || []
      currentFiber.effects.push(oldFiber)
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling || null
    }

    if (index === 0) {
      currentFiber.child = newFiber
    } else if (prevFiber && newChild) {
      // 这里不懂是干嘛的
      prevFiber.sibling = newFiber
    }

    index++
  }
  return currentFiber.child
}

这个函数做了几件事

  • 将孩子 array 化,这么做能够使得 reactrender 函数返回数组
  • currentFiber 是新的 workInProgress 上的一个节点,是属于新的 VDOM 树 ,而此时,我们必须要找到旧的 VDOM 树来进行比对。那么在这里, Alternate 属性就起到了关键性作用,这个属性链接了旧的 VDOM ,使得我们能够获取原来的 VDOM
  • 接下来我们进行对比,如果新的节点的 type 与原来的相同,那么我们将新建一个 Fiber ,标记这个 FiberUPDATE
  • 如果新的节点的 type 与原来的不相同,那我们使用 PALCEMENT 来标记他
  • 如果旧的节点数量比新的节点少,那就证明,我们要删除旧的节点,我们把旧节点标记为 DELETION ,并构建一个 effect list 记录下来
  • 当前遍历的是组件的第一个孩子,那么我们将他记录在 currentFiberchild 字段中
  • 当遍历的不是第一个孩子,我们将 新建的 newFiber 用链表的形式将他们一起推入到 currentFiber
  • 返回当前 currentFiber 下的第一个孩子

看着比较啰嗦,但是实际上做的就是构建链表和 diff 孩子的过程,这个函数有很多优化的空间,使用 key 以后,在这里能提高很多的性能,为了简单,我并没有对 key 进行操作,之后的 Luy 版本一定会的。

completeWork: 收集 effectTag

// 开始遍历
function performUnitOfWork(workInProgress) {
  const nextChild = beginWork(workInProgress)
  if (nextChild) return nextChild

  // 没有 nextChild, 我们看看这个节点有没有 sibling
  let current = workInProgress
  while (current) {
    //收集当前节点的effect,然后向上传递
    completeWork(current)
    if (current.sibling) return current.sibling
    //没有 sibling,回到这个节点的父亲,看看有没有sibling
    current = current.return
  }
}

//收集有 effecttag 的 fiber
function completeWork(currentFiber) {
  if (currentFiber.tag === tag.classComponent) {
    // 用于回溯最高点的 root
    currentFiber.stateNode._internalfiber = currentFiber
  }

  if (currentFiber.return) {
    const currentEffect = currentFiber.effects || [] //收集当前节点的 effect list
    const currentEffectTag = currentFiber.effectTag ? [currentFiber] : []
    const parentEffects = currentFiber.return.effects || []
    currentFiber.return.effects = parentEffects.concat(currentEffect, currentEffectTag)
  } else {
    // 到达最顶端了
    pendingCommit = currentFiber
  }
}

这个函数做了两件事,第一件事情就是收集当前 currentFibereffectTag ,将其 append 到父 Fibereffectlist 中去,通过循环一层一层往上,最终到达顶端 currentFiber.return === void 666 的时候,证明我们到达了 root ,此时我们已经把所有的 effect 收集到了顶端的 currentFiber.effect 上,并把它赋值给 pendingCommit ,进入 commitAllWork 阶段。

第三步:commitAllWork

终于,我们已经通过不断不断的调用 requestIdleCallback 和 大循环,将我们的所有变更都找出来放在了 workInProgress tree 里,我们接下来就要做最后一步:将所有的变更一次性的变更到真实 DOM 中,注意,这个阶段里我们不再运行创建 DOMrender ,因此,虽然我们一次性变更所有的 DOM ,但是性能来说并不是太差。

function commitAllwork(topFiber) {
  topFiber.effects.forEach(f => {
    commitWork(f)
  })

  topFiber.stateNode._rootContainerFiber = topFiber
  topFiber.effects = []
  nextUnitOfWork = null
  pendingCommit = null
}

我们直接拿到 TopFiber 中的 effects list ,遍历,将变更全部打到 DOM 中去,然后我们将全局变量清理干净。

function commitWork(effectFiber) {
  if (effectFiber.tag === tag.HostRoot) {
    // 代表 root 节点没什么必要操作
    return
  }

  // 拿到parent的原因是,我们要将元素插入的点,插在父亲的下面
  let domParentFiber = effectFiber.return
  while (domParentFiber.tag === tag.classComponent || domParentFiber.tag === tag.FunctionalComponent) {
    // 如果是 class 就直接跳过,因为 class 类型的fiber.stateNode 是其本身实例
    domParentFiber = domParentFiber.return
  }

  //拿到父亲的真实 DOM
  const domParent = domParentFiber.stateNode
  if (effectFiber.effectTag === PLACEMENT) {
    if (effectFiber.tag === tag.HostComponent || effectFiber.tag === tag.HostText) {
      //通过 tag 检查是不是真实的节点
      domParent.appendChild(effectFiber.stateNode)
    }
    // 其他情况
  } else if (effectFiber.effectTag == UPDATE) {
    // 更新逻辑 只能是没实现
  } else if (effectFiber.effectTag == DELETION) {
    //删除多余的旧节点
    commitDeletion(effectFiber, domParent)
  }
}

function commitDeletion(fiber, domParent) {
  let node = fiber
  while (true) {
    if (node.tag == tag.classComponent) {
      node = node.child
      continue
    }
    domParent.removeChild(node.stateNode)
    while (node != fiber && !node.sibling) {
      node = node.return
    }
    if (node == fiber) {
      return
    }
    node = node.sibling
  }
}

这一部分代码是最好理解的了,就是做的是删除和插入或者更新 DOM 的操作,值得注意的是,删除操作依旧使用的链表操作。

最后来一段测试代码:

import React from './Luy/index'
import { Component } from './component'
import { render } from './vdom'

class App extends Component {
  state = {
    info: true
  }
  constructor(props) {
    super(props)

    setTimeout(() => {
      this.setState({
        info: !this.state.info
      })
    }, 1000)
  }

  render() {
    return (
      <div>
        <span>hello</span>
        <span>luy</span>
        <div>{this.state.info ? 'imasync' : 'iminfo'}</div>
      </div>
    )
  }
}
render(<App />, document.getElementById('root'))

我们来看看动图吧!当节点 mount 以后,过了 1 秒,就会更新,我们简单的更新就到此结束了

image

image

再看以下调用栈,我们的 requestIdleCallback 函数已经正确的运行了。

如果你想下载代码亲自体验,可以到 Luy 仓库中:

git clone https://github.com/Foveluy/Luy.git
cd Luy
npm i --save-dev
npm run start

目前我能找到的所有资料都放在仓库中:资料

回顾本文几个重要的点

一开始我们就使用了一个数组来记录 update 的信息,通过调用 requestIdleCallback 来将更新一个一个的取出来,大部分时间队列里只有一个。

取出来以后,使用从左向右遍历的方式,用链表链接一个一个的 Fiber ,并做 diff 和创建,最后一次性的 patch 到真实 DOM 中去。

现在 react 的架构已经变得极其复杂,而本文也只是将 React 的整体架构通篇流程描述了一遍,里面的细节依旧值得我们的深究,比如,如何传递 context ,如何实现 ref ,如何实现错误边界处理,声明周期的处理,这些都是很大的话题,在接下去的文章里,我会一步一步的将这些关系讲清楚。

最后,感谢支持我的迷你框架项目:Luy ,现在正在向 Fiber 晋级!如果你喜欢,请给我一点 star🌟 表示鼓励!谢谢

如果有什么问题,可以加入我们的学习 QQ 群: 370262116 ,群里几乎所有的迷你 React 作者都在了,包括 anu 作者司徒正美, omi 作者,我等,一起来学习吧!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容