├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── index.css
├── index.js
└── mini-react
│ ├── commit.js
│ ├── fiber.js
│ ├── react-dom.js
│ ├── react.js
│ └── reconciler.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 前言(必看)
2 | 本章将基于 react17 版本实现一个 mini react,涵盖了 react 源码所有的知识点例如 fiber 架构、render 和 commit 阶段、diff 算法、类组件、函数组件、hooks 等绝大部分 react 原理的知识点。
3 |
4 | 强烈建议对照我仓库的 [commit记录](https://github.com/zh-lx/mini-react/commits/main) 来看本文,里面有本文每一步的代码提交,你可以清晰的看到每一步的代码变动情况:
5 | 
6 | 本文的每节标题中带有 👉 标志的即有对应的 commit 记录,点击标题链接也可直接跳转
7 |
8 | ## [一: 初始化项目 👉](https://github.com/zh-lx/mini-react/commit/2fbbb63bf6686fd301f1418dc07a6bfe255db895)
9 | 首先我们通过 react 官方的脚手架 `create-react-app` 初始化一个 react 项目,在终端执行如下指令:
10 | ```
11 | create-react-app mini-react
12 | ```
13 | 然后执行 `cd ./mini-react` 进入到我们的项目,将多余的文件和代码移除,只保留 `index.js` 和 `index.css` 文件即可,初始化后的项目目录结构如下:
14 | ```
15 | 📦mini-react
16 | ┣ 📂public
17 | ┣ 📂src
18 | ┃ ┣ 📜index.css
19 | ┃ ┗ 📜index.js
20 | ┣ 📜.gitignore
21 | ┣ 📜package.json
22 | ┣ 📜README.md
23 | ┗ 📜yarn.lock
24 | ```
25 | `index.js` 文件中,包含一个 jsx 结构,它包含了类组件、函数组件、普通 dom、条件渲染和列表渲染等多种类型的 jsx 内容(后面会用于我们渲染时要考虑的多种情况的处理),然后通过 `ReactDOM.render`将其渲染在页面上,代码如下:
26 | ```js
27 | import { Component } from 'react';
28 | import ReactDOM from 'react-dom';
29 | import './index.css';
30 |
31 | class ClassComponent extends Component {
32 | constructor(props) {
33 | super(props);
34 | this.state = {};
35 | }
36 |
37 | render() {
38 | return (
39 |
40 |
this is a class Component
41 |
prop value is: {this.props.value}
42 |
43 | );
44 | }
45 | }
46 |
47 | function FunctionComponent(props) {
48 | return (
49 |
50 |
this is a function Component
51 |
prop value is: {props.value}
52 |
53 | );
54 | }
55 |
56 | const jsx = (
57 |
58 |
59 |
60 |
61 |
mini react link
62 |
this is a red p
63 |
64 | {true &&
condition true
}
65 | {false &&
condition false
}
66 |
{
70 | alert('hello');
71 | }}
72 | />
73 |
74 |
75 |
76 | {['item1', 'item2', 'item3'].map((item) => (
77 |
{item}
78 | ))}
79 |
80 |
81 | );
82 |
83 | ReactDOM.render(jsx, document.getElementById('root'));
84 | ```
85 | `index.css` 内容主要是给各种类名添加对应的样式,用以在页面的视觉效果上区分它们的层级关系,代码如下:
86 | ```css
87 | .deep1-box {
88 | border: 1px solid rgb(146, 89, 236);
89 | padding: 8px;
90 | }
91 | .class-component {
92 | border: 1px solid rgb(228, 147, 147);
93 | padding: 8px;
94 | }
95 | .function-component {
96 | margin-top: 8px;
97 | padding: 8px;
98 | border: 1px solid rgb(133, 233, 120);
99 | }
100 | .deep2-box-1 {
101 | margin-top: 8px;
102 | padding: 8px;
103 | border: 1px solid rgb(233, 224, 107);
104 | }
105 | .deep3-box {
106 | padding: 8px;
107 | border: 1px solid rgb(55, 189, 241);
108 | }
109 | .deep2-box-2 {
110 | margin-top: 8px;
111 | padding: 8px;
112 | border: 1px solid rgb(23, 143, 77);
113 | }
114 | ```
115 | 如此一来,项目的初始化就完成了,页面的效果如图所示:
116 | 
117 | 接下来进入到我们相关的源码实现环节。
118 | ## 二: 实现 ReactDOM.render
119 | 我们创建项目用的 react 版本是 17.0.2,所以我们上面的 jsx 内容在运行时已经被 babel 编译为了 `React.Element` 的形式,不需要像 React16.x 及之前的版本需要 `React.createElement` api 进行转换了(这方面的内容可以看我之前的文章 [jsx 转换及 React.createElement](https://juejin.cn/post/7015855371847729166))。
120 |
121 | 所以我们不需要额外实现 `React.createElement` 这个 api 了,直接从 `ReactDOM.render` 的实现开始。
122 |
123 | ### [创建 ReactDOM.render 👉](https://github.com/zh-lx/mini-react/commit/a844706548f3708b7b4ba1647edb67beacf3425c)
124 | 首先我们在 src 目录下创建一个名为 mini-react 的文件夹,用于保存我们自己实现的 react 源码,然后在 `/src/mini-react` 目录下创建一个 `react-dom.js` 文件,在里面导出挂载有 `render` 函数的 `ReactDOM` 对象,`render` 函数要做的事情就是接收 element 和 container 两个参数,并将 element 渲染为真实 dom 挂载到 container 上。
125 | `src/mini-react/react-dom.js` 代码如下:
126 | ```js
127 | function render(element, container) {
128 | const dom = renderDom(element);
129 | container.appendChild(dom);
130 | }
131 |
132 | // 将 React.Element 渲染为真实 dom
133 | function renderDom(element) {}
134 |
135 | const ReactDOM = {
136 | render,
137 | };
138 | export default ReactDOM;
139 | ```
140 | ### [根据 React.element 创建 dom 👉](https://github.com/zh-lx/mini-react/commit/b5ef737086a416d8daf6546f3c7765e73a412413)
141 | 接下来我们要做的在 `renderDom` 函数中实现根据 React.element 创建真实 dom,React.element 的结构在之前 [jsx 转换及 React.createElement](https://juejin.cn/post/7015855371847729166#heading-3) 篇中讲过,这里我们也可以再在控制台打印一下 jsx 的内容:
142 | 
143 | 所以我们要将 React.element 渲染为 dom,主要就是看 React.element 本身的类型以及它的 `type` 和 `props` 参数,所以接下来根据我们上面的 jsx 不同类型的元素,在将 React.element 转换为真实 dom 时要考虑如下情况:
144 | 1. 当 element 为假 (!element === true) 且不为 0 时,表示条件渲染的假值,不进行渲染(或者通过 `document.createDocumentFragment` 创建空的文档节点)
145 | 2. 当 element 自身为 string 类型时,表示为文本节点,通过调用 `document.createTextNode` 进行创建
146 | 3. 当 element 自身为 number 类型时,将其转换为 string 类型,然后通过调用 `document.createTextNode` 创建文本节点
147 | 4. 当 element 自身为 Array 类型是,表示为数组(例如 map 返回的元素数组),需要通过一个 fragment 挂载所有的数组元素,再将 fragment 挂载到对应的父节点下
148 | 5. 当 element 的 `type` 为 string 类型时,表示为常规的 dom 元素,直接调用 `document.createElement` 创建 dom。
149 | 6. 当 element 的 `type` 为 function 类型时,表示类组件或者函数组件,需要针对处理
150 | 7. 如果 element 的 children 不为 null,需要递归创建子元素
151 | 8. 还有其他的情况如 react 的一些内置组件如 `React.fragment`、`Context`、`Portal` 等,我们以实现 react 主功能为主,暂时不考虑这些情况了
152 |
153 | 完整的 `renderDom` 的内容如下:
154 | ```js
155 | // 将 React.Element 渲染为真实 dom
156 | function renderDom(element) {
157 | let dom = null; // 要返回的 dom
158 |
159 | if (!element && element !== 0) {
160 | // 条件渲染为假,返回 null
161 | return null;
162 | }
163 |
164 | if (typeof element === 'string') {
165 | // 如果 element 本身为 string,返回文本节点
166 | dom = document.createTextNode(element);
167 | return dom;
168 | }
169 |
170 | if (typeof element === 'number') {
171 | // 如果 element 本身为 number,将其转为 string 后返回文本节点
172 | dom = document.createTextNode(String(element));
173 | return dom;
174 | }
175 |
176 | if (Array.isArray(element)) {
177 | // 列表渲染
178 | dom = document.createDocumentFragment();
179 | for (let item of element) {
180 | const child = renderDom(item);
181 | dom.appendChild(child);
182 | }
183 | return dom;
184 | }
185 |
186 | const {
187 | type,
188 | props: { children },
189 | } = element;
190 |
191 | if (typeof type === 'string') {
192 | // 常规 dom 节点的渲染
193 | dom = document.createElement(type);
194 | } else if (typeof type === 'function') {
195 | // React组件的渲染
196 | if (type.prototype.isReactComponent) {
197 | // 类组件
198 | const { props, type: Comp } = element;
199 | const component = new Comp(props);
200 | const jsx = component.render();
201 | dom = renderDom(jsx);
202 | } else {
203 | // 函数组件
204 | const { props, type: Fn } = element;
205 | const jsx = Fn(props);
206 | dom = renderDom(jsx);
207 | }
208 | } else {
209 | // 其他情况暂不考虑
210 | return null
211 | }
212 |
213 | if (children) {
214 | // children 存在,对子节点递归渲染
215 | const childrenDom = renderDom(children);
216 | if (childrenDom) {
217 | dom.appendChild(childrenDom);
218 | }
219 | }
220 |
221 | return dom;
222 | }
223 | ```
224 | 然后在我们的 `/src/index.js` 中引入我们自己写的 `react-dom.js` 替换 `react-dom` 包:
225 | ```diff
226 | import React from 'react';
227 | - import ReactDOM from 'react-dom';
228 | + import ReactDOM from './mini-react/react-dom';
229 | import './index.css';
230 | // ...
231 | ```
232 | 运行之后可以看到,现在我们的页面中已经渲染出了相关的 dom 元素了:
233 | 
234 |
235 | ### [更新 dom 属性 👉](https://github.com/zh-lx/mini-react/commit/79a61e710053bcbe58e7bdaf2f3295654a49f904)
236 | 上一步我们的页面中已经渲染出了相关的 dom 元素,但是元素的属性例如我们给 `a` 标签添加的 href、`p` 标签的 style、以及元素的 classname 等等都没生效,而且我们的 `input[button]` 显示的是输入框,说明我们元素上面挂载的属性都没有生效。
237 |
238 | 所以接下来我们需要对元素的各种属性进行挂载,通过打印我们可以得知,元素的属性都是在 React.element 的 `props` 上:
239 | 
240 | 针对元素 `props` 中的各种属性,我们需要考虑如下情况:
241 | 1. 如果是 `children`,表示是元素的子元素,不是其属性
242 | 2. 如果是 `className`,要将其转换为元素对应的 class
243 | 3. 如果是 `style`,需要将对象中的键值对取出一一更新到元素的样式上
244 | 4. 如果是以 `on` 开头的属性,说明是事件,需要作为事件处理
245 | 5. 其他属性直接挂载
246 | 我们用一个 `updateAttributes` 函数处理元素属性的更新,在 `react-dom.js` 文件中添加如下代码:
247 | ```js
248 | // 更新 dom 属性
249 | function updateAttributes(dom, attributes) {
250 | Object.keys(attributes).forEach((key) => {
251 | if (key.startsWith('on')) {
252 | // 事件的处理
253 | const eventName = key.slice(2).toLowerCase();
254 | dom.addEventListener(eventName, attributes[key]);
255 | } else if (key === 'className') {
256 | // className 的处理
257 | const classes = attributes[key].split(' ');
258 | classes.forEach((classKey) => {
259 | dom.classList.add(classKey);
260 | });
261 | } else if (key === 'style') {
262 | // style处理
263 | const style = attributes[key];
264 | Object.keys(style).forEach((styleName) => {
265 | dom.style[styleName] = style[styleName];
266 | });
267 | } else {
268 | // 其他属性的处理
269 | dom[key] = attributes[key];
270 | }
271 | });
272 | }
273 | ```
274 | 然后在 `renderDom` 函数中调用 `updateAttributes` 函数:
275 | ```diff
276 | function renderDom(element) {
277 | // ...
278 | const {
279 | type,
280 | - props: { children },
281 | + props: { children, ...attributes },
282 | } = element
283 | // ...
284 | + updateAttributes(dom, attributes);
285 | return dom;
286 | }
287 | ```
288 | 再来看我们的页面效果,元素属性都已经挂载上去了:
289 | 
290 | ## 三: 实现 fiber 架构
291 | 我们上面 `renderDom` 的代码中,有一个问题,在类组件
292 | 、函数组件、列表渲染以及 children 等情况下,我们都会去递归调用 `renderDom` 函数。如果我们的组件树特别的大,那么我们的 mini-react 会一直递归去渲染,导致整个渲染完成的时间过长。由于 js 是单线程,如果此时有更高级别的任务例如用户输入、动画等,这些任务就需要等待,那么用户视觉上就会感到页面卡顿。
293 |
294 | 这就来到了 react 的核心概念之一 —— fiber,我们将大的渲染任务拆分为多个小的渲染任务,每个小任务都是一个工作单元,用 fiber 结构表示这一个工作单元,fiber 与 fiber 之间构成了一颗 fiber 树。(如果你不知道什么是 fiber,可以看我前面的文章 [深入理解 fiber](https://juejin.cn/post/7016512949330116645))。然后在浏览器的每一帧优先执行高优先级任务,空闲时间去执行低优先级任务。
295 |
296 | [深入理解 fiber](https://juejin.cn/post/7016512949330116645) 中讲过,fiber 与 fiber 之间通过 `child`、`sibling`、`return` 几个字段相互连接构成了一颗 fiber 树。react 处理任务时,会从 root fiber 开始,采用深度优先遍历的策略处理 fiber:处理完当前 fiber 后,如果有 child,则继续处理 child;如果没有 child,则处理其 sibling;当一个 fiber 的child 和 sibling 都处理完后,通过 return 返回上级节点继续处理。如我们此应用中的 jsx 结构,对应的 fiber 树结构如下:
297 | 
298 | 上图中箭头上的数字就是 fiber 的执行顺序。
299 | ### [创建 rootFiber 和 nextUnitOfWork 👉](https://github.com/zh-lx/mini-react/commit/c6441df9abef4eb09f65be2809789d2cd77d44e2)
300 | 我们在 `/src/mini-react` 目录下新建 `fiber.js` 文件,用来存储和 fiber 相关的实现代码。首先因为我们是深度优先遍历去进行迭代处理任务单元及 fiber,所以我们需要一个全局的 `nextUnitOfWork` 变量,作为下一个要处理的任务单元。
301 |
302 | 然后我们说过了 fiber 的迭代是从 root fiber 开始的,因此我们需要根据 `ReactDOM.render` 接收的 `element` 和 `container` 参数,创建一个 `rootFiber`,指向 root fiber。每个 fiber 上都需要挂载 `stateNode` 和 `element` 属性,`stateNode` 指向根据此 fiber 创建的真实 dom 节点,用于渲染,`element` 指向 fiber 所对应的 `React.element`。`rootFiber` 创建好之后,将 `nextUnitOfWork` 指向它,它会作为第一个要处理的任务单元。
303 |
304 | `/src/mini-react/fiber.js` 代码如下:
305 | ```js
306 | let nextUnitOfWork = null;
307 | let rootFiber = null;
308 |
309 | // 创建 rootFiber 作为首个 nextUnitOfWork
310 | export function createRoot(element, container) {
311 | rootFiber = {
312 | stateNode: container, // 记录对应的真实 dom 节点
313 | element: {
314 | // 挂载 element
315 | props: { children: [element] },
316 | },
317 | };
318 | nextUnitOfWork = rootFiber;
319 | }
320 | ```
321 | 然后在 `/src/mini-react/react-dom.js` 文件引入 `createRoot` 函数并在 `render` 函数中调用,传入 element 和 container 去创建 root fiber:
322 | ```diff
323 | + import { createRoot } from './fiber';
324 |
325 | function render(element, container) {
326 | - const dom = renderDom(element);
327 | - container.appendChild(dom);
328 | + createRoot(element, container);
329 | }
330 | ```
331 | ### [递归改成迭代 👉](https://github.com/zh-lx/mini-react/commit/e6967c0374060eb110e83469e1b696c6ffc38855)
332 | #### 去掉递归逻辑
333 | 接下里我们要将递归逻辑改成迭代去执行,所以我们先将 `renderDom` 中所有的递归逻辑去掉,并将其导出以便后面我们再 `/src/mini-react/fiber.js` 文件中引用:
334 | ```diff
335 | // 将 React.Element 渲染为真实 dom
336 | function renderDom(element) {
337 | //...
338 |
339 | - if (Array.isArray(element)) {
340 | - // 列表渲染
341 | - dom = document.createDocumentFragment();
342 | - for (let item of element) {
343 | - const child = renderDom(item);
344 | - dom.appendChild(child);
345 | - }
346 | - return dom;
347 | - }
348 |
349 | const {
350 | type,
351 | props: { children },
352 | } = element;
353 |
354 | if (typeof type === 'string') {
355 | // 常规 dom 节点的渲染
356 | dom = document.createElement(type);
357 | - } else if (typeof type === 'function') {
358 | - // React组件的渲染
359 | - if (type.prototype.isReactComponent) {
360 | - // 类组件
361 | - const { props, type: Comp } = element;
362 | - const component = new Comp(props);
363 | - const jsx = component.render();
364 | - dom = renderDom(jsx);
365 | - } else {
366 | - // 函数组件
367 | - const { props, type: Fn } = element;
368 | - const jsx = Fn(props);
369 | - dom = renderDom(jsx);
370 | - }
371 | } else {
372 | // 其他情况暂不考虑
373 | return null
374 | }
375 |
376 | - if (children) {
377 | - // children 存在,对子节点递归渲染
378 | - const childrenDom = renderDom(children);
379 | - if (childrenDom) {
380 | - dom.appendChild(childrenDom);
381 | - }
382 | - }
383 |
384 | // ...
385 | }
386 | ```
387 | #### 根据 fiber 创建 dom
388 | 然后我们再 `/src/mini-react/fiber.js` 文件中引入 `renderDom` 函数,并新创建一个 `performUnitOfWork` 函数,里面包含迭代处理 fiber 的逻辑。
389 |
390 | 首先我们要根据 fiber 去创建 dom,当 fiber 的 `stateNode` 属性为空时,表示还没有对其创建 dom,所以我们调用 `renderDom` 函数,根据 fiber 的 `element` 属性去创建对应的 dom,并将其挂载到父节点下。
391 |
392 | 父节点根据 fiber 的 `return` 属性去寻找父 fiber,值得注意的是,由于我们在 `renderDom` 中去除了迭代逻辑后,在 React 组件或者条件渲染为假值时返回的 dom 会为空。所以我们只有在创建好的 `stateNode` 不为空时才进行挂载,同样的道理,向上通过 `return` 寻找的父 fiber 的 `stateNode` 也可能为空,这种情况我们继续通过 `return` 向上寻找,直到找到 `stateNode` 不为空的 fiber 节点再进行挂在即可。
393 |
394 | 代码如下:
395 |
396 | ```js
397 | import { renderDom } from './react-dom';
398 |
399 | // ...
400 |
401 | // 执行当前工作单元并设置下一个要执行的工作单元
402 | function performUnitOfWork(workInProgress) {
403 | if (!workInProgress.stateNode) {
404 | // 若当前 fiber 没有 stateNode,则根据 fiber 挂载的 element 的属性创建
405 | workInProgress.stateNode = renderDom(workInProgress.element);
406 | }
407 | if (workInProgress.return && workInProgress.stateNode) {
408 | // 如果 fiber 有父 fiber且有 dom
409 | // 向上寻找能挂载 dom 的节点进行 dom 挂载
410 | let parentFiber = workInProgress.return;
411 | while (!parentFiber.stateNode) {
412 | parentFiber = parentFiber.return;
413 | }
414 | parentFiber.stateNode.appendChild(workInProgress.stateNode);
415 | }
416 | }
417 | ```
418 | #### 构造 fiber 树
419 | 现在我们只有 root fiber 一个 fiber,我们需要构造 fiber 树结构,所以要根据 React.element 去创建对应的 fiber,并通过 `child`、 `sibling` 和 `return` 这几个字段的形成 fiber 树。父子关系除了 React.element 有 `children` 属性这种情况外,React 组件以及列表渲染,也会构成父子关系。所以我们做如下考虑:
420 | 1. 当 React.element 的 `type` 属性是 `function` 时,表示 react 组件,我们将其渲染后所得到的 jsx 作为 children 处理。
421 | 2. 如果 React.element 的 `type` 属性是 `Array`,表示列表渲染,此时 array 这个节点时没有意义的,不需要形成 fiber,所以我们直接将 array 中的子节点打平放到与 array 同级的 children 数组中进行处理,生成对应 fiber
422 | 3. 当前 fiber 的 element 属性的 `children` 不为空时,根据 children 去迭代构建 fiber 树
423 |
424 | 上面三种情况,无论 children 是一个节点还是多个节点的数组,为了代码简洁我们最终都将其处理为数组形式。然后 children 数组的第一个节点生成的 fiber 通过当前 fiber 的 `child` 属性连接到 fiber 树中,其他的 fiber 通过上一个子 fiber 的 `sibling` 属性链接。
425 |
426 | 代码如下:
427 | ```js
428 | // 执行当前工作单元并设置下一个要执行的工作单元
429 | function performUnitOfWork(workInProgress) {
430 | // 根据fiber创建 dom
431 | // ...
432 |
433 | let children = workInProgress.element?.props?.children;
434 | let type = workInProgress.element?.type;
435 |
436 | if (typeof type === 'function') {
437 | // 当前 fiber 对应 React 组件时,对其 return 迭代
438 | if (type.prototype.isReactComponent) {
439 | // 类组件,通过生成的类实例的 render 方法返回 jsx
440 | const { props, type: Comp } = workInProgress.element;
441 | const component = new Comp(props);
442 | const jsx = component.render();
443 | children = [jsx];
444 | } else {
445 | // 函数组件,直接调用函数返回 jsx
446 | const { props, type: Fn } = workInProgress.element;
447 | const jsx = Fn(props);
448 | children = [jsx];
449 | }
450 | }
451 |
452 | if (children || children === 0) {
453 | // children 存在时,对 children 迭代
454 | let elements = Array.isArray(children) ? children : [children];
455 | // 打平列表渲染时二维数组的情况(暂不考虑三维及以上数组的情形)
456 | elements = elements.flat();
457 |
458 | let index = 0; // 当前遍历的子元素在父节点下的下标
459 | let prevSibling = null; // 记录上一个兄弟节点
460 |
461 | while (index < elements.length) {
462 | // 遍历子元素
463 | const element = elements[index];
464 | // 创建新的 fiber
465 | const newFiber = {
466 | element,
467 | return: workInProgress,
468 | stateNode: null,
469 | };
470 | if (index === 0) {
471 | // 如果下标为 0,则将当前 fiber 设置为父 fiber 的 child
472 | workInProgress.child = newFiber;
473 | } else {
474 | // 否则通过 sibling 作为兄弟 fiber 连接
475 | prevSibling.sibling = newFiber;
476 | }
477 | prevSibling = newFiber;
478 | index++;
479 | }
480 | }
481 | }
482 | ```
483 |
484 | #### 设置下一个工作单元
485 | 如我们这一节的开头所说,fiber 树的遍历采用深度优先遍历,如果当前 fiber 有 `child`,则设置 `child` 作为下一个工作单元;若无 `child` 但是有 `sibling`,则设置 `sibling` 作为下一个工作单元;如果都没有则深度优先遍历通过 `return` 返回父 fiber。代码如下:
486 | ```js
487 | function performUnitOfWork(fiber) {
488 | // 根据 fiber 创建 dom
489 | // ...
490 |
491 | // 构建 fiber 树
492 | // ...
493 |
494 | // 设置下一个工作单元
495 | if (workInProgress.child) {
496 | // 如果有子 fiber,则下一个工作单元是子 fiber
497 | nextUnitOfWork = workInProgress.child;
498 | } else {
499 | let nextFiber = workInProgress;
500 | while (nextFiber) {
501 | if (nextFiber.sibling) {
502 | // 没有子 fiber 有兄弟 fiber,则下一个工作单元是兄弟 fiber
503 | nextUnitOfWork = nextFiber.sibling;
504 | return;
505 | } else {
506 | // 子 fiber 和兄弟 fiber 都没有,深度优先遍历返回上一层
507 | nextFiber = nextFiber.return;
508 | }
509 | }
510 | if (!nextFiber) {
511 | // 若返回最顶层,表示迭代结束,将 nextUnitOfWork 置空
512 | nextUnitOfWork = null;
513 | }
514 | }
515 | }
516 | ```
517 | ### [创建 workLoop 👉](https://github.com/zh-lx/mini-react/commit/5b5c5faa7faa354d091dff1ae7e3c4cab1128322)
518 | 现在我们迭代处理的逻辑都实现完成了,那么我们在什么时间出执行迭代逻辑呢?我们要在 `/src/mini-react/fiber.js` 中创建一个名为 `workLoop` 函数,这个函数中我们会浏览器每帧的空闲时间段迭代处理 `nextUnitOfWork`,若一帧处理不完,则中断当前迭代,留到下一帧继续处理。代码如下:
519 | ```js
520 | // 处理循环和中断逻辑
521 | function workLoop(deadline) {
522 | let shouldYield = false;
523 | while (nextUnitOfWork && !shouldYield) {
524 | // 循环执行工作单元任务
525 | performUnitOfWork(nextUnitOfWork);
526 | shouldYield = deadline.timeRemaining() < 1;
527 | }
528 | requestIdleCallback(workLoop);
529 | }
530 | ```
531 | 我们使用 [requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback) 函数在浏览器每帧空闲时期去调用回调函数 `workLoop`,`requestIdleCallback`会给回调函数传入一个 deadline 参数我们可以使用它来检查当前帧还有多少时间浏览器空闲时间。我们用一个 `shouldYied` 的变量表示是否应该中断当前循环,当 `deadline.timeRemaining() < 1` 时, `shouldYied` 为 true,会中断当前迭代,留到下一帧再继续执行。(由于 `requestIdleCallback` 执行较慢及兼容性问题,React 现在不再使用 `requestIdleCallback` 了,而是自己实现了类似的功能,不过这里我们为了方便还是直接使用就行,思想上是相同的。)
532 |
533 | 最后我们在 `/src/mini-react/fiber.js` 中初始化通过 `requestIdleCallback` 去调用 `workLoop` 就大功告成了:
534 | ```js
535 | requestIdleCallback(workLoop);
536 | ```
537 | ## [四: render 和 commit 分离 👉](https://github.com/zh-lx/mini-react/commit/fce38e53a0980deaab6685f9c2073c2e5058d1a8)
538 | 上面的代码中还有一个问题,看下面这一段代码,我们在迭代的过程中,是从 root fiber 开始向子 fiber 迭代的,每处理完一个 fiber,就创建相应 dom 挂载到页面上。但是我们的迭代任务是可中断的,如果中途中断,那么在页面上用户就会看到不完整的 ui:
539 | ```js
540 | function performUnitOfWork(fiber) {
541 | // ...
542 |
543 | if (workInProgress.return && workInProgress.stateNode) {
544 | // 如果 fiber 有父 fiber且有 dom
545 | // 向上寻找能挂载 dom 的节点进行 dom 挂载
546 | let parentFiber = workInProgress.return;
547 | while (!parentFiber.stateNode) {
548 | parentFiber = parentFiber.return;
549 | }
550 | parentFiber.stateNode.appendChild(workInProgress.stateNode);
551 | }
552 |
553 | // ...
554 | }
555 | ```
556 | 这并不是我们理想的效果,我们可以考虑将所有的 dom 都创建完成之后再挂载到页面上。
557 |
558 | 这就来到了 react 的 render 和 commit 阶段,我们在 render 阶段去只处理工作单元,创建 dom 但是不挂载 dom,等到所有的工作单元全部处理完成之后,再在 commit 阶段同步执行 dom 的挂载。
559 |
560 | 所以总结下我们要做的工作如下:
561 | 1. `performUnitOfWork` 中移除 dom 挂载的操作,只处理 fiber 创建对应 dom 但是并不挂载
562 | 2. 实现一个 `commitRoot` 函数,执行 dom 的挂载操作,这个阶段是同步执行的,不可被打断
563 | 3. 在 `workLoop` 中,当 nextUnitOfWork 为 null 且 `rootFiber` 存在时,表示 render 阶段执行结束,开始调用 `commitRoot` 函数进入 commit 阶段。
564 |
565 | 在 `/src/mini-react` 文件夹下新建 `commit.js` 文件,里面导出 `commitRoot` 文件,由于这个过程是不可中断的,所以我们递归去执行 dom 的挂载。同时我们的挂载采用字底向上的挂载,先挂载子节点,最后在挂载父节点,这样可以减少页面的重排和重绘,节省性能。
566 | ```js
567 | // 从根节点开始 commit
568 | export function commitRoot(rootFiber) {
569 | commitWork(rootFiber.child);
570 | }
571 |
572 | // 递归执行 commit,此过程不中断
573 | function commitWork(fiber) {
574 | if (!fiber) {
575 | return;
576 | }
577 | // 深度优先遍历,先遍历 child,后遍历 sibling
578 | commitWork(fiber.child);
579 | let parentDom = fiber.return.stateNode;
580 | parentDom.appendChild(fiber.stateNode);
581 | commitWork(fiber.sibling);
582 | }
583 | ```
584 | 由于是自底向上挂载 dom,所以当 fiber 对应 React 组件时,我们可以在 `renderDom` 函数中返回一个 `document.createDocumentFragment()` 文档节点去挂载组件下面的子节点,这样就不用考虑 `stateNode` 为空的情况了(条件渲染为假值的情况在迭代创建 fiber 的过程中已经被过滤了,所以也不需要考虑)。这样有利于后续我们自己实现类组件和函数组件相关的 api。`/src/mini-react/react-dom.js` 文件改动如下:
585 | ```diff
586 | // 将 React.Element 渲染为真实 dom
587 | export function renderDom(element) {
588 | // ...
589 |
590 | if (typeof type === 'string') {
591 | // 常规 dom 节点的渲染
592 | dom = document.createElement(type);
593 | + } else if (typeof type === 'function') {
594 | + // React 组件的渲染
595 | + dom = document.createDocumentFragment();
596 | } else {
597 | // 其他情况暂不考虑
598 | return null;
599 | }
600 |
601 | // ...
602 | }
603 | ```
604 | 最后在 `/src/mini-react/fiber.js` 中引入 `/src/mini-react/commit.js` 中的 `commitRoot` 函数,并在 render 结束时调用该函数,commit 阶段结束则重置 rootFiber。同时去掉 `performUnitOfWork` 函数中的创建 dom 逻辑:
605 | ```diff
606 | + import { commitRoot } from './commit';
607 |
608 | function performUnitOfWork(fiber) {
609 | // ...
610 |
611 | - if (workInProgress.return && workInProgress.stateNode) {
612 | - // 如果 fiber 有父 fiber且有 dom
613 | - // 向上寻找能挂载 dom 的节点进行 dom 挂载
614 | - let parentFiber = workInProgress.return;
615 | - while (!parentFiber.stateNode) {
616 | - parentFiber = parentFiber.return;
617 | - }
618 | - parentFiber.stateNode.appendChild(workInProgress.stateNode);
619 | - }
620 |
621 | // ...
622 | }
623 |
624 | // 处理循环和中断逻辑
625 | function workLoop(deadline) {
626 | // ...
627 | + if (!nextUnitOfWork && rootFiber) {
628 | + // 表示进入 commit 阶段
629 | + commitRoot(rootFiber);
630 | + rootFiber = null;
631 | + }
632 | requestIdleCallback(workLoop);
633 | }
634 | ```
635 | ## 五: diff 算法 —— 实现更新和删除
636 | 前面我们只说到了首次渲染时 dom 的创建过程,那么元素的删除和更新等情况又是如何处理的呢?这就来到了 react 又一大核心 —— diff 算法。关于 diff 算法的理解,同样可以看我之前的文章 [全面理解 diff 算法](https://juejin.cn/post/7020595059095666724),在这里我们不过多展开了,直接进入到代码的实现环节。
637 | ### [current 和 workInProgess 👉](https://github.com/zh-lx/mini-react/commit/50a52fbb70b5e126eca4a479ad189237889234db)
638 | 我们在 diff 算法中讲过,diff 过程中,react 中有两棵 fiber 树:current fiber 树(上一次渲染时生成的 fiber 树)和 workInProgress fiber 树(本次渲染的 fiber 树),diff 过程实际上就是这两棵 fiber 树之间的 diff。
639 |
640 | 我们代码中每次 render 阶段执行的 fiber 树,实际上就是 workInProgress fiber 树,rootFiber 就是 workInProgress fiber 树的根结点,所以我们需要再维护一棵 current fiber 树。同时为了便于理解,我们将 `rootFiber` 更名为 `workInProgressRoot`:
641 | ```diff
642 | - let rootFiber = null;
643 | + let workInProgressRoot = null; // 当前工作的 fiber 树,
644 | + let currentRoot = null; // 上一次渲染的 fiber 树
645 | ```
646 | 所有用到了 `rootFiber` 的地方全部更名为 `workInProgressRoot`,这里就不展开了。
647 |
648 | 然后我们之前的文章 [深入理解 fiber](https://juejin.cn/post/7016512949330116645) 中也讲过,workInProgress fiber 中有一个 `alternate` 属性,指向对应的 current fiber。在 react 更新流程(commit 阶段)结束后,会将当前的 currentRoot 指向 workInProgressRoot,代码如下:
649 | ```diff
650 | // 创建 rootFiber 作为首个 nextUnitOfWork
651 | export function createRoot(element, container) {
652 | workInProgressFiber = {
653 | stateNode: container, // 记录对应的真实 dom 节点
654 | element: {
655 | // 挂载 element
656 | props: { children: [element] },
657 | },
658 | + alternate: currentRoot
659 | };
660 | nextUnitOfWork = workInProgressFiber;
661 | }
662 |
663 | // ...
664 |
665 | // 处理循环和中断逻辑
666 | function workLoop(deadline) {
667 | // ...
668 | - if (!nextUnitOfWork && rootFiber) {
669 | + if (!nextUnitOfWork && workInProgressRoot) {
670 | // 表示进入 commit 阶段
671 | - commitRoot(rootFiber);
672 | + commitRoot(workInProgressRoot);
673 | // commit 阶段结束,重置变量
674 | - rootFiber = null;
675 | + currentRoot = workInProgressRoot;
676 | + workInProgressRoot = null;
677 | }
678 | requestIdleCallback(workLoop);
679 | }
680 | ```
681 | ### [创建 reconciler 👉](https://github.com/zh-lx/mini-react/commit/7eebaac428789ea2048c92661636378d686b42c7)
682 | 下面开始实现 diff 过程,diff 过程是以 `reconcileChildren` 为入口函数的,在 fiber 树的构建过程中,对 fiber 打上不同的 `flag` 副作用标签。在 `/src/mini-react` 目录下新建 `reconciler.js` 文件,其中导出 `reconcileChildren` 函数,将 `performUnitOfWork` 函数中的 fiber 构造 fiber 树逻辑迁移到该函数中。代码如下:
683 | ```js
684 | export function reconcileChildren(workInProgress, elements) {
685 | let index = 0; // 当前遍历的子元素在父节点下的下标
686 | let prevSibling = null; // 记录上一个兄弟节点
687 |
688 | while (index < elements.length) {
689 | // 遍历子元素
690 | const element = elements[index];
691 | // 创建新的 fiber
692 | const newFiber = {
693 | element,
694 | return: workInProgress,
695 | stateNode: null,
696 | };
697 | if (index === 0) {
698 | // 如果下标为 0,则将当前fiber设置为父 fiber 的 child
699 | workInProgress.child = newFiber;
700 | } else {
701 | // 否则通过 sibling 作为兄弟 fiber 连接
702 | prevSibling.sibling = newFiber;
703 | }
704 | prevSibling = newFiber;
705 | index++;
706 | }
707 | }
708 | ```
709 | 在 `performUnitOfWork` 函数中移除构造 fiber 树的逻辑,并引入引入上面函数`reconcileChildren` 函数:
710 | ```diff
711 | + import { reconcileChildren } from './reconciler';
712 |
713 | // 执行当前工作单元任务并设置下一个要执行的工作单元
714 | function performUnitOfWork(workInProgress) {
715 | // 根据 fiber 创建对应 dom
716 | // ...
717 |
718 | // 迭代处理函数组件、类组件、列表渲染和 children 等情况
719 | // ...
720 | if (children || children === 0) {
721 | // children 存在时,对 children 迭代
722 | let elements = Array.isArray(children) ? children : [children];
723 | // 打平列表渲染时二维数组的情况(暂不考虑三维及以上数组的情形)
724 | elements = elements.flat();
725 | - // 移除此段构造 fiber 树的逻辑
726 | + reconcileChildren(workInProgress, elements);
727 | }
728 |
729 | // 设置下一个工作单元
730 | // ...
731 | }
732 | ```
733 | ### [diff 并添加 flag 👉](https://github.com/zh-lx/mini-react/commit/538421d67d76e2e48ee5fcadbb0369a19fe963cc)
734 | 现在我们 `reconcileChildren` 函数中已经有了 elements 了,elements 使我们想要渲染到页面上的元素,为了使渲染性能最高,我们需要知道如何对旧的 dom 树进行操作的开销最小。所以我们需要就 elements 和旧的 fiber 进行 diff,与 elements 所对应的旧 fiber,就是 `workInProgress.alternate` 下的子元素了。
735 |
736 | 我们对 elements 和 oldFiber 同时遍历,根据 element 的 type 和 olderFiber 对应的 element type 去比较,并对 diff 的结果添加 flag 副作用标签:
737 | - 如果 type 相同,表示是相同的元素,添加 `Update` 的 flag,直接更新 dom 元素的属性
738 | - 如果 type 不同且新的 element 存在,添加 `Placement` 的 flag,表示需要创建新的 dom。同时还要对其添加 `index` 属性,记录在插入时在父节点下的下标位置
739 | - 如果 type 不同且 oldFiber 存在,添加 `Deletion` 的 flag,表示需要对旧的 element 进行删除
740 | (react 中使用 type 和 key 同时比较,这样做在某些情况下例如列表渲染列表项改变时更加高效,但由于实现较为麻烦我们这里只使用 type。同时 react 中除了删除、更新和添加还有其他的副作用标签,因此会使用 flags 二进制运算添加多个标签,这里我们也不考虑那么复杂的情况了。)
741 |
742 | 调整后的 `reconcileChildren` 代码如下:
743 | ```js
744 | import { deleteFiber } from './fiber';
745 |
746 | export function reconcileChildren(workInProgress, elements) {
747 | let index = 0; // 当前遍历的子元素在父节点下的下标
748 | let prevSibling = null; // 记录上一个兄弟节点
749 | let oldFiber = workInProgress?.alternate?.child; // 对应的旧 fiber
750 |
751 | while (index < elements.length || oldFiber) {
752 | // 遍历 elements 和 oldFiber
753 | const element = elements[index];
754 | // 创建新的 fiber
755 | let newFiber = null;
756 | const isSameType =
757 | element?.type &&
758 | oldFiber?.element?.type &&
759 | element.type === oldFiber.element.type;
760 |
761 | // 添加 flag 副作用
762 | if (isSameType) {
763 | // type相同,表示更新
764 | newFiber = {
765 | element: {
766 | ...element,
767 | props: element.props,
768 | },
769 | stateNode: oldFiber.stateNode,
770 | return: workInProgress,
771 | alternate: oldFiber,
772 | flag: 'Update',
773 | };
774 | } else {
775 | // type 不同,表示添加或者删除
776 | if (element || element === 0) {
777 | // element 存在,表示添加
778 | newFiber = {
779 | element,
780 | stateNode: null,
781 | return: workInProgress,
782 | alternate: null,
783 | flag: 'Placement',
784 | index,
785 | };
786 | }
787 | if (oldFiber) {
788 | // oldFiber存在,删除 oldFiber
789 | oldFiber.flag = 'Deletion';
790 | deleteFiber(oldFiber);
791 | }
792 | }
793 |
794 | if (oldFiber) {
795 | // oldFiber 存在,则继续遍历其 sibling
796 | oldFiber = oldFiber.sibling;
797 | }
798 |
799 | if (index === 0) {
800 | // 如果下标为 0,则将当前fiber设置为父 fiber 的 child
801 | workInProgress.child = newFiber;
802 | prevSibling = newFiber;
803 | } else if (newFiber) {
804 | // newFiber 和 prevSibling 存在,通过 sibling 作为兄弟 fiber 连接
805 | prevSibling.sibling = newFiber;
806 | prevSibling = newFiber;
807 | }
808 | index++;
809 | }
810 | }
811 | ```
812 | ### [Placement —— 添加 dom 👉](https://github.com/zh-lx/mini-react/commit/d5d5f6e360417c8f4ff537270f84b0704742bd0f)
813 | 当 fiber 被打上 `Placement` 的 flag 标签时,表示添加元素,我们根据元素所对应 fiber 的 `index` 属性,去寻找要在父元素的哪一个子元素之前插入。如果 `parentDom.childNodes[fiber.index]` 存在,说明要在这个元素前插入,通过 `insertBefore` 插入元素;如果不存在,则说明要插入到父元素最后,直接通过 `appendChild` 插入。
814 |
815 | 代码如下:
816 | ```js
817 | // 递归执行 commit,此过程不中断
818 | function commitWork(fiber) {
819 | // 深度优先遍历,先遍历 child,后遍历 sibling
820 | commitWork(fiber.child);
821 | let parentDom = fiber.return.stateNode;
822 | if (fiber.flag === 'Placement') {
823 | // 添加 dom
824 | const targetPositionDom = parentDom.childNodes[fiber.index]; // 要插入到那个 dom 之前
825 | if (targetPositionDom) {
826 | // targetPositionDom 存在,则插入
827 | parentDom.insertBefore(fiber.stateNode, targetPositionDom);
828 | } else {
829 | // targetPositionDom 不存在,插入到最后
830 | parentDom.appendChild(fiber.stateNode);
831 | }
832 | }
833 | commitWork(fiber.sibling)
834 | }
835 | ```
836 | ### [Update —— 更新 dom 👉](https://github.com/zh-lx/mini-react/commit/bc9053286848749fb9d1b0cbdd2c18f0b7daad76)
837 | 当 fiber 被打上 `Update` 的 flag 标签时,表示更新 dom,那么我们要对旧的 dom 中的属性及监听事件进行移除,并添加新的属性和监听事件。
838 |
839 | 这里我们直接对 `updateAttributes` 函数进行修改并导出,里面添加移除旧的属性的逻辑:
840 | ```js
841 | export function updateAttributes(dom, attributes, oldAttributes) {
842 | if (oldAttributes) {
843 | // 有旧属性,移除旧属性
844 | Object.keys(oldAttributes).forEach((key) => {
845 | if (key.startsWith('on')) {
846 | // 移除旧事件
847 | const eventName = key.slice(2).toLowerCase();
848 | dom.removeEventListener(eventName, oldAttributes[key]);
849 | } else if (key === 'className') {
850 | // className 的处理
851 | const classes = oldAttributes[key].split(' ');
852 | classes.forEach((classKey) => {
853 | dom.classList.remove(classKey);
854 | });
855 | } else if (key === 'style') {
856 | // style处理
857 | const style = oldAttributes[key];
858 | Object.keys(style).forEach((styleName) => {
859 | dom.style[styleName] = 'initial';
860 | });
861 | } else {
862 | // 其他属性的处理
863 | dom[key] = '';
864 | }
865 | });
866 | }
867 |
868 | Object.keys(attributes).forEach((key) => {
869 | // ... 之前添加新属性的逻辑
870 | }
871 | }
872 | ```
873 | 然后再 `/src/mini-react/commit.js` 中引入`updateAttributes` 函数,更新 dom 时去调用它:
874 | ```diff
875 | + import { updateAttributes } from './react-dom';
876 | // ...
877 |
878 | function commitWork(fiber) {
879 | // ...
880 | if (fiber.flag === 'Placement') {
881 | // ...
882 | + } else if (fiber.flag === 'Update') {
883 | + const { children, ...newAttributes } = fiber.element.props;
884 | + const oldAttributes = Object.assign({}, fiber.alternate.element.props);
885 | + delete oldAttributes.children;
886 | + updateAttributes(fiber.stateNode, newAttributes, oldAttributes);
887 | }
888 | commitWork(fiber.sibling)
889 | }
890 | ```
891 | ### [Deletion —— 删除 dom 👉](https://github.com/zh-lx/mini-react/commit/5592e5f32743cbfc7066a87f3a3a85a6754f33e7)
892 | 当 fiber 被打上 `Deletion` 的 flag 标签时,表示删除元素,对于删除元素我们这里要思考两个问题:
893 | 1. 对于打上了 `Deletion` flag 的 fiber,说明是在之前 current fiber 树中有,但是 workInProgress fiber 树中没有的,那么我们在 workInProgress fiber 树中遍历是找不到它的。
894 | 2. 要删除的元素,只需要从它的父节点上直接删除它就行,不需要再去遍历整个 fiber 树
895 | 所以基于以上两点,我们需要一个全局的 `deletions` 数组,存储所有要删除 dom 的对应 fiber。
896 |
897 | 我们在 `/src/mini-react/fiber.js` 中,定义一个全局变量 `deletions`,同时导出获取 `deletions` 和向 `deletions` 中添加 fiber 的方法:
898 | ```js
899 | let deletions = []; // 要执行删除 dom 的 fiber
900 |
901 | // 将某个 fiber 加入 deletions 数组
902 | export function deleteFiber(fiber) {
903 | deletions.push(fiber);
904 | }
905 |
906 | // 获取 deletions 数组
907 | export function getDeletions() {
908 | return deletions;
909 | }
910 | ```
911 | 然后在 `performUnitOfWork` 函数中,每次对 fiber 添加 `Deletion` 的 flag 副作用标签时,调用 `deleteFiber` 函数,将该 fiber 添加到 `deletions` 数组中:
912 | ```diff
913 | + import { deleteFiber } from './fiber';
914 |
915 | export function reconcileChildren(workInProgress, elements) {
916 | // ...
917 |
918 | if (oldFiber) {
919 | // oldFiber存在,删除 oldFiber
920 | oldFiber.flag = 'Deletion';
921 | + deleteFiber(oldFiber);
922 | }
923 |
924 | // ...
925 | }
926 | ```
927 | 然后我们在 `/src/mini-react/commit.js` 中,添加删除 dom 的相关逻辑,对于删除 dom,我们只要对 `deletions` 数组遍历一遍执行删除动作即可,删除完毕直接 return,不需要继续去执行递归操作了。调整后的 `commit.js` 内容如下(代码的删改请看 github 我本次 [commit]((https://github.com/zh-lx/mini-react/commit/5592e5f32743cbfc7066a87f3a3a85a6754f33e7)) 的变动):
928 | ```js
929 | import { updateAttributes } from './react-dom';
930 | import { getDeletions } from './fiber';
931 |
932 | // 从根节点开始 commit
933 | export function commitRoot(rootFiber) {
934 | const deletions = getDeletions();
935 | deletions.forEach(commitWork);
936 |
937 | commitWork(rootFiber.child);
938 | }
939 |
940 | // 递归执行 commit,此过程不中断
941 | function commitWork(fiber) {
942 | if (!fiber) {
943 | return;
944 | }
945 |
946 | let parentDom = fiber.return.stateNode;
947 | if (fiber.flag === 'Deletion') {
948 | if (typeof fiber.element?.type !== 'function') {
949 | parentDom.removeChild(fiber.stateNode);
950 | }
951 | return;
952 | }
953 |
954 | // 深度优先遍历,先遍历 child,后遍历 sibling
955 | commitWork(fiber.child);
956 | if (fiber.flag === 'Placement') {
957 | // 添加 dom
958 | const targetPositionDom = parentDom.childNodes[fiber.index]; // 要插入到那个 dom 之前
959 | if (targetPositionDom) {
960 | // targetPositionDom 存在,则插入
961 | parentDom.insertBefore(fiber.stateNode, targetPositionDom);
962 | } else {
963 | // targetPositionDom 不存在,插入到最后
964 | parentDom.appendChild(fiber.stateNode);
965 | }
966 | } else if (fiber.flag === 'Update') {
967 | const { children, ...newAttributes } = fiber.element.props;
968 | const oldAttributes = Object.assign({}, fiber.alternate.element.props);
969 | delete oldAttributes.children;
970 | updateAttributes(fiber.stateNode, newAttributes, oldAttributes);
971 | }
972 |
973 | commitWork(fiber.sibling);
974 | }
975 | ```
976 | 最后还要记得,当本次 `commitRoot` 执行完毕后,在 `/src/mini-react/fiber.js` 中的 `workLoop` 函数将 `deletions` 数组置空:
977 | ```diff
978 | // 处理循环和中断逻辑
979 | function workLoop(deadline) {
980 | let shouldYield = false;
981 | while (nextUnitOfWork && !shouldYield) {
982 | // 循环执行工作单元任务
983 | performUnitOfWork(nextUnitOfWork);
984 | shouldYield = deadline.timeRemaining() < 1;
985 | }
986 | if (!nextUnitOfWork && workInProgressRoot) {
987 | // 表示进入 commit 阶段
988 | commitRoot(workInProgressRoot);
989 | currentRoot = workInProgressRoot;
990 | workInProgressRoot = null;
991 | + deletions = [];
992 | }
993 | requestIdleCallback(workLoop);
994 | }
995 | ```
996 | ### 检查效果
997 | #### 代码
998 | 如此一来,我们添加、更新和删除 dom 的内容都实现了,我们在 `/src/index.js` 中,设置 5s 的延迟后改变一下 jsx 的内容。5s 后我们删除 `a` 标签、去掉 `p` 标签的红色字体样式,并且给 `li` 标签设置字体大小(此部分代码改动只做效果预览使用,不会提交上去):
999 | ```js
1000 | import { Component } from 'react';
1001 | import ReactDOM from './mini-react/react-dom';
1002 | import './index.css';
1003 |
1004 | class ClassComponent extends Component {
1005 | constructor(props) {
1006 | super(props);
1007 | this.state = {};
1008 | }
1009 |
1010 | render() {
1011 | return (
1012 |
1013 |
this is a class Component
1014 |
prop value is: {this.props.value}
1015 |
1016 | );
1017 | }
1018 | }
1019 |
1020 | function FunctionComponent(props) {
1021 | return (
1022 |
1023 |
this is a function Component
1024 |
prop value is: {props.value}
1025 |
1026 | );
1027 | }
1028 |
1029 | const jsx = (
1030 |
1031 |
1032 |
1033 |
1034 |
mini react link
1035 |
this is a red p
1036 |
1037 | {true &&
condition true
}
1038 | {false &&
condition false
}
1039 |
{
1043 | alert('hello');
1044 | }}
1045 | />
1046 |
1047 |
1048 |
1049 | {['item1', 'item2', 'item3'].map((item) => (
1050 |
{item}
1051 | ))}
1052 |
1053 |
1054 | );
1055 |
1056 | ReactDOM.render(jsx, document.getElementById('root'));
1057 |
1058 | setTimeout(() => {
1059 | const jsx = (
1060 |
1061 |
1062 |
1063 |
1064 |
this is a red p
1065 |
1066 | {true &&
condition true
}
1067 | {false &&
condition false
}
1068 |
{
1072 | alert('hello');
1073 | }}
1074 | />
1075 |
1076 |
1077 |
1078 | {['item1', 'item2', 'item3'].map((item) => (
1079 |
1080 | {item}
1081 |
1082 | ))}
1083 |
1084 |
1085 | );
1086 |
1087 | ReactDOM.render(jsx, document.getElementById('root'));
1088 | }, 5000);
1089 | ```
1090 | #### 预览
1091 | 效果预览如下:
1092 | 
1093 | ## 六: 实现 React.Component
1094 | 现在我们 `/src/index` 中的 `React.Component` api 还是从 react 库中引用的,接下来我们要自己对其实现。
1095 | ### [完善类组件功能 👉](https://github.com/zh-lx/mini-react/commit/6144a2d43143c6860ccb7a48182a1430ee07b1e7)
1096 | 现在我们先完善一下我们的 ClassComponent 组件,添加一个点击按钮让 `count` 加 1 的功能:
1097 | ```js
1098 | class ClassComponent extends Component {
1099 | constructor(props) {
1100 | super(props);
1101 | this.state = { count: 0 };
1102 | }
1103 |
1104 | addCount = () => {
1105 | this.setState({
1106 | count: this.state.count + 1,
1107 | });
1108 | };
1109 |
1110 | render() {
1111 | return (
1112 |
1113 |
this is a class Component
1114 |
prop value is: {this.props.value}
1115 |
count is: {this.state.count}
1116 |
1117 |
1118 | );
1119 | }
1120 | }
1121 | ```
1122 | ### [实现 setState 👉](https://github.com/zh-lx/mini-react/commit/a0e9d46e2f3185e48f1edbd4601c291fdbb149af)
1123 | 我们知道,类组件是通过类创建出一个实例,然后调用实例上的 `render` 方法去返回 jsx。我们执行 `setState` 时,需要改变 `state` 的值,然后触发类组件的更新去渲染新的 dom。
1124 |
1125 | 所以我们需要调整一下类组件的渲染逻辑,我们将 `performUnitOfWork` 函数中类组件迭代的逻辑抽离出来,新建一个 `updateClassComponent` 函数去进行扩展:
1126 | ```diff
1127 | function performUnitOfWork(workInProgress) {
1128 | // 当前 fiber 对应 React 组件时,对其 return 迭代
1129 | if (type.prototype.isReactComponent) {
1130 | // 类组件
1131 | - const { props, type: Comp } = workInProgress.element;
1132 | - const component = new Comp(props);
1133 | - const jsx = component.render();
1134 | - children = [jsx];
1135 | + updateClassComponent(workInProgress);
1136 | }
1137 |
1138 | // ...
1139 | }
1140 | ```
1141 | 然后再 `updateClassComponent` 函数中,我们比较 fiber.alternate 是否存在。如果存在,说明类组件之前渲染过,那我们复用之前的类实例(之前的类实例中保存着最新的 `state` 状态,不然重新创建类实例 `state` 状态会重置),然后在 `Component` 类上创建一个 `_UpdateProps` 方法,更新最新的 props;如果不存在,则调用类方法创建一个新的类实例,进行渲染。
1142 |
1143 | 代码如下:
1144 | ```js
1145 | function updateClassComponent(fiber) {
1146 | let jsx;
1147 | if (fiber.alternate) {
1148 | // 有旧组件,复用
1149 | const component = fiber.alternate.component;
1150 | fiber.component = component;
1151 | component._UpdateProps(fiber.element.props);
1152 | jsx = component.render();
1153 | } else {
1154 | // 没有则创建新组件
1155 | const { props, type: Comp } = fiber.element;
1156 | const component = new Comp(props);
1157 | fiber.component = component;
1158 | jsx = component.render();
1159 | }
1160 |
1161 | reconcileChildren(fiber, [jsx]);
1162 | }
1163 | ```
1164 | 接下来我们在 `/src/mini-react` 中新建 `react.js` 文件,在里面自己实现一下 `Component` 及 `setState` 的相关逻辑Component 类包含以下逻辑
1165 | 1. Component 类接受 props 参数,并挂载到 `this` 对象上
1166 | 2. 在原型链上添加 `isReactComponent` 属性,用于 react 识别是类组件还是函数组件
1167 | 3. 原型链上添加 `setState` 方法,其接受一个 `object` 或者是 `function` 类型的参数,如果是 `function` 类型,该函数接受 `this.state` 和 `this.props` 回参,返回更新后的 state 值,将其合并至 `this.state` 中;如果是 `object` 类型,直接将其合并至 `this.state` 中。然后调用 `commitRender` 函数去出发更新(接下来会说这个函数的逻辑)
1168 | 4. 原型链上添加 `_UpdateProps` 方法,用于更新类组件时更新 props
1169 | 综上的逻辑,`/src/mini-react/react.js` 的内容如下:
1170 | ```js
1171 | import { commitRender } from './fiber';
1172 | export class Component {
1173 | constructor(props) {
1174 | this.props = props;
1175 | }
1176 | }
1177 | Component.prototype.isReactComponent = true;
1178 |
1179 | Component.prototype.setState = function (param) {
1180 | if (typeof param === 'function') {
1181 | const result = param(this.state, this.props);
1182 | this.state = {
1183 | ...this.state,
1184 | ...result,
1185 | };
1186 | } else {
1187 | this.state = {
1188 | ...this.state,
1189 | ...param,
1190 | };
1191 | }
1192 |
1193 | commitRender();
1194 | };
1195 |
1196 | Component.prototype._UpdateProps = function (props) {
1197 | this.props = props;
1198 | };
1199 | ```
1200 | 然后回到上面提到的 `commitRender` 函数,这里面的逻辑比较简单,就是将当前的 `currentRoot` 作为 `workInProgressRoot`,并将 `nextUnitOfWork` 指向它,去触发 render:
1201 | ```js
1202 | export function commitRender() {
1203 | workInProgressRoot = {
1204 | stateNode: currentRoot.stateNode, // 记录对应的真实 dom 节点
1205 | element: currentRoot.element,
1206 | alternate: currentRoot,
1207 | };
1208 | nextUnitOfWork = workInProgressRoot;
1209 | }
1210 | ```
1211 | 最后在 `/src/index.js` 引入我们自己实现的 `React.Component` 即可:
1212 | ```diff
1213 | - import { Component } from 'react';
1214 | + import { Component } from './mini-react/react';
1215 | ```
1216 | ### 效果预览
1217 | 效果预览如下,非常 nice!
1218 | 
1219 | ## [七: 实现 hooks 👉](https://github.com/zh-lx/mini-react/commit/b796bca22792a598b9ac499ecdf9748d2f25aadf)
1220 | 最后我们再来实现一下函数组件的 hooks 功能。
1221 | ### 完善函数组件功能
1222 | 和类组件一样,我们先完善一下函数组件的功能,引入 `useState` hook,然后在点击按钮时让 `count` 的值 +1。代码如下:
1223 | ```js
1224 | import { useState } from 'react';
1225 |
1226 | function FunctionComponent(props) {
1227 | const [count, setCount] = useState(0);
1228 | const addCount = () => {
1229 | setCount(count + 1);
1230 | };
1231 | return (
1232 |
1233 |
this is a function Component
1234 |
prop value is: {props.value}
1235 |
count is: {count}
1236 |
1237 |
1238 | );
1239 | }
1240 | ```
1241 | ### 实现 useState
1242 | 上面说到了,类组件是在调用 `setState` api 时,改变当前类实例中的 state 状态,然后触发更新去渲染 dom。但是类组件的 `setState` 中,可以通过 `this` 获取到类实例然后拿到 `state`,而函数组件无法通过 `this` 获取,那应该如何操作呢?
1243 |
1244 | 我们可以在 `/src/mini-react/fiber.js` 中设置一个全局变量 `currentFunctionFiber`,指向render 过程中当前处理的函数组件对应的 fiber,并用它来挂载这个函数组件当前的 hooks。同时因为一个函数组件中可能有多个 hooks,所以我们还需要有一个全局的 `hookIndex` 变量来记录当前执行的 hooks 是当前函数组件中的第几个,同时导出 `getCurrentFunctionFiber` 和 `getHookIndex` 的函数来获取 `currentFunctionFiber` 和 `hookIndex`,方便后面 `/src/mini-react/react` 文件中引入使用:
1245 | ```js
1246 | let currentFunctionFiber = null; // 当前正在执行的函数组件对应 fiber
1247 | let hookIndex = 0; // 当前正在执行的函数组件 hook 的下标
1248 |
1249 | // 获取当前的执行的函数组件对应的 fiber
1250 | export function getCurrentFunctionFiber() {
1251 | return currentFunctionFiber;
1252 | }
1253 |
1254 | // 获取当前 hook 下标
1255 | export function getHookIndex() {
1256 | return hookIndex++;
1257 | }
1258 | ```
1259 | 然后同类组件一样,我们将 `performUnitOfWork` 函数组件的处理逻辑也单独抽离到一个 `updateFunctionComponent` 函数中:
1260 | ```diff
1261 | function performUnitOfWork(workInProgress) {
1262 | // ...
1263 |
1264 | if (typeof type === 'function') {
1265 | // 当前 fiber 对应 React 组件时,对其 return 迭代
1266 | if (type.prototype.isReactComponent) {
1267 | // 类组件
1268 | updateClassComponent(workInProgress);
1269 | } else {
1270 | // 函数组件
1271 | - const { props, type: Fn } = workInProgress.element;
1272 | - const jsx = Fn(props);
1273 | - children = [jsx];
1274 | + updateFunctionComponent(workInProgress);
1275 | }
1276 | }
1277 |
1278 | // ...
1279 | }
1280 | ```
1281 | `updateFunctionComponent` 函数中,会将 `currentFunctionFiber` 指向 `workInProgress`,并将其上面挂载的 hooks 数组置空,将全局的 `hookIndex` 重置为0。然后调用函数组件构造函数,返回对应的 jsx 结构,代码如下:
1282 | ```js
1283 | // 函数组件的更新
1284 | function updateFunctionComponent(fiber) {
1285 | currentFunctionFiber = fiber;
1286 | currentFunctionFiber.hooks = [];
1287 | hookIndex = 0;
1288 | const { props, type: Fn } = fiber.element;
1289 | const jsx = Fn(props);
1290 | reconcileChildren(fiber, [jsx]);
1291 | }
1292 | ```
1293 | 最后就是去实现我们的 `useState` 这个函数了。首先其接受一个初始值,并返回一个数组,然后通过 `getCurrentFunctionFiber` 和 `getHookIndex` 函数来获取 `currentFunctionFiber` 和 `hookIndex`。
1294 |
1295 | 然后根据 `currentFunctionFiber.alternate.hooks.[hookIndex]` 判断有没有已经存在的对应的旧的 hook,如果有,则直接取过来用以便获取之前的 hook 的状态值;若没有则使用传入的初始值初始化一个 hook。
1296 |
1297 | 一个 hook 上有两个属性:
1298 | - state: 表示当前 `useState` hook 要返回的值
1299 | - queue: 存储了本次 render 过程要对这个 `state` 进行的操作数组
1300 |
1301 | 所以我们 `useState` 的返回值就很明确了,返回一个数组,数组第一个值是 `hook.state`,第二个值是一个函数,这个函数的功能就是将接收的参数 push 到 `hook.queue` 中。
1302 |
1303 | 综上代码如下:
1304 | ```js
1305 | export function useState(initial) {
1306 | const currentFunctionFiber = getCurrentFunctionFiber();
1307 | const hookIndex = getHookIndex();
1308 | // 取当前执行的函数组件之前的 hook
1309 | const oldHook = currentFunctionFiber?.alternate?.hooks?.[hookIndex];
1310 |
1311 | // oldHook存在,取之前的值,否则取现在的值
1312 | const hook = {
1313 | state: oldHook ? oldHook.state : initial,
1314 | queue: [], // 一次函数执行过程中可能调用多次 setState,将其放进队列一并执行
1315 | };
1316 |
1317 | const actions = oldHook ? oldHook.queue : [];
1318 | actions.forEach((action) => {
1319 | hook.state = action(hook.state);
1320 | });
1321 |
1322 | const setState = (action) => {
1323 | if (typeof action === 'function') {
1324 | hook.queue.push(action);
1325 | } else {
1326 | hook.queue.push(() => {
1327 | return action;
1328 | });
1329 | }
1330 | commitRender();
1331 | };
1332 | currentFunctionFiber.hooks.push(hook);
1333 | return [hook.state, setState];
1334 | }
1335 | ```
1336 | ### 效果预览
1337 | 最后在我们的 `/src/index.js` 中引入自己实现的 `useState` 之后,看一下效果:
1338 | 
1339 |
1340 | ## 总结
1341 | OK,到这里我们的 mini react 就实现完成了,基本涵盖了 react 源码所有的知识点例如 fiber 架构、render 和 commit 阶段、diff 算法、类组件、函数组件、hooks 等等。
1342 |
1343 | 当然源码中也有一些不足,例如对于 dom 的创建还未考虑 `React.fragment`、其他内置组件以及嵌套的列表渲染等等。另外我们的 diff 算法的实现是一个简易版的 diff,并未考虑 key 值和 type 共同 diff 等,感兴趣的可以去 [mini react](https://github.com/zh-lx/mini-react) 仓库再次基础上进行扩充和完善。
1344 |
1345 | 通过实现本次的 mini react,希望你对 react 的原理有了一个更清晰的认知,也欢迎关注我的 [react17源码专栏](https://juejin.cn/column/7014776835166699556),里面有我之前写的一系列 react17 源码的阅读及解析~
1346 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-scripts": "4.0.3",
12 | "web-vitals": "^1.0.1"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zh-lx/mini-react/d346ae571defb50d28540ec457cc16e233ba8ec5/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zh-lx/mini-react/d346ae571defb50d28540ec457cc16e233ba8ec5/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zh-lx/mini-react/d346ae571defb50d28540ec457cc16e233ba8ec5/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .deep1-box {
2 | border: 1px solid rgb(146, 89, 236);
3 | padding: 8px;
4 | }
5 | .class-component {
6 | border: 1px solid rgb(228, 147, 147);
7 | padding: 8px;
8 | }
9 | .function-component {
10 | margin-top: 8px;
11 | padding: 8px;
12 | border: 1px solid rgb(133, 233, 120);
13 | }
14 | .deep2-box-1 {
15 | margin-top: 8px;
16 | padding: 8px;
17 | border: 1px solid rgb(233, 224, 107);
18 | }
19 | .deep3-box {
20 | padding: 8px;
21 | border: 1px solid rgb(55, 189, 241);
22 | }
23 | .deep2-box-2 {
24 | margin-top: 8px;
25 | padding: 8px;
26 | border: 1px solid rgb(23, 143, 77);
27 | }
28 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { Component, useState } from './mini-react/react';
2 | import ReactDOM from './mini-react/react-dom';
3 | import './index.css';
4 |
5 | class ClassComponent extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = { count: 0 };
9 | }
10 |
11 | addCount = () => {
12 | this.setState({
13 | count: this.state.count + 1,
14 | });
15 | };
16 |
17 | render() {
18 | return (
19 |
20 |
this is a class Component
21 |
prop value is: {this.props.value}
22 |
count is: {this.state.count}
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | function FunctionComponent(props) {
30 | const [count, setCount] = useState(0);
31 | const addCount = () => {
32 | setCount(count + 1);
33 | };
34 | return (
35 |
36 |
this is a function Component
37 |
prop value is: {props.value}
38 |
count is: {count}
39 |
40 |
41 | );
42 | }
43 |
44 | const jsx = (
45 |
46 |
47 |
48 |
49 |
mini react link
50 |
this is a red p
51 |
52 | {true &&
condition true
}
53 | {false &&
condition false
}
54 |
{
58 | alert('hello');
59 | }}
60 | />
61 |
62 |
63 |
64 | {['item1', 'item2', 'item3'].map((item) => (
65 |
{item}
66 | ))}
67 |
68 |
69 | );
70 |
71 | ReactDOM.render(jsx, document.getElementById('root'));
72 |
--------------------------------------------------------------------------------
/src/mini-react/commit.js:
--------------------------------------------------------------------------------
1 | import { updateAttributes } from './react-dom';
2 | import { getDeletions } from './fiber';
3 |
4 | // 从根节点开始 commit
5 | export function commitRoot(rootFiber) {
6 | const deletions = getDeletions();
7 | deletions.forEach(commitWork);
8 |
9 | commitWork(rootFiber.child);
10 | }
11 |
12 | // 递归执行 commit,此过程不中断
13 | function commitWork(fiber) {
14 | if (!fiber) {
15 | return;
16 | }
17 |
18 | let parentDom = fiber.return.stateNode;
19 | if (fiber.flag === 'Deletion') {
20 | if (typeof fiber.element?.type !== 'function') {
21 | parentDom.removeChild(fiber.stateNode);
22 | }
23 | return;
24 | }
25 |
26 | // 深度优先遍历,先遍历 child,后遍历 sibling
27 | commitWork(fiber.child);
28 | if (fiber.flag === 'Placement') {
29 | // 添加 dom
30 | const targetPositionDom = parentDom.childNodes[fiber.index]; // 要插入到那个 dom 之前
31 | if (targetPositionDom) {
32 | // targetPositionDom 存在,则插入
33 | parentDom.insertBefore(fiber.stateNode, targetPositionDom);
34 | } else {
35 | // targetPositionDom 不存在,插入到最后
36 | parentDom.appendChild(fiber.stateNode);
37 | }
38 | } else if (fiber.flag === 'Update') {
39 | const { children, ...newAttributes } = fiber.element.props;
40 | const oldAttributes = Object.assign({}, fiber.alternate.element.props);
41 | delete oldAttributes.children;
42 | updateAttributes(fiber.stateNode, newAttributes, oldAttributes);
43 | }
44 |
45 | commitWork(fiber.sibling);
46 | }
47 |
--------------------------------------------------------------------------------
/src/mini-react/fiber.js:
--------------------------------------------------------------------------------
1 | import { renderDom } from './react-dom';
2 | import { commitRoot } from './commit';
3 | import { reconcileChildren } from './reconciler';
4 |
5 | let nextUnitOfWork = null;
6 | let workInProgressRoot = null; // 当前工作的 fiber 树
7 | let currentRoot = null; // 上一次渲染的 fiber 树
8 | let deletions = []; // 要执行删除 dom 的 fiber
9 | let currentFunctionFiber = null; // 当前正在执行的函数组件对应 fiber
10 | let hookIndex = 0; // 当前正在执行的函数组件 hook 的下标
11 |
12 | // 将某个 fiber 加入 deletions 数组
13 | export function deleteFiber(fiber) {
14 | deletions.push(fiber);
15 | }
16 |
17 | // 获取 deletions 数组
18 | export function getDeletions() {
19 | return deletions;
20 | }
21 |
22 | // 触发渲染
23 | export function commitRender() {
24 | workInProgressRoot = {
25 | stateNode: currentRoot.stateNode, // 记录对应的真实 dom 节点
26 | element: currentRoot.element,
27 | alternate: currentRoot,
28 | };
29 | nextUnitOfWork = workInProgressRoot;
30 | }
31 |
32 | // 获取当前的执行的函数组件对应的 fiber
33 | export function getCurrentFunctionFiber() {
34 | return currentFunctionFiber;
35 | }
36 |
37 | // 获取当前 hook 下标
38 | export function getHookIndex() {
39 | return hookIndex++;
40 | }
41 |
42 | // 创建 rootFiber 作为首个 nextUnitOfWork
43 | export function createRoot(element, container) {
44 | workInProgressRoot = {
45 | stateNode: container, // 记录对应的真实 dom 节点
46 | element: {
47 | // 挂载 element
48 | props: { children: [element] },
49 | },
50 | alternate: currentRoot,
51 | };
52 | nextUnitOfWork = workInProgressRoot;
53 | }
54 |
55 | // 执行当前工作单元并设置下一个要执行的工作单元
56 | function performUnitOfWork(workInProgress) {
57 | if (!workInProgress.stateNode) {
58 | // 若当前 fiber 没有 stateNode,则根据 fiber 挂载的 element 的属性创建
59 | workInProgress.stateNode = renderDom(workInProgress.element);
60 | }
61 | // if (workInProgress.return && workInProgress.stateNode) {
62 | // // 如果 fiber 有父 fiber且有 dom
63 | // // 向上寻找能挂载 dom 的节点进行 dom 挂载
64 | // let parentFiber = workInProgress.return;
65 | // while (!parentFiber.stateNode) {
66 | // parentFiber = parentFiber.return;
67 | // }
68 | // parentFiber.stateNode.appendChild(workInProgress.stateNode);
69 | // }
70 |
71 | let children = workInProgress.element?.props?.children;
72 |
73 | let type = workInProgress.element?.type;
74 | if (typeof type === 'function') {
75 | // 当前 fiber 对应 React 组件时,对其 return 迭代
76 | if (type.prototype.isReactComponent) {
77 | // 类组件
78 | updateClassComponent(workInProgress);
79 | } else {
80 | // 函数组件
81 | updateFunctionComponent(workInProgress);
82 | }
83 | }
84 |
85 | if (children || children === 0) {
86 | // children 存在时,对 children 迭代
87 | let elements = Array.isArray(children) ? children : [children];
88 | // 打平列表渲染时二维数组的情况(暂不考虑三维及以上数组的情形)
89 | elements = elements.flat();
90 | reconcileChildren(workInProgress, elements);
91 | }
92 |
93 | // 设置下一个工作单元
94 | if (workInProgress.child) {
95 | // 如果有子 fiber,则下一个工作单元是子 fiber
96 | nextUnitOfWork = workInProgress.child;
97 | } else {
98 | let nextFiber = workInProgress;
99 | while (nextFiber) {
100 | if (nextFiber.sibling) {
101 | // 没有子 fiber 有兄弟 fiber,则下一个工作单元是兄弟 fiber
102 | nextUnitOfWork = nextFiber.sibling;
103 | return;
104 | } else {
105 | // 子 fiber 和兄弟 fiber 都没有,深度优先遍历返回上一层
106 | nextFiber = nextFiber.return;
107 | }
108 | }
109 | if (!nextFiber) {
110 | // 若返回最顶层,表示迭代结束,将 nextUnitOfWork 置空
111 | nextUnitOfWork = null;
112 | }
113 | }
114 | }
115 |
116 | // 类组件的更新
117 | function updateClassComponent(fiber) {
118 | let jsx;
119 | if (fiber.alternate) {
120 | // 有旧组件,复用
121 | const component = fiber.alternate.component;
122 | fiber.component = component;
123 | component._UpdateProps(fiber.element.props);
124 | jsx = component.render();
125 | } else {
126 | // 没有则创建新组件
127 | const { props, type: Comp } = fiber.element;
128 | const component = new Comp(props);
129 | fiber.component = component;
130 | jsx = component.render();
131 | }
132 |
133 | reconcileChildren(fiber, [jsx]);
134 | }
135 |
136 | // 函数组件的更新
137 | function updateFunctionComponent(fiber) {
138 | currentFunctionFiber = fiber;
139 | currentFunctionFiber.hooks = [];
140 | hookIndex = 0;
141 | const { props, type: Fn } = fiber.element;
142 | const jsx = Fn(props);
143 | reconcileChildren(fiber, [jsx]);
144 | }
145 |
146 | // 处理循环和中断逻辑
147 | function workLoop(deadline) {
148 | let shouldYield = false;
149 | while (nextUnitOfWork && !shouldYield) {
150 | // 循环执行工作单元任务
151 | performUnitOfWork(nextUnitOfWork);
152 | shouldYield = deadline.timeRemaining() < 1;
153 | }
154 | if (!nextUnitOfWork && workInProgressRoot) {
155 | // 表示进入 commit 阶段
156 | commitRoot(workInProgressRoot);
157 | currentRoot = workInProgressRoot;
158 | workInProgressRoot = null;
159 | deletions = [];
160 | }
161 | requestIdleCallback(workLoop);
162 | }
163 |
164 | requestIdleCallback(workLoop);
165 |
--------------------------------------------------------------------------------
/src/mini-react/react-dom.js:
--------------------------------------------------------------------------------
1 | import { createRoot } from './fiber';
2 |
3 | function render(element, container) {
4 | createRoot(element, container);
5 | }
6 |
7 | // 将 React.Element 渲染为真实 dom
8 | export function renderDom(element) {
9 | let dom = null; // 要返回的 dom
10 |
11 | if (!element && element !== 0) {
12 | // 条件渲染为假,返回 null
13 | return null;
14 | }
15 |
16 | if (typeof element === 'string') {
17 | // 如果 element 本身为 string,返回文本节点
18 | dom = document.createTextNode(element);
19 | return dom;
20 | }
21 |
22 | if (typeof element === 'number') {
23 | // 如果 element 本身为 number,将其转为 string 后返回文本节点
24 | dom = document.createTextNode(String(element));
25 | return dom;
26 | }
27 |
28 | const {
29 | type,
30 | props: { children, ...attributes },
31 | } = element;
32 |
33 | if (typeof type === 'string') {
34 | // 常规 dom 节点的渲染
35 | dom = document.createElement(type);
36 | } else if (typeof type === 'function') {
37 | // React 组件的渲染
38 | dom = document.createDocumentFragment();
39 | } else {
40 | // 其他情况暂不考虑
41 | return null;
42 | }
43 |
44 | updateAttributes(dom, attributes);
45 |
46 | return dom;
47 | }
48 |
49 | // 更新 dom 属性
50 | export function updateAttributes(dom, attributes, oldAttributes) {
51 | if (oldAttributes) {
52 | // 有旧属性,移除旧属性
53 | Object.keys(oldAttributes).forEach((key) => {
54 | if (key.startsWith('on')) {
55 | // 移除旧事件
56 | const eventName = key.slice(2).toLowerCase();
57 | dom.removeEventListener(eventName, oldAttributes[key]);
58 | } else if (key === 'className') {
59 | // className 的处理
60 | const classes = oldAttributes[key].split(' ');
61 | classes.forEach((classKey) => {
62 | dom.classList.remove(classKey);
63 | });
64 | } else if (key === 'style') {
65 | // style处理
66 | const style = oldAttributes[key];
67 | Object.keys(style).forEach((styleName) => {
68 | dom.style[styleName] = 'initial';
69 | });
70 | } else {
71 | // 其他属性的处理
72 | dom[key] = '';
73 | }
74 | });
75 | }
76 |
77 | Object.keys(attributes).forEach((key) => {
78 | if (key.startsWith('on')) {
79 | // 事件的处理
80 | const eventName = key.slice(2).toLowerCase();
81 | dom.addEventListener(eventName, attributes[key]);
82 | } else if (key === 'className') {
83 | // className 的处理
84 | const classes = attributes[key].split(' ');
85 | classes.forEach((classKey) => {
86 | dom.classList.add(classKey);
87 | });
88 | } else if (key === 'style') {
89 | // style处理
90 | const style = attributes[key];
91 | Object.keys(style).forEach((styleName) => {
92 | dom.style[styleName] = style[styleName];
93 | });
94 | } else {
95 | // 其他属性的处理
96 | dom[key] = attributes[key];
97 | }
98 | });
99 | }
100 |
101 | const ReactDOM = {
102 | render,
103 | };
104 | export default ReactDOM;
105 |
--------------------------------------------------------------------------------
/src/mini-react/react.js:
--------------------------------------------------------------------------------
1 | import { commitRender, getCurrentFunctionFiber, getHookIndex } from './fiber';
2 | export class Component {
3 | constructor(props) {
4 | this.props = props;
5 | }
6 | }
7 | Component.prototype.isReactComponent = true;
8 |
9 | Component.prototype.setState = function (param) {
10 | if (typeof param === 'function') {
11 | const result = param(this.state, this.props);
12 | this.state = {
13 | ...this.state,
14 | ...result,
15 | };
16 | } else {
17 | this.state = {
18 | ...this.state,
19 | ...param,
20 | };
21 | }
22 |
23 | commitRender();
24 | };
25 |
26 | Component.prototype._UpdateProps = function (props) {
27 | this.props = props;
28 | };
29 |
30 | export function useState(initial) {
31 | const currentFunctionFiber = getCurrentFunctionFiber();
32 | const hookIndex = getHookIndex();
33 | // 取当前执行的函数组件之前的 hook
34 | const oldHook = currentFunctionFiber?.alternate?.hooks?.[hookIndex];
35 |
36 | // oldHook存在,取之前的值,否则取现在的值
37 | const hook = {
38 | state: oldHook ? oldHook.state : initial,
39 | queue: [], // 一次函数执行过程中可能调用多次 setState,将其放进队列一并执行
40 | };
41 |
42 | const actions = oldHook ? oldHook.queue : [];
43 | actions.forEach((action) => {
44 | hook.state = action(hook.state);
45 | });
46 |
47 | const setState = (action) => {
48 | if (typeof action === 'function') {
49 | hook.queue.push(action);
50 | } else {
51 | hook.queue.push(() => {
52 | return action;
53 | });
54 | }
55 | commitRender();
56 | };
57 | currentFunctionFiber.hooks.push(hook);
58 | return [hook.state, setState];
59 | }
60 |
--------------------------------------------------------------------------------
/src/mini-react/reconciler.js:
--------------------------------------------------------------------------------
1 | import { deleteFiber } from './fiber';
2 |
3 | export function reconcileChildren(workInProgress, elements) {
4 | let index = 0; // 当前遍历的子元素在父节点下的下标
5 | let prevSibling = null; // 记录上一个兄弟节点
6 | let oldFiber = workInProgress?.alternate?.child; // 对应的旧 fiber
7 |
8 | while (index < elements.length || oldFiber) {
9 | // 遍历 elements 和 oldFiber
10 | const element = elements[index];
11 | // 创建新的 fiber
12 | let newFiber = null;
13 | const isSameType =
14 | element?.type &&
15 | oldFiber?.element?.type &&
16 | element.type === oldFiber.element.type;
17 |
18 | // 添加 flag 副作用
19 | if (isSameType) {
20 | // type相同,表示更新
21 | newFiber = {
22 | element: {
23 | ...element,
24 | props: element.props,
25 | },
26 | stateNode: oldFiber.stateNode,
27 | return: workInProgress,
28 | alternate: oldFiber,
29 | flag: 'Update',
30 | };
31 | } else {
32 | // type 不同,表示添加或者删除
33 | if (element || element === 0) {
34 | // element 存在,表示添加
35 | newFiber = {
36 | element,
37 | stateNode: null,
38 | return: workInProgress,
39 | alternate: null,
40 | flag: 'Placement',
41 | index,
42 | };
43 | }
44 | if (oldFiber) {
45 | // oldFiber存在,删除 oldFiber
46 | oldFiber.flag = 'Deletion';
47 | deleteFiber(oldFiber);
48 | }
49 | }
50 |
51 | if (oldFiber) {
52 | // oldFiber 存在,则继续遍历其 sibling
53 | oldFiber = oldFiber.sibling;
54 | }
55 |
56 | if (index === 0) {
57 | // 如果下标为 0,则将当前fiber设置为父 fiber 的 child
58 | workInProgress.child = newFiber;
59 | prevSibling = newFiber;
60 | } else if (newFiber) {
61 | // newFiber 和 prevSibling 存在,通过 sibling 作为兄弟 fiber 连接
62 | prevSibling.sibling = newFiber;
63 | prevSibling = newFiber;
64 | }
65 | index++;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------