从 0 开始实现一个最简单版本的 React
React JSXimport App from "./App.tsx" ReactDOM.createRoot(document.getElementById("root")!).render(<App />)
React JSXfunction App() { return <>This is App</>; } export default App;
先来实现一个 react-dom.js
React JSXfunction ReactDOMRoot(internalRoot) { this._internalRoot = internalRoot } ReactDOMRoot.prototype.render = function (children) { const root = this._internalRoot console.log("sedationh render", root, children) } function createRoot(container) { const root = { containerInfo: container } return new ReactDOMRoot(root) } export default { createRoot }
render 函数可以拿到
<App />
会 形成一个对象「Virtual DOM」,大概是下面的样子,更详细的内容 可看 或者 这里
这一层是由编译来做的,被转为一个函数调用,返回对象
JavaScript{ type: 'marquee', props: { bgcolor: '#ffa7c4', children: 'hi', }, key: null, ref: null, $$typeof: Symbol.for('react.element'), }
大白话就是只渲染 原生 DOM
React JSXReactDOM.createRoot(document.getElementById("root")!).render( <div> <h1>App</h1> <a href="https://baidu.com">baidu</a> This is App </div> )
在 render 后 去 updateContainer
React JSXfunction updateContainer(element, container) { const { containerInfo } = container const fiber = createFiber(element, { type: containerInfo.nodeName.toLocaleLowerCase(), stateNode: containerInfo, }) // 组件初次渲染 scheduleUpdateOnFiber(fiber) } ReactDOMRoot.prototype.render = function (children) { const root = this._internalRoot console.log("sedationh render", root, children) + updateContainer(children, root) }
containerInfo
就是 document.getElementById("root")
React JSXconst fiber = createFiber(element, { type: containerInfo.nodeName.toLocaleLowerCase(), stateNode: containerInfo, })
这里用 containerInfo
直接创建了一个 root node 的 fiber
JavaScriptexport function createFiber(vnode, returnFiber) {
createFiber 会利用 Virtual DOM 和 returnFiber 构建 fiber 结构
returnFiber 会给 return 构建 fiber 关系
React JSX// 第一个子fiber child: null, // 下一个兄弟节点 sibling: null, // 父亲节点 return: returnFiber,
React JSXexport function createFiber(vnode, returnFiber) { console.log("sedationh createFiber", vnode, returnFiber) const fiber = { // 类型 type: vnode.type, key: vnode.key, // 属性 props: vnode.props, // 不同类型的组件, stateNode也不同 // 原生标签 dom节点 // class 实例 stateNode: null, // 第一个子fiber child: null, // 下一个兄弟节点 sibling: null, // 父亲节点 return: returnFiber, flags: Placement, // 记录节点在当前层级下的位置 index: null, } if (isString(vnode.type)) { fiber.tag = HostComponent } return fiber }
形成的 fiber 会喂给 scheduleUpdateOnFiber
JavaScriptlet wip = null // work in progress 当前正在工作中的 let wipRoot = null export function scheduleUpdateOnFiber(fiber) { wip = fiber wipRoot = fiber }
到这里,render 的调用栈就结束了,requestIdleCallback
登场
React JSX/** - @param {IdleDeadline} idleDeadline */ function workLoop(idleDeadline) { while (wip && idleDeadline.timeRemaining() > 0) { performUnitOfWork() } if (!wip && wipRoot) { commitRoot() } } requestIdleCallback(workLoop)
这个可以理解为死循环一直执行 workLoop 函数,但只会在浏览器的渲染进程空闲的时候进行
RequestIdleCallback 简单的说,判断一帧有空闲时间,则去执行某个任务。 目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。 故 RequestIdleCallback 定位处理的是: 不重要且不紧急的任务。
workLoop 干两件事情
以下为 GPT 的说法
在 React Fiber 架构中,workLoop
函数是一个循环,用于驱动 React 应用程序的工作进程。它负责执行两个主要任务:performUnitOfWork
和 commitRoot
。
performUnitOfWork
:
performUnitOfWork
是 workLoop
的第一个任务。performUnitOfWork
会返回下一个要执行的工作单元,以便继续进行下一轮的工作。commitRoot
:
commitRoot
是 workLoop
的第二个任务。commitRoot
中,React 会遍历整个 Fiber 树,将需要更新的 DOM 节点进行插入、更新或删除操作,以反映应用程序的最新状态。commitRoot
执行完成,React 应用程序的界面就会得到更新,并呈现给用户。通过 performUnitOfWork
和 commitRoot
的交替执行,React 能够以递增的方式处理组件的更新,同时保持对用户界面的响应和流畅度。这种增量更新的方式也是 React Fiber 架构的核心思想之一。
需要注意的是,workLoop
函数还可能执行其他任务,如处理错误、调度优先级等,但 performUnitOfWork
和 commitRoot
是其最重要的两个任务,负责驱动 React 应用程序的工作。
GPT 说法结束
React JSXfunction performUnitOfWork() { const { tag } = wip switch (tag) { case HostComponent: updateHostComponent(wip) break default: break } // dfs if (wip.child) { wip = wip.child return } let next = wip while (next) { if (next.sibling) { wip = next.sibling return } next = next.return } wip = null }
performUnitOfWork 以 dfs 的方式走 fiber 链
遍历过程中,根据 fiber 结构中的 tag 进行区分
ReactWorkTags.js
JavaScriptexport const FunctionComponent = 0 export const ClassComponent = 1 export const IndeterminateComponent = 2 // Before we know whether it is function or class export const HostRoot = 3 // Root of a host tree. Could be nested inside another node. export const HostPortal = 4 // A subtree. Could be an entry point to a different renderer. export const HostComponent = 5 export const HostText = 6 export const Fragment = 7 export const Mode = 8 export const ContextConsumer = 9 export const ContextProvider = 10 export const ForwardRef = 11 export const Profiler = 12 export const SuspenseComponent = 13 export const MemoComponent = 14 export const SimpleMemoComponent = 15 export const LazyComponent = 16 export const IncompleteClassComponent = 17 export const DehydratedFragment = 18 export const SuspenseListComponent = 19 export const ScopeComponent = 21 export const OffscreenComponent = 22 export const LegacyHiddenComponent = 23 export const CacheComponent = 24 export const TracingMarkerComponent = 25
updateHostComponent 的工作是两块
Arrau<Vritual DOM>
children 接着完善 fiber 链JavaScriptexport function updateHostComponent(wip) { if (!wip.stateNode) { wip.stateNode = document.createElement(wip.type) updateNode(wip.stateNode, wip.props) } reconcileChildren(wip, wip.props.children) } export function updateNode(node, nextVal) { Object.keys(nextVal).forEach((key) => { if (key === "children") { if (isStringOrNumber(nextVal[key])) { node.textContent = nextVal[key] } } else { node[key] = nextVal[key] } }) } function reconcileChildren(wip, children) { if (isStringOrNumber(children)) { return } const newChildren = isArray(children) ? children : [children] let previousNewFiber = null for (let i = 0; i < newChildren.length; i++) { const newChild = newChildren[i] if (newChild == null) { continue } const newFiber = createFiber(newChild, wip) if (previousNewFiber === null) { // head node wip.child = newFiber } else { previousNewFiber.sibling = newFiber } previousNewFiber = newFiber } }!
接下来看 commitRoot 的工作。以 dfs 的方式进行 DOM 改动,改动的流程就是找父亲节点,然后吧在 performUnitOfWork 中准备的 DOM 节点塞进去
JavaScriptfunction commitRoot() { commitWorker(wipRoot) wipRoot = null } function commitWorker(wip) { if (!wip) { return } // 1. 提交自己 const parentNode = getParentNode(wip.return) const { flags, stateNode } = wip if (flags & Placement && stateNode) { parentNode.appendChild(stateNode) } // 2. 提交子节点 commitWorker(wip.child) // 3. 提交兄弟 commitWorker(wip.sibling) } function getParentNode(wip) { let p = wip while (p) { if (p.stateNode) { return p.stateNode } p = p.return } }
至此,完成 「支持 HostComponent、仅仅创建流程,最简 React」
看效果 👇
React JSXReactDOM.createRoot(document.getElementById("root")!).render( <div> <h1>App</h1> <a href="https://baidu.com">baidu</a> This is App </div> )
在 createFiber
的时候,增加 fiber.tag = FunctionComponent
JavaScriptexport function createFiber(vnode, returnFiber) { console.log("sedationh createFiber", vnode, returnFiber) const fiber = { // 类型 type: vnode.type, key: vnode.key, // 属性 props: vnode.props, // 不同类型的组件, stateNode也不同 // 原生标签 dom节点 // class 实例 stateNode: null, // 第一个子fiber child: null, // 下一个兄弟节点 sibling: null, return: returnFiber, flags: Placement, // 记录节点在当前层级下的位置 index: null, } if (isString(vnode.type)) { fiber.tag = HostComponent } if (isFunction(vnode.type)) { fiber.tag = FunctionComponent } return fiber }
然后在 performUnitOfWork
处理 FunctionComponent
,调用函数,返回值作为 children 接着构建 fiber 链
JavaScriptfunction performUnitOfWork() { const { tag } = wip switch (tag) { case HostComponent: updateHostComponent(wip) break case FunctionComponent: updateFunctionComponent(wip) break export function updateFunctionComponent(wip) { const { type, props } = wip const children = type(props) reconcileChildren(wip, children) }
接下来 ClassComponent 和 HostText
JavaScriptfunction performUnitOfWork() { const { tag } = wip switch (tag) { case HostComponent: updateHostComponent(wip) break case FunctionComponent: updateFunctionComponent(wip) break case ClassComponent: updateClassComponent(wip) break case HostText: updateHostComponent(wip) break export function updateClassComponent(wip) { const { type, props } = wip const instance = new type(props) const children = instance.render() reconcileChildren(wip, children) } export function updateHostTextComponent(wip) { wip.stateNode = document.createTextNode(wip.props.children) }
解释下 HostText 「就是 同级 下有至少两个的节点、且有文本节点的情况」,如下面的 「有其他同级元素的文本」,「App」 不算
React JSX<div> <h1>App</h1> <a href="https://baidu.com">baidu</a> 有其他同级元素的文本 {/* @ts-ignore */} <ClassComp /> </div>
「App」 的这种情况在 updateNode
的时候进行了处理
React JSXexport function updateNode(node, nextVal) { Object.keys(nextVal).forEach((key) => { if (key === "children") { if (isStringOrNumber(nextVal[key])) { // STUDY: seda 文本节点处理 node.textContent = nextVal[key] } } else { node[key] = nextVal[key] } }) }
并且会在 reconcileChildren
的时候进行返回
React JSXfunction reconcileChildren(wip, children) { if (isStringOrNumber(children)) { return }
Fragment 的处理比较简单,略
前面提到
requestIdleCallback
工作只有 20FPS,一般对用户来感觉来说,需要到 60FPS 才是流畅的, 即一帧时间为 16.7 ms,所以这也是react
团队自己实现requestIdleCallback
的原因。实现大致思路是在requestAnimationFrame
获取一桢的开始时间,触发一个postMessage
,在空闲的时候调用idleTick
来完成异步任务。 -- https://juejin.cn/post/6844904196345430023#heading-11
React Scheduler 为什么使用 MessageChannel 实现
基本思路可参考 https://github.com/kodecocodes/swift-algorithm-club/blob/master/Heap/README.markdown
先去除之前用 requestIdleCallback 的调用方式
React JSXfunction workLoop() { while (wip) { performUnitOfWork() } if (!wip && wipRoot) { commitRoot() } } // requestIdleCallback(workLoop)
workLoop 的触发时机是在
React JSXexport function scheduleUpdateOnFiber(fiber) { wip = fiber wipRoot = fiber scheduleCallback(workLoop) }
scheduleCallback
负责调度我们的 workLoop
实现如下
React JSXimport { Heap } from "./heap" let taskIdCounter = 0 const taskHeap = new Heap((parent, child) => { if (parent.expirationTime === child.expirationTime) { return parent.id < child.id } return parent.expirationTime < child.expirationTime }) export function scheduleCallback(callback) { const currentTime = getCurrentTime() const timeout = -1 const expirationTime = currentTime - timeout const newTask = { id: taskIdCounter, callback, expirationTime, } taskIdCounter += 1 taskHeap.add(newTask) requestHostCallback() } const channel = new MessageChannel() function requestHostCallback() { channel.port1.postMessage(null) } channel.port2.onmessage = function () { workLoop() } function workLoop() { let currentTask = taskHeap.pop() while (currentTask) { const callback = currentTask.callback callback() currentTask = taskHeap.pop() } } export function getCurrentTime() { return performance.now() }
我们传入的 workLoop
作为 scheduleCallback
的 callback
进入,被组织为 task
加入 taskHeap
下面是我们测试自己写的 useReducer 的例子
React JSXfunction FunctionComp() { const [cnt, dispatchCnt] = useReducer((state, action) => { console.log("sedationh action", action, n++) return state + 1 }, 0) const [cnt2, dispatchCnt2] = useReducer((state, action) => { console.log("sedationh action", action, n++) return state + 2 }, 0) return ( <div> <button onClick={() => { dispatchCnt("action") }} > dispatchCnt cnt1 </button> {cnt} <hr /> <button onClick={() => { dispatchCnt2("action") }} > dispatchCnt cnt2 </button> {cnt2} <h1>FunctionComp</h1> </div> ) }
先简单处理下事件行为 「onClick」
React JSXexport function updateNode(node, nextVal) { Object.keys(nextVal).forEach((key) => { if (key === "children") { if (isStringOrNumber(nextVal[key])) { node.textContent = nextVal[key] } } else if (key.slice(0, 2) === "on") { const eventName = key.slice(2).toLowerCase() node.addEventListener(eventName, nextVal[key]) } else { node[key] = nextVal[key] } }) }
hook 的结构如下
React JSXhook = { memoriedState: null, // 状态 next: null, // 下一个 hook }
关键逻辑
React JSXexport const useReducer = (reducer, initialState) => { const hook = updateWorkInProgressHook() if (!currentlyRenderingFiber.alternate) { // 初次渲染 hook.memoriedState = initialState } const dispatch = (action) => { hook.memoriedState = reducer(hook.memoriedState, action) currentlyRenderingFiber.alternate = { ...currentlyRenderingFiber } scheduleUpdateOnFiber(currentlyRenderingFiber) } return [hook.memoriedState, dispatch] }
为了能满足上面的代码,需要在 fiber 上增加一些信息
React JSXexport function createFiber(vnode, returnFiber) { const fiber = { ... // old fiber alternate: null, // function component, hook0 memoriedState: null, } ...
React JSXlet currentlyRenderingFiber = null let workInProgressHook = null function updateWorkInProgressHook() { let hook const current = currentlyRenderingFiber.alternate if (!current) { // 初次渲染 hook = { memoriedState: null, next: null, } if (!workInProgressHook) { // hook0 workInProgressHook = currentlyRenderingFiber.memoriedState = hook } else { // hook1, hook2, hook3 ... workInProgressHook = workInProgressHook.next = hook } } else { // 更新 currentlyRenderingFiber.memoriedState = current.memoriedState if (!workInProgressHook) { // hook0 hook = workInProgressHook = currentlyRenderingFiber.memoriedState } else { // hook1, hook2, hook3 ... hook = workInProgressHook = workInProgressHook.next } } return hook }
currentlyRenderingFiber 怎么拿到呢?
React JSXexport const renderWithHooks = (wip) => { currentlyRenderingFiber = wip currentlyRenderingFiber.memoriedState = null workInProgressHook = null }
React JSXexport function updateFunctionComponent(wip) { renderWithHooks(wip) const { type, props } = wip const children = type(props) reconcileChildren(wip, children) }
接下来在 reconcileChildren 中进行 diff,进行更新标记
React JSX// 协调(diff) // 创建新 fiber function reconcileChildren(wip, children) { if (isStringOrNumber(children)) { return } const newChildren = isArray(children) ? children : [children] let oldFiber = wip.alternate?.child let previousNewFiber = null for (let i = 0; i < newChildren.length; i++) { const newChild = newChildren[i] if (newChild == null) { continue } const newFiber = createFiber(newChild, wip) const same = isSame(oldFiber, newFiber) if (same) { Object.assign(newFiber, { alternate: oldFiber, stateNode: oldFiber.stateNode, flags: Update, }) } if (oldFiber) { oldFiber = oldFiber.sibling } if (previousNewFiber === null) { // head node wip.child = newFiber } else { previousNewFiber.sibling = newFiber } previousNewFiber = newFiber } }
接下来完善 commit 环节
React JSXfunction commitWorker(wip) { if (!wip) { return } // 1. 提交自己 const parentNode = getParentNode(wip.return) const { flags, stateNode } = wip if (flags & Placement && stateNode) { parentNode.appendChild(stateNode) } if (flags & Update && stateNode) { updateNode(stateNode, wip.alternate.props, wip.props) } // 2. 提交子节点 commitWorker(wip.child) // 3. 提交兄弟 commitWorker(wip.sibling) }
因为需要比较,所有原来的 updateNode 也要改下
React JSXexport function updateNode(node, prev, nextVal) { Object.keys(prev).forEach((key) => { if (key === "children") { if (isStringOrNumber(nextVal[key])) { node.textContent = "" } } else if (key.slice(0, 2) === "on") { const eventName = key.slice(2).toLowerCase() node.removeEventListener(eventName, prev[key]) } else { node[key] = "" } }) Object.keys(nextVal).forEach((key) => { if (key === "children") { if (isStringOrNumber(nextVal[key])) { // STUDY: seda 文本节点处理 node.textContent = nextVal[key] } } else if (key.slice(0, 2) === "on") { const eventName = key.slice(2).toLowerCase() node.addEventListener(eventName, nextVal[key]) } else { node[key] = nextVal[key] } }) }
现在来整体看下流程
思路是差不多的
React JSXexport const useState = (initialState) => { const hook = updateWorkInProgressHook() if (!currentlyRenderingFiber.alternate) { // 初次渲染 hook.memoriedState = initialState } const fiber = currentlyRenderingFiber const dispatch = (newState) => { hook.memoriedState = typeof newState === "function" ? newState(hook.memoriedState) : newState fiber.alternate = { ...fiber } fiber.sibling = null scheduleUpdateOnFiber(fiber) } return [hook.memoriedState, dispatch] }
目前的更新能力都是通过位置来比较的,具体来说如
CODEnew abcd old cd
a ! c b ! d
这俩都会被删除
React JSXexport function createFiber(vnode, returnFiber) { const fiber = { ... // 处理删除 deletoins: null, }
React JSXfunction reconcileChildren(returnFiber, children) { if (isStringOrNumber(children)) { return } const newChildren = isArray(children) ? children : [children] let oldFiber = returnFiber.alternate?.child let previousNewFiber = null let newIndex for (newIndex = 0; newIndex < newChildren.length; newIndex++) { const newChild = newChildren[newIndex] if (newChild == null) { continue } const newFiber = createFiber(newChild, returnFiber) const same = isSame(oldFiber, newFiber) if (same) { Object.assign(newFiber, { alternate: oldFiber, stateNode: oldFiber.stateNode, flags: Update, }) } if (!same && oldFiber) { // 删除节点 deleteChild(returnFiber, oldFiber) } if (oldFiber) { oldFiber = oldFiber.sibling } if (previousNewFiber === null) { // head node returnFiber.child = newFiber } else { previousNewFiber.sibling = newFiber } previousNewFiber = newFiber } /** - new ab - old abcdef * - 通过遍历 ab 是走不到 cdef 的 */ if (newIndex === newChildren.length) { deleteRemainingChildren(returnFiber, oldFiber) return } } function deleteRemainingChildren(returnFiber, currentFirstChild) { let childToDelete = currentFirstChild while (childToDelete) { deleteChild(returnFiber, childToDelete) childToDelete = childToDelete.sibling } }