• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上节我们写了 mini react。

它和真实的 react 渲染流程是否一样呢?

这节我们就调试下 react 源码,对比下两者的差别。

用 cra 创建个 react 项目:

npx create-react-app --template=typescript react-source-debug

把开发服务跑起来:

npm run start

浏览器访问下:

没啥问题。

点击 create a launch.json file 创建个调试配置:

{
  "type": "chrome",
  "request": "launch",
  "name": "Launch Chrome against localhost",
  "url": "http://localhost:3000",
  "webRoot": "${workspaceFolder}"
}

在 App.tsx 打个断点:

点击调试启动:

代码会在这里断住。

前面讲过,jsx 会编译成 render function,然后执行后产生 React Element:

关掉 sorucemap 重新调试:

可以看到这个 jsxDEV 就是 render function:

它是从 react 包引入的:

和我们在 babel playground 里看到的结果一样:

在这里打个断点:

然后点击跳断点执行和进入函数内部:

在返回值这里打个断点:

可以看到,render function 返回的是一个 React Element,有 type、props 等属性。

我们的 mini react 里也实现了 render function:

接下来再看 schedule 和 reconcile 部分:

打开 sourcemap,重新跑调试:

在调用栈可以看到 workLoop:

这个是 schduler 包里的,这个包是 react 实现的类似 requestIdleCallback 的功能。

可以看到,每次取一个任务的回调来跑:

然后回调里会判断是否要用时间分片:

时间分片前面讲过,就是把 reconcile 过程分散到多个宏任务中跑:

在 scheduler 里搜一下,可以看到,这个时间分片是 5ms:

也就是说,如果超过 5ms,就会放到下个任务里跑。

这就是为啥 performance 看到的 event loop 是这样的:

react 并发渲染的时候,就通过时间片是否到了来判断是否继续 reconcile:

当然,我们实现的时候没有自己实现 schduler 的时间分片,而是直接用的浏览器的 requestIdleCallback 的 api,效果一样:

接下来看下 reconcile 的过程:

在 react 源码里,处理每个 fiber 节点的时候,会先调用 beginWork 处理,等 fiber 节点全部处理完,也就是没有 next 的 fiber 节点时,再调用 completeWork 处理。

那 beginWork 和 completeWork 里都做了啥呢?

可以看到,根据 fiber 节点的类型来走了不同的分支,我们只处理了 FunctionComponent 和 HostComponent 类型。

看下 FunctionComponent 的处理:

也是调用函数组件,拿到 children 之后继续 reconcileChildren。

reconcileChildren 里要对比新旧 fiber,做下 diff,打上增删改的标记:

diff 之后,会分别打上 Place、ChildDeletion 等标记:

这部分和我们 mini react 实现的 reconcile 逻辑差不多。

那 completeWork 是干啥的呢?

看下 HostComponent 的 reconcile 逻辑,你会发现它并没有创建 dom:

而我们的 mini react 里是创建了 dom 的。

其实不是没有创建,而是这部分逻辑在 completeWork 里。

completeWork 里处理到 HostComponent 就会创建对应的 dom,保存在 fiber.stateNode 属性上:

为什么要分为 beginWork 和 completeWork 两个阶段呢?

其实也很容易搞懂,比如创建 dom 这件事,需要先把所有子节点的 dom 都创建好,然后 appendChild 才行。

所以就需要 beginWork 处理完所有 fiber 之后,再递归从下往上处理。

然后是 commit 阶段,在 react 源码里可以看到,这个阶段分为了 before mutation、mutation、layout 这三个小阶段:

mutation 阶段就是更新 dom 的:

可以看到,mutation 阶段会把 reconcile 阶段创建好的 dom 更新到 dom 树。

那啥时候执行的 effect呢?

刚进入 commitRoot 的时候,就会调度所有的 useEffect 的回调异步执行。

还有,useState、useEffect 等 hook 在 react 源码里是怎么实现的呢?

添加几个 hook:

在 return 那里打个断点,可以看到现在的 fiber 是这样的:

在 fiber 上有个 memoizedState 的链表,每个节点保存一个 hook 的信息。

调用 useState、useRef、useEffect 等 hook 的时候,会往对应的链表节点上存取内容。

hook 链表的创建分为 mount、update 两个阶段,第一次创建链表节点,第二次更新链表节点。

比如 useRef 就是在对应 hook 节点的 momoizedState 属性保存一个有 current 属性的对象,第二次调用返回这个对象:

比如 useCallback 就是在对应 hook 节点的 momoizedState 属性保存一个数组,再次调用判断下 deps 是否一样,一样的话就返回之前的数组的第一个元素,否则更新:

useMemo 和 useCallback 实现差不多,只不过保存的是函数的值:

这样,和 mini react 对应的 react 源码里的实现就理清了。

总结

我们调试了下 react 源码,和前面写的 mini react 对比了下。

实现 render function 返回 React Element。

React Element 树经过 reconcile 变成 fiber 树,reconcile 的时候根据不同类型做不同处理,然后 commit 阶段执行 dom 增删改和 effect 等。

这些都差不多。

只不过 react 源码里 render 阶段 reconcile 分成了 beginWork、completeWork 两个小阶段,dom 的创建和组装是在 completeWork 里做的。

commit 阶段分成了 before mutation、mutation、layout 这三个小阶段。

react 的调度也是用自己实现的 schduler 做的,实现了时间分片,而我们用的 requestIdleCallback 做的调度。

react 的 hook 的值是存放在 fiber.memoizedState 链表上的,每个 hook 对应一个节点,在其中存取值,而我们是用的别的属性。

包括保存 dom 的节点,在 react 里是用 fiber.stateNode 属性保存。

但总体来说,流程上是差不多的,通过学习 mini react,能够很好的帮你理解 react 的实现原理。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
63.手写MiniReact:代码实现
Next
65.React18的并发机制是怎么实现的