han's bolg - 年糕記

react16源码学习

基于这篇文章,自己实现了一个简单的react,记录笔记如下:

1. 定义 react.createElement(type, props, …children)

  • 注:2020.10.25 使用最新的 create react app,自定义的 react.createElement 无效。

原因:package.json 中的”react-scripts”升级至”4.0.0”,本地启动时会调用 react-dev-jsx-runtime.development.js 中的 jsxWithValidation,而不是自定义的 createElement 方法。

解决方法:将 package.json 中的”react-scripts”降至 3.x.x,会直接调用自定义的 React.createElement 方法。

2. 定义 ReactDOM.render(vdom, container)

  • 根据 vdom.type,创建真实 dom,document.createElement or document.createTextNode.
  • 将 vdom.props 中除 children 之外的所有属性赋给刚创建的 dom 元素。
  • 对于 children 进行递归调用 render 方法
  • container 调用 apppenChild 添加 vdom

3. requestIdleCallback 空闲调度逻辑

ReactDOM.render 只是做了初始化的工作。
剩下的任务是在 requestIdleCallback()中进行的,浏览器会自动在空闲的时候执行,完成 dom diff, 真实 dom 修改的工作

performUnitWork 每个单元执行以下工作内容:

  • 生成新 dom
  • 处理 fiber
  • 返回下一个要操作的 fiber
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function performUnitWork(fiber) {
// 1. 为当前fiber创建dom
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 2. 为当前fiber的儿子元素(仅限这一代,不包含儿子的儿子)更新fiber信息,要跟上次渲染的状态做diff

...
// 3. 找下一个调度任务
// 如果子元素存在,找子元素
if (fiber.child) {
return fiber.child
}
// 子元素不存在,找兄弟元素;
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 没有兄弟元素了,找父元素
nextFiber = nextFiber.parent
}
}

4. fiber 的数据结构,就是一个 jsx+一些链表属性的对象类型

第一个fiber来源自ReactDOM.render(element, document.getElementById('root'))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var element = {
type: '节点类型',
props: {
...props, // 真实dom属性
children: [], // jsxObject类型数组
},
}
var firstFiber = {
dom: document.getElementById('root'), // 真实dom元素
props: {
children: [element],
},
type: 'div', // dom类型
parent: '父fiber',
child: '第一个子fiber',
sibling: '下一个兄弟fiber',
prev: '上一次commit的rootFiber',
effectTag: '更改状态,替换/删除/更新',
hooks: [{
state: '',
queue: []
}]
}

所以fiber的数据结构如下:

1
2
3
4
5
6
7
8
9
10
var fiber = {
dom: DomObject,
props: {
...props, //真实dom属性
children: [jsxObject],
},
parent: '父fiber',
child: '第一个子fiber',
sibling: '下一个兄弟fiber',
}

5. commit 真实 dom 添加操作

1
2
3
4
5
6
7
8
9
10
11
function performUnitWork(fiber) {
// 1. add dom node
if (!fiber.dom) {
createDom(fiber)
}
// 如果在单元执行这里进行真实dom操作,会导致dom执行不连续。所以fiber调用可以空闲完成,但是dom操作必须一气呵成。
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
...
}

当所有 fiber 都执行完毕,就开始操作真实 dom 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 调度任务
function workLoop(deadline) {
while (currentFiber && deadline.timeRemaining() > 1) {
currentFiber = performUnitWork(currentFiber)
}
// 当前已经没有要调度的fiber了,那么就可以操作dom了
if (!currentFiber && rootJsxNode) {
commitDom()
}
requestIdleCallback(workLoop)
}

function commitDom() {
// TODO add nodes to dom
// 先添加第一个子节点的dom,在里面递归操作其他dom
commitWork(rootJsxNode.child)
// 根节点置为空,这样workLoop就停止工作了
rootJsxNode = null
}

function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}

