# 实现一个mini-react
# 前言
- 造轮子系列,更多的是了解思想
react
更新机制- fiber架构是什么
hooks
如何实现- 源码预览链接 (opens new window)
# 入口
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'
const container = document.querySelector('#root')
ReactDOM.render(<App />, container)
1
2
3
4
5
6
7
2
3
4
5
6
7
App.jsx
function App () {
return <div>Hello, react!</div>
}
export default App;
1
2
3
4
5
6
2
3
4
5
6
上面这一小段代码包含几个点
- 引入
react.js
这个核心包,最终jsx文件会被babel转译成react识别的语法,最终调用React.createElement
等
2.
react-dom
这个库主要作用管理DO
M组件,提供一些常用的api,如render、findDOMNode、unmountComponentAtNode等
- 本篇主要介绍render这个api
# render函数基本实现
function render(element, container) {
const dom =
element.type === 'TEXT_ELEMENT'
? createTextElementDom()
: document.createElement(element.type);
//将属性赋值到dom元素上
const isProperty = (key) => key !== 'children';
Object.keys(element?.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = element.props[name];
});
//递归遍历 子节点塞入html文档中
// TODO:这一步需要优化
element?.props?.children?.forEach((child) => render(child, dom));
container.appendChild(dom);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# createElement方法
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
};
}
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children?.map((child) => {
return typeof child === 'object' ? child : createTextElement(child);
})
}
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
调用
import MyReact from './mini-react/';
const element = MyReact.createElement(
'div',
{
id: 'app',
title: 'my-react'
},
'实现一个mini-react',
MyReact.createElement('a', null, 'hello')
);
console.log('element', element);
const container = document.querySelector('#root');
MyReact.render(element, container);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面这样调用最基本的就实现了一个简单的页面渲染函数,页面可以挂载
但是我们发现几个问题
- 如果DOM树层级很深,主线程一直被js占用,那么很容易就会造成页面渲染的卡顿
- 不能打断,浏览器空闲时间无法捕捉
由此引发下文并发模式
# 优化并发模式渲染(concurrentModeAndFiber)
- react团队在设计的时候,遵循虚拟DOM -> fiber -> 真实DOM渲染。
- fiber是react团队提出的数据模型(DSL深度优先遍历节点)
3. 组件在render的时候走schecule + reconcile(diff算法)
- 并发模式仿照react流程,没有react那么精细,更多的学习大家自行查阅文档源码等
let nextUnitOfWork = null;
...
function workLoop(deadline) {
let shouldYield = true; //是否超越当前桢剩余时间
//下一个工作单元存在并且浏览器可以继续渲染 不会阻塞浏览器渲染
while (nextUnitOfWork && shouldYield) {
//得到浏览器当前桢剩余时间
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() > 1;
// 例如开启 debugger
// 之所以优化这里是当网络出现异常,渲染会被打断
//TODO: 优化我们希望遍历完所有的fiber树 全部提交一次性渲染
//优化点见renderCommit.js
}
//requestIdleCallback 会为参数注入一个当前桢剩余时间方法
requestIdleCallback(workLoop);
}
// requestIdleCallback这个方法可以侦测浏览器空余时间
requestIdleCallback(workLoop);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
nextUnitOfWork
下一个工作单元 可以理解为一个fiber结构
requestIdleCallback
不会阻塞浏览器的渲染,这个方法可以侦测浏览器空余时间只有在浏览器剩余时间满足的情况下渲染回调函数。会为回调函数注入形参表示一个当前桢剩余时间方法 可以参照下图理解
function createDom(element) {
const dom =
element.type === 'TEXT_ELEMENT'
? createTextElementDom()
: document.createElement(element.type);
//将属性赋值到dom元素上
const isProperty = (key) => key !== 'children';
Object.keys(element?.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = element.props[name];
});
return dom;
}
createDom不再操作dom,而是把逻辑放在了`performUnitOfWork`这个方法中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
performUnitOfWork
function performUnitOfWork(fiber) {
// reactELement 转换成一个真实的dom
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
if (fiber.parent) {
fiber.parent?.dom.appendChild(fiber.dom);
}
// 为当前的节点创建子节点的fiber
// parent \ child \ sibling
let nextSibling = null;
let elements = fiber?.props?.children;
elements?.forEach((child, index) => {
const newFiber = {
type: child.type,
parent: fiber,
props: child.props,
dom: null
};
if (index === 0) {
//第一个子节点 挂载到父节点child上
fiber.child = newFiber;
} else {
// 子节点的兄弟节点
nextSibling.sibling = newFiber;
}
nextSibling = newFiber;
});
// 处理下一个单元
//return 下一个任务单元
if (fiber.child) {
//子节点
return fiber.child;
}
//递归兄弟节点
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
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
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
而render函数改造
为
function render(element, container) {
// 优先创建一个链表头指向根节点
nextUnitOfWork = {
dom: container,
props: {
children: [element]
}
};
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
总结几个点
- 通过·requestIdleCallback·这个api监听
workLoop
函数workLoop
根据浏览器剩余时间响应和当前有没有剩余任务需要执行循环performUnitOfWork
这个方法performUnitOfWork
这个函数通过遍历子节点,并为每个子节点创建一个fiber结构的数据(链表)用于追踪节点间的信息- 通过深度优先遍历子节点,兄弟节点...一次溯源到根节点完成一次完整的渲染
思考问题
- 上述确实一定程度上解决了并发渲染问题
- 但是节点渲染不可规避因素会造成一些潜在的问题,比如节点是分片式渲染,如遇到网络问题,或者其
debug
模式还是会有影响
# 一次性commit
workLoop.js
//当没有工作单元且定义一个用于追踪fiber树 有没有遍历完
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
1
2
3
4
2
3
4
render.js
function render(element, container) {
// 优先创建一个链表头指向根节点
wipRoot = {
dom: container,
props: {
children: [element]
}
};
//这里建立了一个映射
nextUnitOfWork = wipRoot;
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
commitRoot
function commitWork(fiber) {
if (!fiber) {
return;
}
//获取父节点
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child); //循环遍历子节点
commitWork(fiber.sibling); //遍历兄弟节点
}
function commitRoot() {
//做真实dom渲染操作
commitWork(wipRoot.child);
wipRoot = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
总结
- 一定程度上解决了首次渲染并发模式带来的问题
- 如果更新阶段呢?一次都要循环遍历吗?
- reconcile(diff算法)
# reconcile
performUnitOfWork
/ 处理下一个单元
//return 下一个任务单元
// diff比较
reconcileChildren(fiber, elements);
1
2
3
4
5
2
3
4
5
reconcileChildren
function reconcileChildren(wipFiber, elements) {
//FIXME: 新老fiber树比较
let index = 0;
let prevSibling = null;
//alternate定义为缓存旧节点fiber
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
while (index < elements.length || !!oldFiber) {
const child = elements[index];
console.log('child', child);
// 简易比较
let newFiber = null;
const sameType = oldFiber && child && child.type === oldFiber.type;
//同节点比较
if (sameType) {
newFiber = {
type: oldFiber.type,
props: child.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE'
};
}
//新增
if (!sameType && child) {
newFiber = {
type: child.type,
props: child.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: 'PLACEMENT'
};
}
//删除
if (!sameType && oldFiber) {
//deletion操作
oldFiber.effectTag = 'DELETION';
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
//第一个子节点 挂载到父节点child上
wipFiber.child = newFiber;
} else {
// 子节点的兄弟节点
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
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
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
commitWork
function commitWork(fiber) {
if (!fiber) {
return;
}
// debugger;
//获取父节点
const domParent = fiber.parent.dom;
switch (fiber.effectTag) {
case 'PLACEMENT':
!!fiber.dom && domParent.appendChild(fiber.dom);
break;
case 'UPDATE':
!!fiber.dom && updateDom(fiber.dom, fiber.alternate, fiber.props);
break;
case 'DELETION':
!!fiber.dom && domParent.removeChild(fiber.dom);
break;
default:
break;
}
commitWork(fiber.child); //循环遍历子节点
commitWork(fiber.sibling); //遍历兄弟节点
}
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
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
本文实现的reconcile细粒化程度远不如react那么精细,只是粗暴比对是否是相同节点、增删改灯操作