Skip to content

jsx 方法 ​

是一切的开端,需要将用户书写的 jsx 通过本地编译成 React 认识的形式,才能进一步转换为虚拟 DOM

jsx 方法相当于 React 18 以前的 React.createElement 方法

jsx 是什么 ​

jsx 是一个 JavaScript 的语法扩展,jsx 可以很好地描述 UI 应该呈现出它应有交互的本质形式 astexplorer 可以把代码转换成 AST 树 react/jsx-runtime 和 react/jsx-dev-runtime 中的函数只能由编译器转换使用。如果你需要在代码中手动创建 元素,你可以继续使用 React.createElement

js
// 在React17 以前,babel转换是老的写法
<h1>
	hello<span style={{ color:"red"}}>world</span>
</h1>

// 上面的代码会在 @babel/core 以及 @babel/plugin-transform-react(该插件 runtime 选项设置 classic 值时) 插件转换为如下代码
React.createElement("h1",null,"hello",
/*_PURE_*/React.createElement("span",{
    style:{
    color:"red"
    }
},"world"))

所以 react 18 以后在 .jsx 文件中无需强制导入 React ,因为新版写法会自动导入所依赖的 jsx 函数,而旧版不会自动导入所依赖的 React.createElement 函数

js
// 18 后新写法
<h1>
	hello<span style={{ color:red’}}>world</span>
</h1>

// 上面的代码会在 @babel/core 以及 @babel/plugin-transform-react(该插件 runtime 选项设置 automatic 值时) 插件转换为如下代码
import { jsx } from “react/jsx-runtime";
/*#__PURE*/
jsx("h1",{
    children:["hello",/*#_PURE_*/jsx("span",{
        style:{
        	color:'red'
        },
    	children:"world"
  })]
})
// React.createElement 相当于 jsx

创建FiberRoot ​

创建 FiberRootNode ​

通过 FiberRootNode 创建 Fiber 根节点。

简单来说 FiberRootNode = containerInfo,它的本质就是一个真实的容器DOM节点 div#root 其实就是一个真实的 DOM 只不过包装了一下。

js
function FiberRootNode(containerInfo){
	this.containerInfo = containerInfo; // div#root
}
export function createFiberRoot(containerInfo){
    const root = new FiberRootNode(containerInfo);
    return root;
}

fiber ​

通过Fiber架构,让自己的调和过程变成可被中断。适时地让出CPU执行权,让浏览器及时地响应用户的交互

每个虚拟 DOM 对应一个 Fiber 节点,每个 Fiber 节点对应一个真实 DOM

fiber 用于记录虚拟 DOM 的节点信息,如当前节点的更新队列,副作用标识(即要执行增删改何种操作)等,并且 fiber 是一个链表结构,这样更新任务就变得可中断

Fiber是一个执行单元 ​

每次执行完一个执行单元,React就会检查现在还剩多少时间,如果没有时间就将控制权让出去

fiber是一种数据结构 ​

React目前的做法是使用链表,每个虚拟节点内部表示为Fiber 从顶点开始遍历 如果有第一个儿子,先遍历第一个儿子 如果没有第一个儿子,标志着此节点遍历完成 如果有弟弟遍历弟弟 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔 没有父节点遍历结束

其他补充 ​

性能瓶颈 ​

JS任务执行时间过长,浏览器刷新频率为60Hz,大概16.6毫秒渲染一次,而JS线程和渲染线程是互斥的,所以如果JS线程执行任务时间超过16.6ms的话,就会导致掉帧,导致卡顿,解决方案就是React利用空闲的时间进行更新,不影响渲染进行的渲染 把一个耗时任务切分成一个个小任务,分布在每一帧里的方式就叫时间切片

屏幕刷新率 ​

目前大多数设备的屏幕刷新率为60次/秒 浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值时,用户会感觉到卡顿 每个帧的预算时间是16.66毫秒(1秒/60) 1s 60帧,所以每一帧分到的时间是1000/60~16ms,所以我们书写代码时力求不让一帧的工作量超过16ms

帧 ​

每个帧的开头包括样式计算、布局和绘制 JavaScript执行Javascript引擎和页面染引擎在同一个染线程,GUI渲染和Javascript执行两者是互斥的 如果某个任务执行时间过长,浏览器会推迟渲染

requestldleCallback ​

我们希望快速响应用户,让用户觉得够快,不能阻塞用户的交互 requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应 正常帧任务完成后没超过16ms,说明时间有富余,此时就会执行requestIdleCallback里注册的任务

react 内部自己实现了一个用来模拟 requestldleCallback ,原因是:1)兼容性问题 2)富余时间不可控导致执行时间不可控

总结 ​

1.在初始化阶段,根据 jsx 方法将用户书写的 jsx 文件代码转换为虚拟 DOM。

2.创建 FiberRootNode Fiber 根节点,创建根 Fiber (HostRootFiber,即 Fiber 树根节点)并互相指向(Fiber 根节点的 current 指向根 Fiber ,根 Fiber 的 stateNode 指向 Fiber 根节点)

FiberRootNode 是一个真实 DOM 节点(即根容器),HostRootFiber 是 Fiber 树的根节点(即根容器对应的 Fiber 节点)

3.把更新的虚拟 DOM 放到根 Fiber 的更新队列上

4.根据根 Fiber 的更新队列与老状态创建一个新的 Fiber 树(上一步是初始化 Fiber 树只有一个节点),然后新的 Fiber 树根节点的 alternate 指向老的 Fiber 树,老的 Fiber 树根节点的 alternate 指向新的 Fiber 树(双缓冲技术)。

补充:等新的 Fiber 树构建完成同步到页面上后,第一步创建的 FiberRootNode 的 current 会指向这个新的 Fiber 树,等虚拟 DOM 变更后会直接在老的 Fiber 树上更新 Fiber 树变成最新的 Fiber 树(复用),再次将第一步创建的 FiberRootNode 的 current 指向这个更新后的 Fiber 树,此后虚拟 DOM 再变更后重复这个步骤

