一、前言
本文基于开源项目:
useState()是hook中用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
setState()是class组件中,用于对一个组件的 state 对象安排一次更新。当 state 改变了,该组件就会重新渲染。
下面且听广东靓仔徐徐道来~
二、setState 同步or异步?why?
细心的小伙伴在看React官方文档的时候,应该有看到过以下的描述:
- setState——数据驱动视图
- setState——工作机制太复杂
广东靓仔先给出结论:
在React16.8之前,setState() 并不是单纯同步/异步的,它的表现会因调用场景不同而不同。在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为同步。有了hook后,也就是16.8版本后,fiber对state修改都是异步了。
接下来我们来仔细分析setState()
调用之后,React内做了什么?我们先来看一张图,如下所示:
从上面的图中我们可以看出,当我们调用setState() 之后,在生命周期中会进行 re-render,也就是我们常说的重渲染。而re-render会涉及对Dom的操作,这会有较大的开销。不难看出一个完整的更新流程里面涉及到很多操作。
这里我们大胆猜测,如果每次使用setState() ,都要走一遍完整更新流程,无疑刷新几次就直接崩了。
下面我们通过两个简单的Demo来说明,这里广东靓仔采用了React15版本截取了部分代码:
testSetState = () => { console.log('前端早茶, age:', this.state.age) for(let i = 0; i < 18; i++) { this.setState({ count: this.state.age + 1 }) } console.log('前端早茶,循环18次后age:', this.state.age) }
对React很熟悉的小伙伴很容易就可以看出,输出如下:
前端早茶, age:0
前端早茶,循环18次后age:0
从答案我们可以得出,尽管是for循环setState() React内是会把每一个setState放到队列里边,等到合适的时机再把最新的State执行一遍更新流程。我们可以把这个称为“批量更新”,也就是会合并state任务。
再来一个简单的Demo:
reduce = () => { setTimeout(() => { console.log('前端早茶, 前age:', this.state.age) this.setState({ age: this.state.age + 1 }); console.log('前端早茶,后的age', this.state.age) },0); }
我们会发现age会++,很显然,是这个setTimeout 起到了某种作用。我们试着把setTimeout 去掉,又不会++了。这里广东靓仔跟小伙伴们讲,其实并不是setTimeout改变了 setState,只是 setTimeout 让 setState 脱离了React对setState的管控。因此只要是在 React 管控下的 setState,一定是异步的。
对这里的调用链感兴趣的小伙伴可以自行去阅读源码~
三、useState实现机制
开始讲解机制之前,广东靓仔先说个题外话,除了我们在React官网看到的hook API,其他的都是属于自定义hook。
官网hook,如下图所示:
广东靓仔使用React16.8版本进行讲解,这里稍微注意下,我们在使用hook api的时候,别在其前面加if 哦。如果你加了if又没有使用eslint,会报错哦。当然拉,如果你之前看了前端早茶的React17那篇文章,开始使用了React17,会直接给你提示 *** is not a function了~
下面我们开始梳理useState() 来分析Hook的调用链路,广东靓仔通过process on画了下面这张图:
从上面这个图中我们可以看出,useState首次渲染会触发一些操作,最后会执行mountState,下面我们看看这个核心函数。
function mountState(initialState) { // 将新的 hook 对象追加进链表尾部 var hook = mountWorkInProgressHook(); // initialState 可以是一个回调,若是回调,则取回调执行后的值 if (typeof initialState === 'function') { initialState = initialState(); } // 创建当前 hook 对象的更新队列,这一步主要是为了能够依序保留 dispatch const queue = hook.queue = { last: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }; // 将 initialState 作为一个“记忆值”存下来 hook.memoizedState = hook.baseState = initialState; // dispatch 是由上下文中一个叫 dispatchAction 的方法创建的 var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue); // 返回目标数组 return [hook.memoizedState, dispatch]; }
从上面代码可以看出mounState 的主要工作是初始化 Hooks,dispatch其实就是我们经常看到的setxxx函数。
新的 hook 对象追加进链表尾部,我们下面来看看核心函数mountWorkInProgressHook :
function mountWorkInProgressHook() { // 单个 hook 是以对象的形式存在的 var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; if (workInProgressHook === null) { // 将 hook 作为链表的头节点处理 firstWorkInProgressHook = workInProgressHook = hook; } else { // 若链表不为空,则将 hook 追加到链表尾部 workInProgressHook = workInProgressHook.next = hook; } // 返回当前的 hook return workInProgressHook; }
从上面代码中我们可以看到hook 对象之间是用单向链表的形式相互串联。
最后我们来看看更新过程,同样上图:
从上图可以看出,我们更新的时候是调用updateState。调用链路其实很好理解:它按顺序遍历之前构建好的链表,读取对应的数据信息进行渲染。 从上面的内容,我们很容易得出结论,Hooks 的本质其实是链表。
四、总结
平时我们在使用React开发项目的时候,有时会遇到一些莫名bug,然后通过搜索引擎寻找解决方式。当我们解决完bug后,广东靓仔建议大家做个小结,顺便举一反三,顺便把某原理梳理梳理。下次就能很好的避免这些问题。