实现 React 基础版本
可以参考:https://www.yangyitao.com/react18/
simple-mini-react
├── src
│ ├── mini-react // 基础版本 react 实现
│ │ ├── Component.js // 类组件(类组件的 setState、更新、批量更新等)
│ │ ├── constant.js // 常量
│ │ ├── events.js // 事件系统
│ │ ├── react-dom.js // render 函数,渲染真实 dom
│ │ ├── react.js // createElement 创建虚拟 dom
│ │ └── utils.js // 辅助工具函数
│ └── index.js // mini-react 使用入口
└── readme.md
提示:需要切换 node 18+ 版本
装包
yarn install启动
yarn start:classic说明: 简单版的,对照的是旧版的 react,需要在文件顶部引入 react 的,所以使用 DISABLE_NEW_JSX_TRANSFORM=true 来告诉 babel 要使用 Classic 方式编译 jsx
- 实现 createElement 函数,转化为虚拟 dom
- 实现 render 函数,主要做初始化渲染,将虚拟 dom 渲染为真实 dom 调用 mount 函数实现
- 实现 mount 函数,分两步:
- 第一步:通过 createDOM 函数将虚拟 dom 转换为真实 dom
- 第二步:将真实 dom 挂载到页面上的具体函数
- 实现 createDOM 函数
- 创建真实 dom
- 处理 children(单个或者数组形式)
- 处理 props 属性(原始渲染主要是 style 样式)
- 挂载真实 dom
- 实现函数组件的基础功能
- 实现类组件的基础功能
- 实现类组件更新机制: setState 功能
- 合并 state
- 执行更新,重新渲染
- 批量更新机制的实现
- 实现简单的事件合成机制
- 利用时间冒泡机制,将事件绑定到最外层的 document 上
- 合成事件,抹平浏览器差异
- 触发批量更新
- 实现 ref
- 在原生标签上,ref 的 current 值是 createDOM 中赋值的 dom()
- 在类组件上,ref 的 current 值是类组件实例()
- 在函数组件上,需要包裹在 forwardRef 中(因为函数组件没有实例,每次渲染时,函数组件会重新执行,没有持久化的对象可供引用)
- forwordRef(FuncComp)
- const FuncComp = (props, ref) => (函数组件)
- 函数组件的 forwardRef
先比对根节点,确定是否有进行 diff 的必要(从根节点开始的,子节点按顺序逐一比对):
- 新、旧节点都不存在,不做处理
- 新节点存在,旧节点不存在,新增新节点
- 旧节点存在,新节点不存在,直接移除旧节点
- 新、旧节点都存在,但是类型不一样,先移除旧节点,再创建新节点
- 新、旧节点都存在,类型也一样,进行 diff 比对
diff 比对,分为(实际上,类组件和函数组件最终都会是原生节点的比对,这只是一个递归的过程):
- 原生节点
- 更新属性
- 进入子节点的复杂 diff 比对
- 类组件
- 函数组件
- 文本节点
子节点复杂 diff 的比对:
- old: A B C D E
- new: C B E F A
- 不动: C E,因为它们的索引是递增的
- 移动: B A,因为它们的索引小于前面已处理节点的最大索引
- 新增: F
- 删除: D
遍历新的 VNode,找到:
- 可以复用,但需要移动的节点
- 需要新创建的节点
- 需要删除的节点
- 剩下的就是可以复用且不需要移动的节点
-
实现 componentDidMount() 生命周期
- 组件挂载到页面之后调用
-
实现 componentDidUpdate(prevProps, prevState, snapshot?) 生命周期
- 组件更新时调用,初始化渲染不会调用
- 当当前的 props 和之前的 props 不同的时候,这里会调用
- 如果 shouldComponentUpdate 返回 false,这里不会调用
- 如果实现了 getSnapshotBeforeUpdate,那么 componentDidUpdate 会接收第三个参数
-
实现 componentWillUnmount() 生命周期
- 组件从 DOM 树上卸载完成前调用
-
实现 shouldComponentUpdate(nextProps, nextState) 生命周期
- 组件更新之前调用(在 render 函数之前),初始化渲染不会调用
- 当 shouldComponentUpdate 返回 false,组件不会更新
-
实现 getDerivedStateFromProps(props, state) 生命周期
- 在 render 函数之前调用(无论是初始化渲染还是更新)
- 组件的 state 依赖 props,且需要在 props 变化时同步更新 state 时使用
-
实现 getSnapshotBeforeUpdate(prevProps, prevState) 生命周期
- 在组件更新前(DOM 即将变化但尚未渲染到屏幕时)被调用,用来捕获更新前的 DOM 状态(如滚动位置)
- 返回值会作为参数传递给 componentDidUpdate
- 实现 PureComponent
- 利用 shouldComponentUpdate 进行浅比较
- 浅比较的原理是:
- 比较两个对象是否相等,首先比较两个对象的引用地址是否相等(===)
- 如果引用地址不相等,再比较两个对象的属性个数是否相等(length)
- 如果属性个数相等,再比较两个对象的属性名和属性值是否相等(hasOwnProperty)
- 实现 memo
- 主要用于函数组件
- 浅比较 props
- 实现 useState
- 注意点:从根节点开始更新
- 实现 useReducer
- 实现 useEffect 和 useLayoutEffect
- useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新
- 使用 setTimeout 模拟宏任务
- useLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新
- queueMicrotask:用于将微任务加入微任务队列,微任务不会让出主线程,会阻塞渲染,直到队列清空
- js 任务执行顺序:同步任务 --> 微任务 --> 宏任务
- useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新
- 实现 useRef
- 返回一个对象,对象上有一个 current 属性,指向当前的值
- 实现 useImperativeHandle
- 自定义子组件暴露给父组件的方法或属性。与 forwardRef 配合使用,让父组件可以调用子组件的特定方法,限制父组件只能调用子组件的特定方法
- 实现 useMemo 和 useCallback
- useMemo:用于缓存计算结果,只有依赖项发生变化时,才会重新计算
- useCallback:用于缓存函数,只有依赖项发生变化时,才会重新创建函数

