阅读此文,首先要知道大概知道一些vue响应式系统(点击跳转官网链接)
相关链接:
- vue2-响应式
持续更新中…
起步
const state = reactive({a: 1})
effect(() => {
document.body.innerHTML = state.a
});
首先举个超级简单的例子,当js执行后,页面会显示出a的值“1”,当我们为state.a
赋值时,页面也会根据赋值的不同而改变,本文的目的就是理解这块代码的实现原理
第一步
读本篇博客的小伙伴们应该有了解,Vue3中使用了Proxy
来创建响应式对象(暂不考虑ref
),在get
时收集依赖,set
时触发依赖更新,最终暴露出reactive
等常用的api
首先上一段官网上的代码片段来简单理解一下reactive
的实现逻辑
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // 收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, value) {
target[key] = value
trigger(target, key) // 触发依赖
Reflect.set(target, key, value, receiver)
}
})
}
代码解释:当创建reactive
后,会对传入对象进行proxy代理,在被订阅者使用(get)时,触发track函数 收集依赖,记录下“订阅者在这里用到当前reactive
啦!”,在对象被修改(set)时,触发trigger函数 触发依赖,“要去更新所有用到当前reactive
的订阅者!”
那么这个订阅者是什么呢?
订阅者可以是一个组件、computed、watch、effect、watchEffect…
但这些东西的核心都是ReactiveEffect
第二步
ReactiveEffect
class ReactiveEffect
主要是用来收集依赖的,在其实例.run
时,会将当前实例放在全局activeEffect
上,然后调用传入的函数(比如组件的render函数/effect、computed、watch的回调函数),在调用的过程中响应式数据->get
触发执行track
函数收集依赖,就可以将“当前effect和响应式数据关联起来”啦
// 省略computed等一系列逻辑
let activeEffect; // 全局变量
class ReactiveEffect {
constructor(fn,scheduler) {...} // 此处只关注该流程的简单实现,忽略scheduler相关内容
run(){
try {
this.parent = activeEffect // 兼容嵌套关系(可忽略)
activeEffect = this // 将当前实例放在全局变量activeEffect上
return this.fn() // 调用传入的函数
} finally {
// 恢复activeEffect
activeEffect = this.parent
this.parent = undefined
}
}
}
小总结
到这里,整体的流程已经有眉目了,我们以目前理解的概念 举例重新梳理一下
首先执行代码时,effect
函数会创建new ReactiveEffect(fn)
,然后会执行.run()
,也就是会执行例子中传入的函数方法,在执行的过程中用到响应式数据state.a
会通过reactive -> get
触发track
收集依赖activeEffect
,后续更新时(state.a = 2
),reactive -> set
触发后,会调用trigger
触发依赖进行更新
然后让我们继续往下看,track
收集依赖 和 trigger
触发依赖都是什么?
第三步
track 依赖收集
targetMap
在说track
之前我们必须要了解一个全局变量targetMap
,他是一个weakMap
对象(key、value都是引用类型),所有响应式数据和effect对应的关系都会被储存在这个对象中
内部数据储存的结构是:key=响应式对象
,value=Map对象
,这个Map对象被命名为depsMap
depsMap
存储的是响应式对象的属性和effect
的对应关系,结构为key=响应式对象中的属性名
,value=Set对象
Set对象被命名为dep
中存储的就是对应的effect
让我们说回track
方法
对照上边的图,track
方法会将全局变量activeEffect
与当前响应式收集起来,除了放在targetMap
中,也会在当前activeEffect.deps
数组中存储图中的dep(Set格式)对象,达到双向收集的效果,也用于清依赖时使用
/**
* @param {Object} target 响应式对象
* @param {String} key 响应式对象中使用到的属性
*/
function track(target, key) {
if (activeEffect) { // 全局effect为空的话就什么都不做
// 1.获取并判断targetMap有没有当前响应式对象,没有就创建一个
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 2.获取并判断当前depsMap中有没有响应式对象中的当前“key”属性,没有就调createDeo创建
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 3.继续向dep对象中放值,同时将dep存入当前effect.deps数组中去
trackEffects(dep)
}
}
// 3.继续向dep对象中放值,同时将dep存入当前effect.deps数组中去
function trackEffects(dep) {
dep.add(activeEffect);
activeEffect && activeEffect.deps.push(dep);
}
trigger 触发依赖
触发依赖就是通过传入的target
、key
,来找到并执行 响应式对象.key
的所有已被收集effect
,来达到触发更新的目的(也就是执行对应effect
的所有run
方法)
function trigger(target, key) {
const depsMap = targetMap.get(target); // 获取到depsMap中的
if (!depsMap) {return}
// 本次代码忽略对clear、数组等处理,只关注对象
const dep = depsMap.get(key); // 当前响应式对象.属性 需要响应的所有effect(Set格式)
const effect = [...dep]
triggerEffects(dep); // 执行effect
}
// 执行effect
triggerEffects(effects) {
for (const effect of effects) {
effect.run()
}
}
到此位置,整个响应式的大概流程就已经明确了!
const state = reactive({a: 1})
effect(() => {
document.body.innerHTML = state.a
});
还是开头时的那个例子,首先执行代码时,会执行effect
,effect
函数会创建new ReactiveEffect(fn)
,然后会执行.run()
,在.run()
中执行fn
的过程中用到响应式数据state.a
会通过reactive -> get
触发track
,为activeEffect.deps
和targetMap (weakMap对象)
相互收集依赖;
后续更新响应式数据state.a = 2
时,reactive -> set
触发后,会调用trigger
通知所有的订阅者ReactiveEffect
更新,ReactiveEffect
就会重新走.run
方法,执行fn
函数重新渲染页面/收集依赖