5.递归遍历 Fiber 树,执行 Fiber 上的副作用(副作用就是对 DOM 节点的操作,增删改等且处理副作用时是先递归遍历到最深的节点并执行其副作用,然后向上传递依次执行副作用。), 等所有 Fiber 节点的副作用执行完毕后(即 DOM 变更后),将根容器的 current 指向这个新的 Fiber 树,至此初始化渲染流程完毕

事件处理 ​

事件 ​

事件是用户或浏览器自身执行的某种动作,而响应某个事件的函数叫做事件处理程序

DOM 事件流 ​

首先发生的是事件捕获,然后是实际的目标接收到事件,最后阶段是冒泡阶段

事件代理 ​

事件代理是把原本需要绑定在子元素的事件委托给父元素,让父元素负责事件监听

合成事件 ​

合成事件是围绕浏览器原生事件充当跨浏览器包装器的对象,它们将不同浏览器的行为合并为一个 API 这样做是为了确保事件在不同浏览器中显示一致的属性

react 中绑定的事件都委托到了根容器上

简单实现合成事件。主要是通过自定义属性以及事件委托实现

html
<div id="root">
    <div id="parent">
        <div id="child">点我</div>
    </div>
</div>

<script>
    const child = document.querySelector('#child')
    const parent = document.querySelector('#parent')
    const root = document.querySelector('#root')
    
    // 绑定自定义属性
    child.myclick = ()=>{console.log('child')}
    parent.myclick = ()=>{console.log('parent')}
    
    // 事件委托
    root.addEventListener('click', e => {
        const paths = []
        let target = e.target
        while (target) {
            paths.push(target)
            target = target.parentNode
        }
        paths.forEach(item => item.myclick && item.myclick())
    })
</script>

具体实现 ​

1.react 在创建根容器时就进行事件绑定(即在根容器绑定各种事件,如 click 事件且会绑定捕获阶段和冒泡阶段),且只会绑定一次。

ts
export function createRoot(container){
    const root = createContainer(container)
    // 事件绑定
    listenToAllSupportedEvents(container)
    return new ReactDOMRoot(root)
}

2.修改原生事件源参数。将事件参数(即 event )做一些扩展并将一些原生 API 做一些兼容性处理

3.依次派发事件并将处理过的事件源参数传递过去。

React Hooks ​

useReducer ​

基本使用

tsx
function counter(state, action) {
  if (action.type === 'add') return state + action.payload
  return state
}

function FunctionComponent() {
  const [number, setNumber] = React.useReducer(counter, 0)
  return <button onclick={() => setNumber({ type: 'add', payload: 1 })}>{number}</button>
}

具体实现 ​

1.先在全局声明一个变量 ReactCurrentDispatcher,在执行函数组件函数之前给给此变量赋值一个 mountReducer(挂载阶段的 useReducer 函数)

ts
const HooksDispatcherOnMount = {
  useReducer: mountReducer
}
function mountReducer(reducer, initialArg) {
  // ...实现逻辑...
}
// 当前正在渲染中的 fiber
let currentRenderingFiber = null;

export function renderWithHooks(current, workInProgress, Component, props) {
  // 保存当前正在渲染中的 fiber,方便下一步将其与构建好的 hook 链接
  currentRenderingFiber = workInProgress
  // 需要要函数组件执行前给 ReactCurrentDispatcher.current 赋值
  ReactCurrentDispatcher.current = HooksDispatcherOnMount
  const children = Component(props)
  return children
}

2.初始化 hook 链表(不同 hook 之前的单向链表)关系,并将当前正在渲染中的 fiber 与构建好的 hook 链接

ts
function mountReducer(reducer, initialArg) {
  const hook = mountWorkInProgressHook();
}
// 用于保存当前的 hook
let workInProgressHook = null

function mountWorkInProgressHook() {
  const hook = {
    // 存放本 hook 的状态。即 React.useReducer(counter, 0) 括号中的 0
    memoizedState: null,
    /*
		存放本 hook 的更新队列。当一个函数组件中执行多次 hook 生成一个循环链表,即如下场景
        <button onclick={() => {
            setNumber({ type: 'add', payload: 1 })
            setNumber({ type: 'add', payload: 2 })
            setNumber({ type: 'add', payload: 3 })
        }}>{number}</button>
       如上代码调用了 3 次 hook 此时会生成一个循环链表:updata1 => updata2 => updata3 => updata1
 	*/
    queue: null,
    /*
    执向下一个 hook
    function App() {
      const [number, setNumber] = React.useReducer(counter, 0)
      const [number2, setNumber2] = React.useReducer(counter2, 0)
      return ''
    }
    如上代码中存在多个 hook,会生成一个单向链表,此时 number 对应的 hook 的 next 会指向 number2 对应的 hook
    */
    next: null //指向下一个hook,I
  }
  // 构建 hook 单向链表
  if (workInProgressHook === null) {
    // 将当前正在渲染中的 fiber 与构建好的 hook 链接
    currentRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    /*
    workInProgressHook = workInProgressHook.next = hook 等价于如下写法:
        workInProgressHook.next = hook;
        workInProgressHook =hook;
    */
    workInProgressHook = workInProgressHook.next = hook
  }
  return workInProgressHook
}

3.完成 hook 的初次挂载。

ts
function mountReducer(reducer, initialArg) {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = initialArg;
  const queue = { pending:null }
  hook.queue = queue;
  const dispatch = dispatchReducerAction.bind(null, currentRenderingFiber,queue);
  // 此处返回的参数相当于 const [number, setNumber] = React.useReducer(counter, 0) 的数组
  return [hook.memoizedState,dispatch];
}

/**
 * 执行派发动作的方法,它要更新状态,并且让界面重新更新
 * @param fiber function对应的fiber
 * @param queue ok对应的更新队列
 * @param action 发的动作
 */
function dispatchReducerAction(fiber, queue, action) {
  // 实现逻辑...
}

4.处理 Hook 内部更新逻辑。即将当前 Hook 创建更新队列,维护成循环链表。

ts
function dispatchReducerAction(fiber, queue, action) {
  //在每个hook里会存放一个更新队列,更新队列是一个更新对象的循环链表update1.next=update2.next=update1
  const update = {
    action, //{ type:'add',payload:1}
    next: null
  }
  // 把当前的最新的更添的添加更新队列中,并且返回当前的根 fiiber
  const root = enqueueConcurrentHookupdate(fiber, queue, update)
}

5.从根节点(div#root)开始调度更新。在构建 fiber 树时将 Hook 更新链表存放到对应 fiber 的更新列表中

6.当有老的 fiber,并且有老的 hook 链表时(更新阶段),进入更新。根据当前老的 hook 构建出新的 hook,并结合老 hook 的状态计算新 hook 的状态

ts
function updateReducer(reducer){
    // 获取将要构建的新的 hook 对应的老 hook,根据老 hook 创建新 hook
    const hook = updateWorkInProgressHook();
    // 结合老 hook 的状态计算新 hook 的状态相关逻辑......
    // 返回新 hook 
    return [hook.memoizedState,queue.dispatch]
}

useState实现 ​

其实现逻辑与 useReducer 非常相似

1.和 useReducer 相似,在 HooksDispatcherOnMount 和 HooksDispatcherOnUpdate 中分别添加挂载时和更新时的调用函数

ts
// 和 useReducer 类似,挂载阶段会创建 hook ,更新阶段会基于老 hook 更新,得到新 hook
// 挂载
const HooksDispatcherOnMount = {
    useReducer: mountReducer,
    useState: mountState
}
// 更新
const HooksDispatcherOnUpdate = {
    useReducer: mountReducer,
    useState:updateState
}

function mountState(initialState) {
    // 和 useReducer 类似,初始化 hook 队列以及状态
    const hook = mountWorkInProgressHook()
    hook.memoizedState = initialState
    const queue = {
        pending:null,
        dispatch:null
    }
    hook.queue = queue
    const dispatch =(queue.dispatch = dispatchSetState.bind(null,currentlyRenderingFiber,queue))
    return [hook.memoizedState,dispatch]
}

2.实现更新时的 updateState

ts
// 更新时的 updateState 会直接使用 updateReducer 的逻辑
function updateState() {
    return updateReducer(baseStateReducer)
}

// useState 其实就是一个内置了 reducer 的 useReducer
function baseStateReducer(state,action){
    return typeof action === 'function'? action(state):action;
}

// 也就是说用户在使用 useState hook 时可以直接传递一个回调函数,通过回调函数计算新状态,useReducer 内部会将老状态传递给用户的回调函数,也就是说用户的回调函数参数中可以拿到老状态
const [number,setNumber]= React.useState(0);
setNumber(number => number + 1)

dom diff ​

紧接上一步,进行 dom diff 计算新旧 fiber 的差异

1.如上图,当新的 vdom 是一个单节点时的流程。

2.节点属性比较:比较新老属性(style、子节点文本等)的差异,最终存放在一个数组内,

ts
// 表示哪些属性发生了变化
['id',1666517027029,'onClick',f,'children',6]

3.重新渲染更新页面

完整实现 ​

单节点 key 不同,类型相同 ​

1.当是单节点 key 不同,类型相同,创建添加新节点,直接删除老节点,然后在此 fiber 的父 fiber 上 deletions (记录将要删除的子 fiber 列表) 属性添加此 fiber ,并且将此 fiber 的父 fiber 上的 flags (用于记录此 fiber 的副作用,增删改等) 添加删除标记。

ts
// 具体实现
function deleteChild(returnFiber, childToDelete) {
    // 判断如果不需要更新副作用则不做处理
    if (!shouldTrackSideEffects) return
    // 获取父 fiber 的 deletions 
    const deletions = returnFiber.deletions
    if (deletions === null) {
        // 在父 fiber 上添加此 fiber (删除列表)和删除标记
        returnFiber.deletions = [childToDelete]
        // 在父 fiber 上添加删除副作用标记
        returnFiber.flags |= ChildDeletion
    } else {
        returnFiber.deletions.push(childToDelete)
    }
}

// 用户代码
function FunctionComponent() {
  const [number, setNumber] = React.useState(0)
  // 类型相同 key 不同
  return number === 0 ? (
    <div onClick={() => setNumber(number + 1)} key="title1" id="title">
      title
    </div>
  ) : (
    <div onclick={() => setNumber(number + 1)} key="title2" id="title2">
      title2
    </div>
  )
}

2.在提交阶段删除节点。先递归遍历删除子 fiber 上 deletions 对应节点,再删除自身 fiber 上的 deletions 对应节点。之所以不直接删除自己(删除自己后就无需再递归遍历子 fiber 将其删除)是因为有些事情需要做(如类组件的存在,需要触发类组件的卸载钩子)。

ts
function recursivelyTraverseMutationEffects(root, parentFiber) {
    // 获取父 fiber 上的删除列表 deletions
    const deletions = parentFiber.deletions
    if (deletions !== null) {
        // 循环遍历删除此列表中的节点
        for (let i = 0; i < deletions.length; i++) {
            const childToDelete = deletions[i]
            // 递归遍历所有子 fiber 再删除其上的删除列表中的节点
            commitDeletionEffects(root, parentFiber, childToDelete)
        }
    }
}
单节点key相同,类型不同 ​

不能复用此老 fiber,删除包括当前 fiber 在内的所有的老 fiber。

ts
// 具体实现
function deleteRemainingChildren(returnFiber, currentFirstChild) {
    // 判断如果不需要更新副作用则不做处理
    if (!shouldTrackSideEffects) return
    // 获取要删除的子节点
    let childToDelete = currentFirstChild
    while (childToDelete !== null) {
        // 遍历当前节点的所有兄弟节点删除
        deleteChild(returnFiber, childToDelete)
        childToDelete = childToDelete.sibling
    }
    return null
}

// 用户代码
function FunctionComponent() {
  const [number, setNumber] = React.useState(0)
  // 类型不同 key 相同
  return number === 0 ? (
    <div onClick={() => setNumber(number + 1)} key="title1" id="title">
      title
    </div>
  ) : (
    <p onclick={() => setNumber(number + 1)} key="title1" id="title1">
      title2
    </p>
  )
}

多节点diff ​

DOM DIFF的三个规则:1.对同级元素进行比较,不同层级不对比。2.不同的类型对应不同的元素3.可以通过key来标识同一个节点

多个节点的数量和 key 相同,有的 type 不同 ​

1.先进行第一轮遍历。将老 fiber 中 key 与新的相同 type 与新的不同的 fiber 标记删除,将老 fiber 中 key 和 type 与新的都相同的 fiber 复用。

ts
// 用户代码
function FunctionComponent() {
  const [number, setNumber] = React.useState(0)
  // 类型不同 key 相同
  return number === 0 ? (
    <ul key="container" onClick={() => setNumber(number + 1)}>
      <li key="A" id="A">A</li>
      <li key="B" id="B">B</li>
      <li key="C" id="C">C</li>
    </ul>
  ) : (
    <ul key="container" onClick={() => setNumber(number + 1)}>
      <li key="A" id="A2">A2</li>
      <p key="B" id="B">B</p>
      <li key="C" id="C2">C2</li>
    </ul>
  )
}
ts
function reconcilechildrenArray(returnFiber, currentFirstchild, newchildren) {
  letresultingFirstChild = null // 返回的第一个新儿子
  letpreviousNewFiber = null // 上一个的一个新的儿 fiber
  letnewIdx = 0 // 用来遍历新的虚拟 DoM 的索引
  letoldFiber = currentFirstChild // 第一个老 fiber
  letnextoldFiber = null // 下一个老 fiber
  // 开始第一轮循环如果老fiber有值,新的虚拟DOM也有值
  for (; oldFiber !== null && newIdx < newchildren.length; newIdx++) {
    //先暂存下一个老fiber
    nextoldFiber = oldFiber.sibling
    // 试图更新或者复用老的fiber
    const newFiber = updateSlot(returnFiber, oldFiber, newchildren[newIdx])
    // 如果没有创建新的 fiber 也没有复用老的 fiber,跳出循环
    if (newFiber === null) break
    // 如果需要跟踪副作用
    if (shouldTrackSideEffects) {
      // 如果有老fiber,但是新的fiber并没有成功复用老fiber和老的真实DoM,那就删除老fiber,在提交阶段会删除真实 DOM
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber)
      }
    }
    // 根据索引放置 fiber
    placeChild(newFiber, newIdx)
    // 构建新的 fiber 树
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }
    previousNewFiber = newFiber
    oldFFiber = nextoldFiber
  }
}
多个节点的类型和key全部相同,有新增元素 ​

2.进行第二轮循环。老 fiber 遍历完了,而新的 fiber 还有,将剩下的 fiber 标记为插入,DIFF结束

ts
function reconcilechildrenArray(returnFiber, currentFirstchild, newchildren) {
    // 第一轮循环逻辑代码......
    for (; oldFiber !== null && newIdx < newchildren.length; newIdx++) {
        // 第一轮循环逻辑代码......
    }
    // 如果老的fiber已经没有了,新的虚拟DOM还有,进入插入新节点的逻辑
    if(oldFiber === null){
        for (; newIdx < newchildren.length; newIdx++) {
            const newFiber = createChild(returnFiber, newchildren[newIdx])
            if (newFiber === null) continue
            placeChild(newFiber, newIdx)
            // 如果previousNewFiber为null,说明这是第一个fiber
            if (previousNewFiber === null) {
                resultingFirstChild = newFiber //这个newFiber就是大儿子
            } else {
                //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
                previousNewFiber.sibling = newFiber
            }
            //让newFiber成为最后一个或者说上一个子fiber
            previousNewFiber = newFiber
        }
    }
}
多个节点的类型和key全部相同,有删除老元素 ​

3.新的 fiber 已经循环完毕,还有老的 fiber ,将剩下的全部删除。至此第二轮遍历完成

ts
// 用户代码
function FunctionComponent() {
  const [number, setNumber] = React.useState(0)
  // 类型不同 key 相同
  return number === 0 ? (
    <ul key="container" onClick={() => setNumber(number + 1)}>
      <li key="A" id="A">A</li>
      <li key="B" id="B">B</li>
      <li key="C" id="C">C</li>
    </ul>
  ) : (
    <ul key="container" onClick={() => setNumber(number + 1)}>
      <li key="A" id="A2">A2</li>
      <p key="B" id="B">B</p>
    </ul>
  )
}
ts
function reconcilechildrenArray(returnFiber, currentFirstchild, newchildren) {
    // 第一轮循环逻辑代码......

    // 新的虚拟 DOM 已经循环完毕,还有老的 fiber
    if (newIdx === newChildren.length) {
        // 删除剩下的老 fiber
        deleteRemainingChildren(returnFiber, oldFiber)
    }

    // 第二轮循环逻辑代码......
}
多个节点数量不同、key 不同(重点) ​

4.新旧 fiber 都没有完成,进行节点移动的逻辑

useEffect ​

会在页面绘制完成之后执行。

1.添加挂载阶段的 useEffect 和更新阶段的 useEffect 函数。使用了 useEffect 或者 useLayoutEffect 的组件 fiber 上除了会维护一个 hook 链表,还会维护一个 effect 链表,将该组件中所有 useEffect 、useLayoutEffect 链接起来,用于执行 effect 时更加方便。

ts
const HooksDispatcherOnMount = {
    useReducer: mountReducer,
    useState: mountState,
    useEffect:mountEffect
}
// 更新
const HooksDispatcherOnUpdate = {
    useReducer: mountReducer,
    useState:updateState,
    useEffect:updateEffect
}
// 如果函数组件的里面使用了useEffect,那么此函数组件对应的 fiber 上会有一个 flags ,用于标记此组件使用了 useEffect
function mountEffect(create, deps) {
  // PassiveEffect 这是给 fiber 使用的,表示此 fiber 使用了 effect
  // HookPassive 表示此 hook 是 useEffect
  return mountEffectImpl(PassiveEffect, HookPassive, create, deps)
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = mountWorkInProgressHook()
  const nextDeps = deps === undefined ? null : deps
  // 给当前的函数组件fiber添加flags
  currentlyRenderingFiber.flags |= fiberFlags
  // 构建 effect 链表
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps)
}

2.实现 pushEffect 用于创建 effect 链表。

ts
// tag:efect的标签,create:effect的创建函数,destroy:effect的销毁函数,deps:依赖数组
function pushEffect(tag, create, destroy, deps) {
    const effect = {
        tag,
        create,
        destroy,
        deps,
        next: null
    }
    // 如果当前 fiber 的更新队列,创建一个新的函数组件的更新队列
    let componentUpdateQueue = currentlyRenderingFiber.updateQueue
    if (componentUpdateQueue === null) {
        componentUpdateQueue = createFunctionComponentUpdateQueue()
        currentlyRenderingFiber.updateQueue = componentUpdateQueue
        // 构建 effect 链表
        componentUpdateQueue.lastEffect = effect.next = effect
    }
    return effect
}

3.然后在提交阶段(页面更新)判断当前 fiber 是否有 effect 标识,如果有会开启一个宏任务(如果是 useLayoutEffect 会开启一个微任务)用于执行

4.实现更新时执行的 useEffect (updateEffect)。更新时会比对当前 fiber 上的 effect 的依赖数组(内部遍历每一项使用 Object.is 进行对比)是否一样,不一样则会重新执行此 effect ,不过不管是否一致都会重新构建 effect 循环链表

ts
// 更新
const HooksDispatcherOnUpdate = {
    useEffect:updateEffect
}

function updateEffect(create, deps) {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps)
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = updateWorkInProgressHook()
  const nextDeps = deps === undefined ? null : deps
  let destroy
  // 上一个老 hook
  if (currentHook !== null) {
    // 获取此 useEffect 这个 Hook 上老的 effect 对象 create deps destroy
    const prevEffect = currentHook.memoizedState
    destroy = prevEffect.destroy
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps
      // 用新数组和老数组进行对比(即用户使用 useEffect 传入的第二个参数依赖数组,内部遍历每一项使用 Object.is 进行对比),如果一样的话
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 不管要不要重新执行,都需要把新的 effect 组成完整的循环链表放到 fiber.updateQueue 中
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps)
        return
      }
    }
  }
  // 如果走到这里,说明需要重新执行。如果要执行的话需要修改 fiber 的 flags 标识
  currentlyRenderingFiber.flags |= fiberFlags
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, destroy, nextDeps)
}

useLayoutEffect ​

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect

useEffect 不会阻塞浏览器渲染,而 useLayoutEffect 会阻塞浏览器渲染

useEffect 会在浏览器渲染结束后执行,useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行

1.添加挂载阶段的 useLayoutEffect 和更新阶段的 useLayoutEffect 函数。直接复用 useEffect 使用的 mountEffectImpl、updateEffectImpl 函数,只是参数不一样。

ts
const HooksDispatcherOnMount = {
    useEffect:mountEffect,
    useLayoutEffect: mountLayoutEffect
}
// 更新
const HooksDispatcherOnUpdate = {
    useEffect:updateEffect,
    useLayoutEffect: updateLayoutEffect
}
function mountLayoutEffect(create, deps) {
  // 这里复用了 useEffect 使用的 mountEffectImpl 函数,只是传递给 fiber 的标识不一致
  return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
}

function updateLayoutEffect(create, deps) {
  // 这里复用了 useEffect 使用的 updateEffectImpl 函数,只是传递给 fiber 的标识不一致
  return updateEffectImpl(UpdateEffect, HockLayout, create, deps);
}

2.在提交阶段,DOM 执行变更之后页面变更之前递归遍历 fiber 树执行所有的 useLayoutEffect

ts
function commitRoot(root) {
    // 其他逻辑......
    commitMutationEffectsOnFiber(finishedWork, root)
    // 当 DOM 执行变更之后执行 useLayoutEffect
    commitLayoutEffects(finishedWork, root)
}

useRef ​

1.创建更新和挂载阶段执行的函数。在挂载的时候创建一个 hook 并在其上添加一个 current 值,在更新时将挂载时保存的 current 值取出。

ts
// 挂载阶段的 useRef
function mountRef(initialValue) {
    // 这里的 initialValue 是用户传递过来的初始值 useRef(initialValue)
    // 在挂载的时候创建一个 hook 并在其上添加一个 current 值
    const hook = mountWorkInProgressHook()
    const ref = {
        current: initialValue
    }
    hook.memoizedState = ref
    return ref
}

// 更新阶段的 useRef
function updateRef() {
    // 更新时将挂载时保存的 current 值取出
    const hook = updateWorkInProgressHook()
    return hook.memoizedState
}

2.在构建 fiber 树和更新时,给具有 ref 副作用的 fiber 打上标记

ts
if (workInProgress.ref !== null) {
  // 当前 fiber 有 ref 属性,在当前 fiber 上打上 ref 副作用标记
  markRef(workInProgress)
}

3.遍历 fiber 树执行其上副作用时,如果当前 fiber 有 ref 副作用时会将当前 fiber 对应的真实节点保存到 ref.current之上。这样用户就能获取真实节点了。

ts
function commitAttachRef(finishedWork) {
    const ref = finishedwork.ref
    if (ref !== null) {
        // 获取此 fiber 的实例上的真实节点
        const instance = finishedwork.stateNode
        // 如果 ref 是函数,则调用它,并传入实例。这只是 ref 的另一种用法而已。
        // 如:<button ref={buttonDoM => console.log(buttonDom)}>按钮</button>
        // 和 <button ref={buttonDom}>按钮</button> 差不多
        if (typeof ref === 'function') {
            ref(instance)
        } else {
            // 否则,将此 fiber 对应的真实节点赋值给 ref.current
            ref.current = instance
        }
    }
}

任务调度系统 ​

在 react 中会将更新任务划分优先级,优先级高的任务的 sortIndx 越小,每次取 sortIndx 最小的先执行(如果 sortIndx 一样则会比较 id 的大小,id 为每个任务的自增 id),这时就需要对这些任务进行排序。

核心算法 ​

最小堆 ​

树越靠近顶部的数据越小

最小堆是一种经过排序的完全二叉树(叶子节点只能出现最下层或次下层,且最下层的节点集中在树的左侧)

MessageChannel ​

React利用 MessageChannel 模拟了 requestldleCallback(requestIdleCallback 兼容性不佳),将回调延迟到绘制操作之后执行

MessageChannel API 允许我们创建一个新的消息通道!并通过它的两个 MessagePort 属性发送数据

MessageChannel 创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过 postMessage 发送数据,而一个端口只要绑定了 onmessage 回调方法,就可以接收从另一个端口传过来的数据

MessageChannel 是一个宏任务

调度优先级 ​

1.根据任务优先级给每个任务打上对应超时时间以及其他信息,然后将任务依次放入队列中,再根据上一步的最小堆算法给任务按优先级排序。

ts
// 按优先级执行任务 priorityLevel 优先级 callback  回调函数
export function scheduleCallback(priorityLevel, callback) {
    // 获取当前页面启动到现在的时间
    const currentTime = getCurrentTime()
    // 此任务的开时间
    const startTime = currentTime
    // 每个任务的超时时间,超时时间之内任何优先级比此任务高的任务都会打断此任务,到达超时时间后,此任务会被执行。不同任务超时时间不同,优先级越高,超时时间越短
    let timeout
    switch (prioritylevel) {
            // 根据任务的优先级(react 中有 5 个调度优先级),设置超时时间
        case ImmediatePriority:
            timeout = IMMEDIATE_PRIORITY TIMEOUT;
            break;
        case UserBlockingPriority:
            timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
            break;
            // 其他优先级赋值逻辑......
    }
    // 计算此任务的过期时间
    const expirationTime = startTime + timeout
    const newTask = {
        id: taskIdcounter++, // 任务自增 id
        callback, // 回调函数或者说任务函数
        priorityLevel, // 优先级别
        startTime, // 任务的开始时间
        expirationTime, // 任务的过期时间
        sortIndex: expirationTime // 排序依赖
    }
    // 根据上一步实现的最小堆算法,将任务入队
    push(taskQueue, newTask)
    // 开始执行任务
    requestHostCallback(flushWork)
    return newTask
}

2.实现 requestHostCallback,依次将任务存入队列中并借助最小堆算法按优先级排序,再依次取出任务队列中优先级最高的执行。

ts
let scheduleHostCallback = null
function requestHostCallback(flushWork) {
    // 缓存回调函数
    scheduleHostCallback = flushWork
    // 执行工作直到截止时间,在规定的时间内执行任务
    schedulePerformWorkUntilDeadline()
}

function schedulePerformWorkUntilDeadline() {
    // 如果没有回调函数,则退出
    if (!scheduleHostCallback) {
        return
    }
    // 获取当前时间
    const currentTime = getCurrentTime()
    //是否有更多的工作要做
    let hasMoreWork = true
    try {
        // 执行 flushWork,并判断有没有返回值
        hasMoreWork = scheduleHostCallback(startTime)
    } finally {
        // 执行完以后如果为 true,说明还有更多工作要做
        if (hasMoreWork) {
            // 递归调用,继续执行
            schedulePerformWorkUntilDeadline()
        } else {
            // 否则,清空回调函数
            scheduleHostCallback = null
        }
    }
}

// 开始执行任务队列中的任务
function flushWork(startTime) {
    // 依次取出任务队列中优先级最高的任务执行,如果此任务没有过期但需要放弃执行(时间片到期,5毫秒),则会跳出工作循环(中断此任务)
    // React每一帧向浏览申请5毫秒用于自己任务执行,如果5毫秒内没有完成,React也会放弃控制权,把控制交还给浏览器
    return workLoop(startTime)
}

更新优先级 ​

lane ​

react中用 lane(车道)模型来表示任务优先级

一共有 31 条优先级车道 15 个更新优先级(有些车道的优先级相同),数字越小优先级越高

react 会先将更新任务放入这 31 车道中,再转化为上一步的优先级

代码实现到此时,更新任务都是一口气按顺序执行,但在此处会引入更新优先级,会优先执行优先级更高的任务(优先级低的会被跳过),同时会记录本次更新链表(记录本次更新 fiber 的链表头和尾,再合并此次新任务的链表重新构建一个单向更新链表,方便下次继续更新,不打乱顺序,也就是说第二次按链表继续执行更新任务时,也会把之前更新优先级高的任务连带着再次执行)。

1.在 updateContainer 方法中添加 requestUpdateLane 方法用于获取当前更新任务对应的车道,并在更新任务上标记记录当前车道并依次传入到根 Fiber 上

ts
// 把虚拟 dome element 变成真实 DOM 插入到 container 容器中
function updateContainer(element, container) {
    // 获取当前的根fiber
    const current = container.current
    // 请求一个更新车道
    const lane = requestUpdateLane(current)
    // 创建更新时将车道传递过去。在当前更新任务标记记录当前车道
    const update = createUpdate(lane);
    // 把此更新对象添加到 current 这个根 Fiber 的更新队列上,返回根节点
    const root = enqueueupdate(current, update, lane)
    // 调度
    scheduleUpdateOnFiber(root, current, lane)
}

2.实现 requestUpdateLane,获取当前任务车道

ts
function requestUpdateLane() {
    // 获取当前更新优先级,默认是 NoLanes 即无优先级
    const updateLane = getCurrentUpdatePriority()
    if (updateLane !== NoLanes) return updateLane
    // 获取当前的事件优先级,默认是 DefaultEventPriority 即默认事件车道。会根据当前的事件类型来决定优先级
    // 事件优先级:离散事件优先级(click onchange 等) > 连续事件优先级(mousemove mouseover 等) > 默认事件优先级 > 空闲事件优先级
    const eventLane = getCurrentEventPriority()
    return eventLane
}

3.修改 enqueueupdate。之前都只是简单的将所有任务组成一个循环链表,这次还需将每个任务的车道记录在更新队列中。

ts
function enqueueUpdate(fiber,update,lane) {
    // 获取更新队列
    const updateQueue = fiber.updateQueue
    // 获取共享队列
    const sharedQueue = updateQueue.shared
    // 将车道记录在更新队列中
    returnenqueueConcurirentclassUpdate(fiber, sharedQueue, update, lane)
}

4.修改第一步中的 scheduleUpdateOnFiber 。通过 markRootUpdated 标记根节点上有哪些车道等待被处理,并将车道转换为调度优先级

ts
function scheduleUpdateOnFiber(root, fiber, lane) {
    // 标记根节点上有哪些车道等待被处理
    markRootUpdated(root, lane)
    // 确保调度执行 root 上的更新。渲染根节点
    ensureRootIsScheduled(root)
}

5.修改上一步中的 ensureRootIsScheduled

ts
function ensureRootIsScheduled(root) {
    // 获取当前有更新的所有车道
    const nextLanes = getNextLanes(root, NoLanes)
    // 获取所有车道中优先级最高的车道
    let newCallbackPriority = getHighestPriorityLane(nextLanes);
    // 如果是同步优先级的话
    if (newCallbackPriority === SyncLane) {
        //TODO
    } else {
        // 如果不是同步,就需要调度一个新的任务
        let schedulerPriorityLevel
        // lanesToEventPricrity 会将车道转换成对应的事件优先级(事件优先级有 4 个)
        // 根据事件优先级将其转换为调度优先级(之前步骤中的优先级,只有 5 个优先级)
        switch (lanesToEventPricrity(nextLanes)) {
            case ImmediatePriority:
                schedulerPriorityLevel = ImmediateSchedulerPriority
                break
            case UserBlockingPriority:
                schedulerPriorityLevel = UserBlockingSchedulerPriority
                break
            case NormalPriority:
                schedulerPriorityLevel = NormalSchedulerPriority
                break
            case LowPriority:
                schedulerPriorityLevel = LowSchedulerPriority
                break
            case IdlePriority:
                schedulerPriorityLevel = IdleSchedulerPriority
                break
            default:
                schedulerPriorityLevel = NormalSchedulerPriority
        }
    }
}

更新渲染 ​

以上已经实现引入车道初次挂载的情景。以下将实现引入车道初次更新的情景。

用户代码,使用了 useState 点击按钮会触发页面更新。

tsx
function FunctionComponent() {
  const [number, setNumber] = React.useState(0)
  return <button onclick={() => setNumber(number => number + 1)}>{number}</button>
}

let element = <FunctionComponent />
const root = createRoot(document.getElementById('root'))
root.render(element)

1.在派发更新时,获取此更新的车道,并在此更新上记录当前车道

ts
function dispatchSetState(fiber, queue, action) {
    // 获取当前的更新赛道。此方法已经在前面的步骤中实现(lane 中的第二个步骤,此函数会获取当前任务的事件优先级)。此处时点击事件,会根据当前复制对应的车道
    const lane = requestUpdateLane()
    const update = {
        lane, //本次更新优先级就是1
        action,
        hasEagerState: false, //是否有急切的更新
        eagerState: null, //急切的更新状态
        next: null
    }
}

2.修改 ensureRootIsScheduled 方法(上一步 lane 的第五个步骤),实现同步车道的实现逻辑。将当前所有在同步车道的更新任务放入同步队列,然后在浏览器执行微任务的时候遍历当前同步队列,依次执行并清空

ts
function ensureRootIsScheduled(root) {
    // 其他逻辑......
    if (newCallbackPriority === SyncLane) {
        // 把当前 fiber 树上所有优先级最高的更新任务放入同步队列当中
        scheduleSynccallback(performSyncWorkonRoot.bind(null, root))
        // 在浏览器执行微任务的时候遍历当前同步队列,依次执行并清空
        queueMicrotask(flushSyncCallbacks)
    } else {
        // 其他逻辑......
    }
}

并发渲染 ​

用户代码。使用了 useState 点击按钮会触发页面更新,使用了 useEffect 页面渲染之后更新

ts
function FunctionComponent() {
    const [number, setNumber] = React.useState(0)
    React.useEffect(() => {
        setNumber(number => number + 1)
    }, [])
    return <button onclick={() => setNumber(number => number + 1)}>{number}</button>
}

let element = <FunctionComponent />
const root = createRoot(document.getElementById('root'))
root.render(element)

1.先拿到所有任务优先级最高的车道,然后执行这些任务,如果在超时时间内未完成这些任务,会将未完成的任务保存起来,下次继续执行,直到全部执行完毕。

ts
function performConcurrentWorkonRoot(root, didTimeout) {
    // 先获取当前根节点上的任务
    const originalcallbackNode = root.callbackNode
    // 获取当前优先级最高的车道
    const lanes = getNextLanes(root, NoLanes)
    // 如果不包含阻塞的车道,并且没有超时,就可以并行渲染,就可以启用时间分片
    const shouldTimeSlice = !includesBlockingLane(root, lanes) && !didTimeout
    // 执行同步(并发异步)渲染,得到退出的状态(执行完毕或者还有未执行完的任务)。
    const exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes)
    // 如果是已经执行完,则提交根节点
    if (exitStatus !== RootInProgress) {
        const finishedWork = root.current.alternate
        root.finishedwork = finishedWork
        commitRoot(root)
    }
    if (root.callbackNode === originalcallbackNode) {
        // 如果没有执行完则把此任务返回下次接着执行
        return performConcurrentWorkonRoot.bind(null, root)
    }
    return null
}

高优更新打断低优更新 ​

1.先获取当前正在执行的任务以及对应的优先级,再获取新的任务优先级(如正在构建 fiber 时,突然用户触发点击事件,点击事件任务优先级较高),如果新的优先级比当前正在执行的任务优先级高则会打断当前任务,根据当前新的优先级任务调度创建 fiber

ts
function ensureRootIsScheduled(root) {
    // 先获取当前根上正在执行的任务
    const existingCallbackNode = root.callbackNode
    // 获取当前优先级最高的车道
    const nextLanes = getNextLanes(root, workInProgressRootRenderLanes)
    // 获取新的调度优先级(新的更新任务的优先级)
    let newCallbackPriority = getHighestPriorityLane(nextLanes)
    // 获取当前正在执行的任务优先级
    const existingCallbackPriority = root.callbackPriority
    // 如果新的优先级和老的优先级一样,则进行批量更新
    if (existingCallbackPriority === newCallbackPriorjty) return
    if (existingCallbackNode !== null) {
        // 如果当前有任务正在执行,则取消当前任务
        Scheduler_cancelCallback(existingCallbackNode)
    }
}

饥饿问题 ​

如果遇到高优任务就跳过低优任务,长时间如此,低优任务就永远不会被执行,就会造成饥饿问题。

解决:每个任务根据优先级不同自身会有一个不同的过期时间(等待),一旦超过这个时间此任务还没有被执行,会立刻将此任务从异步任务变成同步,立即执行。

1.在 fiber 根上新增两个变量。一个用于存放每个赛道过期时间,一个用于存放过期的赛道

ts
function FiberRootNode(containerInfo) {
    // 存放每个赛道过期时间
    this.expirationTimes = CreateLaneMap(NoTimestamp)
    // 过期的赛道
    this.expiredLanes = NoLanes
}

2.在调度更新时,获取所有有更新的车道并根据不同车道设置不同的过期时间,如果当前车道已经过期则会把当前车道添加到过期车道里。

ts
// 把所有饿死的赛道标记为过期
function markStarvedLanesAsExpired(root, currentTime) {
    // 获取当前有更新赛道
    const pendingLanes = root.pendingLanes
    // 记录每个赛道上的过期时间
    const expirationTimes = root.expirationTimes
    let lanes = pendinglanes
    while (lanes > 0) {
        const index = pickArbitraryLaneIndex(lanes)
        const lane = 1 << index
        const expirationTime = expirationTimes[index]
        // 如果此赛道上没有过期时间,说明没有为此车道设置过期时间,那就给其设置一个
        if (expirationTime === NoTimestamp) {
            // 根据不同车道设置不同的过期时间
            expirationTimes[index] = computeExpirationTime(lane, currentTime)
        } else if (expirationTime <= currentTime) {
            // 如果过期时间小于当前时间,说明已经过期,把此车道添加到过期车道里
            root.expiredLanes |= lane
        }
        lanes &= ~lane
    }
}

3.修改触发并发渲染的机制。只有当不包含阻塞车道、不包含过期的车道、时间片没有过期才进行并发渲染,也就是说只要包含过期的车道就会立即将当前车道当成同步任务立即执行

ts
function performConcurrentWorkonR(root, didTimeout) {
    // 是否不包含阻塞车道
    const nonIncludesBlockingLane = !includesBlockinglane(root, lanes)
    // 是否不包含过期的车道
    const nonIncludesExpiredLane = !includesExpiredLane(root, lanes)
    // 时间片没有过期
    const nonTimeout = !didTimeout
    // 三个变量都是真,才能进行时间分片,也就是进行并发渲染,也就是可以中断执行
    const shouldTimeSlice = nonIncludesBlockingLane && nonIncludesExpiredLane && nonTimeout
}

4.获取当前新的根上剩下的车道,再从其中找出已经更新过的车道,并将其从过期车道中清除。

ts
// 合并统计当前新的根上剩下的车道,即未更新的车道
const remainingLanes = mergeLanes(finishedWork.lanes, nonTimeout.childLanes)
markRootFinished(root, remainingLanes)

function markRootFinished(root, remainingLanes) {
    // 获取已经更新过的车道
    const noLongerPendingLanes = root.pendingLanes & ~remainingLanes
    const expirationTimes = root.expirationTimes
    let lanes = noLongerPendingLanes
    while (lanes > 0) {
        const index = pickArbitraryLaneIndex(lanes)
        const lane = 1 << index
        // 清除已经计算过的车道的过期时间,即已经更新过的车道就将其从过期车道中清除
        expirationTimes[index] = NoTimestamp
        lanes &= ~lane
    }
}