在vue3中,因为reactive创建的响应式对象是通过Proxy来实现的,所以传入数据不能为基础类型,所以 ref 对象是对reactive不支持的数据的一个补充。
在 ref 和 reactive 中还有一个重要的工作就是收集、触发依赖,蓝狮官网那么依赖是什么呢?怎么收集触发?一起来看一下吧:
我们先来看一下 ref 的源码实现:
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
const toReactive = (value) => isObject(value) ? reactive(value) : value;
function createRef(rawValue: unknown, shallow: boolean) {
// 如果是ref则直接返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl {
private _value: T
// 存放 raw 原始值
private _rawValue: T
// 存放依赖
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// toRaw 拿到value的原始值
this._rawValue = __v_isShallow ? value : toRaw(value)
// 如果不是shallowRef,使用 reactive 转成响应式对象
this._value = __v_isShallow ? value : toReactive(value)
}
// getter拦截器
get value() {
// 收集依赖
trackRefValue(this)
return this._value
}
// setter拦截器
set value(newVal) {
// 如果是需要深度响应的则获取 入参的raw
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 新值与旧值是否改变
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
// 更新value 如果是深入创建并且是对象的话 还需要转化为reactive代理
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 触发依赖
triggerRefValue(this, newVal)
}
}
}
RefImpl 采用ES6类的写法,包含 get 、 set ,其实大家可以用 webpack 等打包工具打包成 ES5 的代码, 发现其实就是 Object.defineProperty 。
可以看到, shallowRef 和 ref 都调用了 createRef ,只是传入的参数不同。当使用 shallowRef 时,不会调用 toReactive 去将对象转换为响应式,由此可见,shallowRef对象只支持对value值的响应式,ref对象支持对value深度响应式,ref.value.a.b.c中的修改都能被拦截,举个:chestnut::
let refData = ref({
a: ‘ref’
})
let shallowRefData = shallowRef({
a: ‘shallowRef’
})
const handleChange = () => {
refData.value.a = “ref1”
shallowRefData.value.a = “shallowRef1”
}
当我们点击按钮修改数据后,界面上的 refData.a 的值会变为 ref1 ,而 shallowRefData.a 应该会不发生变化,但其实在这个例子里, shallowRefData.a 在视图上也会发生变化的:dog:,因为修改 refData.a 时候,触发了setter函数,内会去调用 triggerRefValue(this, newVal) 从而触发了 视图更新 ,所以shallow的最新数据也会被更新到了视图上 (把 refData.value.a = “ref1” 去掉它就不会变了)。
在 ref 里最关键的还是 trackRefValue 和 triggerRefValue ,负责收集触发依赖。
如何收集依赖:
function trackRefValue(ref) {
// 判断是否需要收集依赖
// shouldTrack 全局变量,代表当前是否需要 track 收集依赖
// activeEffect 全局变量,代表当前的副作用对象 ReactiveEffect
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
{
// 如果没有 dep 属性,则初始化 dep,dep 是一个 Set,存储副作用函数
// trackEffects 收集依赖
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: “get”,
key: ‘value’
});
}
}
}
为什么要判断 shouldTrack 和 activeEffect ,因为在Vue3中有些时候不需要收集依赖:
当没有 effect 包裹时,比如定义了一个ref变量,但没有任何地方使用到,这时候就没有依赖,activeEffect 为 undefined,就不需要收集依赖了
比如在数组的一些会改变自身长度的方法里,也不应该收集依赖,容易造成死循环,此时 shouldTrack 为 false
*依赖是什么?
ref.dep 用于储存 依赖 (副作用对象),ref 被修改时就会触发,那么依赖是什么呢?依赖就是 ReactiveEffect :
为什么要收集依赖(副作用对象),因为在Vue3中,一个响应式变量的变化,往往会触发一些副作用,比如视图更新、计算属性变化等等,需要在响应式变量变化时去触发其它一些副作用函数。
在我看来 ReactiveEffect 其实就和 Vue2 中的 Watcher 的作用差不多,我之前写的 《Vue源码学习-响应式原理》 里做过说明:
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
// 传入一个副作用函数
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
// 存储 Dep 对象,如上面的 ref.dep
// 用于在触发依赖后, ref.dep.delete(effect),双向删除依赖)
this.deps = [];
this.parent = undefined;
recordEffectScope(this, scope);
}
run() {
// 如果当前effect已经被stop
if (!this.active) {
return this.fn();
}
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
// 保存上一个 activeEffect
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
// trackOpBit: 根据深度生成 trackOpBit
trackOpBit = 1 << ++effectTrackDepth;
// 如果不超过最大嵌套深度,使用优化方案
if (effectTrackDepth <= maxMarkerBits) {
// 标记所有的 dep 为 was
initDepMarkers(this);
}
// 否则使用降级方案
else {
cleanupEffect(this);
}
// 执行过程中重新收集依赖标记新的 dep 为 new
return this.fn();
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
// 优化方案:删除失效的依赖
finalizeDepMarkers(this);
}
// 嵌套深度自 + 重置操作的位数
trackOpBit = 1 << –effectTrackDepth;
// 恢复上一个 activeEffect
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
if (this.deferStop) {
this.stop();
}
}
}
}
ReactiveEffect 是副作用对象,它就是被收集依赖的实际对象,一个响应式变量可以有多个依赖,其中最主要的就是 run 方法,里面有两套方案,当 effect 嵌套次数不超过最大嵌套次数的时候,使用优化方案,否则使用降级方案。
降级方案:
function cleanupEffect(effect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
// 从 ref.dep 中删除 ReactiveEffect
deps[i].delete(effect);
}
deps.length = 0;
}
}
这个很简单,删除全部依赖,然后重新收集。在各个 dep 中,删除该 ReactiveEffect 对象,然后执行 this.fn() (副作用函数) 时,当获取响应式变量触发 getter 时,又会重新收集依赖。之所以要先删除然后重新收集,是因为随着响应式变量的变化,收集到的依赖前后可能不一样。
const toggle = ref(false)
const visible = ref(‘show’)
effect(() = {
if (toggle.value) {
console.log(visible.value)
} else {
console.log(‘xxxxxxxxxxx’)
}
})
toggle.value = true
当 toggle 为 true 时,toggle、visible 都能收集到依赖
当 toggle 为 false 时,只有visible 可以收集到依赖
优化方案:
全部删除,再重新收集,明显太消耗性能了,很多依赖其实是不需要被删除的,所以优化方案的做法是:
// 响应式变量上都有一个 dep 用来保存依赖
const createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0;
dep.n = 0;
return dep;
};
执行副作用函数前,给 ReactiveEffect 依赖的响应式变量 ,加上 w(was的意思) 标记。
执行 this.fn(),track 重新收集依赖时,给 ReactiveEffect 的每个依赖,加上 n(new的意思) 标记。
最后,对有 w 但是没有 n 的依赖进行删除。
其实就是一个筛选的过程,我们现在来第一步,如何加上 was 标记:
// 在 ReactiveEffect 的 run 方法里
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
}
const initDepMarkers = ({ deps }) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit;
}
}
};
这里使用了位运算,快捷高效。trackOpBit是什么呢?代表当前嵌套深度 (effect可以嵌套) ,在Vue3中有一个全局变量 effectTrackDepth
// 全局变量 嵌套深度
let effectTrackDepth = 0;
// 在 ReactiveEffect 的 run 方法里
// 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1
trackOpBit = 1 << ++effectTrackDepth
// 执行完副作用函数后会自减
trackOpBit = 1 << –effectTrackDepth;
当深度为 1 时,trackOpBit为 2(二进制:00000010),这样执行 deps[i].w |= trackOpBit 时,操作的是第二位,所以第一位是用不到的。
为什么Vue3中嵌套深度最大是 30 ?
1 << 30
// 0100 0000 0000 0000 0000 0000 0000 0000
// 1073741824
1 << 31
// 1000 0000 0000 0000 0000 0000 0000 0000
// -2147483648 溢出
因为js中位运算是以32位带符号的整数进行运算的,最左边一位是符号位,所以可用的正数最多只能到30位。
可以看到,在执行副作用函数之前,使用 deps[i].w |= trackOpBit ,对依赖在不同深度是否被依赖( w )进行标记,然后执行 this.fn() ,重新收集依赖,上面说到收集依赖调用 trackRefValue 方法,该方法内会调用 trackEffects :
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
// 查看是否记录过当前依赖
if (!newTracked(dep)) {
dep.n |= trackOpBit;
// 如果 w 在当前深度有值,说明effect之前已经收集过
// 不是新增依赖,不需要再次收集
shouldTrack = !wasTracked(dep);
}
}
else {
shouldTrack = !dep.has(activeEffect);
}
if (shouldTrack) {
// dep添加当前正在使用的effect
dep.add(activeEffect);
// effect的deps也记录当前dep 双向引用
activeEffect.deps.push(dep);
}
}
可以看到再重新收集依赖的时候,使用 dep.n |= trackOpBit 对依赖在不同深度是否被依赖( n )进行标记,这里还用到两个工具函数:
const wasTracked = (dep) => (dep.w & trackOpBit) > 0;
const newTracked = (dep) => (dep.n & trackOpBit) > 0;
使用 wasTracked 和 newTracked,判断 dep 是否在当前深度被标记。比如判断依赖在深度 1 时 (trackOpBit第二位是1) 是否被标记,采用按位与:
最后,如果已经超过最大深度,因为采用降级方案,是全部删除然后重新收集的,所以肯定是最新的,所以只需要把 trackOpBit 恢复,恢复上一个 activeEffect:
finally {
if (effectTrackDepth <= maxMarkerBits) {
// 优化方案:删除失效的依赖
finalizeDepMarkers(this);
}
trackOpBit = 1 << –effectTrackDepth;
// 恢复上一个 activeEffect
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
if (this.deferStop) {
this.stop();
}
}
如果没超过最大深度,就像之前说的把失效的依赖删除掉,然后更新一下deps的顺序:
const finalizeDepMarkers = (effect) => {
const { deps } = effect;
if (deps.length) {
let ptr = 0;
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
// 把有 w 没有 n 的删除
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect);
}
else {
// 更新deps,因为有的可能会被删掉
// 所以要把前面空的补上,用 ptr 单独控制下标
deps[ptr++] = dep;
}
// 与非,恢复到进入时的状态
dep.w &= ~trackOpBit;
dep.n &= ~trackOpBit;
}
deps.length = ptr;
}
};
举个简单的:chestnut:,理解起来可能简单点,有两个组件,一个父组件,一个子组件,子组件接收父组件传递的 toggle 参数显示在界面上, toggle 还控制着 visible 的显示,点击按钮切换 toggle 的值:
// Parent
// Child
第一次渲染,因为toggle 默认为 true,我们可以收集到 toggle 、 visible 的依赖,
Parent 组件, 执行 run 方法中的 initDepMarkers 方法,首次进入,还未收集依赖, ReactiveEffect 中 deps 长度为0,跳过。
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
toggle 的 dep = {n: 2, w: 0} , shouldTrack 为 true,收集依赖。
visible 的 dep = {n: 2, w: 0} , shouldTrack 为 true,收集依赖。
进入 Child 组件,执行 run 方法中的 initDepMarkers 方法,首次进入,还为收集依赖,deps长度为0,跳过。
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
toggle 的 dep = {n: 4, w: 0} , shouldTrack 为 true,收集依赖。
这样首次进入页面的收集依赖就结束了,然后我们点击按钮,把 toggle 改为 false:
Parent 组件: 执行 run 方法中的 initDepMarkers 方法,之前在 Parent 组件里收集到了两个变量的依赖,所以将他们 w 标记:
dep = {n: 0, w: 2}
dep = {n: 0, w: 2}
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
toggle 的 dep = {n: 2, w: 2} , shouldTrack 为 false, 不用 收集依赖。
visible 不显示了 ,所以没有重新收集到,还是 {n: 0, w: 2} 。
进入 Child 组件,执行 run 方法中的 initDepMarkers 方法,之前 收集过 toggle 依赖了,将 toggle 的 w 做标记,toggle 的 dep = {n: 0, w: 4} 。
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
toggle 的 dep = {n: 4, w: 4} , shouldTrack 为 false,不用收集依赖。
最后发现 visible 有 w 没有 n ,在 finalizeDepMarkers 中删除掉失效依赖。
如何触发依赖:
在一开始讲到的 ref 源码里,可以看到在 setter 时会调用 triggerRefValue 触发依赖:
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
if (ref.dep) {
{
triggerEffects(ref.dep, {
target: ref,
type: “set”,
key: ‘value’,
newValue: newVal
});
}
}
}
function triggerEffects(
dep: Dep | ReactiveEffect[]
) {
// 循环去取每个依赖的副作用对象 ReactiveEffect
for (const effect of isArray(dep) ? dep : […dep]) {
// effect !== activeEffect 防止递归,造成死循环
if (effect !== activeEffect || effect.allowRecurse) {
// effect.scheduler可以先不管,ref 和 reactive 都没有
if (effect.scheduler) {
effect.scheduler()
} else {
// 执行 effect 的副作用函数
effect.run()
}
}
}
}
触发依赖最终的目的其实就是去执行 依赖 上 每个的副作用对象 的 副作用函数 ,这里的副作用函数可能是执行更新视图、watch数据监听、计算属性等。
我个人再看源码的时候还遇到了一个问题,不知道大家遇到没有(我看的代码版本算是比较新v3.2.37),一开始我也是上网看一些源码的解析文章,看到好多讲解 effect 这个函数的,先来看看这个方法的源码:
function effect(fn, options) {
if (fn.effect) {
fn = fn.effect.fn;
}
const _effect = new ReactiveEffect(fn);
if (options) {
extend(_effect, options);
if (options.scope)
recordEffectScope(_effect, options.scope);
}
if (!options || !options.lazy) {
_effect.run();
}
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
// 返回一个包装后的函数,执行收集依赖
return runner;
}
这个函数看上去挺简单的,创建一个 ReactiveEffect 副作用对象,将用户传入的参数附加到对象上,然后调用 run 方法收集依赖,如果有 lazy 配置不会自动去收集依赖,用户主动执行 effect 包装后的函数,也能够正确的收集依赖。
但我找了一圈,发现源码里一个地方都没调用,于是我就在想是不是以前用到过,现在去掉了,去commit记录里找了一圈,还真找到了:
这次更新把 ReactiveEffect 改为用类来实现,避免不必要时也创建 effect runner ,节省了17%的内存等。
原来的 effect 方法包括了现在的 ReactiveEffect ,在视图更新渲染、watch等地方都直接引用了这个方法,但更新后都是直接 new ReactiveEffect ,然后去触发 run 方法,不走 effect 了,可以说现在的 ReactiveEffect 类就是之前的 effect 方法 。
export function effect(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
const effect = createReactiveEffect(fn, options)
return effect
}
let uid = 0
function createReactiveEffect(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n – 1] : undefined
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
结尾
我是周小羊,一个前端萌新,蓝狮注册写文章是为了记录自己日常工作遇到的问题和学习的内容,提升自己,如果您觉得本文对你有用的话,麻烦点个赞鼓励一下哟~
0 Comments