在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<T> { 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中的批改都能被拦挡,举个:
<template> <p>{{ refData.a }}</p> <p>{{ shallowRefData.a }}</p> <button @click="handleChange">change</button></template>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
在视图上也会发生变化的,因为批改 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<ReactiveEffect>,存储副作用函数 // 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 副作用函数前,全局变量嵌套深度会自增1trackOpBit = 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// 10737418241 << 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; }};
举个简略的,了解起来可能简略点,有两个组件,一个父组件,一个子组件,子组件接管父组件传递的 toggle
参数显示在界面上,toggle
还管制着 visible
的显示,点击按钮切换 toggle
的值:
// Parent<script setup lang="ts">const toggle = ref(true)const visible = ref('show')const handleChange = () => { toggle.value = false}</script><template> <div> <p v-if="toggle">{{ visible }}</p> <p v-else>xxxxxxxxxxx</p> <button @click="handleChange">change</button> <Child :toggle="toggle" /> </div></template>
// Child<script setup lang="ts">const props = defineProps({ toggle: { type: Boolean, },});</script><template> <p>{{ toggle }}</p></template>
第一次渲染,因为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,收集依赖。
- toggle 的
- 进入
Child
组件,执行 run 办法中的initDepMarkers
办法,首次进入,还为收集依赖,deps长度为0,跳过。 执行 run 办法中的
this.fn
,从新收集依赖,触发 trackEffects:- toggle 的
dep = {n: 4, w: 0}
,shouldTrack
为 true,收集依赖。
- toggle 的
这样首次进入页面的收集依赖就完结了,而后咱们点击按钮,把 toggle
改为 false:
Parent
组件: 执行 run 办法中的initDepMarkers
办法,之前在Parent
组件里收集到了两个变量的依赖,所以将他们w
标记:- toggle 的
dep = {n: 0, w: 2}
- visible 的
dep = {n: 0, w: 2}
- toggle 的
执行 run 办法中的
this.fn
,从新收集依赖,触发 trackEffects:- toggle 的
dep = {n: 2, w: 2}
,shouldTrack
为 false,不必
收集依赖。 - visible
不显示了
,所以没有从新收集到,还是{n: 0, w: 2}
。
- toggle 的
- 进入
Child
组件,执行 run 办法中的initDepMarkers
办法,之前 收集过toggle
依赖了,将 toggle 的 w 做标记,toggle 的dep = {n: 0, w: 4}
。 执行 run 办法中的
this.fn
,从新收集依赖,触发 trackEffects:- toggle 的
dep = {n: 4, w: 4}
,shouldTrack
为 false,不必收集依赖。
- toggle 的
最初发现 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<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect<T> { const effect = createReactiveEffect(fn, options) return effect}let uid = 0function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions): ReactiveEffect<T> { 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}
结尾
我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~