一、前言
本文基于 https://pomb.us/build-your-own-react/ 实现简略版 React。
本文学习思路来自 卡颂 - b 站 -React 源码,你在第几层。
模仿的版本为 React 16.8。
将实现以下性能:
- createElement(虚构 DOM);
- render;
- 可中断渲染 ;
- Fibers;
- Render and Commit Phases;
- 协调(Diff 算法);
- 函数组件 ;
- hooks;
上面上正餐,请持续浏览。
二、筹备
1. React Demo
先来看看一个简略的 React Demo,代码如下:
const element = <div title="foo">hello</div>
const container = document.getElementById('container')
ReactDOM.render(element, container);
本例残缺源码见:reactDemo
在浏览器中关上 reactDemo.html,展现如下:
咱们须要实现本人的 React,那么就须要晓得下面的代码到底做了什么。
1.1 element
const element = <div>123</div>
实际上是 JSX 语法。
React 官网 对 JSX 的解释如下:
JSX 是一个 JavaScript 语法扩大。它相似于模板语言,但它具备 JavaScript 的全副能力。JSX 最终会被 babel 编译为 React.createElement() 函数调用。
通过 babel 在线编译 const element = <div>123</div>
。
可知 const element = <div>123</div>
通过编译后的理论代码如下:
const element = React.createElement("div", {title: "foo"}, "hello");
再来看看上文的 React.createElement 理论生成了一个怎么样的对象。
在 demo 中打印试试:
const element = <div title="foo">hello</div>
console.log(element)
const container = document.getElementById('container')
ReactDOM.render(element, container);
能够看到输入的 element 如下:
简化一下 element:
const element = {
type: 'div',
props: {
title: 'foo',
children: 'hello'
}
}
简略总结一下,React.createElement
实际上是生成了一个 element 对象,该对象领有以下属性:
- type: 标签名
-
props
- title: 标签属性
- children: 子节点
1.2 render
ReactDOM.render()
将 element 增加到 id 为 container 的 DOM 节点中,上面咱们将简略手写一个办法代替 ReactDOM.render()
。
- 创立标签名为 element.type 的节点;
const node = document.createElement(element.type)
-
设置 node 节点的 title 为 element.props.title;
node["title"] = element.props.title
-
创立一个空的文本节点 text;
const text = document.createTextNode("")
-
设置文本节点的 nodeValue 为 element.props.children;
text["nodeValue"] = element.props.children
-
将文本节点 text 增加进 node 节点;
node.appendChild(text)
-
将 node 节点增加进 container 节点
container.appendChild(node)
本例残缺源码见:reactDemo2
运行源码,后果如下,和引入 React 的后果统一:
三、开始
上文通过模仿 React,简略代替了 React.createElement、ReactDOM.render 办法,接下来将真正开始实现 React 的各个性能。
1. createElement(虚构 DOM)
下面有理解到 createElement 的作用是创立一个 element 对象,构造如下:
// 虚构 DOM 构造
const element = {
type: 'div', // 标签名
props: { // 节点属性,蕴含 children
title: 'foo', // title 属性
children: 'hello' // 子节点,注:实际上这里应该是数组构造,帮忙咱们存储更多子节点
}
}
依据 element 的构造,设计了 createElement 函数,代码如下:
/**
* 创立虚构 DOM 构造
* @param {type} 标签名
* @param {props} 属性对象
* @param {children} 子节点
* @return {element} 虚构 DOM
*/
function createElement (type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
}
}
这里有思考到,当 children 是非对象时,应该创立一个 textElement 元素,代码如下:
/**
* 创立文本节点
* @param {text} 文本值
* @return {element} 虚构 DOM
*/
function createTextElement (text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []}
}
}
接下来试一下,代码如下:
const myReact = {createElement}
const element = myReact.createElement(
"div",
{id: "foo"},
myReact.createElement("a", null, "bar"),
myReact.createElement("b")
)
console.log(element)
本例残缺源码见:reactDemo3
失去的 element 对象如下:
const element = {
"type": "div",
"props": {
"id": "foo",
"children": [
{
"type": "a",
"props": {
"children": [
{
"type": "TEXT_ELEMENT",
"props": {
"nodeValue": "bar",
"children": []}
}
]
}
},
{
"type": "b",
"props": {"children": []
}
}
]
}
}
JSX
实际上咱们在应用 react 开发的过程中,并不会这样创立组件:
const element = myReact.createElement(
"div",
{id: "foo"},
myReact.createElement("a", null, "bar"),
myReact.createElement("b")
)
而是通过 JSX 语法,代码如下:
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)
在 myReact 中,能够通过增加正文的模式,通知 babel 转译咱们指定的函数,来应用 JSX 语法,代码如下:
/** @jsx myReact.createElement */
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)
本例残缺源码见:reactDemo4
2. render
render 函数帮忙咱们将 element 增加至实在节点中。
将分为以下步骤实现:
- 创立 element.type 类型的 dom 节点,并增加至容器中;
/**
* 将虚构 DOM 增加至实在 DOM
* @param {element} 虚构 DOM
* @param {container} 实在 DOM
*/
function render (element, container) {const dom = document.createElement(element.type)
container.appendChild(dom)
}
- 将 element.children 都增加至 dom 节点中;
element.props.children.forEach(child =>
render(child, dom)
)
- 对文本节点进行非凡解决;
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(element.type)
- 将 element 的 props 属性增加至 dom;
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {dom[name] = element.props[name]
})
以上咱们实现了将 JSX 渲染到实在 DOM 的性能,接下来试一下,代码如下:
const myReact = {
createElement,
render
}
/** @jsx myReact.createElement */
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)
myReact.render(element, document.getElementById('container'))
本例残缺源码见:reactDemo5
后果如图,胜利输入:
3. 可中断渲染(requestIdleCallback)
再来看看下面写的 render 办法中对于子节点的解决,代码如下:
/**
* 将虚构 DOM 增加至实在 DOM
* @param {element} 虚构 DOM
* @param {container} 实在 DOM
*/
function render (element, container) {
// 省略
// 遍历所有子节点,并进行渲染
element.props.children.forEach(child =>
render(child, dom)
)
// 省略
}
这个递归调用是有问题的,一旦开始渲染,就会将所有节点及其子节点全副渲染实现这个过程才会完结。
当 dom tree 很大的状况下,在渲染过程中,页面上是卡住的状态,无奈进行用户输出等交互操作。
可分为以下步骤解决上述问题:
- 容许中断渲染工作,如果有优先级更高的工作插入,则临时中断浏览器渲染,待实现该工作后,复原浏览器渲染;
- 将渲染工作进行合成,分解成一个个小单元;
应用 requestIdleCallback 来解决容许中断渲染工作的问题。
window.requestIdleCallback 将在浏览器的闲暇时段内调用的函数排队。这使开发者可能在主事件循环上执行后盾和低优先级工作,而不会影响提早要害事件,如动画和输出响应。
window.requestIdleCallback 具体介绍可查看文档:文档
代码如下:
// 下一个工作单元
let nextUnitOfWork = null
/**
* workLoop 工作循环函数
* @param {deadline} 截止工夫
*/
function workLoop(deadline) {
// 是否应该进行工作循环函数
let shouldYield = false
// 如果存在下一个工作单元,且没有优先级更高的其余工作时,循环执行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 如果截止工夫快到了,进行工作循环函数
shouldYield = deadline.timeRemaining() < 1}
// 告诉浏览器,闲暇工夫应该执行 workLoop
requestIdleCallback(workLoop)
}
// 告诉浏览器,闲暇工夫应该执行 workLoop
requestIdleCallback(workLoop)
// 执行单元事件,并返回下一个单元事件
function performUnitOfWork(nextUnitOfWork) {// TODO}
performUnitOfWork 是用来执行单元事件,并返回下一个单元事件的,具体实现将在下文介绍。
4. Fiber
上文介绍了通过 requestIdleCallback 让浏览器在闲暇工夫渲染工作单元,防止渲染过久导致页面卡顿的问题。
注:实际上 requestIdleCallback 性能并不稳固,不倡议用于生产环境,本例仅用于模仿 React 的思路,React 自身并不是通过 requestIdleCallback 来实现让浏览器在闲暇工夫渲染工作单元的。
另一方面,为了让渲染工作能够拆散成一个个小单元,React 设计了 fiber。
每一个 element 都是一个 fiber 构造,每一个 fiber 都是一个渲染工作单元。
所以 fiber 既是一种数据结构,也是一个工作单元 。
下文将通过简略的示例对 fiber 进行介绍。
假如须要渲染这样一个 element 树:
myReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
生成的 fiber tree 如图:
橙色代表子节点,黄色代表父节点,蓝色代表兄弟节点。
每个 fiber 都有一个链接指向它的第一个子节点、下一个兄弟节点和它的父节点。这种数据结构能够让咱们更不便的查找下一个工作单元。
上图的箭头也表明了 fiber 的渲染过程,渲染过程详细描述如下:
- 从 root 开始,找到第一个子节点 div;
- 找到 div 的第一个子节点 h1;
- 找到 h1 的第一个子节点 p;
- 找 p 的第一个子节点, 如无子节点,则找下一个兄弟节点 ,找到 p 的兄弟节点 a;
- 找 a 的第一个子节点, 如无子节点,也无兄弟节点,则找它的父节点的下一个兄弟节点 ,找到 a 的 父节点的兄弟节点 h2;
- 找 h2 的第一个子节点,找不到,找兄弟节点,找不到,找父节点 div 的兄弟节点,也找不到,持续找 div 的父节点的兄弟节点,找到 root;
- 第 6 步曾经找到了 root 节点,渲染已全副实现。
上面将渲染过程用代码实现。
- 将 render 中创立 DOM 节点的局部抽离为 creactDOM 函数;
/**
* createDom 创立 DOM 节点
* @param {fiber} fiber 节点
* @return {dom} dom 节点
*/
function createDom (fiber) {
// 如果是文本类型,创立空的文本节点,如果不是文本类型,按 type 类型创立节点
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type)
// isProperty 示意不是 children 的属性
const isProperty = key => key !== "children"
// 遍历 props,为 dom 增加属性
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {dom[name] = fiber.props[name]
})
// 返回 dom
return dom
}
- 在 render 中设置第一个工作单元为 fiber 根节点;
fiber 根节点仅蕴含 children 属性,值为参数 fiber。
// 下一个工作单元
let nextUnitOfWork = null
/**
* 将 fiber 增加至实在 DOM
* @param {element} fiber
* @param {container} 实在 DOM
*/
function render (element, container) {
nextUnitOfWork = {
dom: container,
props: {children: [element]
}
}
}
- 通过 requestIdleCallback 在浏览器闲暇时,渲染 fiber;
/**
* workLoop 工作循环函数
* @param {deadline} 截止工夫
*/
function workLoop(deadline) {
// 是否应该进行工作循环函数
let shouldYield = false
// 如果存在下一个工作单元,且没有优先级更高的其余工作时,循环执行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 如果截止工夫快到了,进行工作循环函数
shouldYield = deadline.timeRemaining() < 1}
// 告诉浏览器,闲暇工夫应该执行 workLoop
requestIdleCallback(workLoop)
}
// 告诉浏览器,闲暇工夫应该执行 workLoop
requestIdleCallback(workLoop)
- 渲染 fiber 的函数 performUnitOfWork;
/**
* performUnitOfWork 解决工作单元
* @param {fiber} fiber
* @return {nextUnitOfWork} 下一个工作单元
*/
function performUnitOfWork(fiber) {
// TODO 增加 dom 节点
// TODO 新建 filber
// TODO 返回下一个工作单元(fiber)}
4.1 增加 dom 节点
function performUnitOfWork(fiber) {
// 如果 fiber 没有 dom 节点,为它创立一个 dom 节点
if (!fiber.dom) {fiber.dom = createDom(fiber)
}
// 如果 fiber 有父节点,将 fiber.dom 增加至父节点
if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom)
}
}
4.2 新建 filber
function performUnitOfWork(fiber) {
// ~~省略~~
// 子节点
const elements = fiber.props.children
// 索引
let index = 0
// 上一个兄弟节点
let prevSibling = null
// 遍历子节点
while (index < elements.length) {const element = elements[index]
// 创立 fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 将第一个子节点设置为 fiber 的子节点
if (index === 0) {fiber.child = newFiber} else if (element) {
// 第一个之外的子节点设置为该节点的兄弟节点
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
4.3 返回下一个工作单元(fiber)
function performUnitOfWork(fiber) {
// ~~省略~~
// 如果有子节点,返回子节点
if (fiber.child) {return fiber.child}
let nextFiber = fiber
while (nextFiber) {
// 如果有兄弟节点,返回兄弟节点
if (nextFiber.sibling) {return nextFiber.sibling}
// 否则持续走 while 循环,直到找到 root。nextFiber = nextFiber.parent
}
}
以上咱们实现了将 fiber 渲染到页面的性能,且渲染过程是可中断的。
当初试一下,代码如下:
const element = (
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
)
myReact.render(element, document.getElementById('container'))
本例残缺源码见:reactDemo7
如预期输入 dom,如图:
5. 渲染提交阶段
因为渲染过程被咱们做了可中断的,那么中断的时候,咱们必定不心愿浏览器给用户展现的是渲染了一半的 UI。
对渲染提交阶段优化的解决如下:
- 把 performUnitOfWork 中对于把子节点增加至父节点的逻辑删除;
function performUnitOfWork(fiber) {
// 把这段删了
if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom)
}
}
- 新增一个根节点变量,存储 fiber 根节点;
// 根节点
let wipRoot = null
function render (element, container) {
wipRoot = {
dom: container,
props: {children: [element]
}
}
// 下一个工作单元是根节点
nextUnitOfWork = wipRoot
}
- 当所有 fiber 都工作实现时,nextUnitOfWork 为 undefined,这时再渲染实在 DOM;
function workLoop (deadline) {
// 省略
if (!nextUnitOfWork && wipRoot) {commitRoot()
}
// 省略
}
- 新增 commitRoot 函数,执行渲染实在 DOM 操作,递归将 fiber tree 渲染为实在 DOM;
// 全副工作单元实现后,将 fiber tree 渲染为实在 DOM;function commitRoot () {commitWork(wipRoot.child)
// 须要设置为 null,否则 workLoop 在浏览器闲暇时一直的执行。wipRoot = null
}
/**
* performUnitOfWork 解决工作单元
* @param {fiber} fiber
*/
function commitWork (fiber) {if (!fiber) return
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
// 渲染子节点
commitWork(fiber.child)
// 渲染兄弟节点
commitWork(fiber.sibling)
}
本例残缺源码见:reactDemo8
源码运行后果如图:
6. 协调(diff 算法)
当 element 有更新时,须要将更新前的 fiber tree 和更新后的 fiber tree 进行比拟,失去比拟后果后,仅对有变动的 fiber 对应的 dom 节点进行更新。
通过协调,缩小对实在 DOM 的操作次数。
1. currentRoot
新增 currentRoot 变量,保留根节点更新前的 fiber tree,为 fiber 新增 alternate 属性,保留 fiber 更新前的 fiber tree;
let currentRoot = null
function render (element, container) {
wipRoot = {
// 省略
alternate: currentRoot
}
}
function commitRoot () {commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
2. performUnitOfWork
将 performUnitOfWork 中对于新建 fiber 的逻辑,抽离到 reconcileChildren 函数;
/**
* 协调子节点
* @param {fiber} fiber
* @param {elements} fiber 的 子节点
*/
function reconcileChildren (fiber, elements) {
// 用于统计子节点的索引值
let index = 0
// 上一个兄弟节点
let prevSibling = null
// 遍历子节点
while (index < elements.length) {const element = elements[index]
// 新建 fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// fiber 的第一个子节点是它的子节点
if (index === 0) {fiber.child = newFiber} else if (element) {
// fiber 的其余子节点,是它第一个子节点的兄弟节点
prevSibling.sibling = newFiber
}
// 把新建的 newFiber 赋值给 prevSibling,这样就不便为 newFiber 增加兄弟节点了
prevSibling = newFiber
// 索引值 + 1
index++
}
}
3. reconcileChildren
在 reconcileChildren 中比照新旧 fiber;
3.1 当新旧 fiber 类型雷同时
保留 dom,仅更新 props,设置 effectTag 为 UPDATE;
function reconcileChildren (wipFiber, elements) {
// ~~省略~~
// oldFiber 能够在 wipFiber.alternate 中找到
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
while (index < elements.length || oldFiber != null) {const element = elements[index]
let newFiber = null
// fiber 类型是否雷同
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
// 如果类型雷同,仅更新 props
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
// ~~省略~~
}
// ~~省略~~
}
3.2 当新旧 fiber 类型不同,且有新元素时
创立一个新的 dom 节点,设置 effectTag 为 PLACEMENT;
function reconcileChildren (wipFiber, elements) {
// ~~省略~~
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
// ~~省略~~
}
3.3 当新旧 fiber 类型不同,且有旧 fiber 时
删除旧 fiber,设置 effectTag 为 DELETION;
function reconcileChildren (wipFiber, elements) {
// ~~省略~~
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// ~~省略~~
}
4. deletions
新建 deletions 数组存储需删除的 fiber 节点,渲染 DOM 时,遍历 deletions 删除旧 fiber;
let deletions = null
function render (element, container) {
// 省略
// render 时,初始化 deletions 数组
deletions = []}
// 渲染 DOM 时,遍历 deletions 删除旧 fiber
function commitRoot () {deletions.forEach(commitWork)
}
5. commitWork
在 commitWork 中对 fiber 的 effectTag 进行判断,并别离解决。
5.1 PLACEMENT
当 fiber 的 effectTag 为 PLACEMENT 时,示意是新增 fiber,将该节点新增至父节点中。
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {domParent.appendChild(fiber.dom)
}
5.2 DELETION
当 fiber 的 effectTag 为 DELETION 时,示意是删除 fiber,将父节点的该节点删除。
else if (fiber.effectTag === "DELETION") {domParent.removeChild(fiber.dom)
}
5.3 UPDATE
当 fiber 的 effectTag 为 UPDATE 时,示意是更新 fiber,更新 props 属性。
else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}
updateDom 函数依据不同的更新类型,对 props 属性进行更新。
const isProperty = key => key !== "children"
// 是否是新属性
const isNew = (prev, next) => key => prev[key] !== next[key]
// 是否是旧属性
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]
})
}
另外,为 updateDom 增加事件属性的更新、删除,便于追踪 fiber 事件的更新。
function updateDom(dom, prevProps, nextProps) {
// ~~省略~~
const isEvent = key => key.startsWith("on")
// 删除旧的或者有变动的事件
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// 注册新事件
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
// ~~省略~~
}
替换 creactDOM 中设置 props 的逻辑。
function createDom (fiber) {
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type)
// 看这里鸭
updateDom(dom, {}, fiber.props)
return dom
}
新建一个蕴含输出表单项的例子,尝试更新 element,代码如下:
/** @jsx myReact.createElement */
const container = document.getElementById("container")
const updateValue = e => {rerender(e.target.value)
}
const rerender = value => {
const element = (
<div>
<input onInput={updateValue} value={value} />
<h2>Hello {value}</h2>
</div>
)
myReact.render(element, container)
}
rerender("World")
本例残缺源码见:reactDemo9
输入后果如图:
7. 函数式组件
先来看一个简略的函数式组件示例:
myReact 还不反对函数式组件,上面代码运行会报错,这里仅用于对比函数式组件的惯例应用形式。
/** @jsx myReact.createElement */
const container = document.getElementById("container")
function App (props) {
return (<h1>hi~ {props.name}</h1>
)
}
const element = (<App name='foo' />)
myReact.render(element, container)
函数式组件和 html 标签组件相比,有以下两点不同:
- 函数组件的 fiber 没有 dom 节点;
- 函数组件的 children 须要运行函数后失去;
通过下列步骤实现函数组件:
- 批改 performUnitOfWork,依据 fiber 类型,执行 fiber 工作单元;
function performUnitOfWork(fiber) {
// 是否是函数类型组件
const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
// 如果是函数组件,执行 updateFunctionComponent 函数
if (isFunctionComponent) {updateFunctionComponent(fiber)
} else {
// 如果不是函数组件,执行 updateHostComponent 函数
updateHostComponent(fiber)
}
// 省略
}
- 定义 updateHostComponent 函数,执行非函数组件;
非函数式组件可间接将 fiber.props.children 作为参数传递。
function updateHostComponent(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
- 定义 updateFunctionComponent 函数,执行函数组件;
函数组件须要运行来取得 fiber.children。
function updateFunctionComponent(fiber) {
// fiber.type 就是函数组件自身,fiber.props 就是函数组件的参数
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
- 批改 commitWork 函数,兼容没有 dom 节点的 fiber;
4.1 批改 domParent 的获取逻辑,通过 while 循环不断向上寻找,直到找到有 dom 节点的父 fiber;
function commitWork (fiber) {
// 省略
let domParentFiber = fiber.parent
// 如果 fiber.parent 没有 dom 节点,则持续找 fiber.parent.parent.dom,直到有 dom 节点。while (!domParentFiber.dom) {domParentFiber = domParentFiber.parent}
const domParent = domParentFiber.dom
// 省略
}
4.2 批改删除节点的逻辑,当删除节点时,须要一直向下寻找,直到找到有 dom 节点的子 fiber;
function commitWork (fiber) {
// 省略
// 如果 fiber 的更新类型是删除,执行 commitDeletion
else if (fiber.effectTag === "DELETION") {commitDeletion(fiber.dom, domParent)
}
// 省略
}
// 删除节点
function commitDeletion (fiber, domParent) {
// 如果该 fiber 有 dom 节点,间接删除
if (fiber.dom) {domParent.removeChild(fiber.dom)
} else {
// 如果该 fiber 没有 dom 节点,则持续找它的子节点进行删除
commitDeletion(fiber.child, domParent)
}
}
下面试一下下面的例子,代码如下:
/** @jsx myReact.createElement */
const container = document.getElementById("container")
function App (props) {
return (<h1>hi~ {props.name}</h1>
)
}
const element = (<App name='foo' />)
myReact.render(element, container)
本例残缺源码见:reactDemo10
运行后果如图:
8. hooks
上面持续为 myReact 增加治理状态的性能,冀望是函数组件领有本人的状态,且能够获取、更新状态。
一个领有计数性能的函数组件如下:
function Counter() {const [state, setState] = myReact.useState(1)
return (<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
已知须要一个 useState 办法用来获取、更新状态。
这里再重申一下, 渲染函数组件的前提是,执行该函数组件 ,因而,上述 Counter 想要更新计数,就会在每次更新都执行一次 Counter 函数。
通过以下步骤实现:
- 新增全局变量 wipFiber;
// 当前工作单元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// 当前工作单元 fiber 的 hook
wipFiber.hook = []
// 省略
}
- 新增 useState 函数;
// initial 示意初始参数,在本例中,initial=1
function useState (initial) {
// 是否有旧钩子,旧钩子存储了上一次更新的 hook
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hook
// 初始化钩子,钩子的状态是旧钩子的状态或者初始状态
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],}
// 从旧的钩子队列中获取所有动作,而后将它们一一利用到新的钩子状态
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {hook.state = action(hook.state)
})
// 设置钩子状态
const setState = action => {
// 将动作增加至钩子队列
hook.queue.push(action)
// 更新渲染
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []}
// 把钩子增加至工作单元
wipFiber.hook = hook
// 返回钩子的状态和设置钩子的函数
return [hook.state, setState]
}
上面运行一下计数组件,代码如下:
function Counter() {const [state, setState] = myReact.useState(1)
return (<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
本例残缺源码见:reactDemo11
运行后果如图:
本章节简略实现了 myReact 的 hooks 性能。
撒花完结,react 还有很多实现值得咱们去学习和钻研,心愿有下期,和大家一起手写 react 的更多功能。
总结
本文参考 pomb.us 进行学习,实现了包含虚构 DOM、Fiber、Diff 算法、函数式组件、hooks 等性能的自定义 React。
在实现过程中小编对 React 的根本术语及实现思路有了大略的把握,pomb.us 是非常适合初学者的学习材料,能够间接通过 pomb.us 进行学习,也举荐跟着本文一步步实现 React 的常见性能。
本文源码:github 源码。
倡议跟着一步步敲,进行实操练习。
心愿能对你有所帮忙,感激浏览~
别忘了点个赞激励一下我哦,笔芯❤️
参考资料
- https://pomb.us/build-your-own-react/
- 卡颂 - b 站 -React 源码,你在第几层
-
手写一个简略的 React
欢送关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。