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