React用过是吧,请说一下setState的大致流程...

这篇文章去年中旬的时候就想写了,无奈在读源码的过程中,遇到了很多卡点,直到今天,才有勇气去写这篇文章,同时也希望可以给正在读React源码的小伙伴一个借鉴。

本文针对的是React18版本的代码,最新版本是React19,是否要追真正的新,大家自行决定。

请看下面一段代码:

javascript 复制代码
import { useState } from 'react'

function Clock(){
    const [count, setCount] = useState(50);
    const [flag, setFlag] = useState(true);
    const update1 = function(prevCount) {
        return prevCount + 1;
    }
    const clickDiv = () => {
        setCount(
            update1
        )
    }
    return <div className='father-fiber'>
        <div className = 'className-1' onClick={clickDiv}>
            { count }
        </div>
    </div>
}

什么是Fiber?

它是一个object对象,这个对象可以描述组件的一切信息,object之间通过某些key关联,可以描述页面的整体结构。

上述代码对应的Fiber大致如下:

javascript 复制代码
{
    elementType: f Clock(props),
    memoizedState: {
        memoizedState: 50,
        baseState: 50,
        queue: {
            dispatch: ƒ (),
            lastRenderedState: 50,
            pending: null
        },
        next: {
            memoizedState: true,
            baseState: true,
            next: null,
            queue: {
                dispatch: ƒ (),
                lastRenderedState: true,
                pending: null
            }
        }
    },
    child: {
        elementType: 'div',
        memoizedState: null,
        props: {
            className: 'father-fiber'
        },
        child: {
            elementType: 'div',
            memoizedState: null,
            props: {
                className: 'className-1',
                onClick: ƒ clickDiv()
            }
        }
    }
}

从上面的代码我们可以看到,一个组件可以是一个Fiber,组件里render的dom元素也会分别对应一个Fiber,在组件对应的Fiber里,会有那么一个地方,存放着组件内部维护的state。

触发更新

流程大致如下:

产生更新 -》调度更新 -》协调(Diff)-》提交DOM更新

useState伪代码

javascript 复制代码
function useState(initState){
    // 创建hook
    let hook = {
        baseState: initState,
        memoizedState: initState,
        next: null,
        queue: {
            dispatch: null,
            lastRenderedState: initState,
            pending: null
        }
    }
    const dispatch = hook.queue.dispatch = f().bind(
        null,
        curFiberInfo,
        hook.queue
    )
    return [
        hook.memoizedState,
        dispatch
    ]
}

上面是伪代码,主要讲逻辑,在这篇文章讲解的代码里,setCount、setState就是dispatch。调用一次useState,就会创建一个hook信息并为其分配一个执行更新的dispatch函数。

产生更新

当我们调用一次setState,就会产生一个update对象,连续调用同一个setState,会产生N个update对象,这个时候update对象之间通过环形链表的形式来维护。

javascript 复制代码
// 这里的action就是我们调用setCount时传入的参数
function dispatch(fiber, queue, action){
    // 产生更新
    const update = {
        lane: requestUpdateLane(),
        action: action,
        eagerReducer: null,
        eagerState: null,
        next: null
    }
    let pending = queue.pending
    if (pending === null) {
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update
}

注意,这里有个"Lane"。从这里就要开始区分版本了,如果你是通过 "ReactDOM.render" 来启动的应用,这个阶段,所有的更新都一样,都需要立即执行,不存在优先级的概念。否则并发模式下,这里需要根据触发事件的类型,人为的分配优先级,为接下来的优化手段做准备。

在并发模式下,React给所有的事件分配了优先级,如何做的呢?就是初始化的时候,将所有的事件进行分类,分类相同的,都包裹在一个wrap函数里,这样在触发合成事件时,根据代码逻辑与事件委托,自然能知道当前的合成事件在整个机制里处于什么样的优先级。以下就是合成事件与优先级关联的大致伪代码:

javascript 复制代码
// 初始化全局变量
var DiscreteEventPriority = SyncLane;
var ContinuousEventPriority = InputContinuousLane;
var DefaultEventPriority = DefaultLane;
var IdleEventPriority = IdleLane;
var currentUpdatePriority = NoLane;

// 初始化事件机制
function createEventListenerWrapperWithPriority(domEventName){
    var eventPriority = getEventPriority(domEventName);
    var listenerWrapper;
    switch (eventPriority) {
        case DiscreteEventPriority:
            listenerWrapper = dispatchDiscreteEvent;
            break;
        // 其余的优先级的绑定逻辑...
    }
    return listenerWrapper.bind(null, domEventName);
}

function setCurrentUpdatePriority(newPriority) {
    currentUpdatePriority = newPriority;
}

function dispatchDiscreteEvent(domEventName){
    setCurrentUpdatePriority(DiscreteEventPriority);
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
}

在这个阶段,对于Hook来,我们还需要注意2个点,如下:

  • 当前组件产生的state信息维护在哪里了?
  • 某个state产生的update信息维护在哪了?

在本篇代码中,state信息维护在了(组件对应的Fiber).memoizedState,多个state信息则是通过next属性进行关联,形成了以memoizedState为首的单链表。

这个过程中,如果某个state产生了update,这个update会维护在(state对应的memoizedState).queue里。

开始调度

调度,调度,负责指挥交通的,定义调度任务以怎样的顺序去执行。

这里需要关注以下几点:

  • 这个过程中,会将"更新任务"通过lane转换为"调度任务"。
  • 并不是所有的调度任务都要入堆,并不是所有的更新都要立即执行。
  • 入堆入的是调度任务,而不是update信息。

如果当前的更新占用较长时间(超过5ms),或者存在多个不同优先级的更新任务时,React就会将这些任务封装成调度任务并放入堆中。堆会根据任务的优先级(通过 Lane 模型表示)对任务进行排序,高优先级的任务会优先执行。

我们来试一下就知道了:

javascript 复制代码
// 合成事件1
const update1 = function(){
    setCount(1)
}

// 合成事件2
const update2 = async function() {
    await new Promise(
        (resolve, reject) => {
            setTimeout(
                () => resolve('1')
                ,3000
            )
        }
    )
    await setCount(2)
}

将这两个事件分别绑定到不同的dom上,通过click触发一下事件,然后在heap.push处打个断点,会发现1没进,2进了,证明刚才的结论成立。

同时,在进断点前,我们会发现,在上面又创建了一个Task,最后是将Task推入heap里的。

接下来我们看一看,"调度器"是如何将"更新信息"、"调度任务"这2者关联起来的。

这块的开始,我们还得回到创建update的位置,根据源码,我们可以得到下面的伪代码:

javascript 复制代码
const update = {
    // 一些信息...
}
let pending = queue.pending
if (pending === null) {
  update.next = update
} else {
  update.next = pending.next
  pending.next = update
}
queue.pending = update
const root = markUpdateLaneFromFiberToRoot(fiber, update.lane)
if (root !== null) {
    var eventTime = requestEventTime()
    scheduleUpdateOnFiber(root, fiber, update.lane, eventTime)
}

function scheduleUpdateOnFiber(root, fiber, lane, eventTime){
    ensureRootIsScheduled(root, eventTime);
}

其中,这个markUpdateLaneFromFiberToRoot函数是比较重要的一环,从发起更新的Fiber节点开始,向上遍历直到根节点,一路标记发起更新的Fiber的update.lane

在读源码的过程中,大家会遇到 mergeLanes 函数,用于将当前 Fiber 节点已有的 lanes 和新的 lane 进行合并,如何合并的呢?通过按位"|",即a | b

我们都知道,lane是通过进制的形式表现的,根据代码逻辑,所有的更新,都会向上遍历至根节点,最后根节点会汇总所有的lane,所以问题是通过按位"|"的形式,得到的汇总lane,汇总lane为什么能表示是否存在更新,为什么能表示有哪些更新存在?举个例子大家就知道了,如下:

javascript 复制代码
// 按位或嘛,全是0,才是0,否则都是1
const noLane = 0b00000000; // 没有更新
const lane1 = 0b00000001;  // 优先级1
const lane2 = 0b00000010;  // 优先级2

const allLane = lane1 | lane2 
// allLane的结果: 0b00000011

// 是否存在优先级
const isHaveLane = (allLane | noLane) === noLane

// 是否存在优先级1
const isHaveLane1 = (allLane | noLane) === lane1

所以,React团队通过"按位或"的形式去实现汇总更新的功能,确实巧妙,非常值得学习!

ensureRootIsScheduled负责根节点的调度任务能否被正确安排,同时根据根节点的状态和当前更新信息的 lane,决定是否需要创建新的调度任务。伪代码如下:

javascript 复制代码
function ensureRootIsScheduled(root, eventTime){
    // 获取根节点当前正在调度的回调任务
    var existingCallbackNode = root.callbackNode;
    // 根据根节点的状态和当前渲染情况,确定下一个要处理的更新任务集合
    const nextLanes = getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
    // 根据上一步得到的更新任务集合,确定最新的调度优先级
    var schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
        case DiscreteEventPriority:
            schedulerPriorityLevel = ImmediatePriority;
            break;
        // 其余的逻辑判断...
    }
    // 创建调度任务
    var newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}

function scheduleCallback$1(priorityLevel, callback) {
    // 省略其余逻辑...
    var newTask = {
      id: taskIdCounter++,
      callback: callback,
      priorityLevel: priorityLevel,
      startTime: startTime,
      expirationTime: expirationTime,
      sortIndex: -1
    };
    // 省略其余逻辑...
    push(taskQueue, newTask);
}

调度至此就差不多讲完了,就说了个大概,中间很多细小且重要的逻辑也没说到(比如更新优先级与合成事件之间的双向转换、哪些更新会立即执行、入堆的任务是如何触发执行的等等),以后的文章会慢慢的讲。

Diff算法

Diff是页面实现局部更新的根本,通过Diff算法可以找到真实DOM里需要做出更新的部分,最后只更新真实DOM中对应的节点。

在具体的实现中,至少分为了2类,一类是单节点的Diff,一类是多节点的Diff。分别如下:

理论上,对比2棵树,单节点、多节点,只要存在一个算法就可以实现2棵树的对比,React为什么要同时存在多个对比算法?2个算法真的就比一个算法性能更好吗?

对于我这个问题,有大神的话,可以评论区里沟通一下,嘿嘿。

提交DOM更新

这一块主要是渲染差异,根据网上的理论,这一阶段就是常说的commit阶段。

javascript 复制代码
function commitRootImpl(root, renderPriorityLevel) { 
    
    commitBeforeMutationEffects(); 
    
    commitMutationEffects(root, renderPriorityLevel); 
    
    commitLayoutEffects(root, renderPriorityLevel); 
    // 副作用阶段 
    if (rootDoesHavePassiveEffects) { 
        scheduleCallback(NormalSchedulerPriority, () => { 
            flushPassiveEffects(); 
            return null; 
        }); 
    }
}

mutation阶段,主要执行一些具体的DOM操作,如下:

javascript 复制代码
function commitPlacement(finishedWork) {
    const parentFiber = getParentFiber(finishedWork);
    const parentStateNode = parentFiber.stateNode;
    const newNode = finishedWork.stateNode; // 将新节点插入到父节点中 parentStateNode.appendChild(newNode); 
}

随后的Passive阶段,会执行useEffect等副作用,如下:

javascript 复制代码
function flushPassiveEffects() { 
    if (!rootWithPendingPassiveEffects) { 
        return false; 
    } 
    const root = rootWithPendingPassiveEffects; 
    rootWithPendingPassiveEffects = null; // 执行副作用清理函数 
    commitPassiveUnmountEffects(root.current); // 执行副作用回调函数 
    commitPassiveMountEffects(root, root.current); 
    return true; 
}

最后

上述过程只是一种情况对应的理想状态,其中还有很多细小的点没有在文章中说出来,如果在面试中遇到了这个问题,是要拼深度+广度的。

那么,我们下期再见啦,拜拜~~

相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax