In Vue3, because the reactive object created by reactive is implemented through Proxy, the incoming data cannot be the basic type, so the ref
object is a supplement to the data that reactive does not support.
Another important job in ref
and reactive
is to collect and trigger dependencies, so what are dependencies? How to collect triggers? Let’s take a look together:
Let’s take a look at the source code implementation of 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
.
It can be seen that shallowRef
and ref
both call createRef
, but the incoming parameters are different. When shallowRef
toReactive
not be called to convert the object to responsive. It can be seen that the shallowRef object only supports responsiveness to the value, and the ref object supports the depth of the value. Responsive, modifications in ref.value.abc can be intercepted, for example 🌰:
<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" }
When we click the button to modify the data, the value of refData.a
on the interface will become ref1
, and shallowRefData.a
should not change, but in this example In, shallowRefData.a
will also change on the view 🐶, because the modification refData.a
triggers the setter function, which will call triggerRefValue(this, newVal)
-398 视图更新
, so the latest data of shallow will also be updated to the view (remove refData.value.a = "ref1"
and it will not change).
The most critical ones in ref
are trackRefValue
and triggerRefValue
, which are responsible for collecting trigger dependencies.
Contents
How to collect dependencies:
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' }); } } }
Why judge shouldTrack
and activeEffect
, because in Vue3 sometimes it is not necessary to collect dependencies:
- When there is no effect package, for example, a ref variable is defined but not used anywhere, then there is no dependency, and activeEffect is undefined, so there is no need to collect dependencies
- For example, in some methods of the array that will change its own length, dependencies should not be collected, which is easy to cause an infinite loop. At this time, shouldTrack is false.
*What is the dependency?
ref.dep
for storage 依赖
(side effect object), it will be triggered when ref is modified, so what is the dependency? The dependency is ReactiveEffect
:
Why collect dependencies (side effect objects), because in Vue3, the change of a responsive variable often triggers some side effects, such as view updates, changes in computed properties, etc., and other side effect functions need to be triggered when the responsive variable changes .
In my opinion ReactiveEffect
is actually similar to the Watcher
in Vue2, as explained in the “Vue Source Learning – Responsive Principles” I wrote before:
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
is the side effect object, which is the actual object of the collected dependencies. A responsive variable can have multiple dependencies, the most important of which is the run
method, which has two schemes. When effect
the number of nesting does not exceed the maximum number of nesting, use the optimization scheme, otherwise use the degraded scheme.
Downgrade plan:
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; } }
This is very simple, delete all dependencies, and then re-collect. In each dep, delete the ReactiveEffect
object, and then execute this.fn()
(side effect function), when the responsive variable is obtained and trigger getter
, it will be collected again rely. The reason for deleting and then re-collecting is that the collected dependencies may not be the same before and after as the reactive variables change.
const toggle = ref(false) const visible = ref('show') effect(() = { if (toggle.value) { console.log(visible.value) } else { console.log('xxxxxxxxxxx') } }) toggle.value = true
- When toggle is true, both toggle and visible can collect dependencies
- When toggle is false, only visible can collect dependencies
Optimization:
Deleting all of them and collecting them again is obviously too performance-intensive. In fact, many dependencies do not need to be deleted. Therefore, the optimization scheme is as follows:
// 响应式变量上都有一个 dep 用来保存依赖 const createDep = (effects) => { const dep = new Set(effects); dep.w = 0; dep.n = 0; return dep; };
- Before executing the side effect function, give
ReactiveEffect 依赖的响应式变量
and add thew(was的意思)
flag. - Execute this.fn(), when track recollects dependencies, add
n(new的意思)
to each dependency of ReactiveEffect. - Finally, remove the dependencies that have
w
but notn
.
In fact, it is a screening process. Let’s take the first step now, how to add the was
tag:
// 在 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; } } };
Bit operations are used here, which is fast and efficient. What is trackOpBit? Represents the current nesting depth (effect可以嵌套)
, there is a global variable in effectTrackDepth
// 全局变量 嵌套深度 let effectTrackDepth = 0; // 在 ReactiveEffect 的 run 方法里 // 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1 trackOpBit = 1 << ++effectTrackDepth // 执行完副作用函数后会自减 trackOpBit = 1 << --effectTrackDepth;
When the depth is 1, the trackOpBit is 2 (binary: 00000010), so when executing deps[i].w |= trackOpBit
, the operation is the second bit, so the first bit is not used.
Why is the maximum nesting depth 30 in Vue3?
1 << 30 // 0100 0000 0000 0000 0000 0000 0000 0000 // 1073741824 1 << 31 // 1000 0000 0000 0000 0000 0000 0000 0000 // -2147483648 溢出
Because the js median operation is performed on a 32-bit signed integer, and the leftmost bit is the sign bit, the available positive numbers can only be up to 30 bits at most.
It can be seen that before executing the side effect function, use deps[i].w |= trackOpBit
to mark whether the dependency is depended on at different depths ( w ), and then execute this.fn()
, recollect the dependencies, as mentioned above Collection dependency calls trackRefValue
method, which will call 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); } }
It can be seen that when re-collecting the dependencies, use dep.n |= trackOpBit
to mark whether the dependencies are depended on ( n ) at different depths. Two tool functions are also used here:
const wasTracked = (dep) => (dep.w & trackOpBit) > 0; const newTracked = (dep) => (dep.n & trackOpBit) > 0;
Use wasTracked and newTracked to determine whether dep
is marked at the current depth. For example, to determine whether the dependency is marked at depth 1 (the second bit of trackOpBit is 1), use bitwise AND:
Finally, if the maximum depth has been exceeded, because the downgrade scheme is adopted, it is all deleted and then collected again, so it must be the latest, so you only need to restore trackOpBit
and restore the last activeEffect:
finally { if (effectTrackDepth <= maxMarkerBits) { // 优化方案:删除失效的依赖 finalizeDepMarkers(this); } trackOpBit = 1 << --effectTrackDepth; // 恢复上一个 activeEffect activeEffect = this.parent; shouldTrack = lastShouldTrack; this.parent = undefined; if (this.deferStop) { this.stop(); } }
If the maximum depth is not exceeded, delete the invalid dependencies as mentioned before, and then update the order of 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; } };
For a simple 🌰, it may be easier to understand. There are two components, a parent component and a child component. The child component receives the toggle
parameters passed by the parent component are displayed on the interface, toggle
–It also controls the display of toggle
visible
, click the button to switch the value of 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>
The first rendering, because toggle defaults to true, we can collect the dependencies of toggle
and visible
,
Parent
, 执行run 方法中的initDepMarkers
方法,首次进入,还未收集依赖,ReactiveEffect
中deps
0, jump over.- Execute
this.fn
in the run method, recollect dependencies, and trigger trackEffects:- toggle
dep = {n: 2, w: 0}
,shouldTrack
to true to collect dependencies. dep = {n: 2, w: 0}
,shouldTrack
is true, collecting dependencies.
- toggle
- Enter the
Child
component, execute theinitDepMarkers
method in the run method, enter for the first time, and also collect dependencies, the deps length is 0, skip. - Execute
this.fn
in the run method, re-collect dependencies, and trigger trackEffects:- toggle
dep = {n: 4, w: 0}
,shouldTrack
to true to collect dependencies.
- toggle
In this way, the collection dependency of entering the page for the first time is over, and then we click the button and change toggle
to false:
Parent
: 执行run 方法中的initDepMarkers
方法,Parent
里收集到了两个变量的依赖,w
:- toggle
dep = {n: 0, w: 2}
- visible
dep = {n: 0, w: 2}
- toggle
- Execute
this.fn
in the run method, re-collect dependencies, and trigger trackEffects:- toggle
dep = {n: 2, w: 2}
,shouldTrack
to false,不用
to collect dependencies. - visible
不显示了
, so it is not recollected, or{n: 0, w: 2}
.
- toggle
- 进入
Child
,执行run 方法中的initDepMarkers
方法,之前收集过toggle
,将toggle 的w 做标记,toggle 的dep = {n: 0, w: 4}
. - Execute
this.fn
in the run method, re-collect dependencies, and trigger trackEffects:- The toggle
dep = {n: 4, w: 4}
andshouldTrack
are false, no need to collect dependencies.
- The toggle
Finally found visible
with w
without n
, delete the invalid dependency in finalizeDepMarkers
.
How to trigger dependencies:
In the source code of ref
mentioned at the beginning, you can see that setter
will be called when triggerRefValue
trigger dependencies:
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() } } } }
The ultimate purpose of triggering dependency is actually to execute 依赖
每个的副作用对象
of 副作用函数
, and the side effect function here may be to execute update view, watch data monitoring, calculation attribute, etc. .
🤨🧐 I personally encountered a problem when I looked at the source code again. I don’t know if you have encountered it (the code version I read is relatively new v3.2.37). At first, I also read some source code analysis articles on the Internet and saw a lot of them. effect
this function, let’s take a look at the source code of this method:
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; }
This function looks very simple, create a ReactiveEffect
side effect object, append the parameters passed in by the user to the object, and then call the run
method to collect dependencies, if there is lazy
Configuration will not automatically collect dependencies. Users can actively execute the function packaged by the effect, and they can also collect dependencies correctly.
🤨🧐 But I looked around and found that there is no place in the source code called, so I wondered if I had used it before, and now I have removed it. I went to the commit record to find it, and I found it:
This update changes ReactiveEffect
to a class to avoid creating effect runner
when unnecessary, saving 17% of memory, etc.
The original effect
method includes the current ReactiveEffect
, which is directly referenced in view update rendering, watch and other places, but after the update, it is directly new ReactiveEffect
, and then go to trigger the run
method, do not go effect
, it can be said that the current ReactiveEffect
class is the former 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
}