你应该要知道的细粒度更新
前言
在vue
、svelte
和Solid
这些框架,不需要显示的指明依赖
就能达到自变量state
的改变因变量computed
也能随之改变。 但作为react框架
的用户为什么需要显示指明依赖
以及如何实现一个react HOOKS版本自动追踪依赖技术
也称之为细粒度更新
。
框架的实现原理以及分类
基础原理
在实现细粒度更新
之前,我们先来了解一下框架的基础原理
,框架最基本的理论可以由如下公式诠释:
- state代表我们定义的数据状态(
自变量和因变量
) - f代表处理数据变量的逻辑(
框架核心机制
) - UI代表你所看到的视图(
宿主机的界面
)
如上state状态
分为自变量
和因变量
。 那么什么是自变量
,什么又是因变量
呢?
- 自变量:由自己控制和改变的变量,不受其他变量影响
- 因变量: 依赖于自变量产生的变量,自变量变更因变量也随着变更
其在react中的定义:
// 自变量
const [name, setName] = useState('AK');
// 因变量 (随着name自变量的更新,fullName也会跟着变化)
const fullName = useMemo(() =>{
return name + 'clown'
}, [name])
如上诉公式UI = f(state)
表述只能通过state状态
的变化才能导致UI的更新
。框架的作者在设计组件时需要提供一些灵活度
,使得开发者在定义UI
与逻辑
时能够跳出组件
的限制,执行一些有副作用的操作
。 例如: 有时候希望直接操作DOM元素
或者记录页面渲染次数,无论那个state变量的变化
等。
因此在react中提供了useRef
这个hooks来实现此需求。useRef
产生的值我们通常称之为ref(reference)
表示引用的意思,其用于组件多次渲染(render)
之间缓存的一个引用类型的值
。 因为他的作用可以在下面的那一个箭头里
框架分类
框架的类别分为三类: 应用级框架(react)
、 组件级框架(vue)
、 元素级框架(solid)
那么这三类型在在更新上有何差异呢? 接下来定义一下A、B两个组件。
接下来看看
各个类型
是如何更新的
- 元素级框架(solid)
- name变化导致A组件的{name}更新
- name变化导致B组件的{name+‘clown’}更新
- name变化导致B组件的{'https'+name}更新
- age变化导致B组件的{age}更新
- 组件级框架(vue)
- name变化导致A组件更新
- name变化导致B组件更新
- age变化导致B组件更新
- 应用级框架(react)
- name变化导致应用
- age变化导致应用
由上所示: 自变量到UI的变化
路径越多,意味着框架在运行时消耗在寻找自变量与UI的对应关系
上的时间也就越少
细粒度更新的实现
再了解了框架的基础原理和分类
,接下来就来了解一下自动追踪依赖技术
也称之为细粒度更新
.本质底层实现就是发布订阅设计模式
。在state的get值时进行一些effect
的订阅。在set值将更新推送给订阅的effct
从而实现订阅者的更新。
首先来实现useState来声明自变量
function useState(value){
// 获取值
const getter = () => value
// 设置值
const setter = (newValue) =>{
value = newValue
}
return [getter]
}
初始化的时候传入初始值value
,通过getter
获取到闭包的value
值,通过setter
可以修改闭包的value
值。
紧接着来实现useEffect,带有副作用的因变量函数
。其行为有如下三步:
- useEffect执行后会自动调用回调函数
因变量
变化时回调函数也会被执行- 无需显示指定
依赖(因变量)
实现上述三个步骤的期望,实际上就是建立useState
和useEffect
的发布订阅关系:
- 在useEffect的回调中,执行
getter
函数获取因变量
时建立订阅关系。即当前effect
会定义该state的变化
- 在useState中通过
setter
设置因变量
时,执行所有订阅者effect的回调函数
。从而达到useEffect回调函数的执行
// 全局effect执行栈
const effectStack = [];
// 清空effect里的所有依赖项
function cleanup(effect){
// 从订阅的所有state对应的subs中移除掉effect (就是移除掉useState里的subs保存与该effect的关系)
for(let subs of effect.deps){
subs.delete(effect)
}
// 清空依赖
effect.deps.clear()
}
function useEffect(callback){
const executed = () =>{
// 清空effect的依赖deps
cleanup(effect)
// 将当前effect退入到effectStack顶部 (在useState中才能获取到当前正在执行effect,也才能知道与那个effect建立发布订阅关系)
effectStack.push(effect)
try{
callback()
}finally{
// 将当前执行effect从栈顶移除掉
effectStack.pop()
}
}
const effect = {
executed,
deps:new Set()
}
// 立刻执行
effect.executed()
}
紧接着来实现一下useState中是如何与该useEffect建立发布订阅关系的
。
// 定义关系的实现
function subscribe(effect,subs){
// 通过这一步就建立了相互关系: subs <===> effect.deps
subs.add(effect)
effect.deps.add(subs)
}
function useState(value){
// 记录 关联的effect
const subs = new Set()
// 获取值
const getter = () => {
// 从全局的effectStack的栈顶获取到当前正在指向的`effect上下文`
const effect = effectStack.at(-1);
if(effect){
// 存在effect建立订阅关系 (该state就已经能够拿到useEffect的回调了)
subscribe(effect,subs)
}
return value
}
// 设置值
const setter = (newValue) =>{
value = newValue
// 进行发布行为
for(let sub of [...subs] ){
// 这一步执行了useEffect的回调
effect.executed()
}
}
return [getter]
}
经过上面步骤我们就完成了useState
和useEffect
的发布订阅模式,也就是实现了自动追踪依赖技术(细粒度更新)
的功能。那么接下来思考三个问题:
-
useState中是如何知道当前正在执行那个effect呢? 解答: 通过全局的
effectStack
栈来记录当前正在执行的useEffect
。进入执行useEffect
时,将当前effect
推入栈顶,执行callback回调函数
时会触发到useState
里的getter函数
获取state值(自变量)
,那么此时是不是就可以在effectStack的栈顶
拿到正在执行的effect
了从而建立关系。等callback回调函数
执行完成之后,将effect
从effectStack的栈顶
移除掉。 -
为什么在useEffect中每次执行
effect.executed
都要重新清除订阅关系,再重新订阅呢? 解答:
const [name, setName] = useState('AKclown')
const [age, setAge] = useState(18)
const [isShowAge, setIsShowAge] = useState(true)
useEffect(() =>{
if(!isShowAge()){
return '名称:' + name()
}
return '名称:' + name() + '年龄:' + age()
})
因为isShowAge
为true
那么这个effect
会跟name建立发布订阅关系
以及跟age也建立发布订阅关系
。此时name
和age
中任意一个自变量
发生变化时,在对应的useSteta
内部的setter
都会执行effect.excuted
。因此当前useEffect
回调函数就会被执行。
那么如果每次执行
effect.excuted
不重置发布订阅关系
带来的效果是如何呢? 假设现在执行setIsShowAge(false)
,那么本质上只有name的更新
才会触发useEffect
的回调,而age的更新
就不应该触发useEffect
的回调了。但因为没有重新建立发布订阅关系
useEffect依旧保留了对age的更新
的订阅。
因此我们需要在每次执行effect.executed
时,重新建立与state(自变量)
的订阅关系。最终关系图如下:
- 为什么React Hooks中没有使用细粒度更新呢?
解答: 原因在于React属于
应用级别框架
,从而其关注的是自变量与应用之间的关系
,从这个角度看,其更细粒度不需要很细,因此无须使用细粒度更新
总结
本章我们学习框架的基础理论UI = F(state)
、也学习到了因变量和自变量
在框架中的含义。以及知道前端框架的分类,其分为应用级别框架
、组件级别框架
、元素级别框架
、三大类。最后学习了React HOOKS自动追踪依赖的版本实现
。
转载自:https://juejin.cn/post/7383457343876759591