我们在之前的文章当中梳理了 React Fiber 相关内容,了解了 Fiber
的基本作用以及它内部一些简单的运行原理,所以在本章当中我们来继续深入理解其原理,尝试尝试手动的模拟实现一个简易版本的 Fiber
架构,本文主要参考的是 Build your own React ,内容有所调整,主要是为了方便自己理解,更多详细内容可以查看原文,下面我们就先从 JSX
开始看起
完整代码地址可见 Fiber 架构的简单实现
JSX 我们都知道,JSX
是一种特殊的语法,在之前版本的 React
当中如果想要支持 JSX
语法的话还需要一个额外库的来进行支持(JSXTransformer.js
),不过后来 JSX
的转换工作全部都集成到了 Babel
当中,比如下面这段简单的代码
1 2 3 4 5 6 7 8 9 10 11 const App = ( <div> <h1 id="title" >Title</h1> <a href="###">Link</ a> <section> <p> Article </p> </ section> </div> )
我们可以通过 Babel
的 在线预览功能 来查看它转化后的样子,它是下面这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var App = React.createElement( 'div' , null , React.createElement( 'h1' , { id: 'title' , }, 'Title' , ), React.createElement( 'a' , { href: '###' , }, 'Link' , ), React.createElement( 'section' , null , React.createElement('p' , null , 'Article' ), ), )
可以发现,我们书写的 JSX
代码已经被转换成了 React.createElement
的写法,同时从转换后的代码我们也可以发现 React.createElement
是支持多个参数的
type
,也就是节点类型
config
,节点上的属性,比如 id
和 href
children
,从第三个参数开始就全部是子元素,子元素可以有多个,类型可以是简单的文本,也可以还是 React.createElement
,如果是 React.createElement
的话其实就是子节点了,子节点下面还可以有子节点,这样就用 React.createElement
的嵌套关系实现了 HTML
节点的树形结构
而我们上面这段 JSX
代码如果想在 React
框架下运行起来,还需要 React
提供的额外两个库来进行支持,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from 'react' import ReactDOM from 'react-dom' const App = _ => { return ( <div> <h1 id="title" >Title</h1> <a href="###">Link</ a> <section> <p> Article </p> </ section> </div> ) } ReactDOM.render(<App / >, document .getElementById('root' ))
通过观察我们可以发现,这里面用到了 React
的地方其实就两个,一个是 JSX
,也就是 React.createElement
,另一个就是 ReactDOM.render
,所以下面我们就先来简单的看看 createElement
和 render
这两个方法
createElement 对于 <h1 id="title">Title</h1>
这样一个简单的节点,我们都知道原生 DOM
会附加一大堆属性和方法在上面,所以我们在 createElement
的时候最好能将它转换为一种比较简单的数据结构,只包含我们需要的元素,比如下面这样
1 2 3 4 5 6 7 { type: 'h1' , props: { id: 'title' , children: 'Title' } }
有了这个数据结构后,我们对于 DOM
的操作其实可以转化为对这个数据结构的操作,新老 DOM
的对比其实也可以转化为这个数据结构的对比,这样我们就不需要每次操作都去渲染页面,而是等到需要渲染的时候才将这个数据结构渲染到页面上,这其实就是所谓的『虚拟 DOM
』(以下我们简称为 vDom
),而我们 createElement
就是负责来构建这个 vDom
的方法
这里关于 createElement
的实现就不具体展开了,详细可以参考我们之前已经整理过的 什么是 Virtual DOM 系列文章或是 官方源码 ,核心逻辑并不复杂,这里我们只需要知道它是用来帮助我们构建 vDom
的方法即可
render 在上面的代码中我们使用 createElement
将 JSX
代码转换成了 vDom
,但是我们又该如何将 vDom
渲染为真实的 DOM
节点呢?所以我们还需要一个 render
方法来帮助我们实现这个功能,我们通过上面的使用可以发现,render
方法接收两个参数
根组件,其实是一个 JSX
组件,也就是一个 createElement
返回的 vDom
父节点,也就是我们要将这个 vDom
渲染的位置
有了这些了解以后我们就可以来实现我们自己的 render
方法了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function render (vDom, container ) { let dom if (typeof vDom !== 'object' ) { dom = document .createTextNode(vDom) } else { dom = document .createElement(vDom.type) } if (vDom.props) { Object .keys(vDom.props) .filter(key => key != 'children' ) .forEach(item => { dom[item] = vDom.props[item] }) } if (vDom.props && vDom.props.children && vDom.props.children.length) { vDom.props.children.forEach(child => render(child, dom)) } container.appendChild(dom) }
当然,上述代码只是简化版本的 render
方法,我们没有考虑节点具体类型的区别,又或是挂载和更新的不同处理逻辑等,详细内容同样可以参考我们之前已经整理过的 什么是 Virtual DOM 系列文章或是对应的 官方源码 来了解更多,因为这一部分在本文当中不是我们重点关注的内容,所以我们只是简单介绍一二,下面我们主要来看看 Fiber
的相关内容
render 的拆分 我们在上面实现了将 vDom
渲染到页面上的代码,这部分的工作在 React
官方当中被称为 renderer
,renderer
是第三方可以自己实现的一个模块,其中有个核心模块叫做 reconsiler
,而 reconsiler
的一大功能就是大家熟知的 Diff
,它会计算出应该更新哪些页面节点,然后将需要更新的节点 vDom
传递给 renderer
,renderer
负责将这些节点渲染到页面上
但是这个流程有个问题,也是我们在 React Fiber 章节开头部分所提到的,那就是虽然 React
的 Diff
算法是经过优化的,但是它却是同步的,renderer
负责操作 DOM
的一些操作也是同步的,也就是说如果有大量节点需要更新,JavaScript
线程的运行时间可能会比较长,在这段时间浏览器是不会响应其它事件的,因为 JavaScript
线程和 GUI
线程是互斥的,如果这个时间太长了,用户就可能看到卡顿,这也就是为什么 React
会推出 Fiber
的原因,Fiber
可以将长时间的同步任务拆分成多个小任务,从而让浏览器能够抽身去响应其它事件,等它有空了再回来继续计算
但是我们在上面实现的 render
方法,它是直接递归遍历了整个树,如果我们在中途某一步停下来,下次再调用时其实并不知道上次在哪里停下来的,不知道从哪里开始,即使你将上次的结束节点记下来了,你也不知道下一个该执行哪个,所以之前简单的 vDom
树形结构并不满足中途暂停,下次继续的需求,所以我们就需要改造它的数据结构
而另一个需要解决的问题是,拆分下来的小任务什么时候执行?我们的目的是让用户有更流畅的体验,所以我们最好不要阻塞高优先级的任务,比如用户输入,动画之类,等它们执行完了我们再计算,那我怎么知道现在有没有高优先级任务,浏览器是不是空闲呢?所以总结下来,Fiber
要想达到目的,需要解决两个问题
新的任务调度,有高优先级任务的时候将浏览器让出来,等浏览器空了再继续执行
新的数据结构,可以随时中断,下次进来可以接着执行
所幸,针对这两点我们都已经有了对应的解决方式,也就是 之前文章 当中所提及到的 Fiber
数据结构与 requestIdleCallback
这个 API
,这里我们就不过多介绍了,只简单提及一二
requestIdleCallback requestIdleCallback
接收一个回调,这个回调会在浏览器空闲时调用,每次调用会传入一个 IdleDeadline
,可以得到当前还空余多久,options
可以传入参数最多等多久,等到了时间浏览器还不空就强制执行了,使用这个 API
可以解决我们之前提到的任务调度的问题,让浏览器在空闲时才计算 Diff
并渲染,调用方式如下
1 2 3 4 5 var handle = window .requestIdleCallback(callback[, options])Window.cancelIdleCallback(handle)
但是这个 API
还处在实验阶段,兼容性不好,所以 React
官方自己实现了一套,但是在这里我们就不考虑那么多了,还是使用 requestIdleCallback
来进行任务调度,我们进行任务调度的思想是将任务拆分成多个小任务,requestIdleCallback
里面不断的把小任务拿出来执行,当所有任务都执行完或者超时了就结束本次执行,同时要注册下次执行
这里我们可以借住官方的 workLoopSync 实现方式得出大致架子,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function workLoop (deadline ) { while (nextUnitOfWork && deadline.timeRemaining() > 1 ) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) } requestIdleCallback(workLoop) } function performUnitOfWork (fiber ) {} requestIdleCallback(workLoop)
Fiber 数据结构 在上面的代码中,我们完成了任务的拆分,下面我们就来看看如果能让我们的任务可以随时中断,并且下次进来的时候还可以接着执行,上面示例中的 performUnitOfWork
方法我们暂时还没有实现,但是从上面的结构可以看出来,它接收的参数是一个小任务,同时通过这个小任务还可以找到它的下一个小任务,而 Fiber
构建的就是这样一个数据结构,而我们之前的 vDom
的数据结构是一棵树,父节点的 children
指向了子节点,但是只有这一个指针是不能实现中断继续的,所以我们需要对之前的结构进行一定的调整,可以参考官方演讲当中的方式,如下
我们可以发现和之前 vDom
当中父节点指向所有子节点不同,这里有三个指针
child
,父节点指向第一个子元素的指针
sibling
,从第一个子元素往后,指向下一个兄弟元素
return
,所有子元素都有的指向父元素的指针
有了这几个指针后,我们可以在任意一个元素中断遍历并恢复,比如在上图 List
处中断了,恢复的时候可以通过 child
找到他的子元素,也可以通过 return
找到他的父元素,如果他还有兄弟节点也可以用 sibling
找到,Fiber
这个结构外形看着还是棵树,但是没有了指向所有子元素的指针,父节点只指向第一个子节点,然后子节点有指向其他子节点的指针,所以可以发现这其实是个『链表结构』
这里需要注意的是,真正的 Fiber
结构并不仅仅只多了这三个节点,但是这里我们只使用这三个节点来理解其原理就足够了,更多 Fiber
节点相关内容可见 Fiber 节点
Fiber 的实现 有了以上内容的铺垫以后,下面我们就可以来实现一下我们自己的 Fiber
了,我们需要将我们之前的 vDom
结构转换为 Fiber
的数据结构,同时需要能够通过其中任意一个节点返回下一个节点,其实就是遍历这个链表
遍历的时候从根节点出发,先找子元素,如果子元素存在,直接返回,如果没有子元素了就找兄弟元素,找完所有的兄弟元素后再返回父元素,然后再找这个父元素的兄弟元素,整个遍历过程其实是个『深度优先遍历』,从上到下,然后最后一行开始从左到右遍历
比如下图从 div1
开始遍历的话,遍历的顺序就应该是 div1 ==> div2 ==> h1 ==> a ==> div2 ==> p ==> div1
,可以看到这个序列中,当我们 return
父节点时,这些父节点会被第二次遍历,所以我们在设计的时候,return
的父节点不会作为下一个任务返回,只有 sibling
和 child
才会作为下一个任务返回
同样我们可以参考官方当中的 performUnitOfWork 来进行实现,它的作用是用来执行任务,参数是我们当前的 Fiber
任务,返回值是下一个任务,我们这里只是简单的模拟实现,官方版本当中的实现方式远比我们要复杂很多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 function performUnitOfWork (fiber ) { if (!fiber.dom) { fiber.dom = createDom(fiber) } if (fiber.return) { fiber.return.dom.appendChild(fiber.dom) } const elements = fiber.children let prevSibling = null if (elements && elements.length) { for (let i = 0 ; i < elements.length; i++) { const element = elements[i] const newFiber = { type: element.type, props: element.props, return : fiber, dom: null } if (i === 0 ) { fiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber } } if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.return } }
统一提交 上面我们的 performUnitOfWork
一边构建 Fiber
结构一边操作 DOM
(appendChild
),但是如果某次我们同时更新了好几个节点,而且在操作了第一个节点之后就中断了,那么我们可能只会看到第一个节点渲染到了页面,后续几个节点要等到浏览器空了才会去陆续渲染
为了避免这种情况,我们应该将 DOM
操作都搜集起来,最后统一执行,而这就是 commit
操作,为了能够记录位置,我们还需要一个全局变量 workInProgressRoot
来记录根节点,然后在 workLoop
检测如果任务执行完了,就统一 commit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function workLoop (deadline ) { while (nextUnitOfWork && deadline.timeRemaining() > 1 ) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) } if (!nextUnitOfWork && workInProgressRoot) { commitRoot() } requestIdleCallback(workLoop) }
因为我们是在 Fiber
树完全构建后再执行的 commit
,而且有一个变量 workInProgressRoot
指向了 Fiber
的根节点,所以我们可以直接把 workInProgressRoot
拿过来递归渲染就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function commitRoot ( ) { commitRootImpl(workInProgressRoot.child) workInProgressRoot = null } function commitRootImpl (fiber ) { if (!fiber) { return } const parentDom = fiber.return.dom parentDom.appendChild(fiber.dom) commitRootImpl(fiber.child) commitRootImpl(fiber.sibling) }
协调 协调其实就是 vDom
的 Diff
操作,需要添加新的节点,删除不需要的节点和更新修改过的节点,为了能在中断后能回到工作位置,我们还需要一个变量 currentRoot
,然后在 fiber
节点里面添加一个属性 alternate
,这个属性指向上一次运行的根节点,也就是 currentRoot
currentRoot
会在第一次 render
后的 commit
阶段赋值,也就是每次计算完后都会把当次状态记录在 alternate
上,后面更新了就可以把 alternate
拿出来跟新的状态做 Diff
,然后 performUnitOfWork
里面需要添加协调子元素的代码,所以我们可以新增一个比对函数 reconcileChildren
来将老节点跟新节点进行对比,逻辑如下
如果新老节点类型一样,复用老节点 DOM
,更新 props
如果类型不一样,而且新的节点存在,创建新节点替换老节点
如果类型不一样,没有新节点,有老节点,删除老节点
注意删除老节点的操作是直接将 oldFiber
加上一个删除标记就行,同时用一个全局变量 deletions
记录所有需要删除的节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 function reconcileChildren (workInProgressFiber, elements ) { let oldFiber = workInProgressFiber.alternate && workInProgressFiber.alternate.child let prevSibling = null let index = 0 if (elements && elements.length) { if (!oldFiber) { for (let i = 0 ; i < elements.length; i++) { const element = elements[i] const newFiber = buildNewFiber(element, workInProgressFiber) if (i === 0 ) { workInProgressFiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber } } while (index < elements.length && oldFiber) { let element = elements[index] let newFiber = null const sameType = oldFiber && element && oldFiber.type === element.type if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, return : workInProgressFiber, alternate: oldFiber, effectTag: 'UPDATE' } } else if (!sameType && element) { newFiber = buildNewFiber(element, workInProgressFiber) } else if (!sameType && oldFiber) { oldFiber.effectTag = 'DELETION' deletions.push(oldFiber) } oldFiber = oldFiber.sibling if (index === 0 ) { workInProgressFiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } } }
然后就是在 commit
阶段处理真正的 DOM
操作,具体的操作是根据我们的 effectTag
来进行判断的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function commitRootImpl (fiber ) { if (!fiber) { return } const parentDom = fiber.return.dom if (fiber.effectTag === 'REPLACEMENT' && fiber.dom) { parentDom.appendChild(fiber.dom) } else if (fiber.effectTag === 'DELETION' ) { parentDom.removeChild(fiber.dom) } else if (fiber.effectTag === 'UPDATE' && fiber.dom) { updateDom(fiber.dom, fiber.alternate.props, fiber.props) } commitRootImpl(fiber.child) commitRootImpl(fiber.sibling) }
替换和删除的 DOM
操作都比较简单,更新属性的会稍微麻烦点,需要再写一个辅助函数 updateDom
来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function updateDom (dom, prevProps, nextProps ) { Object .keys(prevProps) .filter(name => name !== 'children' ) .filter(name => !(name in nextProps)) .forEach(name => { if (name.indexOf('on' ) === 0 ) { dom.removeEventListener(name.substr(2 ).toLowerCase(), prevProps[name], false ) } else { dom[name] = '' } }) Object .keys(nextProps) .filter(name => name !== 'children' ) .forEach(name => { if (name.indexOf('on' ) === 0 ) { dom.addEventListener(name.substr(2 ).toLowerCase(), nextProps[name], false ) } else { dom[name] = nextProps[name] } }) }
这里我们只是简单处理了 on
开头的一些事件,并且兼容性可能也会存在问题,而且 prevProps
和 nextProps
可能会遍历到相同的属性,有重复赋值,但是在这里我们也就不多做处理了,这里的主要目的其实是为了让我们了解其原理,更为完整的实现方式可以参考我们之前已经整理过的 渲染器的核心 Diff 算法
下面我们来看看如何支持函数组件
函数组件 函数组件是 React
里面很常见的一种组件,但是我们之前的 fiber
节点上的 type
都是 DOM
节点的类型,比如 h1
之类的,而函数组件的节点 type
应该就是一个函数了,所以我们需要对这种节点进行单独处理,首先需要在更新的时候检测当前节点是不是函数组件,如果是的话那么 children
的处理逻辑会稍微有些不太一样,我们首先来调整一下我们的 performUnitOfWork
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function performUnitOfWork (fiber ) { const isFunctionComponent = fiber.type instanceof Function if (isFunctionComponent) { updateFunctionComponent(fiber) } else { updateHostComponent(fiber) } } function updateFunctionComponent (fiber ) { const children = [fiber.type(fiber.props)] reconcileChildren(fiber, children) } function updateHostComponent (fiber ) { if (!fiber.dom) { fiber.dom = createDom(fiber) } const elements = fiber.props.children reconcileChildren(fiber, elements) }
然后在我们提交 DOM
操作的时候因为函数组件没有 DOM
元素,所以需要注意两点
获取父级 DOM
元素的时候需要递归往上找到真正的 DOM
删除节点的时候需要递归往下找到真正的节点
所以我们来修改下 commitRootImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function commitRootImpl (fiber ) { if (!fiber) { return } let parentFiber = fiber.return while (!parentFiber.dom) { parentFiber = parentFiber.return } const parentDom = parentFiber.dom if (fiber.effectTag === 'REPLACEMENT' && fiber.dom) { parentDom.appendChild(fiber.dom) } else if (fiber.effectTag === 'DELETION' ) { commitDeletion(fiber, parentDom) } else if (fiber.effectTag === 'UPDATE' && fiber.dom) { updateDom(fiber.dom, fiber.alternate.props, fiber.props) } commitRootImpl(fiber.child) commitRootImpl(fiber.sibling) } function commitDeletion (fiber, domParent ) { if (fiber.dom) { domParent.removeChild(fiber.dom) } else { commitDeletion(fiber.child, domParent) } }
现在我们可以传入函数组件了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React from './myReact' const ReactDOM = Reactfunction App (props ) { return ( <div> <h1 id="title" >{props.title}</h1> <a href="xxx">Jump</ a> <section> <p> Article </p> </ section> </div> ) } ReactDOM.render( <App / >, document .getElementById('root' ) )
实现 useState useState
是 React Hooks
里面的一个 API
,相当于之前 Class Component
里面的 state
,用来管理组件内部状态,现在我们已经有一个简化版的 React
了,我们也可以尝试下来实现这个 API
简单版 我们还是从用法入手来实现最简单的功能,我们一般使用 useState
是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function App (props ) { const [count, setCount] = React.useState(1 ) const onClickHandler = () => { setCount(count + 1 ) } return ( <div> <h1>Count: {count}</h1> <button onClick={onClickHandler}>Count + 1</ button> </div> ) } ReactDOM.render( <App / >, document .getElementById('root' ) )
上述代码可以看出,我们的 useState
接收一个初始值,返回一个数组,里面有这个 state
的当前值和改变 state
的方法,但是需要注意的是 App
作为一个函数组件,每次 render
的时候都会运行,也就是说里面的局部变量每次 render
的时候都会重置,那我们的 state
就不能作为一个局部变量,而是应该作为一个全部变量来进行存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let state = null function useState (init ) { state = state === null ? init : state const setState = value => { state = value workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } nextUnitOfWork = workInProgressRoot deletions = [] } return [state, setState] }
这样其实我们就可以使用了
支持多个 state 但是上面的代码当中只有一个 state
变量,如果我们有多个 useState
怎么办呢?为了能支持多个 useState
,我们的 state
就不能是一个简单的值了,我们可以考虑把他改成一个数组,多个 useState
按照调用顺序放进这个数组里面,访问的时候通过下标来访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let state = []let hookIndex = 0 function useState (init ) { const currentIndex = hookIndex state[currentIndex] = state[currentIndex] === undefined ? init : state[currentIndex] const setState = value => { state[currentIndex] = value workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } nextUnitOfWork = workInProgressRoot deletions = [] } hookIndex++ return [state[currentIndex], setState] }
支持多个组件 上面的代码虽然我们支持了多个 useState
,但是仍然只有一套全局变量,如果有多个函数组件,每个组件都来操作这个全局变量,那相互之间不就是污染了数据了吗?所以我们数据还不能全都存在全局变量上面,而是应该存在每个 Fiber
节点上,处理这个节点的时候再将状态放到全局变量用来通讯
1 2 3 4 5 let wipFiber = null let hookIndex = null
因为 useState
只在函数组件里面可以用,所以我们之前的 updateFunctionComponent
里面需要初始化处理 useState
变量
1 2 3 4 5 6 7 8 function updateFunctionComponent (fiber ) { wipFiber = fiber hookIndex = 0 wipFiber.hooks = [] }
因为 Hooks
队列放到 Fiber
节点上去了,所以我们在 useState
取之前的值时需要从 fiber.alternate
上取,完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function useState (init ) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] const hook = { state: oldHook ? oldHook.state : init } wipFiber.hooks.push(hook) hookIndex++ const setState = value => { hook.state = value workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } nextUnitOfWork = workInProgressRoot deletions = [] } return [hook.state, setState] }
在上面的代码当中可以看出,我们在将 useState
和存储的 state
进行匹配的时候是用的 useState
的调用顺序匹配 state
的下标,如果这个下标匹配不上了,state
就错了,所以在 React
文档当中特意强调了不要在判断当中使用 useState
1 2 3 if (something) { const [state, setState] = useState(1 ) }
比如上述代码就不能保证每次 something
都满足,可能导致 useState
这次 render
执行了,下次又没执行,这样新老节点的下标就匹配不上了,对于这种代码,React
就会直接报错
用 Hooks 模拟 Class 组件 这个算是一个扩展功能,通过前面实现的 Hooks
来模拟实现 Class
组件,我们可以写一个方法将 Class
组件转化为前面的函数组件
1 2 3 4 5 6 7 8 9 10 function transfer (Component ) { return function (props ) { const component = new Component(props) let [state, setState] = useState(component.state) component.props = props component.state = state component.setState = setState return component.render() } }
然后就可以写 Class
了,这个 Class
长得很像我们在 React
里面写的 Class
,有 state
,setState
和 render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React from './myReact' class Count { constructor (props) { this .props = props this .state = { count: 1 } } onClickHandler = () => { this .setState({ count: this .state.count + 1 }) } render() { return ( <div> <h3>Class Component Count: {this .state.count}</h3> <button onClick={this.onClickHandler}>Count + 1</ button> </div> ) } } / / export 的时候用 transfer 包装下 export default React.transfer(Count)
然后使用的时候直接使用 <Count>
就行了
1 2 3 <div> <Count></Count > </div>
当然你也可以在 React
里面建一个空的 Class Component
,让 Count
继承它,这样就更像了
完整代码 汇总以后的代码如下,完整代码地址可见 Fiber 架构的简单实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 function createTextVDom (text ) { return { type: 'TEXT' , props: { nodeValue: text, children: [] } } } function createElement (type, props, ...children ) { return { type, props: { ...props, children: children.map(child => { return typeof child === 'object' ? child : createTextVDom(child) }) } } } function createDom (vDom ) { let dom if (vDom.type === 'TEXT' ) { dom = document .createTextNode(vDom.props.nodeValue) } else { dom = document .createElement(vDom.type) if (vDom.props) { Object .keys(vDom.props) .filter(key => key !== 'children' ) .forEach(item => { if (item.indexOf('on' ) === 0 ) { dom.addEventListener(item.substr(2 ).toLowerCase(), vDom.props[item], false ) } else { dom[item] = vDom.props[item] } }) } } return dom } function updateDom (dom, prevProps, nextProps ) { Object .keys(prevProps) .filter(name => name !== 'children' ) .filter(name => !(name in nextProps)) .forEach(name => { if (name.indexOf('on' ) === 0 ) { dom.removeEventListener(name.substr(2 ).toLowerCase(), prevProps[name], false ) } else { dom[name] = '' } }) Object .keys(nextProps) .filter(name => name !== 'children' ) .forEach(name => { if (name.indexOf('on' ) === 0 ) { dom.addEventListener(name.substr(2 ).toLowerCase(), nextProps[name], false ) } else { dom[name] = nextProps[name] } }) } function commitRoot ( ) { deletions.forEach(commitRootImpl) commitRootImpl(workInProgressRoot.child) currentRoot = workInProgressRoot workInProgressRoot = null } function commitDeletion (fiber, domParent ) { if (fiber.dom) { domParent.removeChild(fiber.dom) } else { commitDeletion(fiber.child, domParent) } } function commitRootImpl (fiber ) { if (!fiber) { return } let parentFiber = fiber.return while (!parentFiber.dom) { parentFiber = parentFiber.return } const parentDom = parentFiber.dom if (fiber.effectTag === 'REPLACEMENT' && fiber.dom) { parentDom.appendChild(fiber.dom) } else if (fiber.effectTag === 'DELETION' ) { commitDeletion(fiber, parentDom) } else if (fiber.effectTag === 'UPDATE' && fiber.dom) { updateDom(fiber.dom, fiber.alternate.props, fiber.props) } commitRootImpl(fiber.child) commitRootImpl(fiber.sibling) } let nextUnitOfWork = null let workInProgressRoot = null let currentRoot = null let deletions = null function workLoop (deadline ) { while (nextUnitOfWork && deadline.timeRemaining() > 1 ) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) } if (!nextUnitOfWork && workInProgressRoot) { commitRoot() } requestIdleCallback(workLoop) } function buildNewFiber (fiber, workInProgressFiber ) { return { type: fiber.type, props: fiber.props, dom: null , return : workInProgressFiber, alternate: null , effectTag: 'REPLACEMENT' } } function reconcileChildren (workInProgressFiber, elements ) { let oldFiber = workInProgressFiber.alternate && workInProgressFiber.alternate.child let prevSibling = null let index = 0 if (elements && elements.length) { if (!oldFiber) { for (let i = 0 ; i < elements.length; i++) { const element = elements[i] const newFiber = buildNewFiber(element, workInProgressFiber) if (i === 0 ) { workInProgressFiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber } } while (index < elements.length && oldFiber) { let element = elements[index] let newFiber = null const sameType = oldFiber && element && oldFiber.type === element.type if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, return : workInProgressFiber, alternate: oldFiber, effectTag: 'UPDATE' } } else if (!sameType && element) { newFiber = buildNewFiber(element, workInProgressFiber) } else if (!sameType && oldFiber) { oldFiber.effectTag = 'DELETION' deletions.push(oldFiber) } oldFiber = oldFiber.sibling if (index === 0 ) { workInProgressFiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } } } let wipFiber = null let hookIndex = null function useState (init ) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] const hook = { state: oldHook ? oldHook.state : init } wipFiber.hooks.push(hook) hookIndex++ const setState = value => { hook.state = value workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } nextUnitOfWork = workInProgressRoot deletions = [] } return [hook.state, setState] } function updateFunctionComponent (fiber ) { wipFiber = fiber hookIndex = 0 wipFiber.hooks = [] const children = [fiber.type(fiber.props)] reconcileChildren(fiber, children) } function updateHostComponent (fiber ) { if (!fiber.dom) { fiber.dom = createDom(fiber) } const elements = fiber.props.children reconcileChildren(fiber, elements) } function performUnitOfWork (fiber ) { const isFunctionComponent = fiber.type instanceof Function if (isFunctionComponent) { updateFunctionComponent(fiber) } else { updateHostComponent(fiber) } if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.return } } requestIdleCallback(workLoop) function render (vDom, container ) { workInProgressRoot = { dom: container, props: { children: [vDom] }, alternate: currentRoot } deletions = [] nextUnitOfWork = workInProgressRoot } class Component { constructor (props) { this .props = props } } function transfer (Component ) { return function (props ) { const component = new Component(props) let [state, setState] = useState(component.state) component.props = props component.state = state component.setState = setState return component.render() } } export default { createElement, render, useState, Component, transfer }
总结 我们简单总结一下上面涉及到的一些知识点
我们写的 JSX
代码最终会被 Babel
转化成了 React.createElement()
的形式
React.createElement()
返回的其实就是虚拟 DOM
结构
ReactDOM.render
方法是将 vDom
渲染到页面上
vDom
的协调和渲染可以简单粗暴的递归,但是这个过程是同步的,如果需要处理的节点过多,可能会阻塞用户输入和动画播放,造成卡顿
为了解决这个问题,React
引入了 Fiber
结构,目的是将同步的协调变成异步的
Fiber
改造了 vDom
的结构,形成具有『父元素 ==> 第一个子元素』,『子元素 ==> 兄弟元素』,『子元素 ==> 父元素』这样的链表结构,有了这几个指针,可以从任意一个 Fiber
节点找到其他节点
Fiber
将整棵树的同步任务拆分成了每个节点可以单独执行的异步执行结构
Fiber
可以从任意一个节点开始遍历,遍历是深度优先遍历,顺序是『父元素 ==> 子元素 ==> 兄弟元素 ==> 父元素』,也就是从上往下,从左往右
Fiber
的协调阶段可以是异步的小任务,但是提交阶段(commit
)必须是同步的,因为异步的 commit
可能让用户看到节点一个一个接连出现,体验不好
函数组件其实就是这个节点的 type
是个函数,直接将 type
拿来运行就可以得到 vDom
useState
是在 Fiber
节点上添加了一个数组,数组里面的每个值对应了一个 useState
,useState
调用顺序必须和这个数组下标匹配,不然会报错(所以不能在判断当中使用 useState
)
参考