Build your own React
Build your own React 的学习笔记
预览
Build your own React CN
前言
重写React, 遵循React代码中的架构, 然而没有进行优化。基于React16.8, 应用hook并删除了所有与类相干的代码。
零: review
首先回顾一些React的概念,上面是一个简略的React应用程序。一共三行代码,第一行定义了一个React元素, 第二行获取了DOM节点, 最初一行将React元素渲染到容器中。
const element = <h1 title="foo">Hello</h1>const container = document.getElementById("root")ReactDOM.render(element, container)
第一行中,咱们应用了JSX, JSX不是无效的JavaScript,咱们应用原生js替换它。通常通过Babel等构建工具,JSX转换为JS。应用createElement
替换JSX标记,并将标签名,props,子级作为参数。
const element = React.createElement( "h1", { title: "foo" }, "Hello");
React.createElement
, 会依据参数创立一个对象。除了一些验证外,这就是React.createElement
所做的全副。咱们能够间接React.createElement
函数替换成它的输入。
const element = { type: "h1", props: { title: "foo", children: "Hello", },}
一个一般的JavaScript对象, 次要有两个属性type
和props
。type
属性是一个字符串,示意咱们创立的DOM节点的类型。它也能够是一个函数,然而咱们留在前面说。props
是一个对象, props
中有一个非凡的属性children
。在以后的状况children
是字符串,然而通常状况下它是蕴含更多元素的数组。接下来咱们须要替换ReactDOM.render
。
首先应用type
属性,创立一个节点。咱们将element
的所有props
调配给该节点,目前只有title
属性。而后咱们为子节点创立节点。咱们的children
是一个字符串,因而咱们创立一个文本节点。
为什么应用createTextNode
而不是innerText
呢?因为在之后都会以雷同的形式解决所有元素。
最初将textNode增加到h1中,h1增加到container中。
const element = { type: "h1", props: { title: "foo", children: "Hello", },}const container = document.getElementById("root")const node = document.createElement(element.type)node["title"] = element.props.titleconst text = document.createTextNode("")text["nodeValue"] = element.props.childrennode.appendChild(text)container.appendChild(node)
目前咱们领有了和之前一样的程序,然而没有应用React。
一: createElement
咱们从一个新的程序开始,这次咱们应用本人的React替换原来的React代码。
const element = ( <div id="foo"> <a>bar</a> <b /> </div>)const container = document.getElementById("root")ReactDOM.render(element, container)
咱们从编写本人的createElement
开始。
const element = createElement( "div", { id: "foo" }, createElement("a", null, "bar"), createElement("b"))const container = document.getElementById("root")render(element, container)
createElement
须要做的就是创立一个type
和props
的对象。createElement
函数中, children
参数应用rest
运算符, children
始终就会为数组。
function createElement(type, props, ...children) { return { type, props: { ...props, children, }, }};
例如, createElement("div", null, a, b)
会返回:
{ "type": "div", "props": { "children": [a, b] }}
目前children
数组中会蕴含原始值,比方字符串和数字。咱们须要对它们进行包装。咱们创立一个非凡的类型TEXT_ELEMENT
。
在React源码中,不会包装原始值, 或者在没有子级的状况下创立空的数组。咱们这样做的目标是为了简化咱们的代码.
function createTextElement(text) { return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [], }, }}function createElement(type, props, ...children) { return { type, props: { ...props, children: children.map(child => typeof child === "object" ? child : createTextElement(child) ), }, }}
咱们如何让Babel
在编译的过程中,应用咱们本人创立的createElement
呢?咱们在配置babel
的@babel/preset-react
插件时自定义pragma
参数
二: render
接下来咱们须要编写本人的ReactDOM.render
。
目前咱们只关怀向DOM中增加内容,稍后解决更新和删除
咱们首先应用元素的类型创立DOM
节点,而后将新节点增加到容器中
function render(element, container) { const dom = document.createElement(element.type) container.appendChild(dom)}
咱们须要递归的为每一个children
元素做雷同的事件
function render(element, container) { const dom = document.createElement(element.type) element.props.children.forEach(child => render(child, dom) ) container.appendChild(dom)}
之前增加了文本元素的节点,所以在创立节点时须要判断元素的类型
function render(element, container) { const dom = element.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type) element.props.children.forEach(child => render(child, dom) ) container.appendChild(dom)}
最初咱们须要将元素的props增加到节点的属性上
function render(element, container) { const dom = 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 => render(child, dom) ) container.appendChild(dom)}
目前为止,咱们曾经有了一个将JSX出现到DOM的库。
三: 并发模式
在这之前,咱们须要重构代码。
递归渲染存在问题,一旦开始渲染就无奈进行,直到咱们渲染实现整个树。如果树很大,会阻塞主线程过长的工夫。
????️: React Fiber架构应用了链表树实现了可中断渲染,如果大家有趣味能够参考这篇文章
因而咱们须要把工作分解成几个小单元,在咱们实现每个单元后,有重要的事件要做,咱们中断渲染。
咱们应用requestIdleCallback
实现循环, 浏览器会在闲暇时,执行requestIdleCallback
的回调。React的外部并不应用requestIdleCallback
, React外部应用scheduler package, 通过requestIdleCallback
咱们还能够取得咱们还有多少可用工夫用于渲染。
????️: 对于requestIdleCallback的更多细节能够查看这篇文章,详解 requestIdleCallback
let nextUnitOfWork = nullfunction workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop)}requestIdleCallback(workLoop)function performUnitOfWork(nextUnitOfWork) { // TODO}
????️: nextUnitOfWork变量放弃了Fiber中须要工作节点援用或者为null, 如果是null示意没有工作。
要开始咱们的workLoop
, 咱们须要第一个工作单元(Fiber节点),而后编写performUnitOfWork
函数,performUnitOfWork
函数执行工作,并返回下一个须要工作的节点。
四: Fibers
咱们须要一个数据结构Fiber树(链表树)。每一个元素都有对应的Fiber节点, 每一个Fiber是一个工作单元。
假如咱们须要渲染这样的一颗树:
render( <div> <h1> <p /> <a /> </h1> <h2 /> </div>, container)
在render
中,创立Fiber,并将根节点的Fiber调配给nextUnitOfWork
变量。余下的工作在performUnitOfWork
函数进行,须要做三件事:
- 将元素增加到DOM
- 为子节创立Fiber
- 返回下一个工作单元
Fiber树是一个链表树,每一个Fiber节点有child
, parent
, sibling
属性
child
, 第一个子级的援用sibling
, 第一个同级的援用parent
, 父级的援用
????️: 在React的Fiber节点中,应用return
字段保留了对父Fiber节点的援用
遍历Fiber树(链表树)时应用了深度优先遍历,说一下遍历的过程:
- 从根节点root获取第一个子节点
- 如果root有子节点,将以后指针设置为第一个子节点,并进入下一次迭代。(深度优先遍历)
- 如果root的第一个子节点,没有子节点,则尝试获取它的第一个兄弟节点。
- 如果有兄弟节点,将以后指针设置为第一个子节点,而后兄弟节点进入深度优先遍历。
- 如果没有兄弟节点,则返回根节点root。尝试获取父节点的兄弟节点。
- 如果父节点没有兄弟节点,则返回根节点root。最初完结遍历。
好,接下来咱们开始增加代码, 将创立的DOM的代码独自抽离出, 稍后应用它
function createDom(fiber) { const dom = fiber.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] }) return dom}
在render
函数中,将nextUnitOfWork
变量设置为Fiber节点树的根
function render(element, container) { nextUnitOfWork = { dom: container, props: { children: [element], }, }}
当浏览器准备就绪,调用workLoop,开始解决根节点
let nextUnitOfWork = nullfunction workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop)}requestIdleCallback(workLoop)function performUnitOfWork(fiber) { // 增加DOM节点 // 创立Fiber // 获取下一个解决工作的Fiber节点}
首先创立DOM, 并增加到Fiber节点的dom
字段中,咱们在dom
字段中保留对dom
的援用
function performUnitOfWork(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber) } if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom) }}
????️: 在React的Fiber节点中,stateNode
字段,保留对class组件实例的援用, DOM节点或其余与Fiber节点相关联的React元素类实例的援用。
接下来为每一个子元素创立Fiber节点。同时因为Fiber树是一个链表树,所以咱们须要为Fiber节点增加child
, parent
, sibling
字段
function performUnitOfWork(nextUnitOfWork) { if (!fiber.dom) { fiber.dom = createDom(fiber) } if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom) } const elements = fiber.props.children let index = 0 let prevSibling = null while (index < elements.length) { const element = elements[index] const newFiber = { type: element.type, props: element.props, parent: fiber, // 父Fiber节点的援用 dom: null, } if (index === 0) { // 父Fiber节点增加child字段 fiber.child = newFiber } else { // 同级的Fiber节点增加sibling字段 prevSibling.sibling = newFiber } prevSibling = newFiber index++ }}
在实现的以后节点的工作后,咱们须要返回下一个节点。因为是深度优先遍历,首先尝试遍历child
,而后是sibling
, 最初回溯到parent
, 尝试遍历parent
的sibling
function performUnitOfWork(nextUnitOfWork) { if (!fiber.dom) { fiber.dom = createDom(fiber) } if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom) } const elements = fiber.props.children let index = 0 let prevSibling = null while (index < elements.length) { const element = elements[index] const newFiber = { type: element.type, props: element.props, parent: fiber, // 父节点的援用 dom: null, } if (index === 0) { // 父Fiber节点增加child字段 fiber.child = newFiber } else { // 同级的Fiber节点增加sibling字段 prevSibling.sibling = newFiber } prevSibling = newFiber index++ } // 首先尝试子节点 if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { // 尝试同级节点 if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent }}
五: render 和 commit
目前存在的问题,在遍历Fiber树的时候,咱们目前会在这里向DOM中增加新节点,因为咱们应用requestIdleCallback
, 浏览器可能会中断咱们的渲染,用户会看到不残缺的UI。这违反了一致性的准则。
????️: React的外围准则之一是"一致性", 它总是一次性更新DOM, 不会显示局部后果。????️: 在React的源码中, React分为两个阶段执行工作,
render
阶段和commit
阶段。render
阶段的工作是能够异步执行的,React依据可用工夫解决一个或者多个Fiber节点。当产生一些更重要的事件时,React会进行并保留已实现的工作。等重要的事件解决实现后,React从中断处持续实现工作。然而有时可能会放弃曾经实现的工作,从顶层从新开始。此阶段执行的工作是对用户是不可见的,因而能够实现暂停。然而在commit
阶段始终是同步的它会产生用户可见的变动, 例如DOM的批改. 这就是React须要一次性实现它们的起因。
咱们须要删除performUnitOfWork
函数中更改DOM的代码。
function performUnitOfWork(nextUnitOfWork) { if (!fiber.dom) { fiber.dom = createDom(fiber) } const elements = fiber.props.children // ...
咱们须要保留Fiber树根的援用, 咱们称其为正在工作的root或wipRoot
。
????️: 在React中Fiber树的根被称为HostRoot
。咱们能够在通过容器的DOM节点获取,容器DOM._reactRootContainer._internalRoot.current
。????️:
wipRoot
相似React源码中workInProgress tree
的根节点,在React应用程序中,咱们能够通过容器DOM._reactRootContainer._internalRoot.current.alternate
, 获取workInProgress tree
的根节点。
let wipRoot = nullfunction render(element, container) { wipRoot = { dom: container, props: { children: [element], }, } nextUnitOfWork = wipRoot}
实现了所有的工作。咱们须要把整个Fiber树更新到DOM上。咱们须要在commitRoot
函数中实现这个性能。
function commitWork(fiber) { if (!fiber) { return } const domParent = fiber.parent.dom domParent.appendChild(fiber.dom) // 递归子节点 commitWork(fiber.child) commitWork(fiber.sibling)}function commitRoot() { commitWork(wipRoot.child) wipRoot = null}function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } // 如果nextUnitOfWork为假, 阐明所有的工作都曾经做完了, 咱们须要进入commit阶段 if (!nextUnitOfWork && wipRoot) { // 增加dom commitRoot() }}
????️: 在React的源码中commit
阶段从completeRoot
函数开始,在开始任何工作前,它将FiberRoot
的finishedWork
属性设置为null。
六: 协调
目前为止,咱们仅仅向DOM中增加了内容,然而更新和删除呢?咱们须要将render函数接管到元素和提交到DOM上的最初的Fiber树进行比照。
因而在commit
咱们须要保留最初的Fiber树的援用,咱们称之为currentRoot
。咱们还将alternate
字段增加到每一个Fiber节点上,alternate
字段上保留了currentRoot
的援用。
????️: 在React源码中,在第一次渲染实现后,React会生成一个Fiber树。该树映射了应用程序的状态,这颗树被称为current tree
。当应用程序开始更新时,React会构建一个workInProgress tree
,workInProgress tree
映射了将来的状态。????️: 所有的工作都是在
workInProgress tree
上的Fiber上进行的。当React开始遍历Fiber时,它会为每一个现有的Fiber节点创立一个备份, 在alternate
字段中,备份形成了workInProgress tree
。
let nextUnitOfWork = nulllet wipRoot = nulllet currentRoot = nullfunction commitWork(fiber) { if (!fiber) { return } const domParent = fiber.parent.dom domParent.appendChild(fiber.dom) // 递归子节点 commitWork(fiber.child) commitWork(fiber.sibling)}function commitRoot() { commitWork(wipRoot.child) // 保留最近一次输入到页面上的Fiber树 currentRoot = wipRoot wipRoot = null}function render(element, container) { wipRoot = { dom: container, props: { children: [element], }, alternate: currentRoot, } nextUnitOfWork = wipRoot}
接下来咱们须要从performUnitOfWork
函数中将创立Fiber的代码提取进去,一个新的reconcileChildren
函数。在这里咱们将对currentRoot
(当前页面对应的Fiber树)与新元素进行协调。
function reconcileChildren(wipFiber, elements) { let index = 0 let prevSibling = null while (index < elements.length) { const element = elements[index] const newFiber = { type: element.type, props: element.props, parent: fiber, // 父节点的援用 dom: null, } if (index === 0) { // 父Fiber节点增加child字段,child指向了第一个子节点 wipFiber.child = newFiber } else { // 同级的Fiber节点增加sibling字段 prevSibling.sibling = newFiber } prevSibling = newFiber index++ }}function performUnitOfWork(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber) } const elements = fiber.props.children reconcileChildren(wipFiber, elements) // 首先尝试子节点 if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { // 尝试同级节点 if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent }}
咱们同时遍历旧的Fiber树,既wipFiber.alternate
,和须要协调的新的元素。如果咱们疏忽遍历链表和数组的模版代码。那么在while
循环中,最重要的就是oldFiber
和element
。element
是咱们须要渲染的DOM, oldFiber
是上次渲染的Fiber。咱们须要比拟它们,以确定DOM是否须要任何的更改。
function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while ( index < elements.length || oldFiber !== null ) { const element = elements[index] let newFiber = null // TODO compare oldFiber to element // .... if (oldFiber) { oldFiber = oldFiber.sibling } if (index === 0) { // 父Fiber节点增加child字段,child指向了第一个子节点 wipFiber.child = newFiber } else { // 同级的Fiber节点增加sibling字段 prevSibling.sibling = newFiber } prevSibling = newFiber index++ }}
为了比拟它们咱们应用以下的规定:
- 如果
oldFiber
和element
具备雷同的类型,咱们保留DOM节点,并应用新的props更新 - 如果类型不同,并且有新元素。咱们须要创立一个新的DOM节点。
- 如果类型不同,存在之前的Fiber,咱们须要移除旧节点
function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while ( index < elements.length || oldFiber !== null ) { const element = elements[index] let newFiber = null // 判断是否是同类型 const sameType = oldFiber && element && element.type == oldFiber.type if (sameType) { // 更新节点 } if (!sameType && element) { // 新增节点 } if (!sameType && oldFiber) { // 删除节点 } if (oldFiber) { oldFiber = oldFiber.sibling } if (index === 0) { // 父Fiber节点增加child字段,child指向了第一个子节点 wipFiber.child = newFiber } else { // 同级的Fiber节点增加sibling字段 prevSibling.sibling = newFiber } prevSibling = newFiber index++ }}
在React中,React应用了key
, 能够更好的进行协调,应用key
能够检测元素在列表中地位是否扭转,更好的复用节点。
当之前的Fiber和新元素具备雷同的类型时,咱们创立一个新的Fiber节点,保留旧Fiber的DOM节点和元素的props。
并且为Fiber增加了一个新的属性effectTag
, 稍后在commit
阶段应用
????️: 在React源码中effectTag
,effectTag
编码的是与Fiber节点相干的effects
(副作用)。React中effectTag
应用了数字的模式存储,应用了按位或结构了一个属性集。更多内容请查看
function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while ( index < elements.length || oldFiber !== null ) { const element = elements[index] let newFiber = null // 判断是否是同类型 const sameType = oldFiber && element && element.type == oldFiber.type if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", } } if (!sameType && element) { // 新增节点 } if (!sameType && oldFiber) { // 删除节点 } // ... }}
对于新增的节点,咱们在effectTag
属性上,应用PLACEMENT
标记进行标记。
function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while ( index < elements.length || oldFiber !== null ) { const element = elements[index] let newFiber = null // 判断是否是同类型 const sameType = oldFiber && element && element.type == oldFiber.type // ... if (!sameType && element) { // 新增节点 newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", } } if (!sameType && oldFiber) { // 删除节点 } // ... }}
对于须要删除节点,咱们不创立新的Fiber,而是将effectTag
设置为DELETION
, 并增加到旧的Fiber节点上。
function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while ( index < elements.length || oldFiber !== null ) { const element = elements[index] let newFiber = null // 判断是否是同类型 const sameType = oldFiber && element && element.type == oldFiber.type // ... if (!sameType && oldFiber) { // 删除节点 oldFiber.effectTag = "DELETION" deletions.push(oldFiber) } // ... }}
当咱们在commit
时, 咱们从新构建的Fiber节点树开始遍历,因为没有须要保留删除的旧节点。所以咱们须要额定应用一个数组deletions
保留须要删除的旧节点
????️: 在React的源码,workInProgress tree
的Fiber节点领有current tree
对应节点的援用。反之亦然。????️: 在React的源码中,会把所有须要在
commit
阶段,执行副作用的Fiber节点,构建为线性列表,以不便疾速迭代。迭代线性列表要比迭代树快的多,因为不须要迭代没有side-effects的节点。
let deletions = nullfunction render(element, container) { wipRoot = { dom: container, props: { children: [element], }, alternate: currentRoot, } deletions = [] nextUnitOfWork = wipRoot}
当咱们进入commit
阶段时,应用该数组中的Fiber
function commitRoot() { deletions.forEach(commitWork) commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null}
当初让我批改commitWork
函数以解决新的effectTag
字段
如果effectTag
是PLACEMENT
, 与之前一样,将DOM增加增加到父节点上
function commitWork(fiber) { if (!fiber) { return } const domParent = fiber.parent.dom // 对于新增节点的解决 if ( fiber.effectTag === "PLACEMENT" && fiber.dom != null ) { domParent.appendChild(fiber.dom) } // 递归解决子节点 commitWork(fiber.child) commitWork(fiber.sibling)}
如果effectTag
是DELETION
, 咱们从父节点上删除节点
function 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 === "DELETION") { // 对于删除节点的解决 domParent.removeChild(fiber.dom) } // 递归解决子节点 commitWork(fiber.child) commitWork(fiber.sibling)}
如果effectTag
是UPDATE
, 咱们应用新的props
更新当初的DOM
function 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 === "DELETION") { // 对于删除节点的解决 domParent.removeChild(fiber.dom) } else if ( fiber.effectTag === "UPDATE" && fiber.dom != null ) { // 对于须要更新节点的解决 updateDom( fiber.dom, fiber.alternate.props, fiber.props ) } // 递归解决子节点 commitWork(fiber.child) commitWork(fiber.sibling)}
接下来须要实现updateDom
函数
function updateDom(dom, prevProps, nextProps) { // TODO}
咱们应用旧的Fiber的props和新的Fiber的props进行比拟,移除删除的的props,增加或更新已更改的props
// 用于排除children属性const isProperty = key => key !== "children"// 用于判断是否更新了属性const isNew = (prev, next) => key => prev[key] !== next[key]// 用于判断在新的props上是否有属性const isGone = (prev, next) => key => !(key in next)function updateDom(dom, prevProps, nextProps) { // 删除之前的属性 Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = "" }) // 增加或者更新属性 Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name] })}
咱们须要对事件监听器做非凡的解决,如果props以on
结尾, 咱们应用不同的形式去解决它们
// 判断props是否是on结尾const isEvent = key => key.startsWith("on")// 用于排除children属性,和on结尾的属性const isProperty = key => key !== "children" && !isEvent(key)
如果事件处理程序产生了更改,咱们须要首先删除,而后增加新的处理程序
????️: 间接在DOM上增加事件处理程序的形式,有点相似preact
中的解决形式
function updateDom(dom, prevProps, nextProps) { Object.keys(prevProps) .filter(isEvent) .filter( key => // 如果事件处理程序产生了更新,获取新的props上没有 // 须要先删除之前的处理程序 !(key in nextProps) || isNew(prevProps, nextProps)(key) ) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.removeEventListener( eventType, prevProps[name] ) }) // 删除之前的属性 Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = "" }) // 增加或者更新属性 Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name] }) // 增加事件监听 Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.addEventListener( eventType, nextProps[name] ) })}
七: Function 组件
咱们须要增加的下一件事是对Function组件的反对。咱们批改下咱们的例子。
function App(props) { return <h1>Hi {props.name}</h1>}const element = <App name="foo" />const container = document.getElementById("root")render(element, container)
咱们将jsx转换为js
function App(props) { return createElement( "h1", null, "Hi ", props.name )}const element = createElement(App, { name: "foo",})
Function组件和DOM次要有两个不同
- Function组件的Fiber没有DOM节点
- children来自Function, 而不是间接从DOM中间接获取
咱们查看Fiber的类型是否为函数,并依据类型由不同的函数进行解决,如果是不同的DOM,传入updateHostComponent
function performUnitOfWork(fiber) { // 判断是不是函数组件 const isFunctionComponent = fiber.type instanceof Function if (isFunctionComponent) { updateFunctionComponent(fiber) } else { updateHostComponent(fiber) } // 接下来返回下一个须要解决的Fiber节点,因为是深度优先遍历,优先从子节点开始 if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent }}
updateHostComponent
和咱们之前做的一样
function updateHostComponent () { if (!fiber.dom) { // 创立dom节点 fiber.dom = createDom(fiber) } // 子元素 const elements = fiber.props.children // 子元素与旧的Fiber进行子协调 reconcileChildren(wipFiber, elements)}
updateFunctionComponent
运行函数组件获取children
。在咱们的例子中App会返回h1元素。一旦有了children
, 协调就能够依照之前的形式进行了。不须要进行任何批改。
function updateFunctionComponent () { // 获取Function组件的children const children = [fiber.type(fiber.props)] reconcileChildren(fiber, children)}
上面咱们须要批改commitWork
函数。因为咱们的Function组件的Fiber节点没有DOM节点。咱们须要批改两件事。
首先如果要找到DOM节点的父节点,咱们须要顺次向上查找,找到带有DOM节点的Fiber
function commitWork(fiber) { if (!fiber) { return } // 父级Fiber let domParentFiber = fiber.parent // 直到找到含有dom的Fiber节点 while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent } const domParent = domParentFiber.dom // ...}
在删除节点时,咱们须要向下直到找到含有DOM节点的Fiber
function commitDeletion (fiber, domParent) { if (fiber.dom) { domParent.removeChild(fiber.dom) } else { commitDeletion(fiber.child, domParent) }}function commitWork(fiber) { // ... if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { // 解决新增 domParent.appendChild(fiber.dom) } else if (fiber.effectTag === "DELETION") { // 解决删除 commitDeletion(fiber, domParent) } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { // 解决更新 updateDom( fiber.dom, fiber.alternate.props, fiber.props ) } // ...}
八: hooks
最初一步。目前咱们有了Function组件,当初让咱们增加状态。上面是一个计数器的例子
function Counter() { const [state, setState] = Didact.useState(1) return ( <h1 onClick={() => setState(c => c + 1)}> Count: {state} </h1> )}const element = <Counter />onst container = document.getElementById("root")render(element, container)
咱们应用useState
获取和更新计数器的值。在调用函数组件前,咱们须要初始化一些全局变量,以便在useState
函数中应用它们。
首先获取正在工作的Fiber,咱们在Fiber节点中增加hooks
数组,应用数组的目标是为了反对多个useState
。并且援用以后hooks
的索引。
// 以后正在工作的Fiberlet wipFiber = null// 以后Fiber的hooks的索引let hookIndex = nullfunction updateFunctionComponent () { // 正在工作的Fiber wipFiber = fiber // 以后hooks的索引默认为0 hookIndex = 0 // hooks的汇合 wipFiber.hooks = [] // 获取Function组件的children const children = [fiber.type(fiber.props)] reconcileChildren(fiber, children)}
当组件调用useState
时,首先咱们查看是否之前是否有hook,如果存在旧的hook把之前的状态复制到新hook。否则,应用初始值初始化hook。
而后将hook增加到Fiber,并将hook的索引加1
function useState(initial) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] // 判断之前是否有状态 const hook = { state: oldHook ? oldHook.state : initial, } wipFiber.hooks.push(hook) hookIndex++ return [hook.state]}
useState
还应该返回一个函数,更新状态。因而咱们定义setState
用于接管action
, 用于更新状态。setState
会将action
推入到hook
的队列上。
而后咱们执行与render
函数中相似的操作,咱们设置nextUnitOfWork
开始进行新的渲染阶段。
function useState(initial) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] // 判断之前是否有状态 const hook = { state: oldHook ? oldHook.state : initial, queue: [], // 更新队列 } const setState = (action) => { // action增加到队列中 hook.queue.push(action) wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, } // 当nextUnitOfWork不为空时,就会进入渲染阶段 nextUnitOfWork = wipRoot deletions = [] } wipFiber.hooks.push(hook) hookIndex++ return [hook.state, setState]}
????️: 这里简化了setState, setState只接管函数作为参数。
然而目前咱们还没有更新state
。在下次渲染组件时,咱们从旧的队列中获取所有action
。而后将它们逐个利用到新的hook state上。当咱们返回状态时,state会被更新。
????️: 调用setState,不会立即更新state。而是在进入render
阶段后更新state,而后useState
会返回新的状态。
function useState(initial) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] // 判断之前是否有状态 const hook = { state: oldHook ? oldHook.state : initial, queue: [], // 更新队列 } const actions = oldHook ? oldHook.queue : [] actions.forEach(action => { hook.state = action(hook.state) }) const setState = (action) => { // action增加到队列中 hook.queue.push(action) wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, } // 当nextUnitOfWork不为空时,就会进入渲染阶段 nextUnitOfWork = wipRoot deletions = [] } wipFiber.hooks.push(hook) hookIndex++ return [hook.state, setState]}
咱们曾经建设了好了本人的React。
结语
除了帮忙你了解react是工作原理外,本文的另一个目标是让你在后续可能更轻松深刻React。所以咱们屡次应用了和react源码中一样的函数名以及变量名。
咱们省略了很多了React的优化
- 在
render
阶段遍历整棵树,然而React中会跳过没有任何更改的子树。 commit
阶段,React会进行线性遍历- 目前咱们会每次都创立一个新的Fiber,而React中会复用之前的Fiber节点
还有很多...
咱们还能够持续增加性能,比方:
- 增加key
- 增加useEffect
- 应用对象作为款式的props
- children扁平化
参考
- Build your own React(基于hooks实现)
- Didact: a DIY guide to build your own React(基于class实现)
- didact