Closed wbccb closed 1 year ago
本文参考自https://pomb.us/build-your-own-react/, 基于React 16.8版本
一步一步从头开始重写React,构建一个简单的React库
我们将使用miniReact代替React,比如React.createElement()会替换为miniReact.createElement()
miniReact
React
React.createElement()
miniReact.createElement()
下面是React最基础的使用
const element = <h1 title="foo">Hello</h1> const container = document.getElementById("root") ReactDOM.render(element, container)
上面的<h1 title="foo">Hello</h1>不是原生的JS代码,而是JSX,还需要通过Babel等构建工具转化为原生的JS
<h1 title="foo">Hello</h1>
JSX
Babel
转换规则也很简单,就是通过createElement()替换标签内的代码,将标签名称、props、children作为参数传入createElement()
createElement()
// 最终经过babel等工具,转化jsx为React.createElement的格式 const element = React.createElement( "h1", {title: "foo"}, "Hello" ) const container = document.getElementById("root") ReactDOM.render(element, container)
上面使用React等同于下面直接使用原生JS创建元素的形式
这代表后面我们的createElement()和render()的内容本质就是下面这些原生代码
const element = { type: "h1", props: { title: "foo", children: "Hello", }, } const container = document.getElementById("root") const node = document.createElement(element.type) node["title"] = element.props.title const text = document.createTextNode("") text["nodeValue"] = element.props.children node.appendChild(text) container.appendChild(node)
const element = ( <div id="foo"> <a>bar</a> <b/> </div> )
我们使用上面的示例,最终会被转化为createElement()
const element = miniReact.createElement( "div", {id: "foo"}, React.createElement("a", null, "bar"), React.createElement("b") )
createElement()要返回什么内容呢?
根据上面代码块,我们知道,createElement()要返回一个对象数据element,它至少包括type和props, 然后我们才能根据返回的对象调用对应的document.createElement() 创建对应的DOM数据
element
type
props
document.createElement()
DOM
而一些原始值的node,比如const text = document.createTextNode("")这种没有具体的类型,我们创建一个TEXT_ELEMENT进行赋值
const text = document.createTextNode("")
TEXT_ELEMENT
const miniReact = { createTextElement(text) { return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [] } } }, createElement(type, props, ...children) { const newChildren = children.map(child => typeof child === "object" ? child : this.createTextElement(child)); return { type, props: { ...props, children: newChildren } } } };
在上面1.1 createElement()的分析中,我们重写了React.createElement(),接下来我们进行ReactDOM.render()的重写
1.1 createElement()
ReactDOM.render()
实现render函数,本质就是
render
document.createElement
dom[name]=element.props[name]
props.children.forEach(()=>render())
container.appendChild(dom)
render(element, container) { const dom = element.type === ELEMENT_TYPE.TEXT_ELEMENT ? document.createTextNode("") : document.createElement(element.type); const isProperty = key => key !== "children"; Object.keys(element.props) .filter(isProperty) .forEach(name => { dom[name] = element.props[name]; }); element.props.children.forEach(child => { this.render(child, dom); }); container.appendChild(dom); }
在上面我们的render()函数中,我们递归进行DOM的创建,如果DOM的数量很多,可能会堵塞主线程的运行
render()
因此我们需要一种机制: 要把渲染工作分解成小的单元,当我们完成每个渲染单元后,如果还有其他优先级比较高的事情(比如动画和输入响应),我们会让浏览器中断渲染,等待空闲时,再继续渲染单元工作的执行
浏览器一帧会经过下面这几个过程:
window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。
这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
函数一般会按先进先调用的顺序执行,然而,如果requestIdleCallback指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
requestIdleCallback是有空闲时间才会执行,但是如果制定了timeout,如果到达限定时间还没执行,那么就会超时,强行执行任务,虽然它可能会造成用户操作卡顿以及打乱顺序等情况
但是RequestIdelCallback并不是每一帧都会执行,而是在每一个帧做完上面列举的6个步骤之后如果还有空闲时间才会执行,如果没有剩余时间,则拖入下一帧考虑。
而且RequestIdelCallback如果执行时间过长,长时间不将控制权交还给浏览器,则会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时
那我们怎么知道浏览器某一帧还有多少剩余时间呢?
requestIdleCallback((deadline) => { // deadline 有两个参数 // deadline.timeRemaining(): 当前帧还剩下多少时间,最大值50ms // deadline.didTimeout: 是否超时,即整个callback是否超时才触发执行的 // 另外 requestIdleCallback 后如果跟上第二个参数 {timeout: ...} 表示强制浏览器回调在timeout毫秒过后还没有被调用,那么回调任务将放入事件循环中排队 if (deadline.timeRemaining() > 0) { // TODO } else { requestIdleCallback(otherTasks); } }, {timeout: 1000});
虽然React已经不使用requestIdleCallback进行并发的控制,它自己内部实现了一个scheduler package,但是原理跟requestIdleCallback是一样的,在miniReact 中我们使用requestIdleCallback进行并发渲染的控制
requestIdleCallback
主要流程为:
requestIdleCallback(workLoop)
workLoop()
performUnitOfWork()
let nextUnitOfWork = null; function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop); function performUnitOfWork(nextUnitOfWork) { // TODO }
为了实现上面以unit为单位的工作任务,我们需要一种数据结构:一棵fiber树
每一个元素都是一个fiber,而每一个fiber都是一个unit单位的工作量
比如我们渲染
miniReact.render( <div> <h1> <p/> <a/> </h1> <h2/> </div>, container )
上面元素对应的fiber tree就是
fiber tree
然后我们就可以改造render()和``
function render(element, container) { nextUnitOfWork = { dom: container, props: { children: [element], }, } } // 上面的render()改造为下面代码 function render(element, container) { miniReact.nextUnitOfWork = { dom: container, props: { children: [element] } } }
然后我们直接调用requestIdleCallback()进行workLoop()的执行
requestIdleCallback()
// 调用requestIdleCallback等待浏览器有空闲时间再执行 requestIdleCallback(miniReact.workLoop);
融合上面的代码,如下面代码块,就是
miniReact.render
nextUnitOfWork
const miniReact = { nextUnitOfWork: null, workLoop(deadline) { // 检测当前浏览器剩余时间是否能够执行一个unit的任务 // 如果不能,则触发requestIdleCallback()等待浏览器的下一个空闲时间 let shouldYield = false; while (this.nextUnitOfWork && !shouldYield) { // 剩余时间足够的前提下,执行performUnitOfWork()执行一个unit的任务 this.nextUnitOfWork = this.performUnitOfWork(this.nextUnitOfWork); // 剩余时间足够的话:shouldYield=false shouldYield = deadline.timeRemaining() < 1; } // 如果剩余时间不够了,则调用requestIdleCallback等待浏览器有空闲时间再执行 requestIdleCallback(this.workLoop); }, render(element, container) { miniReact.nextUnitOfWork = { dom: container, props: { children: [element] } } } } const element = miniReact.createElement( "div", {id: "foo"}, miniReact.createElement("a", null, "bar"), miniReact.createElement("b") ) const container = document.getElementById("root") miniReact.render(element, container); // 调用requestIdleCallback等待浏览器有空闲时间再执行 requestIdleCallback(miniReact.workLoop);
而performUnitOfWork()方法的内容也非常明确,就是
parent.dom.appendChild
performUnitOfWork(fiber) { // 执行每一个unit的任务: if (!fiber.dom) { fiber.dom = this.createDom(fiber); } if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom); } // 新的fiber先寻找它的children数据 const elements = fiber.props.children; let index = 0; let prevSibling = null; while (index < elements.length) { const element = elements[i]; const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null } if (index === 0) { fiber.child = newFiber; } else { // 此时prevSibling是newFiber的左边元素 prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; } if (fiber.child) { return fiber.child; } let nextFiber = fiber; while (nextFiber) { // 新的fiber先寻找它的sibling数据 if (nextFiber.sibling) { return nextFiber.sibling; } // 没有children,没有sibling,则直接找它的parent nextFiber = nextFiber.parent; } }
在上面performUnitOfWork()的分析中,我们每次执行一个fiber任务都会进行DOM的添加,在需要创建DOM很多的情况下,需要多次浏览器帧才能完成所有的绘制任务,这会导致用户看到绘制不完整的DOM情况
因此我们最好的做法是在每一次performUnitOfWork()中不进行DOM的添加,等到最终任务都完成了,我们再进行DOM的添加
performUnitOfWork(fiber) { // 执行每一个unit的任务: if (!fiber.dom) { fiber.dom = this.createDom(fiber); } // if(fiber.parent) { // fiber.parent.dom.appendChild(fiber.dom); // } // 新的fiber先寻找它的children数据 const elements = fiber.props.children; let index = 0; let prevSibling = null; while (index < elements.length) { const element = elements[i]; const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null } if (index === 0) { fiber.child = newFiber; } else { // 此时prevSibling是newFiber的左边元素 prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; } if (fiber.child) { return fiber.child; } let nextFiber = fiber; while (nextFiber) { // 新的fiber先寻找它的sibling数据 if (nextFiber.sibling) { return nextFiber.sibling; } // 没有children,没有sibling,则直接找它的parent nextFiber = nextFiber.parent; } }
因此我们需要创建新的变量指向Root节点,然后所有任务完成后,再进行DOM的添加
Root
const miniReact = { wipRoot: null, render(element, container) { // 本来render()是进行DOM的创建!现在改为nextUnitOfWork的赋值 // DOM的详细创建方法调用放在performUnitOfWork()中 // DOM的详细创建方法放在createDOM()中 this.wipRoot = { dom: container, props: { children: [element] } } this.nextUnitOfWork = this.wipRoot; }, commitRoot() { this.commitWork(this.wipRoot.child); this.wipRoot = null; }, commitWork(fiber) { if (!fiber) return; const domParent = fiber.parent.dom; domParent.appendChild(fiber.dom); this.commitWork(fiber.child); this.commitRoot(fiber.sibling); }, workLoop(deadline) { // ... // 检测当前浏览器剩余时间是否能够执行一个unit的任务 // 如果不能,则触发requestIdleCallback()等待浏览器的下一个空闲时间 if (!this.nextUnitOfWork && this.wipRoot) { commitRoot(); } // ... // 如果剩余时间不够了,则调用requestIdleCallback等待浏览器有空闲时间再执行 }, }
在上面的分析中,我们实现了初次渲染逻辑,接下来我们要实现渲染更新时的逻辑代码
每次渲染更新时,我们需要比对两次节点有何不同,因此我们需要使用新的变量alternate保留上一次的fiber tree
alternate
在这个环节中,我们需要实现
commitWork()
const miniReact = { deletions: [], // 还是按照child->sibling的顺序寻找新的fiber,只是会检测能否复用之前的DOM reconcileChildren(wipFiber, elements) { let index = 0; let oldFiber = wipFiber.alternate && wipFiber.alternate.child; let prevSibling = null; while (index < elements.length) { const element = elements[i]; const sameType = oldFiber && element && element.type === oldFiber.type; let newFiber = null; if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, efectTag: "UPDATE" } } else if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT" } } else if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION"; this.deletions.push(oldFiber); } if (index === 0) { wipFiber.child = newFiber; } else { // 此时prevSibling是newFiber的左边元素 prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; } }, performUnitOfWork(fiber) { // 执行每一个unit的任务: //... const elements = fiber.props.children; // 区分: // 1.哪些能够复用 // 2.哪些要删除 // 3.哪些要重新创建 this.reconcileChildren(fiber, elements); //... } }
const miniReact = { commitWork(fiber) { if (!fiber) return; const domParent = fiber.parent.dom; // 以前这里只有新增的逻辑,现在我们要完善更新和删除逻辑 if (fiber.effectTag === "PLACEMENT" && fiber.dom !== null) { // 新增逻辑 domParent.appendChild(fiber.dom); } else if (fiber.effectTag === "PLACEMENT" && fiber.dom !== null) { this.updateDom( fiber.dom, fiber.alternate.props, fiber.props ); } else if (fiber.effectTag === "DELETION") { domParent.removeChild(fiber.dom); } this.commitWork(fiber.child); this.commitRoot(fiber.sibling); }, updateDom(dom, prevProps, nextProps) { const isProperty = key => key !== "children"; const isGone = (prev, next) => key => !(key in next); const isAddOrUpdate = (prev, next) => key => prev[key] !== next[key]; // 删除旧的props Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = ""; }); // 赋值新的props Object.keys(nextProps) .filter(isProperty) .filter(isAddOrUpdate(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name]; }); } }
const miniReact = { updateDom(dom, prevProps, nextProps) { const isProperty = key => key !== "children"; const isGone = (prev, next) => key => !(key in next); const isAddOrUpdate = (prev, next) => key => prev[key] !== next[key]; const isEvent = key => key.startsWith("on"); // 特殊处理事件on Object.keys(prevProps) .filter(isEvent) .filter((key) => { return !(key in nextProps) || isAddOrUpdate(prevProps, nextProps) }) .forEach(name => { // name=onClick onTouch等等 const eventType = name.toLowerCase().substring(2); dom.removeEventListener(eventType, prevProps[name]); }); //... // 特殊处理事件on Object.keys(nextProps) .filter(isEvent) .filter(isAddOrUpdate(prevProps, nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }) //... } }
在React中,还有一种function components,我们需要对这种类型的组件进行支持
function components
function App(props) { return miniReact.createElement( "h1", null, "Hi ", props.name ) }
上面的代码经过babel等工具转化之后代码为:
babel
const element = miniReact.createElement(App, { name: "foo", })
function components有两个比较特殊的地方:
因此我们需要检测是否是function类型的组件,然后另外进行处理
function
function performUnitOfWork(fiber) { // 执行每一个unit的任务: const isFunctionComponent = fiber.type instanceof Function; if (isFunctionComponent) { this.updateFunctionComponent(fiber); } else { this.updateHostComponent(fiber); } }
如果是function components,直接通过执行方法获取对应的children
children
function updateFunctionComponent(fiber) { const children = [fiber.type(fiber.props)]; this.reconcileChildren(fiber, elements); }
如果不是function components,则还是手动创建DOM
function updateHostComponent(fiber) { if (!fiber.dom) { fiber.dom = this.createDom(fiber); } const elements = fiber.props.children; // 区分哪些能够复用哪些要删除哪些要新增 this.reconcileChildren(fiber, elements); }
function commitWork(fiber) { if (!fiber) return; let domParentFiber = fiber.parent; while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent; } const domParent = domParentFiber.dom; //... }
function commitDelete(fiber, domParent) { if (fiber.dom) { domParent.removeChild(fiber.dom); } else { this.commitDelete(fiber.child, domParent); } }
React还支持hooks的使用,比如useState()
hooks
useState()
如下面代码块所示: useState放在function Component中,hooks是跟fiber进行绑定,考虑最简单情况 只有一个useXXX,只有一个function Component,因此只需要一个wipFiber
useState
function Component
fiber
useXXX
wipFiber
function Counter() { const [state, setState] = miniReact.useState(1) return ( <h1 onClick={() => setState(c => c + 1)}> Count: {state} </h1> ) }
在function Component中初始化wipFiber和hookIndex
hookIndex
function updateFunctionComponent(fiber) { // useState放在Component中,hooks是跟fiber进行绑定,考虑最简单情况 // 只有一个useXXX,只有一个function Component,因此只需要一个wipFiber this.wipFiber = fiber; this.hookIndex = 0; this.wipFiber.hooks = []; const children = [fiber.type(fiber.props)]; this.reconcileChildren(fiber, elements); }
// TODO ???这一块怎么理解呢?得看看useHook的源码后才能明白它的做法 在useState()中进行hook的值初始化,然后
hook
function useState(initial) { // 是否之前就存在该hook const oldHook = this.wipFiber.alternate && this.wipFiber.alternate.hooks && this.wipFiber.alternate.hooks[this.hookIndex]; const hook = { state: oldHook ? oldHook.state : initial, queue: [] } const setState = (action) => { hook.queue.push(action); // 模仿render()函数 this.wipRoot = { dom: this.currentRoot.dom, props: this.currentRoot.props, alternate: this.currentRoot } this.nextUnitOfWork = this.wipRoot; this.deletions = []; } this.wipFiber.hooks.push(hook); this.hookIndex++; return [hook.state, setState]; }
构建React框架
我们将使用
miniReact
代替React
,比如React.createElement()
会替换为miniReact.createElement()
1.createElement() & render()
下面是
React
最基础的使用上面的
<h1 title="foo">Hello</h1>
不是原生的JS代码,而是JSX
,还需要通过Babel
等构建工具转化为原生的JS上面使用
React
等同于下面直接使用原生JS创建元素的形式1.1 createElement()
我们使用上面的示例,最终会被转化为
createElement()
根据上面代码块,我们知道,
createElement()
要返回一个对象数据element
,它至少包括type
和props
, 然后我们才能根据返回的对象调用对应的document.createElement()
创建对应的DOM
数据而一些原始值的node,比如
const text = document.createTextNode("")
这种没有具体的类型,我们创建一个TEXT_ELEMENT
进行赋值1.2 render()
在上面
1.1 createElement()
的分析中,我们重写了React.createElement()
,接下来我们进行ReactDOM.render()
的重写实现
render
函数,本质就是document.createElement
dom[name]=element.props[name]
props.children.forEach(()=>render())
container.appendChild(dom)
2.Concurrent Mode
在上面我们的
render()
函数中,我们递归进行DOM
的创建,如果DOM
的数量很多,可能会堵塞主线程的运行因此我们需要一种机制: 要把渲染工作分解成小的单元,当我们完成每个渲染单元后,如果还有其他优先级比较高的事情(比如动画和输入响应),我们会让浏览器中断渲染,等待空闲时,再继续渲染单元工作的执行
2.1 requestIdleCallback
浏览器一帧会经过下面这几个过程:
window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。
这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
函数一般会按先进先调用的顺序执行,然而,如果requestIdleCallback指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
但是RequestIdelCallback并不是每一帧都会执行,而是在每一个帧做完上面列举的6个步骤之后如果还有空闲时间才会执行,如果没有剩余时间,则拖入下一帧考虑。
而且RequestIdelCallback如果执行时间过长,长时间不将控制权交还给浏览器,则会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时
2.2 实现逻辑
虽然
React
已经不使用requestIdleCallback
进行并发的控制,它自己内部实现了一个scheduler package,但是原理跟requestIdleCallback
是一样的,在miniReact
中我们使用requestIdleCallback
进行并发渲染的控制主要流程为:
requestIdleCallback(workLoop)
等待浏览器有空闲时间workLoop()
中,主要检测浏览器这一帧是否有剩余时间,如果有剩余时间,则触发任务执行,如果没有,则再次使用requestIdleCallback(workLoop)
等待浏览器有空闲时间performUnitOfWork()
中执行每一个unit任务,然后返回下一个unit任务3. Fibers
为了实现上面以unit为单位的工作任务,我们需要一种数据结构:一棵fiber树
每一个元素都是一个fiber,而每一个fiber都是一个unit单位的工作量
比如我们渲染
上面元素对应的
fiber tree
就是然后我们就可以改造
render()
和``然后我们直接调用
requestIdleCallback()
进行workLoop()
的执行融合上面的代码,如下面代码块,就是
miniReact.render
赋值nextUnitOfWorkrequestIdleCallback()
监听浏览器空闲时间开始workLoop()
检测当前浏览器剩余时间是否能够执行一个unit的任务,如果可以,则使用performUnitOfWork()
处理nextUnitOfWork
而
performUnitOfWork()
方法的内容也非常明确,就是parent.dom.appendChild
新的DOM4.Render and Commit Phases
在上面
performUnitOfWork()
的分析中,我们每次执行一个fiber任务都会进行DOM的添加,在需要创建DOM很多的情况下,需要多次浏览器帧才能完成所有的绘制任务,这会导致用户看到绘制不完整的DOM情况因此我们最好的做法是在每一次
performUnitOfWork()
中不进行DOM的添加,等到最终任务都完成了,我们再进行DOM的添加因此我们需要创建新的变量指向
Root
节点,然后所有任务完成后,再进行DOM的添加5.Reconciliation
在上面的分析中,我们实现了初次渲染逻辑,接下来我们要实现渲染更新时的逻辑代码
每次渲染更新时,我们需要比对两次节点有何不同,因此我们需要使用新的变量
alternate
保留上一次的fiber tree
在这个环节中,我们需要实现
performUnitOfWork()
中寻找新的fiber时检测旧的fiber能否复用的逻辑commitWork()
根据不同类型,新增/替换/删除进行对应的DOM操作commitWork()
处理事件类型的增加和删除逻辑5.1 检测旧的fiber能否复用
5.2 根据不同类型(新增/替换/删除)进行对应的DOM操作
5.3 处理特殊props(事件类型)的增加和删除逻辑
6. Function Component
在
React
中,还有一种function components
,我们需要对这种类型的组件进行支持上面的代码经过
babel
等工具转化之后代码为:function components
有两个比较特殊的地方:6.1 获取children元素
因此我们需要检测是否是
function
类型的组件,然后另外进行处理如果是
function components
,直接通过执行方法获取对应的children
如果不是
function components
,则还是手动创建DOM6.2 commitWork()获取fiber的parent必须拥有dom
6.3 commitWork()删除操作时,fiber必须拥有dom
7. Hooks
React
还支持hooks
的使用,比如useState()
如下面代码块所示:
useState
放在function Component
中,hooks
是跟fiber
进行绑定,考虑最简单情况 只有一个useXXX
,只有一个function Component
,因此只需要一个wipFiber
在
function Component
中初始化wipFiber
和hookIndex
// TODO ???这一块怎么理解呢?得看看useHook的源码后才能明白它的做法 在
useState()
中进行hook
的值初始化,然后