├── .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 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58bb8cc3aac04c3ca608a4a0fe877504~tplv-k3u1fbpfcp-watermark.image?) 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 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/95b7775d25604a12a4ce57d10081ff1f~tplv-k3u1fbpfcp-watermark.image?) 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 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3314766840674717aaae3e4c37ef031e~tplv-k3u1fbpfcp-watermark.image?) 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 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c4a19bc267724f16bcb8e6bdd22b3ecf~tplv-k3u1fbpfcp-watermark.image?) 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 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/616e761e1d8e4d69a45659ef517433ba~tplv-k3u1fbpfcp-watermark.image?) 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 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b486583f939475aadb652bff5252ebf~tplv-k3u1fbpfcp-watermark.image?) 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 | ![fiber.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c358c8e604d1414885cdb7c24568598a~tplv-k3u1fbpfcp-watermark.image?) 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 | ![Nov-11-2021 18-16-57.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3045c9e08e046c2924719557456e129~tplv-k3u1fbpfcp-watermark.image?) 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 | ![Nov-11-2021 20-00-52.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4bee026aa31c4714bdc91245d0ae6170~tplv-k3u1fbpfcp-watermark.image?) 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 | ![Nov-13-2021 22-48-40.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc46d47e6db54a41bb3370226bce5cfe~tplv-k3u1fbpfcp-watermark.image?) 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 | --------------------------------------------------------------------------------