基于这篇文章,自己实现了一个简单的react,记录笔记如下:
1. 定义 react.createElement(type, props, …children)
- 注:2020.10.25 使用最新的 create react app,自定义的 react.createElement 无效。
原因:package.json 中的”react-scripts”升级至”4.0.0”,本地启动时会调用 react-dev-jsx-runtime.development.js 中的 jsxWithValidation,而不是自定义的 createElement 方法。
解决方法:将 package.json 中的”react-scripts”降至 3.x.x,会直接调用自定义的 React.createElement 方法。
2. 定义 ReactDOM.render(vdom, container)
- 根据 vdom.type,创建真实 dom,document.createElement or document.createTextNode.
- 将 vdom.props 中除 children 之外的所有属性赋给刚创建的 dom 元素。
- 对于 children 进行递归调用 render 方法
- container 调用 apppenChild 添加 vdom
3. requestIdleCallback 空闲调度逻辑
ReactDOM.render 只是做了初始化的工作。
剩下的任务是在 requestIdleCallback()中进行的,浏览器会自动在空闲的时候执行,完成 dom diff, 真实 dom 修改的工作
performUnitWork 每个单元执行以下工作内容:
- 生成新 dom
- 处理 fiber
- 返回下一个要操作的 fiber
1 | function performUnitWork(fiber) { |
4. fiber 的数据结构,就是一个 jsx+一些链表属性的对象类型
第一个fiber
来源自ReactDOM.render(element, document.getElementById('root'))
1 | var element = { |
所以fiber
的数据结构如下:
1 | var fiber = { |
5. commit 真实 dom 添加操作
1 | function performUnitWork(fiber) { |
当所有 fiber 都执行完毕,就开始操作真实 dom 了。
1 | // 调度任务 |
6. 真实 dom 更新和删除操作(dom diff)
- performUnitWork 第二个任务处理 fiber
记录上一次渲染后的状态,lastFiber,跟 curFiber 相同层级的虚拟 dom,一一做虚拟 dom diff 阶段,会比较 dom.type:- 类型相同,则标记为更新
- 类型不同,curFiber 存在,则标记为替换(新增也属于替换)
- 类型不同,lastFiber 存在,则标记为删除。删除 fiber 同时要保存到一个 deletTasks 列表里,在 commitDom 阶段删掉。(因为新的 fiber 里没有保存要删除的 fiber 数据)
1 | // 2. 为当前fiber的儿子元素(仅限这一代,不包含儿子的儿子)更新fiber信息 |
- commitDom 阶段 批量删除 deletTasks
1 | function commitDom() { |
- commitWork 具体的 dom 操作
- 删除:把旧节点删掉,removeChild
- 替换:把新节点加进来,appendChild
- 更新:把原有节点旧属性和新属性对比,旧的去掉,换成新的
1 | function updateDom(dom, prevProps, props) { |
7. 函数组件
1 | const App = (props) => ( |
等价于
1 | function createElement(type, props, ...children) { |
AppjsxObj.type = ()=> createElement('div', {}, ...children)
是一个 Function
- 函数组件有两个问题:
- 没有 dom 实例
- children 来源于函数执行返回值,而不是 props.children
所以对于跟 dom 相关的问题,要处理 dom 相关问题。
- fiber 处理阶段
1 | // 处理函数组件的节点fiber |
- dom 操作阶段
1 | function deleteDom(domParent, fiber) { |
8. hooks
1 | // 处理函数组件的节点fiber |
- 问题 1:const [st, setSt] = useState(1), st 是 const 类型,为什么 setSt(2)的时候可以把 st 变成 2 呢?
答:看代码,每一次 useState 执行,都是生成一个新的参数和 set 方法
- 问题 2:为什么 setSt,会重新引发 render
答:setSt 时,会重新配置 rootFiber,进而引发 workLoop 调度
1 | const setState = (newstate) => { |
- 问题 3:setSt 后的,程序执行过程
答:setSt 后,设置 rootFiber -> workLoop() -> performUnitWork(fiber) -> updateFunctionComponent(fiber) -> fiber.type(fiber.props) -> 函数组件从头执行一遍 -> useState() -> hooks.st = newstate -> handleFiber(fiber, elements) -> dom diff -> dom 操作