Open crazylxr opened 4 years ago
在阅读这篇文章之前,我希望你已经了解过 React 的 Fiber 架构,如果还不熟悉,请阅读我的这篇:Deep In React 之浅谈 React Fiber 架构(一)。
在环境搭建上我选择了 Parcel,因为它使用起来非常的简洁,配置少,使用起来方便。首先通过 npm 安装 Parcel:
npm install -g parcel-bundler
创建一个项目目录并且初始化 package.json 文件:
mkdir react-like && cd react-like && npm init -y
接下来创建 index.html 和 index.js,在 index.html 里引入 index.js
const title = <h1 className="title"><h2>fetaoyuan</h2><h2>taoweng</h2></h1>;
这样的一段 jsx 代码其实对于浏览器来说是一段不合法的 js 代码,本质上,jsx 是 js 的语法糖,比如上面的这段代码会被 babel 转成如下代码:
var title = React.createElement("h1", { className: "title" }, React.createElement("h2", null, "fetaoyuan"), React.createElement("h2", null, "taoweng"));
你可以在这里进行在线转换查看转换后的代码
可以看出来转化的逻辑大概是这样:
React.createElement(type, props, child1, child2, child3)
清楚了 babel 的转化逻辑,接下来就来实现以下吧。
首先配置一下 .babelrc:
{ "presets": ["@babel/env"], "plugins": [ ["@babel/transform-react-jsx", { "pragma": "React.createElement" }] ] }
接下来在 index.js 里写一行代码看是否成功。
document.write('前端桃园')
然后让项目跑起来:
parcel index.html
parcel 是一个非常智能的工具,不需要你去安装 babel 相关的包,会根据你的配置,自动的去安装相关的包,在平时的玩具里面用,还是非常方便的。
然后访问 localhost:1234 就可以看到屏幕输出了前端桃园了。
前端桃园
我们知道在 React 里,children 是作为 props 里面的一个属性,这根 jsx 转化出来的不一样。知道了 babel 转化 jsx 的规则,我们要实现 createElement 就非常的简单了,只需要利用 ES6 的 rest 参数,就可以非常容易的拿到所有的 children。
function createElement(type, config, ...children) { return { type, props: { ...config, children } } }
接下来在进行调试一下:
// index.js const React = { createElement } function createElement(type, config, ...children) { return { type, props: { ...config, children } } } const title = <h1 className="title"><h2>fetaoyuan</h2><h2>taoweng</h2></h1>; console.log(title)
输出的结果如下,是符合我们的期望的。实际上这个输出出来的,通过 createElement 方法返回的对象记录了这个 DOM 节点我们需要的信息,这个对象就被称为虚拟DOM。
在了解 fiber 架构之后,你就应该知道 fiber 是如何工作的,在初次渲染的时候:
第一步生成虚拟 DOM 上面已经完成了,接下来了解如何通过并发模式来生成 Fiber。
理想情况下,我们应该把 render 拆成更细分的单元,每完成一个单元的工作,允许浏览器打断渲染响应更高优先级的工作,这个过程称为"并发模式(Concurrent Mode)"。 这里用 requestIdleCallback 这个浏览器 API 来实现,这个 API 可以在线程空闲的时候去执行回调函数(执行我们的工作单元)。
render
由于兼容性的问题,React 目前没有使用这个 API,而是为了这个效果,自己实现了一套方案,但核心思路是类似的。
大致的代码如下:
let nextUnitOfWork; // 下一个执行单元 function workLoop(deadline) { while(nextUnitOfWork) { nextUnitOfWork = performUnitWork(nextUnitOfWork) } } function performUnitWork(currentFiber) { // TODO, 执行单元 } requestIdleCallback(workLoop)
全局遍历 nextUnitOfWork 为下一个执行单元,是一个 Fiber 结构。我们要知道架构改为 fiber 的一个大的特征就是将结构改为了链表,链表的遍历就是一个一个的, performUnitWork 函数就是执行当前的 Fiber,然后返回下一个 Fiber,这样遍历整棵树。但是目前的代码是有问题的,因为没有被打断的逻辑,那咱们再加上被打断的逻辑。
nextUnitOfWork
performUnitWork
let nextUnitOfWork; // 下一个执行单元 // deadline 是还有多少的空闲时间 function workLoop(deadline) { let shouldYield = false; while(nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitWork(nextUnitOfWork) // 回调函数入参 deadline 可以告诉我们在这个渲染周期还剩多少时间可用 // 剩余时间小于1毫秒就被打断,等待浏览器再次空闲 shouldYield = deadline.timeRemaining() < 1; } requestIdleCallback(workLoop); } function performUnitWork(currentFiber) { // TODO, 执行单元 } requestIdleCallback(workLoop)
打断的逻辑就在 shouldYield = deadline.timeRemaining() < 1 这行代码里,如果时间片小于 1 毫秒,就被打断,等待浏览器下次空闲的时候再执行。 有没有忽然觉得如此高大上的概念(并发模式),其实原理很简单。
shouldYield = deadline.timeRemaining() < 1
为了便于理解,现在将文件进行拆分一下,将 React.xxx 的 API 放到 react.js 里。 另外我们都知道 react 要进行渲染需要有个 render 函数,这个是在 ReactDOM 下面的 API,所以再建一个 react-dom.js 用来放 render 函数。 对于刚才我们所写的并发模式相关的代码,放到 schedule.js 里。 另外再增加一个 constants.js 的常量文件,用来存放一些特殊常量。 所以现在就有 6 个文件, index.html 、 index.js 、 react.js 、 react-dom.js 、 schedule.js 、constants.js。 index.html 里需要添加一个 react 挂载的节点。
React.xxx
react.js
react-dom.js
schedule.js
constants.js
index.html
index.js
<body> <div id="root"></div> <script src="./index.js"></script> </body>
index.js 需要导入 React 和 ReactDOM ,然后调用 render 函数进行渲染。
React
ReactDOM
// index.js import React from "./react.js"; import ReactDOM from "./react-dom"; const title = ( <h1 className="title"> <h2>fetaoyuan</h2> <h2>taoweng</h2> </h1> ); ReactDOM.render(title, document.getElementById("root"));
将 createElement 放到 react.js 里,进行简单的改造,并且创建 constants.js 。
createElement
import { ELEMENT_TEXT } from "./constants"; const React = { createElement, }; function createElement(type, config, ...children) { return { type, props: { ...config, children: children.map((child) => { if (typeof child === "object") { return child; } else { return { type: ELEMENT_TEXT, props: { text: child, children: [], }, }; } }), }, }; } export default React;
改造的点主要是针对文本节点,如果是文本节点的时候返回一个跟正常的虚拟 DOM 节点一样的结构,而不是直接返回文本,这样做的目的是为了后面方便统一处理。
tip:react 里并没有做这一步,而是直接返回的文本。
constants.js 里存放着节点的一些类型。
// constants.js // 虚拟DOM 节点类型 export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT'); // Fiber 的类型 export const TAG_ROOT = Symbol.for('TAG_ROOT'); // 根节点 export const TAG_HOST = Symbol.for('TAG_HOST'); // host 节点 export const TAG_TEXT = Symbol.for('TAG_TEXT'); // 文本节点 // effect 类型 export const PLACEMENT = Symbol.for('PLACEMENT'); // 增加元素
react-dom.js 的 render 函数写成这样:
// react-dom.js import { TAG_ROOT } from './constants' import { scheduleRoot } from "./schedule"; function render(element, container) { let rootFiber = { tag: TAG_ROOT, stateNode: container, props: { children: [element] } } scheduleRoot(rootFiber) return rootFiber } export default { render }
新建一个 rootFiber 的 fiber,然后通过 scheduleRoot 进行去调度。schedule.js 目前就是这样:
scheduleRoot
let nextUnitOfWork; // 下一个执行单元 export function scheduleRoot(rootFiber) { nextUnitOfWork = rootFiber } // deadline 是还有多少的空闲时间 function workLoop(deadline) { let shouldYield = false; while(nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitWork(nextUnitOfWork) // 回调函数入参 deadline 可以告诉我们在这个渲染周期还剩多少时间可用 // 剩余时间小于1毫秒就被打断,等待浏览器再次空闲 shouldYield = deadline.timeRemaining() < 1; } requestIdleCallback(workLoop); } function performUnitWork(currentFiber) { // TODO, 执行单元 } requestIdleCallback(workLoop)
scheduleRoot 所要做的事情就是将 nextUnitOfWork 赋值为 rootFiber ,这样 requestIdleCallback 调用的时候 workLoop 里才有值。
rootFiber
requestIdleCallback
workLoop
**performUnitOfWork** 是如何去遍历整棵树的逻辑的函数,同时也会返回下一个要完成的 fiber。Fiber 架构遍历是采用的深度优先遍历,会先遍历子节点,如果子节点没有,再遍历兄弟节点,如果没有兄弟节点,就返回到父节点。
**performUnitOfWork**
TODO:这里应该把 react 如何遍历一棵树的原理讲出来。
所以 performUnitOfWork 的代码如下:
// schedule.js function performUnitWork(currentFiber) { // 把子元素变成子 fiber beginWork(currentFiber) // 如果有子节点就返回以第一个子节点 if(currentFiber.child) { return currentFiber.child } while (currentFiber) { // 没有子节点就代表当前节点已经完成了调和工作, // 就可以结束 fiber 的调和,进入收集副作用的步骤(completeUnitOfWork) completeUnitOfWork(currentFiber); if (currentFiber.sibling) { return currentFiber.sibling; } currentFiber = currentFiber.return; } } // complete的工作就是收集副作用 function completeUnitOfWork(currentFiber) {}
type Fiber = { //标记不同的组件类型 tag: WorkTag, // ReactElement.type,也就是我们调用`createElement`的第一个参数 elementType: any, // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点) stateNode: any, // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回 return: Fiber | null, // 新的变动带来的新的props pendingProps: any, // 上一次渲染完成之后的props memoizedProps: any, // 单链表树结构 // 指向自己的第一个子节点 child: Fiber | null, // 指向自己的兄弟结构 // 兄弟节点的return指向同一个父节点 sibling: Fiber | null, // Effect // 用来记录Side Effect effectTag: SideEffectTag, // 单链表用来快速查找下一个side effect nextEffect: Fiber | null, // 子树中第一个side effect firstEffect: Fiber | null, // 子树中最后一个side effect lastEffect: Fiber | null, }
如果你是了解 fiber 架构的,那么对于 Fiber 是这么一个结构应该不陌生。其中 tag 和 effectTag 放在 constans.js 里,具体的常量的值我这里保持跟 React 里一样,首次更新的也不多,所以 constans.js 增加的常量有:
tag
effectTag
constans.js
// WorkTag export const HostRoot = 3; // 根节点 export const HostComponent = 5; // 一般的 host 节点 export const HostText = 6; // 文本节点 // SideEffectTag export const Placement = 0b00000000010;
将子元素变为 fiber,首先需要判断当前 fiber 的 tag 类型,不同的类型有不同的策略。
function beginWork(currentFiber) { if (currentFiber.tag === HostRoot) { updateHostRoot(currentFiber); } else if (currentFiber.tag === HostText) { updateHostText(currentFiber) } else if(currentFiber.tag === HostComponent) { updateHostComponent(currentFiber); } } function updateHostRoot(currentFiber) {} function updateHostText(currentFiber) {} function updateHostComponent(currentFiber) {}
接下来就是重点了,要实现一个 reconcileChildren 的函数,这个函数理论上就是 diff 的过程,但是由于首次渲染,没有 diff 的过程,就直接创建 fiber 了。 咱们先写根节点的时候的更新方法(updateHostRoot)吧。
reconcileChildren
function updateHostRoot(currentFiber) { // 拿到当前 fiber 的所有子节点,然后将所有子节点变为 fiber const children = currentFiber.props.children reconcileChildren(currentFiber, children) }
接下来实现以下 reconcileChildren 这个函数。
function reconcileChildren(currentFiber, newChildren) { let newChildIndex = 0; // 新虚拟 DOM 数组索引 let prevSibling; // 上一个兄弟节点 // 循环虚拟DOM数组 while(newChildIndex < newChildren.length) { let newChild = newChildren[newChildIndex] // 要根据不同的虚拟 DOM 类型,给到不同的 WorkTag let tag if(newChild.type === ELEMENT_TEXT) { tag = HostText } else if(typeof newChild.type === 'string') { tag = HostComponent } let newFiber = { tag, elementType: newChild.type, stateNode: null, return: currentFiber, pendingProps: newChild.props, effectTag: Placement, // 首次渲染,一定是增加,所以是 Placement } if (newFiber) { // 第一个会被当做父 fiber 的 child,其他的作为 child 的 sibling if (newChildIndex === 0) { currentFiber.child = newFiber; } else { prevSibling.sibling = newFiber; } } prevSibling = newFiber; newChildIndex++ } }
执行完 reconcileChildren 之后,所有的子节点都转化为了 fiber,不过还有一些属性没有添加上去,比如 stateNode 和 nextEffect 。 接下来继续完成 updateHostText 和 updateHostComponent 。这两步需要进行 dom 的操作,所以先创建一个 dom.js 用来存放 dom 相关的操作。
stateNode
nextEffect
updateHostText
updateHostComponent
dom.js
// dom.js // 文本节点直接创建 textNode,host 节点创建 element 之后再进行属性的赋值。 export function createDOM(currentFiber) { if(currentFiber.elementType === ELEMENT_TEXT) { return document.createTextNode(currentFiber.pendingProps.text) } const stateNode = document.createElement(currentFiber.elementType) setProps(stateNode, {}, currentFiber.pendingProps) return stateNode } // 除了 children 属性,其他的都作为 dom 的 Attribute export function setProps(elem, oldProps, newProps) { for (let key in oldProps) { if (key !== "children") { if (newProps.hasOwnProperty(key)) { setProp(elem, key, newProps[key]); } else { elem.removeAttribute(key); } } } for (let key in newProps) { if (key !== "children") { setProp(elem, key, newProps[key]); } } } function setProp(dom, key, value) { if (/^on/.test(key)) { dom[key.toLowerCase()] = value; } else if (key === "style") { if (value) { for (let styleName in value) { if (value.hasOwnProperty(styleName)) { dom.style[styleName] = value[styleName]; } } } } else { dom.setAttribute(key, value); } return dom; }
关于 dom 操作就不多说了,这应该是基础,不算是 react 的核心。updateHostText 和 updateHostComponent 的代码也不复杂,如下:
// schedule.js function updateHostText(currentFiber) { if (!currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点 } } function updateHostComponent(currentFiber) { // 由于 fiber 里面是有 elementType 的, // 所以是可以根据elementType 来创建 dom 节点的, // 那么 stateNode 就可以先创建 if(!currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber) } const children = currentFiber.pendingProps.children reconcileChildren(currentFiber, children) }
到这个时候,fiber list 基本构建完毕,如果在 updateHostRoot 的最后一行打印一下 currentFiber 应该就可以看到整个构建的 fiber 链表。接下来就是完成 effectList 的构建。
updateHostRoot
currentFiber
effect list 是在 completeUnitOfWork 函数里完成的,具体代码如下:
completeUnitOfWork
function completeUnitOfWork(currentFiber) { const returnFiber = currentFiber.return; if (returnFiber) { if (!returnFiber.firstEffect) { returnFiber.firstEffect = currentFiber.firstEffect; } if (!!currentFiber.lastEffect) { if (!!returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber.firstEffect; } returnFiber.lastEffect = currentFiber.lastEffect; } const effectTag = currentFiber.effectTag; if (effectTag) { if (!!returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber; } else { returnFiber.firstEffect = currentFiber; } returnFiber.lastEffect = currentFiber; } } }
构建完 effect list 了就可以开始 commit 了,构建完 effect list 的时机就是没有 nextUnitOfWork 了,就代表已经调和完毕了,到了下一个阶段:commit。
那么在 workLoop 就会有一个判断是否存在下一个执行单元,如果没有就进行提交阶段。
function workLoop(deadline) { let shouldYield = false; while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行一个任务并返回下一个任务 shouldYield = deadline.timeRemaining() < 1;//如果剩余时间小于1毫秒就说明没有时间了,需要把控制权让给浏览器 } //如果没有下一个执行单元了,并且当前渲染树存在,则进行提交阶段 if (!nextUnitOfWork && workInProgressRoot) { commitRoot(); } requestIdleCallback(workLoop); }
我们在提交的时候就要拿到整颗 fiber 链表的头结点,但是之前的 nextUnitOfWork 已经为空了,所以还需要一个变量来存储当前正在渲染的根 fiber,这个 fiber 就是之前学到的 WorkInProgress Tree 。 所以就需要一个变量: workInProgressRoot 的遍历用来存储当前渲染的 fiber 树,并且在 scheduleRoot 的时候把根 fiber 赋值给它
WorkInProgress Tree
workInProgressRoot
let nextUnitOfWork; // 下一个执行单元 let workInProgressRoot; // 当前正在工作的树 export function scheduleRoot(rootFiber) { nextUnitOfWork = rootFiber workInProgressRoot = rootFiber }
所以 commitRoot 就应该是这样:
commitRoot
function commitRoot() { let currentFiber = workInProgressRoot.firstEffect while(currentFiber) { commitWork(currentFiber) currentFiber = currentFiber.nextEffect } workInProgressRoot = null } function commitWork(currentFiber) { if(!currentFiber) { return; } let returnFiber = currentFiber.return; const domReturn = returnFiber.stateNode; if(currentFiber.effectTag === Placement && currentFiber.stateNode != null) { domReturn.append(currentFiber.stateNode) } currentFiber.effectTag = null }
到此,就已经可以渲染出这样的效果了:撒花,结束,接下来将实现元素的更新以及函数式组件,还有 hooks。
demo 代码在这里:https://github.com/crazylxr/luffy/tree/chapter1
珠峰架构公开课
从 0 写一个 React(一)
在阅读这篇文章之前,我希望你已经了解过 React 的 Fiber 架构,如果还不熟悉,请阅读我的这篇:Deep In React 之浅谈 React Fiber 架构(一)。
准备工作
在环境搭建上我选择了 Parcel,因为它使用起来非常的简洁,配置少,使用起来方便。
首先通过 npm 安装 Parcel:
创建一个项目目录并且初始化 package.json 文件:
接下来创建 index.html 和 index.js,在 index.html 里引入 index.js
了解 jsx 并实现虚拟 DOM
jsx 的本质
这样的一段 jsx 代码其实对于浏览器来说是一段不合法的 js 代码,本质上,jsx 是 js 的语法糖,比如上面的这段代码会被 babel 转成如下代码:
可以看出来转化的逻辑大概是这样:
实现 React.createElement
清楚了 babel 的转化逻辑,接下来就来实现以下吧。
babel 配置
首先配置一下 .babelrc:
接下来在 index.js 里写一行代码看是否成功。
然后让项目跑起来:
然后访问 localhost:1234 就可以看到屏幕输出了
前端桃园
了。createElement
我们知道在 React 里,children 是作为 props 里面的一个属性,这根 jsx 转化出来的不一样。知道了 babel 转化 jsx 的规则,我们要实现 createElement 就非常的简单了,只需要利用 ES6 的 rest 参数,就可以非常容易的拿到所有的 children。
接下来在进行调试一下:
输出的结果如下,是符合我们的期望的。
实际上这个输出出来的,通过 createElement 方法返回的对象记录了这个 DOM 节点我们需要的信息,这个对象就被称为虚拟DOM。
初次渲染
在了解 fiber 架构之后,你就应该知道 fiber 是如何工作的,在初次渲染的时候:
第一步生成虚拟 DOM 上面已经完成了,接下来了解如何通过并发模式来生成 Fiber。
并发模式
理想情况下,我们应该把
render
拆成更细分的单元,每完成一个单元的工作,允许浏览器打断渲染响应更高优先级的工作,这个过程称为"并发模式(Concurrent Mode)"。这里用 requestIdleCallback 这个浏览器 API 来实现,这个 API 可以在线程空闲的时候去执行回调函数(执行我们的工作单元)。
大致的代码如下:
全局遍历
nextUnitOfWork
为下一个执行单元,是一个 Fiber 结构。我们要知道架构改为 fiber 的一个大的特征就是将结构改为了链表,链表的遍历就是一个一个的,
performUnitWork
函数就是执行当前的 Fiber,然后返回下一个 Fiber,这样遍历整棵树。但是目前的代码是有问题的,因为没有被打断的逻辑,那咱们再加上被打断的逻辑。
打断的逻辑就在
shouldYield = deadline.timeRemaining() < 1
这行代码里,如果时间片小于 1 毫秒,就被打断,等待浏览器下次空闲的时候再执行。有没有忽然觉得如此高大上的概念(并发模式),其实原理很简单。
合理拆分文件
为了便于理解,现在将文件进行拆分一下,将
React.xxx
的 API 放到react.js
里。另外我们都知道 react 要进行渲染需要有个
render
函数,这个是在 ReactDOM 下面的 API,所以再建一个react-dom.js
用来放render
函数。对于刚才我们所写的并发模式相关的代码,放到
schedule.js
里。另外再增加一个
constants.js
的常量文件,用来存放一些特殊常量。所以现在就有 6 个文件,
index.html
、index.js
、react.js
、react-dom.js
、schedule.js
、constants.js
。index.html 里需要添加一个 react 挂载的节点。
index.js 需要导入
React
和ReactDOM
,然后调用render
函数进行渲染。将
createElement
放到 react.js 里,进行简单的改造,并且创建constants.js
。改造的点主要是针对文本节点,如果是文本节点的时候返回一个跟正常的虚拟 DOM 节点一样的结构,而不是直接返回文本,这样做的目的是为了后面方便统一处理。
constants.js
里存放着节点的一些类型。react-dom.js
的 render 函数写成这样:新建一个 rootFiber 的 fiber,然后通过
scheduleRoot
进行去调度。schedule.js
目前就是这样:scheduleRoot
所要做的事情就是将nextUnitOfWork
赋值为rootFiber
,这样requestIdleCallback
调用的时候workLoop
里才有值。构建 fiber list
遍历整棵树
**performUnitOfWork**
是如何去遍历整棵树的逻辑的函数,同时也会返回下一个要完成的 fiber。Fiber 架构遍历是采用的深度优先遍历,会先遍历子节点,如果子节点没有,再遍历兄弟节点,如果没有兄弟节点,就返回到父节点。
所以 performUnitOfWork 的代码如下:
Fiber 的结构
如果你是了解 fiber 架构的,那么对于 Fiber 是这么一个结构应该不陌生。其中
tag
和effectTag
放在constans.js
里,具体的常量的值我这里保持跟 React 里一样,首次更新的也不多,所以constans.js
增加的常量有:将子元素变为子 Fiber
将子元素变为 fiber,首先需要判断当前 fiber 的 tag 类型,不同的类型有不同的策略。
接下来就是重点了,要实现一个
reconcileChildren
的函数,这个函数理论上就是 diff 的过程,但是由于首次渲染,没有 diff 的过程,就直接创建 fiber 了。咱们先写根节点的时候的更新方法(updateHostRoot)吧。
接下来实现以下
reconcileChildren
这个函数。执行完
reconcileChildren
之后,所有的子节点都转化为了 fiber,不过还有一些属性没有添加上去,比如stateNode
和nextEffect
。接下来继续完成
updateHostText
和updateHostComponent
。这两步需要进行 dom 的操作,所以先创建一个
dom.js
用来存放 dom 相关的操作。关于 dom 操作就不多说了,这应该是基础,不算是 react 的核心。
updateHostText
和updateHostComponent
的代码也不复杂,如下:到这个时候,fiber list 基本构建完毕,如果在
updateHostRoot
的最后一行打印一下currentFiber
应该就可以看到整个构建的 fiber 链表。接下来就是完成 effectList 的构建。
构建 effect list
effect list 是在
completeUnitOfWork
函数里完成的,具体代码如下:commit effect list
构建完 effect list 了就可以开始 commit 了,构建完 effect list 的时机就是没有 nextUnitOfWork 了,就代表已经调和完毕了,到了下一个阶段:commit。
那么在
workLoop
就会有一个判断是否存在下一个执行单元,如果没有就进行提交阶段。我们在提交的时候就要拿到整颗 fiber 链表的头结点,但是之前的
nextUnitOfWork
已经为空了,所以还需要一个变量来存储当前正在渲染的根 fiber,这个 fiber 就是之前学到的WorkInProgress Tree
。所以就需要一个变量:
workInProgressRoot
的遍历用来存储当前渲染的 fiber 树,并且在scheduleRoot
的时候把根 fiber 赋值给它所以
commitRoot
就应该是这样:到此,就已经可以渲染出这样的效果了:
撒花,结束,接下来将实现元素的更新以及函数式组件,还有 hooks。
demo 代码在这里:https://github.com/crazylxr/luffy/tree/chapter1
参考资料
珠峰架构公开课