6. 真实 dom 更新和删除操作(dom diff)

  • performUnitWork 第二个任务处理 fiber
    记录上一次渲染后的状态,lastFiber,跟 curFiber 相同层级的虚拟 dom,一一做虚拟 dom diff 阶段,会比较 dom.type:
    • 类型相同,则标记为更新
    • 类型不同,curFiber 存在,则标记为替换(新增也属于替换)
    • 类型不同,lastFiber 存在,则标记为删除。删除 fiber 同时要保存到一个 deletTasks 列表里,在 commitDom 阶段删掉。(因为新的 fiber 里没有保存要删除的 fiber 数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 2. 为当前fiber的儿子元素(仅限这一代,不包含儿子的儿子)更新fiber信息
// 这里要做对比了,不是单纯创建fiber,而是要跟之前的状态做diff
const oldFiber = fiber?.prev?.child
const elements = fiber.props.children
// 用于处理链接的next
let prevSlibing = null
let index = 0
while (index < elements.length || oldFiber) {
const ele = elements[index]
let newFiber = null
// 对比dom元素类型
const isSameType = oldFiber && ele && oldFiber.type === ele.type

// 如果元素相同,则只改参数即可
if (isSameType) {
newFiber = {
...oldFiber,
props: ele.props,
parent: fiber,
prev: oldFiber,
effectTag: 'UPDATE',
}
}
// 新节点存在且元素类型不同,则替换fiber(其实这里就是新增)
if (ele && !isSameType) {
newFiber = {
type: ele.type,
props: ele.props,
parent: fiber,
dom: null,
base: null,
effectTag: 'PLACEMENT',
}
}
// 删除节点
if (oldFiber && !isSameType) {
oldFiber.effectTag = 'DELETION'
// 加到删除任务列表里
deleteTasks.push(oldFiber)
}

// 这里相当于两个数组oldFiber和curFiber一一对比
// 第一个比较完了,开始比较第二个。如何跳到第二个?
// oldFiber = oldFiber.sibing
// curFiber = elements[index++]
if (oldFiber) {
oldFiber = oldFiber.sibling
}

if (index === 0) {
// 第一个子元素,是父fiber的child
fiber.child = newFiber
} else {
// 这里其实就是生成链表的套路,head.next = node, head = node
prevSlibing.sibling = newFiber
}
prevSlibing = newFiber

index++
}
  • commitDom 阶段 批量删除 deletTasks
1
2
3
4
5
6
7
8
9
10
function commitDom() {
// 递归删掉删除列表里的dom
deleteTasks.forEach((deleteFiber) => commitWork(deleteFiber))
// 先添加第一个子节点的dom,在里面递归操作其他dom
commitWork(rootJsxNode.child)
// 保存最终的rootFiber,跟下次做dom diff用
lastRootFiber = rootJsxNode
// 根节点置为空,这样workLoop就停止工作了
rootJsxNode = null
}
  • commitWork 具体的 dom 操作
    • 删除:把旧节点删掉,removeChild
    • 替换:把新节点加进来,appendChild
    • 更新:把原有节点旧属性和新属性对比,旧的去掉,换成新的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function updateDom(dom, prevProps, props) {
// 旧参数遍历
Object.keys(prevProps)
.filter((propName) => propName !== 'children')
// 筛选出新props里没有的属性,去掉
.filter((propName) => !(propName in props))
.forEach((propName) => {
// 去掉事件处理
if (propName.startsWith('on')) {
dom.removeEventListener(
propName.substring(2).toLowerCase(),
prevProps[propName],
false
)
} else {
// 属性置空
dom[propName] = ''
}
})
// 新参数遍历
Object.keys(props)
.filter((propName) => propName !== 'children')
.forEach((propName) => {
// 事件处理
if (propName.startsWith('on')) {
dom.addEventListener(
propName.substring(2).toLowerCase(),
prevProps[propName],
false
)
} else {
dom[propName] = props[propName]
}
})
return dom
}

function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
// 删除节点
if (fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom)
}
// 替换节点
if (fiber.effectTag === 'PLACEMENT' && fiber.dom) {
// todo:替换不需要删掉旧fiber?答:在👆上面已经删除旧节点了
domParent.appendChild(fiber.dom)
}
if (fiber.effectTag === 'UPDATE' && fiber.dom) {
updateDom(fiber.dom, fiber.prev.props, fiber.props)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}

7. 函数组件

1
2
3
4
5
6
7
const App = (props) => (
<div>
<h1>{props.title}</h1>
<p>源码学习</p>
<a href="https://baidu.com" alt=""></a>
</div>
)

等价于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createElement(type, props, ...children) {
delete props.__source
delete props.__self
return {
type,
props: {
...props,
children: children.map((child) => {
return typeof child === 'object' ? child : createTextElement(child)
}),
},
}
}
const App = createElement(() => createElement('div', {}, ...children), {
title: 'hello world',
})

