文章内容
仿照源码实现react的初始化渲染和更新功能
步骤
-
step0: 准备工作
-
step1: 实现
createElement函数 -
step2: 实现
render函数 -
step3: 并发模式
-
step4: 加入Fibers
-
step5: 渲染和提交
-
step6: 加入协调算法
-
step7: 加入函数组件渲染支持
-
step8: 实现
useStatehook
目录结构:
初始化项目
使用create-react-native脚手架创初始化一个项目
运行npx create-react-native myReact创建一个项目
在React中使用jsx语法
const element = <h1 title="foo">Hello</h1>
babel在编译的时候会转换成
const element = React.createElement("h1", { title: 'foo' }, 'hello world')
因此,这就是为什么平时开发的时候,明明没有使用React,却要引入,不然就会报错
import React from 'react'
然而,在React17中优化这个问题,用户不需用手动导入react,它在内部babel编译jsx的时候自动引入,而且在babel转换jsx的时候不再使用React.createElement函数,使用的是jsx函数
import {jsx} from "react";
function Foo() {
return jsx('div', ...);
}
为了保持统一
如果下载的是React17的版本
// package.json
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.0",
需要先运行yarn eject命令弹出配置,这个时候babel还是会将jsx转换为React.createElement
step0: 准备工作
现在,让我们开始开发一个React
这是原来react的写法
// src\index.js
import React from "react";
import ReactDOM from "react-dom";
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
在经过babel转换jxs后
// src\index.js
import React from "react";
import ReactDOM from "react-dom";
const element = React.createElement("h1", { title: 'foo' }, 'hello world')
const container = document.getElementById("root")
ReactDOM.render(element, container)
经babel转码jsx后,传递给createElement的第一个参数为type,第二参数为props,剩余参数作为children
打印一下element
console.log(element);
简单的理解,createElement的作用根据用户编写的jsx生成一个虚拟DOM树
再来看看ReactDOM.render方法,这里的render方法,不难看出,这里就是将createElement返回的虚拟DOM树渲染成真实的DOM,并添加到容器root下
下面我们将ReactDOM.render方法换成自己的代码
// src\index.js
import React from 'react'
const element = React.createElement("h1", { title: 'foo' }, 'hello world')
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)
step1: 实现createElement函数
实现createElement的代码
// src\myReact\index.js
import { TEXT_ELEMENT } from './const'
/**
* 创建react元素
* @param {*} type 类型
* @param {*} props 属性
* @param {...any} children 子节点
*/
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
),
}
}
}
/**
* 创建文本元素
* @param {*} text 文本
*/
function createTextElement(text) {
return {
type: TEXT_ELEMENT,
props: {
nodeValue: text,
children: []
}
}
}
const React = {
createElement
}
export default React
修改一下我们的例子,使用自己写的React.createElement转换jsx
// src\index.js
import React from './myReact'
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
console.log(element)
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)
打印一下element
返回的数据主要关注
{
type: "div", // 节点类型
// 属性
props:{
id: 'foo', // attr
children: [...], // 子节点,这里是a标签和b标签
}
}
step2: 实现render函数
下面,我们将原来的渲染函数抽出来,加以改造
// src\index.js
import React from './myReact'
import ReactDOM from './myReact/ReactDOM'
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
console.log(element)
const container = document.getElementById("root")
ReactDOM.render(element, container)
实现自己的render函数
// src\myReact\ReactDOM.js
import { TEXT_ELEMENT } from "./const"
/**
* 渲染函数,将vdom转为dom
* @param {*} element react元素
* @param {*} container dom容器
*/
function render(element, container) {
// 创建dom结点
const dom = element.type === TEXT_ELEMENT
? document.createTextNode('')
: document.createElement(element.type)
// 将props分配给结点
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)
}
const ReactDOM = {
render
}
export default ReactDOM
step3: 并发模式
按找上面的渲染方式,当需要渲染的元素多的时候,在一帧(16.6ms)内无法渲染完毕就会造成浏览器的卡顿。
要想个办法优化,在React中,选择的是把一个大的任务才分成很多小的任务,在浏览器空闲的时候执行每个小的任务。这就是React的并发模式。
上图,看图理解👇,占用浏览器的时间被分成了很多小的单元,这样就可以让浏览器优先执行优先级更高的任务(如:用户输入,点击等操作的响应)
这里使用到一个API:requestIdleCallback
这个函数会在每次浏览器空闲的时候调用,这个函数接受一个回调函数,函数会接受一个IdeDeadline的参数,可以使用IdeDeadline.timeRemaining()获取预估的剩余空闲时间毫秒数。
// 下一个浏览器空闲时间要执行的任务
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
}
step4: 加入Fibers
为组织拆分成多个单元的任务,需要一种数据结构,React在这里使用一种叫Fiber的数据结构,React给每一个节点分配一个fiber节点,形成fiber树,可以理解为虚拟dom。
每次执行一个单元的任务就是执行生成一个fiber节点。
为后续更高效的找到下一个单元的任务,React将fiber的数据结构设计成如下的样子:
- 有一个
parent,指向自己的父节点 - 有一个
child,指向自己的第一个孩子节点 - 有一个
sibing,指向下一个同级的兄弟节点
假设现在要渲染的jsx结构是这样的:
React.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
按照我们的例子,生成的fiber树就是下面这个样子的👇
在生成的Fiber树的时候,遵循如下规则:
- 先找第一个孩子节点,找到则返回
- 找不到孩子节点,找同级兄弟节点
- 找不到同级兄弟节点,回父节点,找父节点的兄弟节点
- 父节点的兄弟节点也没有,再往上找爷爷的兄弟节点
- 直到找到根节点,就完成本次渲染的所有工作
下面来看看代码怎么写:
首先给nextUnitOfWork赋值为渲染的根节点,这样在浏览器空闲的时候就会执行workLoop方法👇
// src\myReact\ReactDOM.js
/**
* 渲染函数
* @param {*} element react元素
* @param {*} container dom容器
*/
function render(element, container) {
// 设置nextUnitOfWork为根fiber结点
nextUnitOfWork = {
dom: container,
props: {
children: [element],
}
}
}
function workLoop(deadline) {
let shouldYield = false // 是否阻塞执行任务
while (nextUnitOfWork && !shouldYield) {
// 执行任务
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// 剩余空闲时间不足一毫秒的时候暂停执行
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
// 浏览器处于空闲的时候会调用
requestIdleCallback(workLoop)
然后再完成performUnitOfWork函数👇,分为三步:
- 根据DOM节点,添加到页面,并使用
fiber.dom存起来 - 取出children数组,将每个child转换为fiber结点
- 返回下一个要渲染的fiber节点
// src\myReact\ReactDOM.js
function performUnitOfWork(fiber) {
console.log('current render fiber', fiber)
// 1. 添加dom结点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// 2. 将每个child转换为fiber结点
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
}
// 第一个child结点
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 3. 返回下一个要渲染的fiber结点
// 找child结点
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 找兄弟结点
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 往父节点找
nextFiber = nextFiber.parent
}
}
console.log('current render fiber', fiber)打印结果如下:
补充:
createDom函数
// src\myReact\ReactDOM.js
/**
* 将fiber结点转换为真实DOM结点
* @param {*} fiber fiber结点
*/
function createDom(fiber) {
const dom =
fiber.type === TEXT_ELEMENT
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
最后再来回顾一下完整的fiber的结构
{
parent: null, //父节点
sibling: null, // 下一个兄弟节点
child: null, // 第一子节点
dom: null, // 真实dom节点
type: null, // 节点类型
props: null, // 属性
}
step5: 渲染和提交
上面的代码有一个问题,每次生成一个fiber结点的时候就马上渲染到页面上,但是浏览器可能会在途中打断这个操作,用户就会看到不完整的页面。
现在要解决这个问题,需要将渲染的逻辑提取出来,和fiber tree的生成独立开来:
-
我们用一个变量记录
wipRoot(work in progress root)记录工作中的fiber,以便在浏览器打断之后从该结点起继续工作 -
将原来的渲染的逻辑去掉
-
在整个fiber tree生成后进行提交操作,然后完成渲染操作
代码如下👇:
// src\myReact\ReactDOM.js
import { TEXT_ELEMENT } from "./const"
let nextUnitOfWork = null // 下一个浏览器空闲时间要执行的任务
let wipRoot = null // 用于记录工作过程中的根节点
/**
* 将fiber结点装换为真实DOM结点
* @param {*} fiber fiber结点
*/
function createDom(fiber) {
const dom =
fiber.type === TEXT_ELEMENT
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
/**
* 提交
*/
function commitRoot(){
commitWork(wipRoot.child)
wipRoot = null
}
/**
* 递归渲染dom树
* @param {*} fiber fiber结点
*/
function commitWork(fiber){
if(!fiber){
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
/**
* 渲染函数,将vdom转为dom
* @param {*} element react元素
* @param {*} container dom容器
*/
function render(element, container) {
// 根fiber结点
wipRoot = {
dom: container,
props: {
children: [element],
}
}
// 从根结点开始工作
nextUnitOfWork = wipRoot
}
function workLoop(deadline) {
let shouldYield = false // 是否阻塞执行任务
while (nextUnitOfWork && !shouldYield) {
// 执行任务
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// 剩余空闲时间不足一毫秒的时候暂停执行
shouldYield = deadline.timeRemaining() < 1
}
// 生成整个fiber tree之后提交给DOM
if(!nextUnitOfWork && wipRoot){
commitRoot()
}
requestIdleCallback(workLoop)
}
// 浏览器处于空闲的时候会调用
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
// 1. 添加dom结点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 2. 将每个child转换为fiber结点
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
}
// 第一个child结点
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 3. 返回下一个要生成的fiber结点
// 找child结点
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 找兄弟结点
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 往父节点找
nextFiber = nextFiber.parent
}
}
const ReactDOM = {
render
}
export default ReactDOM
step6: 加入协调算法
到目前为止,我们只完成了页面的元素的添加工作,还有更新和删除工作没有做。在React中,这一步称之为协调,可以理解为虚拟DOM的diff算法。
下面将是我们要做的事👇:
- 首先,我们需要比较上一次提交的fiber tree和这次render中传入的elements,所以我们需要一个变量存储上一次提交的fiber tree,这里使用的是
currentRoot - 其次,我们还需要为每个fiber结点添加一个新的属性
alternate,用于连接旧的fiber结点 - 然后,我们将创建新fiber树的代码从
performUnitOfWork中抽离出来 - 添加
recnocileChildren方法,用于比较新老fiber并进行更新操作
recnocileChildren的具体比较逻辑👇:
这里要明确两个变量,element:代表将要渲染的结点,oldFiber:代表上一次渲染的结点
- 如果old fiber和new elemnt有相同的type,我们保留原来的DOM结点,并只更新它的属性
- 如果type不一样然后有new element,这代表我们需要创建一个新的DOM节点
- 如果type不同然后有old fiber,代表需要移除这个旧的节点
在React原码中,为了进行更好的协调工作,还使用了keys。例如:用于检测子元素在元素数组中的位置变化
为标记不同状态,这里还要在fiber节点中加入一个新的属性:efffectTag,该属性具有以下三种类型:
UPDATE:更新PLACEMENT:新增DELETION:删除
对于删除,我们需要添加一个数组用于记录需要移除的节点,等到render阶段进行移除,这里使用deletions记录。
另外,我们还加入对事件的支持。
完整代码👇:
// src\myReact\ReactDOM.js
import { EFFECT_TAG_UPDATE, TEXT_ELEMENT, EFFECT_TAG_PLACEMENT, EFFECT_TAG_DELETION } from "./const"
let nextUnitOfWork = null // 下一个浏览器空闲时间要执行的任务
let wipRoot = null // 用于记录工作过程中的根节点
let currentRoot = null // 上一个提交的fiber根节点
let deletions = null // 需要删除的fiber结点
// 是不是事件
const isEvent = key => key.startsWith('on')
// 是不是属性
const isProperty = key => key !== "children" && !isEvent(key)
// 是不是新属性
const isNew = (prev, next) => key =>
prev[key] !== next[key]
// 是不是旧属性
const isGone = (prev, next) => key => !(key in next)
/**
* 将fiber结点装换为真实DOM结点
* @param {*} fiber fiber结点
*/
function createDom(fiber) {
const dom =
fiber.type === TEXT_ELEMENT
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props);
return dom
}
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
/**
* 递归渲染dom树
* @param {*} fiber fiber结点
*/
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
if (
fiber.effectTag === EFFECT_TAG_PLACEMENT &&
fiber.dom !== null
) {
// 插入DOM结点
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === EFFECT_TAG_DELETION) {
// 删除DOM结点
domParent.removeChild(fiber.dom)
} else if (
fiber.effectTag === EFFECT_TAG_UPDATE &&
fiber.dom !== null
) {
// 更新DOM节点
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
/**
* 更新dom结点
* @param {*} dom 需要更新的dom结点
* @param {*} prevProps 旧的属性
* @param {*} nextProps 新的属性
*/
function updateDom(dom, prevProps, nextProps) {
// 移除旧的事件监听
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLocaleLowerCase()
.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
.toLocaleLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
/**
* 渲染函数,将vdom转为dom
* @param {*} element react元素
* @param {*} container dom容器
*/
function render(element, container) {
// 根fiber结点
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot, // 记录上一次更新到dom的fiber结点
}
deletions = [] // 初始化删除结点数组
// 从根结点开始工作
nextUnitOfWork = wipRoot
}
function workLoop(deadline) {
let shouldYield = false // 是否阻塞执行任务
while (nextUnitOfWork && !shouldYield) {
// 执行任务
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// 剩余空闲时间不足一毫秒的时候暂停执行
shouldYield = deadline.timeRemaining() < 1
}
// 生成整个fiber tree之后提交给DOM
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
// 浏览器处于空闲的时候会调用
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
// 1. 添加dom结点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 2. 将每个child转换为fiber结点
const elements = fiber.props.children
reconcileChildren(fiber, elements)
// 3. 返回下一个要生成的fiber结点
// 找child结点
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 找兄弟结点
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 往父节点找
nextFiber = nextFiber.parent
}
}
/**
* 协调算法
* @param {*} wipFiber 当前工作中的fiber结点
* @param {*} elements 当前fiber的子元素
*/
function reconcileChildren(wipFiber, elements) {
let index = 0
// 老结点
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibing = null
while (
index < elements.length ||
oldFiber
) {
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: EFFECT_TAG_UPDATE
}
}
if (element && !sameType) {
// 添加节点
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: EFFECT_TAG_PLACEMENT
}
}
if (oldFiber && !sameType) {
// 删除结点
oldFiber.effectTag = EFFECT_TAG_DELETION
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibing.sibling = newFiber
}
prevSibing = newFiber
index++
}
}
const ReactDOM = {
render
}
export default ReactDOM
新fiber结点的数据结构:
{
parent: null, //父节点
sibling: null, // 下一个兄弟节点
child: null, // 第一子节点
dom: null, // 真实dom节点
type: null, // 节点类型
props: null, // 属性
alternate: null, // 老的fiber结点
effectTag: null, // 标记,根据此标记进行不同的dom的操作
}
step7: 加入函数组件渲染支持
下面,我们要加入函数组件的渲染支持。
先修改一下我们的例子:
// src\index.js
import React from './myReact'
import ReactDOM from './myReact/ReactDOM'
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
ReactDOM.render(element, container)
函数组件和普通结点有两点不同
- 来自函数组件的fiber没有DOM结点
- children是来自函数组件的运行结果而不是直接从props中获取
我们用type的类型来判断是不是函数组件,并创建一个updateFunctionComponent用于更新函数组件,用updateHostComponent更新普通节点。
还有一点需要注意的是,函数组件没有带dom属性,需要找到有dom属性的节点进行新增和删除操作。
更新部分代码如下:
// src\myReact\ReactDOM.js
function performUnitOfWork(fiber) {
// 生成fiber结点
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// 返回下一个要生成的fiber结点
// 找child结点
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 找兄弟结点
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 往父节点找
nextFiber = nextFiber.parent
}
}
/**
* 渲染函数组件
* @param {*} fiber fiber结点
*/
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
/**
* 渲染宿主结点
* @param {*} fiber fiber结点
*/
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
step8: 实现useState hook
为体现hook功能,再改一下我们的例子,加入useState:
// src\index.js
import React, { useState } from './myReact'
import ReactDOM from './myReact/ReactDOM'
function Counter() {
const [state, setState] = useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
ReactDOM.render(element, container)
现在我们来看看如何实现:
给fiber加一个属性hooks,用于存放hooks,因为一个组件可能有多个hooks,所以是个数组。
添加一个全局属性hookIndex,记录hook的索引。
在渲染函数组件的时候初始化hooks和hookIndex
// src\myReact\ReactDOM.js
let hookIndex = null // hook索引
/**
* 渲染函数组件
* @param {*} fiber fiber结点
*/
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
useState实现代码如下:
// src\myReact\ReactDOM.js
export function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial, // 有旧的hook就取旧hook的state,没有就使用用户传入的初始值
queue: []
}
// 模拟多次触发setState
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
// state更新,重新设置wipRoot的值,触发更新
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
}
nextUnitOfWork = wipRoot
deletions = []
}
// 每次调用一次useState,入栈一个hook,索引加1
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
后记
来一张整体的函数调用流程图
好了,现在我们就完成一个简单的react库
现在我们和真实的React做个对比:
- 我们在渲染阶段遍历整棵树,而在React中,会利用启发式算法,跳过没有变化的子树
- 我们在提交阶段也遍历整颗树,React则使用一个链表,只保留和访问有变动的fiber结点
- 我们每次在创建fiber树的时候,都会为每个fiber结点创建一个新的对象,React会从之前的fiber树中回收再利用
- 我们每次更新的时候都会丢弃当前正在工作的树,重新从根节点开始遍历,React会使用过期时间戳标记每次更新,然后使用它来决定哪个更新有更高的优先级
- 我们为实现浏览器并发模式,使用requestIdleCallbackAPI,但是该API的兼容性和稳定性并不好,React使用一套更完善机制代替,被称为调度器(Scheduler)
- 等等...
参考:
github文章地址: