*前言:
本次分享将主要自定义实现useState
为主,以通俗易懂的目的让大家了解useState实现的大体逻辑。但内容是非常长的,如果真的想理解的话,还是希望你耐住性子看看,相信即使不能让你读懂源码,但至少能够给你做一些铺垫~~,代码已放在这里了,可以先看下效果再决定值不值得继续看吧
hook
出现的意义是巨大的,在React Conf 2018 会议上,react团队的leader---- Sophie Alpert提出了三个class组件存在的问题(而hook
的出现就是来解决这些的):
class
组件中无非是用高阶组件
,或者render props
来解决,但是如果项目庞大的话,就有可能造成组件层级过深,无限嵌套导致追踪数据流困难,称之为“包装地狱
”class
组件中,你很有可能会经常在componentDidMount
当中订阅数据存储
、发送请求
、设置定时器
,而在componentWillUnmount
里面做相反的事情,取消订阅
、取消请求
、销毁定时器
class
组件理解困难:这个不管是对人还是对机器而言都是困难的,你人要考虑this
的绑定与指向,且有时候甚至你还不知道什么时候该选取class组件
,还是function组件
,还有就是机器要对class
进行解析成组件文件的时候,class
里面有些函数即使没有使用过,它也不会被剔除,因为机器在编译时很难准确判断方法是否被使用。这个在这里就不过多的阐述了,毕竟你都来看本篇文章了,目的也不是为了再教你如何使用了,以下是简单例子:
当点击按钮的时候,能够很符合我们预期的达到和class组件一样的效果,这就是它的强势,让你的函数组件看起来像是具有了状态。
useState
它的特点就是使组件具有状态,且有存储数据的功能,当修改count
的时候(即调用setCount
),函数组件App
将会重新渲染(即重新调用了App函数
,不然也不会每点击按钮就没打印一遍count
是吧)函数组件App
不同之处是,第一次属于挂载(mount
),第二次则属于更新(update
),也就是说两次调用useState
的含义也是不同的,说白了就是有两个大分支,第一次走mount
,后面都走update
mount
阶段吧,这个阶段你要可能要联想下实际工作例子,你很有可能会使用很多个useState
,就像下面这样useState
,它改怎么维护这么多个状态呢,也许你会想到使用数组
,但是你就要再用个下标来维护以后更新时,改更新哪一个state
呢?虽然理论上是可以的,但也伴随着它的灵活性不高,在react源码
中,它是使用的是链表
来维护的,它可以很好的解决上述刚才的问题,源码中它把你每一次使用useState
用一个hook对象
来维护,里面保存着你调用useState
传入的值(memoizedState
),以及next指针
,这也是用来连接下次调用useState
所新建的一个新的hook对象
。在这里,你也许还是搞不懂为啥要用链表,但相信当你看完本篇文章的时候再回过头好好想想就会深有体会了,这里先埋个彩蛋~,就像如下:4.接下来就是update
阶段了,这个阶段是你调用setCount(setXxx)
的时候才会走的阶段,并且你会向这个setCount
函数传一个值,或者函数,来表达你期望得到的值是什么,在此之前,那就得先找到这个hook对象
,并把传来newState
赋值给这个hook旧的的state(即memoizedState
),更改完值后,就是重新reder渲染了,当然同样你也要考虑实际情况,有时候你在处理点击事件的时候会多次使用setCount(setXxx)
,就像如下:
这个时候你就应该会想到类比步骤3一样的方法,用链表(queue
)来维护这么多个setCount
,同样这也是react
中使用的手段,但是比较特殊的是它使用的是环状链表
,其原因是因为react
认为每次setCount
都是有优先级
的,有些优先级低的会被跳过或者排后,比如说你在点击事件中你setCount
一次,在其他地方发请求且请求成功后也有个setCount
,也许react
它就认为前者的优先级更高,让用户提前感知,从而提高与用户的交互度。
其大致流程如下:
标注:图中的A
、B
、C
、D
步骤是为了下面代码实现时方便解释(A、B步骤
)的实际产出,与(C、D步骤
)的实际产出。
fiber对象
,就类似于虚拟dom
,可以说是虚拟dom
的升级版,如它对任务的调度有很多的优化,此处只是为了尽可能与源码对应,至于对它的了解,本文就不再多做阐述了。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()
页面:
2.拼接这多个useState
,在思路中我也是说过,这每调用一次useState
,其实在react
当中是被当作一个hook对象
来管理的,现在就让我们来拼接吧!!
重点关注我框起来的部分
执行完我框起来的部分后其实就算是结束了我们的挂载阶段了,也就是我第三点(useState实现的大体思路)
那里最后一张图的(A、B步骤)
,其产出就是如下的样子:
注意点:
结束这一步,也就体现了为什么reat中说明了要将所有的hook
置于顶端
,且不要放在某些判断语句当中,因为如果未来哪一次执行到num
的useState
的时候,由于某些判断条件导致这个useState
执行不了了,指针的指向可能发生错乱,以及后续的逻辑也会有问题,不过庆幸的是后面比较新的reat
版本是会默认添加相对应的ESLint 插件,来辅助用户更好的约束这个规则。
setXxx
才导致的,所以说先来看dispatchAction(hook, action)
:C、D阶段
了,其产物如下,当然图中的action并不一定是图中这3、2、1个,我这里只是为了方便演示,实际情况有可能有更多个,也有可能没有:queue
对象的里面的pending
始终是指向最后一个update
对象的其次,在重新render
之后,就会又调用一遍App
函数,即又调用一遍useState
函数,且此刻就应该走updateProgress
阶段了
此步骤走完,其实我们的就可以更新当前的hook
对象里面的memoizedState
的值了,且它也是我们用户所希望的到的值,然后再将其返回出去给用户使用,最后再看下效果:
回过头来想想,其实useState
它本质是函数,没办法做到状态化,只是将其交至外面去管理罢了,个人感觉更有点像是Redux
的思想;其实值得注意的一点是:
既然react使用了链表来管理和维护,那你就不得不遵守hook
的规则
代码已放在这里了