AppjsxObj.type = ()=> createElement('div', {}, ...children)是一个 Function

  • 函数组件有两个问题:
    • 没有 dom 实例
    • children 来源于函数执行返回值,而不是 props.children

所以对于跟 dom 相关的问题,要处理 dom 相关问题。

  • fiber 处理阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 处理函数组件的节点fiber
function updateFunctionComponent(fiber) {
// 函数组件的rootFiber.type=()=>{},是一个function,无法createDom
// if (!fiber.dom) {
// fiber.dom = createDom(fiber)
// }
// 如果在单元执行这里进行真实dom操作,会导致dom执行不连续。所以fiber调用可以空闲完成,但是dom操作必须一气呵成。
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }
// 2. 为当前fiber的儿子元素(仅限这一代,不包含儿子的儿子)更新fiber信息
// 这里要做对比了,不是单纯创建fiber,而是要跟之前的状态做diff
// 函数fiber.props没有children属性。它的子元素等于:
const elements = [fiber.type(fiber.props)]
handleFiber(fiber, elements)
}
  • dom 操作阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function deleteDom(domParent, fiber) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
deleteDom(domParent, fiber.child)
}
}
function commitWork(fiber) {
if (!fiber) {
return
}
// function组件一定要层层找,才能找到父元素dom实例/子元素dom实例
let fiberParent = fiber.parent
// 肯定能找到,因为顶级总会有一个ReactDOM.render(ele, $#root)。$#root是一个dom实例
while (!fiberParent.dom) {
fiberParent = fiberParent.parent
}
const domParent = fiberParent.dom
// 删除节点
if (fiber.effectTag === 'DELETION') {
deleteDom(domParent, fiber)
}
// 替换节点
if (fiber.effectTag === 'PLACEMENT' && fiber.dom) {
domParent.appendChild(fiber.dom)
}
if (fiber.effectTag === 'UPDATE' && fiber.dom) {
updateDom(fiber.dom, fiber.prev.props, fiber.props)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}

8. hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 处理函数组件的节点fiber
function updateFunctionComponent(fiber) {
// 每次都要重置hooks
hookIndex = 0
currentFiber.hooks = []
...
}
function useState(init) {
const oldHooks = currentFiber?.prev?.hooks[hookIndex]
const hook = {
state: oldHooks ? oldHooks.state : init,
queue: []
}
const actionQueue = oldHooks ? oldHooks.queue : []
actionQueue.forEach(newstate => {
hook.state = newstate
})

const setState = newstate => {
hook.queue.push(newstate)
// setState的时候触发调度任务。让workLoop再次工作
// 设置rootJsxNode,否则的话第一次调度结束,rootJsxNode一直为null,无法再次操作dom
rootJsxNode = {
dom: lastRootFiber.dom,
props: lastRootFiber.props,
prev: lastRootFiber,
}
currentFiber = rootJsxNode
deleteTasks = []
}
// 这就直接push?
// 答:fiber的每一次单元调用,都会重置currentFiber.hooks=[],hookIndex=0
// 也就是说,每次都会重置hooks。
// 所以设置const [state,setState] = useState(0),已经设置state为const类型了,下次setState()还可以更新state的值。因为每次都是不同的值
currentFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
  • 问题 1:const [st, setSt] = useState(1), st 是 const 类型,为什么 setSt(2)的时候可以把 st 变成 2 呢?

答:看代码,每一次 useState 执行,都是生成一个新的参数和 set 方法

  • 问题 2:为什么 setSt,会重新引发 render

答:setSt 时,会重新配置 rootFiber,进而引发 workLoop 调度

1
2
3
4
5
6
7
8
9
10
11
12
const setState = (newstate) => {
hook.queue.push(newstate)
// setState的时候触发调度任务。让workLoop再次工作
// 设置rootJsxNode,否则的话第一次调度结束,rootJsxNode一直为null,无法再次操作dom
rootJsxNode = {
dom: lastRootFiber.dom,
props: lastRootFiber.props,
prev: lastRootFiber,
}
currentFiber = rootJsxNode
deleteTasks = []
}
  • 问题 3:setSt 后的,程序执行过程

答:setSt 后,设置 rootFiber -> workLoop() -> performUnitWork(fiber) -> updateFunctionComponent(fiber) -> fiber.type(fiber.props) -> 函数组件从头执行一遍 -> useState() -> hooks.st = newstate -> handleFiber(fiber, elements) -> dom diff -> dom 操作