一、前言

本文基于 https://pomb.us/build-your-own-react/ 实现简单版 React。

本文学习思路来自 卡颂-b站-React源码,你在第几层

模拟的版本为 React 16.8。

将实现以下功能:

  1. createElement(虚拟 DOM)
  2. render
  3. 可中断渲染
  4. Fibers
  5. Render and Commit Phases
  6. 协调(Diff 算法)
  7. 函数组件
  8. 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()

  1. 创建标签名为 element.type 的节点;
const node = document.createElement(element.type)
  1. 设置 node 节点的 title 为 element.props.title;
node["title"] = element.props.title
  1. 创建一个空的文本节点 text;
const text = document.createTextNode("")
  1. 设置文本节点的 nodeValue 为 element.props.children;
text["nodeValue"] = element.props.children
  1. 将文本节点 text 添加进 node 节点;
node.appendChild(text)
  1. 将 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 添加至真实节点中。

将分为以下步骤实现:

  1. 创建 element.type 类型的 dom 节点,并添加至容器中;
/**
* 将虚拟 DOM 添加至真实 DOM
* @param {element} 虚拟 DOM
* @param {container} 真实 DOM
*/
function render (element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
  1. 将 element.children 都添加至 dom 节点中;
element.props.children.forEach(child =>
render(child, dom)
)
  1. 对文本节点进行特殊处理;
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(element.type)
  1. 将 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 很大的情况下,在渲染过程中,页面上是卡住的状态,无法进行用户输入等交互操作。

可分为以下步骤解决上述问题:

  1. 允许中断渲染工作,如果有优先级更高的工作插入,则暂时中断浏览器渲染,待完成该工作后,恢复浏览器渲染;
  2. 将渲染工作进行分解,分解成一个个小单元;

使用 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 的渲染过程,渲染过程详细描述如下:

  1. 从 root 开始,找到第一个子节点 div;
  2. 找到 div 的第一个子节点 h1;
  3. 找到 h1 的第一个子节点 p;
  4. 找 p 的第一个子节点,如无子节点,则找下一个兄弟节点,找到 p 的兄弟节点 a;
  5. 找 a 的第一个子节点,如无子节点,也无兄弟节点,则找它的父节点的下一个兄弟节点,找到 a 的 父节点的兄弟节点 h2;
  6. 找 h2 的第一个子节点,找不到,找兄弟节点,找不到,找父节点 div 的兄弟节点,也找不到,继续找 div 的父节点的兄弟节点,找到 root;
  7. 第 6 步已经找到了 root 节点,渲染已全部完成。

下面将渲染过程用代码实现。

  1. 将 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
}
  1. 在 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]
}
}
}
  1. 通过 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)
  1. 渲染 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。

对渲染提交阶段优化的处理如下:

  1. 把 performUnitOfWork 中关于把子节点添加至父节点的逻辑删除;
function performUnitOfWork(fiber) {
// 把这段删了
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
  1. 新增一个根节点变量,存储 fiber 根节点;
// 根节点
let wipRoot = null
function render (element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
}
// 下一个工作单元是根节点
nextUnitOfWork = wipRoot
}
  1. 当所有 fiber 都工作完成时,nextUnitOfWork 为 undefined,这时再渲染真实 DOM;
function workLoop (deadline) {
// 省略
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
// 省略
}
  1. 新增 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 需要运行函数后得到;

通过下列步骤实现函数组件:

  1. 修改 performUnitOfWork,根据 fiber 类型,执行 fiber 工作单元;
function performUnitOfWork(fiber) {
// 是否是函数类型组件
const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
// 如果是函数组件,执行 updateFunctionComponent 函数
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
// 如果不是函数组件,执行 updateHostComponent 函数
updateHostComponent(fiber)
}
// 省略
}
  1. 定义 updateHostComponent 函数,执行非函数组件;

非函数式组件可直接将 fiber.props.children 作为参数传递。

function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
  1. 定义 updateFunctionComponent 函数,执行函数组件;

函数组件需要运行来获得 fiber.children。

function updateFunctionComponent(fiber) {
// fiber.type 就是函数组件本身,fiber.props 就是函数组件的参数
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
  1. 修改 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 函数。

通过以下步骤实现:

  1. 新增全局变量 wipFiber;
// 当前工作单元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// 当前工作单元 fiber 的 hook
wipFiber.hook = []
// 省略
}
  1. 新增 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源码

建议跟着一步步敲,进行实操练习。

希望能对你有所帮助,感谢阅读~

别忘了点个赞鼓励一下我哦,笔芯️

参考资料


欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

手写系列-实现一个铂金段位的 React的更多相关文章

  1. 手写系列:call、apply、bind、函数柯里化

    少废话,show my code call 原理都在注释里了 // 不覆盖原生call方法,起个别名叫myCall,接收this上下文context和参数params Function.prototy ...

  2. 利用sklearn对MNIST手写数据集开始一个简单的二分类判别器项目(在这个过程中学习关于模型性能的评价指标,如accuracy,precision,recall,混淆矩阵)

    .caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px so ...

  3. [手写系列] Spirit带你实现防抖函数和节流函数

    前言 防抖函数和节流函数,无论是写业务的时候还是面试的时候,想必大家已经听过很多次了吧.但是大家在用到的时候,有了解过他们之间的区别嘛,他们是如何实现的呢?还是说只是简单的调用下像lodash和und ...

  4. [手写系列] 带你实现一个简单的Promise

    简介 学习之前 需要先对Promise有个基本了解哦,这里都默认大家都是比较熟悉Promise的 本次将带小伙伴们实现Promise的基本功能 Promise的基本骨架 Promise的then Pr ...

  5. 手写tomcat——编写一个提供servlet能力的 http服务器

    点击查看代码 package com.grady.diytomcat; import com.grady.diytomcat.handler.RequestHandler; import org.do ...

  6. 请手写代码实现一个promise

    第一步:promise的声明 class Promise{ // 构造器 constructor(executor){ // 成功 let resolve = () => { }; // 失败 ...

  7. 手写tomcat——编写一个echo http服务器

    核心代码如下: public class DiyTomcat1 { public void run() throws IOException { ServerSocket serverSocket = ...

  8. 手写一个webpack,看看AST怎么用

    本文开始我会围绕webpack和babel写一系列的工程化文章,这两个工具我虽然天天用,但是对他们的原理理解的其实不是很深入,写这些文章的过程其实也是我深入学习的过程.由于webpack和babel的 ...

  9. 手写一个简单的HashMap

    HashMap简介 HashMap是Java中一中非常常用的数据结构,也基本是面试中的"必考题".它实现了基于"K-V"形式的键值对的高效存取.JDK1.7之前 ...

随机推荐

  1. 基于Docker安装常用软件

    基于Docker安装常用软件 本实验介绍如何基于Docker安装常用的软件,具体包括: Ubuntu Cetnos Nginx Node.js PHP MySQL Tomcat Redis Mongo ...

  2. 搞清楚Spring事件机制后:Spring的源码看起来简单多了

    本文主讲Spring的事件机制,意图说清楚: 什么是观察者模式? 自己实现事件驱动编程,对标Spring的事件机制 彻底搞懂Spring中的事件机制,从而让大家 本文内容较长,代码干货较多,建议收藏后 ...

  3. IntelliJ IDEA不好用?那是因为没掌握这些技巧

    想要愉快的coding,一个好的IDE肯定少不了.而对于Java开发者来说,IntelliJ IDEA绝对是Java-IDE的首屈一指的选择(eclipse党还请绕我一命). 从我第一次下载IDEA到 ...

  4. 管中窥豹-ssh链接过多的问题分析及复盘

    缘起 某一天,产品侧同事联系过来,反馈话单传输程序报错,现象如下: 实际上,该节点仅提供了一个sftp服务,供产品侧传输话单过来进行临时存储,由计费部门取走而已. 分析 于是找运维同事上服务器看了下情 ...

  5. 带你掌握4种Python 排序算法

    摘要:在编程里,排序是一个重要算法,它可以帮助我们更快.更容易地定位数据.在这篇文章中,我们将使用排序算法分类器对我们的数组进行排序,了解它们是如何工作的. 本文分享自华为云社区<Python ...

  6. winform/WPF 多语言的实现

    WPF实现起来非常现代化,可以参考 https://www.cnblogs.com/yang-fei/p/4854460.html winform主要说一下实现过程和注意点,实现参考AutoUpdat ...

  7. CMD批处理(4)——批处理循环语句结构

    FOR函数,对一组文件批量执行命令,基本模式如下 1.文件搜索 for [/D] [/R [路径]] %%变量 in (集合) do (命令) 2.等差数列 for /L %%变量 in (开始,间隔 ...

  8. Java 读取Word表格中的文本和图片

    本文通过Java程序来展示如何读取Word表格,包括读取表格中的文本和图片.下面是具体实现的步骤和方法. 1. 程序环境准备 代码编译工具:IntelliJ IDEA Jdk版本:1.8.0 测试文档 ...

  9. Redis客户端管理

    1.客户端管理 Redis提供了客户端相关API对其状态进行监控和管理,本节将深入介绍各个API的使用方法以及在开发运维中可能遇到的问题. 1.1 客户端API 1.client list clien ...

  10. Docker搭建zabbix+grafana监控系统

    一.准备工作 1.mysql数据库:192.168.1.5 2.nginx服务:192.168.1.10 3.docker服务器:192.168.1.20 4.zabbix客户端若干 二.docker ...