在 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 副作用函数前,全局变量嵌套深度会自增 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;
}
};
举个简略的🌰,了解起来可能简略点,有两个组件,一个父组件,一个子组件,子组件接管父组件传递的 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 = 0
function 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
}
结尾
我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~