react|react技术分享----useState的原理及自定义useState的实现

*前言:
本次分享将主要自定义实现useState为主,以通俗易懂的目的让大家了解useState实现的大体逻辑。但内容是非常长的,如果真的想理解的话,还是希望你耐住性子看看,相信即使不能让你读懂源码,但至少能够给你做一些铺垫~~,代码已放在这里了,可以先看下效果再决定值不值得继续看吧
一、hook的价值: hook出现的意义是巨大的,在React Conf 2018 会议上,react团队的leader---- Sophie Alpert提出了三个class组件存在的问题(而hook的出现就是来解决这些的):

  1. 逻辑复用问题:要逻辑复用,在class组件中无非是用高阶组件,或者render props来解决,但是如果项目庞大的话,就有可能造成组件层级过深,无限嵌套导致追踪数据流困难,称之为“包装地狱
  2. 巨大的组件问题:在class组件中,你很有可能会经常在componentDidMount当中订阅数据存储发送请求设置定时器,而在componentWillUnmount里面做相反的事情,取消订阅取消请求销毁定时器
  3. class组件理解困难:这个不管是对人还是对机器而言都是困难的,你人要考虑this的绑定与指向,且有时候甚至你还不知道什么时候该选取class组件,还是function组件,还有就是机器要对class进行解析成组件文件的时候,class里面有些函数即使没有使用过,它也不会被剔除,因为机器在编译时很难准确判断方法是否被使用。
    react|react技术分享----useState的原理及自定义useState的实现
    文章图片
二、基础hook之useState的使用 这个在这里就不过多的阐述了,毕竟你都来看本篇文章了,目的也不是为了再教你如何使用了,以下是简单例子:react|react技术分享----useState的原理及自定义useState的实现
文章图片

react|react技术分享----useState的原理及自定义useState的实现
文章图片

当点击按钮的时候,能够很符合我们预期的达到和class组件一样的效果,这就是它的强势,让你的函数组件看起来像是具有了状态。
三、useState实现的大体思路:
  1. 首先你知道了useState它的特点就是使组件具有状态,且有存储数据的功能,当修改count的时候(即调用setCount),函数组件App将会重新渲染(即重新调用了App函数,不然也不会每点击按钮就没打印一遍count是吧)
  2. 知道特点之后,就应该在脑中有个大概思路,两次调用函数组件App不同之处是,第一次属于挂载(mount),第二次则属于更新(update),也就是说两次调用useState的含义也是不同的,说白了就是有两个大分支,第一次走mount,后面都走update
  3. 先来说下mount阶段吧,这个阶段你要可能要联想下实际工作例子,你很有可能会使用很多个useState,就像下面这样react|react技术分享----useState的原理及自定义useState的实现
    文章图片

    那你就要考虑这么多个useState,它改怎么维护这么多个状态呢,也许你会想到使用数组,但是你就要再用个下标来维护以后更新时,改更新哪一个state呢?虽然理论上是可以的,但也伴随着它的灵活性不高,在react源码中,它是使用的是链表来维护的,它可以很好的解决上述刚才的问题,源码中它把你每一次使用useState用一个hook对象来维护,里面保存着你调用useState传入的值(memoizedState),以及next指针,这也是用来连接下次调用useState所新建的一个新的hook对象。在这里,你也许还是搞不懂为啥要用链表,但相信当你看完本篇文章的时候再回过头好好想想就会深有体会了,这里先埋个彩蛋~,就像如下:
    react|react技术分享----useState的原理及自定义useState的实现
    文章图片
【react|react技术分享----useState的原理及自定义useState的实现】4.接下来就是update阶段了,这个阶段是你调用setCount(setXxx)的时候才会走的阶段,并且你会向这个setCount函数传一个值,或者函数,来表达你期望得到的值是什么,在此之前,那就得先找到这个hook对象,并把传来newState赋值给这个hook旧的的state(即memoizedState),更改完值后,就是重新reder渲染了,当然同样你也要考虑实际情况,有时候你在处理点击事件的时候会多次使用setCount(setXxx),就像如下:
react|react技术分享----useState的原理及自定义useState的实现
文章图片

这个时候你就应该会想到类比步骤3一样的方法,用链表(queue)来维护这么多个setCount,同样这也是react中使用的手段,但是比较特殊的是它使用的是环状链表,其原因是因为react认为每次setCount都是有优先级的,有些优先级低的会被跳过或者排后,比如说你在点击事件中你setCount一次,在其他地方发请求且请求成功后也有个setCount,也许react它就认为前者的优先级更高,让用户提前感知,从而提高与用户的交互度。
其大致流程如下:
react|react技术分享----useState的原理及自定义useState的实现
文章图片

标注:图中的ABCD步骤是为了下面代码实现时方便解释(A、B步骤)的实际产出,与(C、D步骤)的实际产出。
四、开始实现自定义useState
  1. 搭建架子:
    注:这里有点就是,每个组件都有一个该组件所对应的fiber对象,就类似于虚拟dom,可以说是虚拟dom的升级版,如它对任务的调度有很多的优化,此处只是为了尽可能与源码对应,至于对它的了解,本文就不再多做阐述了。
    html结构:
    react|react技术分享----useState的原理及自定义useState的实现
    文章图片

    js代码:只需要关注我标明关注的位置即可
let fiber = {// 对应着本App组件 type: "FunctionComponent", // 该组件的类型 Node: App, // 所对应的组件 memoizedState: null // 连接所有hook对象的起点 }let workInProgressHook = null; // 用来指向当前正在工作的hook的指指针 let mountOrUpdate = true; // 表示当前组件是 mountProgress 还是 updateProgress,起初应该是true,function useState(initialState) { // useState的实现let state = typeof initialState === 'function' ? initialState() : initialState // 关注 let hook; // 关注//todo return [state, null] }function renderWithHooks() { // render函数 workInProgressHook = fiber.memoizedState; // 每次渲染,就应该把 workInProgressHook 指针指回开头(复原) const app = fiber.Node(); mountOrUpdate = false; // 只要render了,后续都应该是 updateProgres s阶段了 return app; }function App() { // App组件 const [count, setCount] = useState(0) // 关注 const [num, setNum] = useState(() => 10) // 关注document.getElementById("count").innerHTML = `${count}` document.getElementById("num").innerHTML = `${num}` console.log(`count的值:${count},num的值:${num}`)return { handleCount: () => { // 关注 setCount(count + 1) }, handleNum: () => { // 关注setNum((num => num + 10))} } }window.app = App()

页面:react|react技术分享----useState的原理及自定义useState的实现
文章图片

2.拼接这多个useState,在思路中我也是说过,这每调用一次useState,其实在react当中是被当作一个hook对象来管理的,现在就让我们来拼接吧!!
重点关注我框起来的部分react|react技术分享----useState的原理及自定义useState的实现
文章图片

执行完我框起来的部分后其实就算是结束了我们的挂载阶段了,也就是我第三点(useState实现的大体思路)那里最后一张图的(A、B步骤),其产出就是如下的样子:react|react技术分享----useState的原理及自定义useState的实现
文章图片

注意点:
结束这一步,也就体现了为什么reat中说明了要将所有的hook置于顶端,且不要放在某些判断语句当中,因为如果未来哪一次执行到numuseState的时候,由于某些判断条件导致这个useState执行不了了,指针的指向可能发生错乱,以及后续的逻辑也会有问题,不过庆幸的是后面比较新的reat版本是会默认添加相对应的ESLint 插件,来辅助用户更好的约束这个规则。
  1. 接下来就是updateProgress阶段了
    首先,这肯定是用户setXxx才导致的,所以说先来看dispatchAction(hook, action)react|react技术分享----useState的原理及自定义useState的实现
    文章图片

    该步骤之后其实就是我第三步骤的C、D阶段了,其产物如下,当然图中的action并不一定是图中这3、2、1个,我这里只是为了方便演示,实际情况有可能有更多个,也有可能没有:
    react|react技术分享----useState的原理及自定义useState的实现
    文章图片

    *记住这每一个queue对象的里面的pending始终是指向最后一个update对象的
其次,在重新render之后,就会又调用一遍App函数,即又调用一遍useState函数,且此刻就应该走updateProgress阶段了react|react技术分享----useState的原理及自定义useState的实现
文章图片

此步骤走完,其实我们的就可以更新当前的hook对象里面的memoizedState的值了,且它也是我们用户所希望的到的值,然后再将其返回出去给用户使用,最后再看下效果:react|react技术分享----useState的原理及自定义useState的实现
文章图片

总结: 回过头来想想,其实useState它本质是函数,没办法做到状态化,只是将其交至外面去管理罢了,个人感觉更有点像是Redux的思想;其实值得注意的一点是:
既然react使用了链表来管理和维护,那你就不得不遵守hook的规则
代码已放在这里了

    推荐阅读