当前位置: 首页 > news >正文

react的useState源码分析

前言

简单说下为什么React选择函数式组件,主要是class组件比较冗余、生命周期函数写法不友好,骚写法多,functional组件更符合React编程思想等等等。更具体的可以拜读dan大神的blog。其中Function components capture the rendered values这句十分精辟的道出函数式组件的优势。

但是在16.8之前react的函数式组件十分羸弱,基本只能作用于纯展示组件,主要因为缺少state和生命周期。本人曾经在hooks出来前负责过纯函数式的react项目,所有状态处理都必须在reducer中进行,所有副作用都在saga中执行,可以说是十分艰辛的经历了。在hooks出来后我在公司的一个小中台项目中使用,落地效果不错,代码量显著减少的同时提升了代码的可读性。因为通过custom hooks可以更好地剥离代码结构,不会像以前类组件那样在cDU等生命周期堆了一大堆逻辑,在命令式代码和声明式代码中有一个良性的边界。

useState在React中是怎么实现的

Hooks take some getting used to — and especially at the boundary of imperative and declarative code.

如果对hooks不太了解的可以先看看这篇文章:前情提要,十分简明的介绍了hooks的核心原理,但是我对useEffect,useRef等钩子的实现比较好奇,所以开始啃起了源码,下面我会结合源码介绍useState的原理。useState具体逻辑分成三部分:mountState,dispatch, updateState

hook的结构

首先的是hooks的结构,hooks是挂载在组件Fiber结点上memoizedState的

//hook的结构
export type Hook = {memoizedState: any, //上一次的statebaseState: any,  //当前statebaseUpdate: Update<any, any> | null,  // update funcqueue: UpdateQueue<any, any> | null,  //用于缓存多次actionnext: Hook | null, //链表
};

renderWithHooks

在reconciler中处理函数式组件的函数是renderWithHooks,其类型是:

renderWithHooks(current: Fiber | null, //当前的fiber结点workInProgress: Fiber, Component: any, //jsx中用<>调用的函数props: any,refOrContext: any,nextRenderExpirationTime: ExpirationTime, //需要在什么时候结束
): any

在renderWithHooks,核心流程如下:

//从memoizedState中取出hooks
nextCurrentHook = current !== null ? current.memoizedState : null; 
//判断通过有没有hooks判断是mount还是update,两者的函数不同
ReactCurrentDispatcher.current =nextCurrentHook === null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;
//执行传入的type函数
let children = Component(props, refOrContext);
//执行完函数后的dispatcher变成只能调用context的
ReactCurrentDispatcher.current = ContextOnlyDispatcher;return children;

useState构建时流程

mountState

在HooksDispatcherOnMount中,useState调用的是下面的mountState,作用是创建一个新的hook并使用默认值初始化并绑定其触发器,因为useState底层是useReducer,所以数组第二个值返回的是dispatch。

type BasicStateAction<S> = (S => S) | S;function mountState<S>(initialState: (() => S) | S,
){const hook = mountWorkInProgressHook();
//如果入参是func则会调用,但是不提供参数,带参数的需要包一层if (typeof initialState === 'function') {initialState = initialState();}
//上一个state和基本(当前)state都初始化hook.memoizedState = hook.baseState = initialState;const queue = (hook.queue = {last: null,dispatch: null,eagerReducer: basicStateReducer, // useState使用基础reducereagerState: (initialState: any),});
//返回触发器const dispatch: Dispatch<//useState底层是useReducer,所以type是BasicStateAction(queue.dispatch = (dispatchAction.bind(null,//绑定当前fiber结点和queue((currentlyRenderingFiber: any): Fiber),queue,): any));return [hook.memoizedState, dispatch];
}

mountWorkInProgressHook

这个函数是mountState时调用的构建hook的方法,在初始化完毕后会连接到当前hook.next(如果有的话)

function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null,baseState: null,queue: null,baseUpdate: null,next: null,};if (workInProgressHook === null) {// 列表中的第一个hookfirstWorkInProgressHook = workInProgressHook = hook;} else {// 添加到列表的末尾workInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;
}

dispatch分发函数

在上面我们提到,useState底层是useReducer,所以返回的第二个参数是dispatch函数,其中的设计十分巧妙。

假设我们有以下代码:

相关参考视频讲解:进入学习

const [data, setData] = React.useState(0)
setData('first')
setData('second')
setData('third')

在第一次setData后, hooks的结构如上图

在第二次setData后, hooks的结构如上图

在第三次setData后, hooks的结构如上图

在正常情况下,是不会在dispatcher中触发reducer而是将action存入update中在updateState中再执行,但是如果在react没有重渲染需求的前提下是会提前计算state即eagerState。作为性能优化的一环。

function dispatchAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A,
) {const alternate = fiber.alternate;{flushPassiveEffects();//获取当前时间并计算可用时间const currentTime = requestCurrentTime();const expirationTime = computeExpirationForFiber(currentTime, fiber);const update: Update<S, A> = {expirationTime,action,eagerReducer: null,eagerState: null,next: null,};//下面的代码就是为了构建queue.last是最新的更新,然后last.next开始是每一次的action// 取出lastconst last = queue.last;if (last === null) {// 自圆update.next = update;} else {const first = last.next;if (first !== null) {update.next = first;}last.next = update;}queue.last = update;if (fiber.expirationTime === NoWork &&(alternate === null || alternate.expirationTime === NoWork)) {// 当前队列为空,我们可以在进入render阶段前提前计算出下一个状态。如果新的状态和当前状态相同,则可以退出重渲染const lastRenderedReducer = queue.lastRenderedReducer; // 上次更新完后的reducerif (lastRenderedReducer !== null) {let prevDispatcher;if (__DEV__) {prevDispatcher = ReactCurrentDispatcher.current;  // 暂存dispatcherReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;}try {const currentState: S = (queue.lastRenderedState: any);// 计算下次stateconst eagerState = lastRenderedReducer(currentState, action);// 在update对象中存储预计算的完整状态和reducer,如果在进入render阶段前reducer没有变化那么可以服用eagerState而不用重新再次调用reducerupdate.eagerReducer = lastRenderedReducer;update.eagerState = eagerState;if (is(eagerState, currentState)) {// 在后续的时间中,如果这个组件因别的原因被重渲染且在那时reducer更变后,仍有可能重建这次更新return;}} catch (error) {// Suppress the error. It will throw again in the render phase.} finally {if (__DEV__) {ReactCurrentDispatcher.current = prevDispatcher;}}}}scheduleWork(fiber, expirationTime);}
}

useState更新时流程

updateReducer

因为useState底层是useReducer,所以在更新时的流程(即重渲染组件后)是调用updateReducer的。

function updateState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {return updateReducer(basicStateReducer, (initialState: any));
}

所以其reducer十分简单

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action;
}

我们先把复杂情况抛开,跑通updateReducer流程

function updateReducer(reducer: (S, A) => S,initialArg: I,init?: I => S,
){// 获取当前hook,queueconst hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;// action队列的最后一个更新const last = queue.last;// 最后一个更新是基本状态const baseUpdate = hook.baseUpdate;const baseState = hook.baseState;// 找到第一个没处理的更新let first;if (baseUpdate !== null) {if (last !== null) {// 第一次更新时,队列是一个自圆queue.last.next = queue.first。当第一次update提交后,baseUpdate不再为空即可跳出队列last.next = null;}first = baseUpdate.next;} else {first = last !== null ? last.next : null;}if (first !== null) {let newState = baseState;let newBaseState = null;let newBaseUpdate = null;let prevUpdate = baseUpdate;let update = first;let didSkip = false;do {const updateExpirationTime = update.expirationTime;if (updateExpirationTime < renderExpirationTime) {// 优先级不足,跳过这次更新,如果这是第一次跳过更新,上一个update/state是newBaseupdate/stateif (!didSkip) {didSkip = true;newBaseUpdate = prevUpdate;newBaseState = newState;}// 更新优先级if (updateExpirationTime > remainingExpirationTime) {remainingExpirationTime = updateExpirationTime;}} else {// 处理更新if (update.eagerReducer === reducer) {// 如果更新被提前处理了且reducer跟当前reducer匹配,可以复用eagerStatenewState = ((update.eagerState: any): S);} else {// 循环调用reducerconst action = update.action;newState = reducer(newState, action);}}prevUpdate = update;update = update.next;} while (update !== null && update !== first);if (!didSkip) {newBaseUpdate = prevUpdate;newBaseState = newState;}// 只有在前后state变了才会标记if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();}hook.memoizedState = newState;hook.baseUpdate = newBaseUpdate;hook.baseState = newBaseState;queue.lastRenderedState = newState;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];
}
export function markWorkInProgressReceivedUpdate() {didReceiveUpdate = true;
}

后记

作为系列的第一篇文章,我选择了最常用的hooks开始,抛开提前计算及与react-reconciler的互动,整个流程是十分清晰易懂的。mount的时候构建钩子,触发dispatch时按序插入update。updateState的时候再按序触发reducer。可以说就是一个简单的redux。

http://www.lryc.cn/news/4613.html

相关文章:

  • SharpImpersonation:一款基于令牌和Shellcode注入的用户模拟工具
  • 华为OD机试 - 最大相连男生数(Python)| 真题+思路+代码
  • GIS在地质灾害危险性评估与灾后重建中的实践技术应用及python机器学习灾害易发性评价模型建立与优化
  • 2.13、进程互斥的硬件实现方法
  • Leetcode力扣秋招刷题路-2335
  • C语言深度解剖-关键字(6)
  • [多线程进阶]CAS与Synchronized基本原理
  • 【Linux系统编程】02:文件操作
  • 华为OD机试 - 去除多余空格(Python)| 真题+思路+代码
  • 百趣代谢组学分享,补充α-酮酸的低蛋白饮食对肾脏具有保护作用
  • json对象和formData相互转换
  • 【c++面试问答】常量指针和指针常量的区别
  • Ubuntu18下编译android的ffmpeg经验
  • Spring Security in Action 第十三章 实现OAuth2的认证端
  • 本文章提供中国国界、国界十段线原始数据以及加载方法
  • 一文带你搞懂,Python语言运算符
  • JAVA集合专题4 —— Map
  • 二叉树进阶--二叉搜索树
  • 牛客网Python篇数据分析习题(三)
  • Java开发常见关键词集绵
  • 解决idea出现的java.lang.OutOfMemoryError: Java heap space的问题
  • 为什么子进程要继承处理器亲缘性?
  • 【算法】高精度
  • 计算机网络-基本概念
  • 你评论,我赠书~【哈士奇赠书 - 13期】-〖Python程序设计-编程基础、Web开发及数据分析〗参与评论,即可有机获得
  • 【设计模式】我终于读懂了代理模式。。。
  • 每天10个前端小知识 【Day 2】
  • 帮助中心在线制作工具推荐这4款,很不错哟!
  • rabbitMQ相关文章汇总
  • 【C++】异常