「React 16」为 Luy 实现 React Fiber 架构
前言Facebook 的研发能力真是惊人, 可以说,React 16 和 React 15 已经是技巧上的分水岭,但是得益于 React 16 的 经过两个星期的痛苦研究,终于将 React 16 的渲染脉络摸得比较清晰,可以写文章来记录、回顾一下。 如果你已经稍微理解了 什么是 React Fiber ?React 回顾 React 历年来的算法都知道, 那问题就来了,什么是
React 异步渲染流程图
其中,重点就是 那为什么更新到真实 DOM 中不能拆分呢?理论上来说,是可以拆分的,但是这会造成 UI 的不连续性,极大的影响体验。 递归变成了循环
以简单的组件为例子:
每经过一个 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: [] } 这是一个比较完整的
司徒正美的研究中,一个 开始写代码:Component 构造函数讲了那么多的理论,大家一定是晕了,但是没办法, 当然,结合代码来梳理,思路旧更加清晰了。我们在构建新的架构时,老的 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
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) //开始干活 } 我们定义了一个全局变量 实际上这里还有优化的空间,就是多次 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) } } 熟悉
在这里我们注意到,如果一个
所有的 第一步:createWorkInProgressexport 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 这个函数的主要作用就是构建 首先,我们先从 最后,我们将返回一个 第二步: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 } } 我们调用 整个函数做的事情其实就是一个左遍历树的过程。首先,我们调用 然后查找当前节点的父亲节点,是否有兄弟,有就返回,当成下一个处理的节点,如果没有,就继续回溯。 整个过程用图来表示,就是:
在讨论第三部之前,我们仍然有两个迷惑的地方:
beginWorkfunction 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) }
我们来看看比较重要的 function updateClassComponent(currentFiber) { let instance = currentFiber.stateNode if (!instance) { // 如果是 mount 阶段,构建一个 instance instance = currentFiber.stateNode = createInstance(currentFiber) } // 将新的state,newChildren) } function createInstance(fiber) { const instance = new fiber.type(fiber.props) instance._internalfiber = fiber return instance } 如果是首次渲染,那么组件并没有被实例话,此时我们调用 渲染出新儿子之后,来到了新架构下最重要的核心函数 reconcileChildrenArrayconst 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,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,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 } 这个函数做了几件事
看着比较啰嗦,但是实际上做的就是构建链表和 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 } } 这个函数做了两件事,第一件事情就是收集当前 第三步:commitAllWork终于,我们已经通过不断不断的调用 function commitAllwork(topFiber) { topFiber.effects.forEach(f => { commitWork(f) }) topFiber.stateNode._rootContainerFiber = topFiber topFiber.effects = [] nextUnitOfWork = null pendingCommit = null } 我们直接拿到 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 } } 这一部分代码是最好理解的了,就是做的是删除和插入或者更新 最后来一段测试代码: 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')) 我们来看看动图吧!当节点 再看以下调用栈,我们的 如果你想下载代码亲自体验,可以到 Luy 仓库中: git clone https://github.com/Foveluy/Luy.git cd Luy npm i --save-dev npm run start 目前我能找到的所有资料都放在仓库中:资料 回顾本文几个重要的点一开始我们就使用了一个数组来记录 取出来以后,使用从左向右遍历的方式,用链表链接一个一个的 现在 react 的架构已经变得极其复杂,而本文也只是将 React 的整体架构通篇流程描述了一遍,里面的细节依旧值得我们的深究,比如,如何传递 最后,感谢支持我的迷你框架项目:Luy ,现在正在向 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |