开篇
我们以一段代码为例子:
<template>
<img src="./logo.png">
<h1>Hello Vue 3!</h1>
<button @click="add">Clicked {{ state.observe }} times.</button>
</template>
<script>
import { ref } from 'vue'
import {computed, reactive} from "@vue/reactivity";
import {onMounted, watchEffect} from "@vue/runtime-core";
export default {
setup() {
const state = reactive({
observe: 0,
other: 2
})
const add = () => {
state.observe++
}
return {
add,
state,
}
}
}
</script>
<style scoped>
img {
width: 200px;
}
h1 {
font-family: Arial, Helvetica, sans-serif;
}
</style>
Effect安装之前的流程

setupComoponet
这里就会执行我们写的setup函数,从源码来看。
这里主要做了几件事情:
-
执行setup函数,setup函数有我们的
reactive()函数让数据成为一个new Proxy。我们的传入reactive函数的参数是这样的,记住为这个参数对象设置了一个new Proxy代理!!const state = reactive({ observe: 0, other: 2 })
-
拿到setup函数返回的结果就是下面这个对象,并且使用new Proxy对他进行代理,然后存储在instance中
{ add, state, }
setupRenderEffect函数
setupRenderEffect函数创建一个effect为后面的响应式开启之路作铺垫。下面试这个函数的源码
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { // create reactive effect for rendering instance.update = effect(function componentEffect() { ... }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }
函数的的第一个马上执行effect函数,传入的参数为一个 componentEffect的函数。第一个参数执行完之后是一个对象,对象的形式是这样的:
{
scheduler: queueJob,
allowRecurse: true,
onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
}
下面来看下这个effect函数做了什么事情,这个是effect函数的源码:
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
//这个effect是reactiveEffect这个函数
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
目前的步骤中不会执行到第一个if语句的内容,我们直接看第二句话const effect = createReactiveEffect(fn, options)下面是这个createReactiveEffect的源码
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
这里做了几件事情:
-
effect是要给叫做reactiveEffect()的函数
-
然后在effect上定义了十分多的属性,其中有一个属性叫做deps。我们马上联想到2.x的deps属性
-
传入参数fn,不要忘记这个参数fn是一个叫做
componentEffect()函数 -
最终把这个effect返回出去
我们回到上面的effect函数。接下来执行的是下面这段代码,看看上面这个options。并灭有这个lazy属性,那么下面就执行执行刚刚返回的effect函数,effect本质上是reactiveEffect函数,下面看看reactiveEffect函数做了什么事情
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
...
if (!options.lazy) {
effect()
}
return effect
}
//effectStack是在本文件上面定义的一个全局变量。是一个数组
const effectStack: ReactiveEffect[] = []
function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
在上面初始化effect属性的时候effect.active是被设置为false的,所以开始的时候不会执行if语句里面的代码。
然后判断effectStack里面是否函数这个effect。effectStack是在本文件上面定义的一个全局变量。是一个数组,初始化是没有任何东西的,那么代码就会执行进入if语句里面。
if语句第一句代码执行 cleanup(effect),cleanup() 的逻辑其实在Vue 2x的源码中也有的,避免依赖的重复收集。
然后,执行enableTracking()和effectStack.push(effect),前者的逻辑很简单,即可以追踪,用于后续触发 track 的判断,:
//trackStack也是本文件定义的一个全局数组
const trackStack: boolean[] = []
function enableTracking() {
trackStack.push(shouldTrack);
shouldTrack = true;
}
然后把这个effct push 进入这个effectStack.那么此时effectStack就有一个元素,是一个effect,effect就是一个叫做reactiveEffect的函数,trackStack也有一个元素,是一个布尔值,他们两个分别是这样的:
effect [ ƒ reactiveEffect() ] trackStack: [true]
做完这些工作之后把effect赋值给一个叫做activeEffect,最后执行fn,并且返回fn的执行结果。不要忘记fn是什么了,fn是一个叫做componentEffect()函数。
现在我们回头看会最初的setupRenderEffect函数的代码看看这个componentEffect()函数的作用。这个函数的的源码如下:
function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
...
const subTree = (instance.subTree = renderComponentRoot(instance))
...
} else {
...
}
}
我们只需要看第一个if语句块里面执行的结果,接下来进入组件的渲染阶段,我们直接看到renderComponentRoot函数。
下面是这个函数的源码:
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx
} = instance
let result
currentRenderingInstance = instance
if (__DEV__) {
accessedAttrs = false
}
try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
result = normalizeVNode(
//在这里就真正触发响应式依赖的收集和2.x类似
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
}
...
return result
}
开头这个函数从instance上解构出十分多的属性,instance是在前面的阶段产生的一个对象。关键我们来看这段代码:
const proxyToUse = withProxy || proxy
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
withProxy 和 proxy都是从instance上解构出来的的。在我们这个例子中他们两个分别是这样的:withProxy是一个null,proxy是一个new Proxy代理
withProxy: null proxy: Proxy
那很显然执行完const proxyToUse = withProxy || proxy这个之后,const proxyToUse = proxy。在本例子中它大概长这样:

然后接下来执行:
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
先执行render!.call这段代码,这段代码就是真正开始进行响应式响应式收集的入口。
这里会读取这个我们在开始定义的响应式数据即(下面代码的)state.observe,原因在于我们在模板中使用了这个state.observe。
但是注意这里其实会触发两个get。为什么?原因在于在开始的时候setup整一个返回的对象即在本列子中(如下)这个对象也设置了一个new Proxy代理。所以我们在模板首先访问的是state。会触发一次get。然后在访问state.observe.触发第二次get。并且在第一个次get的时候会去校验isRef。我们直接跳过第一个get看第二个get,即当我们访问state.observe的时候出发的get函数。
{
add,
state,
}
在开头的setupComoponet函数中,我们已经把这个reactive执行并且为里面的参数对象设置了一个new Proxy代理。
const state = reactive({
observe: 0,
other: 2
})
那么我们读取这个state.observe就会掉入new Proxy的get函数里面。我们现在来看看在reactive函数中为这个参数对象设置的get函数是长什么样子的。
function get(target: Target, key: string | symbol, receiver: object) {
...
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
//Reflect.get保证了Proxy的原生默认行为
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
const keyIsSymbol = isSymbol(key)
if (
keyIsSymbol
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
这个get函数有三个参数,第一个是target就是我们在reactive传入的对象参数(如下),第二个参数key是就是我是使用的observe
{
observe: 0,
other: 2
}
我省略了代码开头的部分if语句内容,因为他们在此时不会被执行。
目前函数第一句话执行判断target是不是要给Array,显然不是返回结果是false,那么自然不会走到下面的if语句。然后执行
const res = Reflect.get(target, key, receiver)。
这里就是读取observe的值,那么读取后res的值为0.
读取完值后,判断下我们的key是不是Symbol。显然返回false。然后进入到了收集依赖最重要的这句话
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
isReadonly在我的代码中会是false。所以会执行track函数。他三个参数,第一个参数就是我们reative的对象。第二个参数是一个TS的枚举类型,本质就是一个字符串’get‘,第三个参数就是我们的key observe。下面看看track的执行内容。
//targetMap是本文件里面开头的一个全局变量,开始的时候没有内容
const targetMap = new WeakMap<any, KeyToDepMap>()
function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
直接跳过开头的判定语句,来到这句话let depsMap = targetMap.get(target)因为开始的时候这个targetMap是没有东西的,然后它尝试去获取这个target对象对应的键值。显示获取不到,所以depsMap 是一个undefined。
然后下面的if语句就触发了,它为这个targetMap set了一个值。键就是target对象,值是一个Map。顺带把这个Map赋值给了depsMap。
那现在targetMap就有东西了,如下:
targetMap
0: {Object => Map(0)}
键 Object就是
{
observe: 0,
other: 2
}
接下来执行这句话let dep = depsMap.get(key)在上面depsMap已经得到了初始化,那么这句话尝试在这个depsMap Map中获取observe这个键的值,由于刚开始的时候这个Map并没有东西。所以dep也是一个undefined。
接下来就进入了if语句给这个dep和depsMap赋值
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
//执行完这句话后,depsMap长这样
depsMap: Map(1) {"observe" => Set(0)}
dep同时变成了一个Set
接下来执行这句话,这句话(如下)就是正式收集依赖
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
还记得这个activeEffect是什么吗。这个activeEffect是一个函数名字叫做reactiveEffect()的函数。大家可以翻到上面去看看。
现在就把这个函数正式收录进入这个dep中。并且将当前 dep 添加到 activeEffect 的 deps 数组中。和2.x十分得相似。2.x代码是这样的:
if (Dep.target) {
dep.depend()
...
}
那么整个track的收集依赖的过程结束了。
最后get函数把访问属性的结果的值返回出去。就完成了这个get函数的过程。
上面介绍完怎么收集依赖。后面的执行过程就不细讲。总结一下整个get的流程(借用别人的一张图片):

感觉其实和2.x的思想有相同的地方。只是一些细节表现上不同。
触发set方法收集依赖的过程
假如我们尝试点击按钮,然这个state.observe进行++。现在就触发了set方法
我看看set函数的代码:
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
第一句话就是重新获取旧的值,然后执行toRaw方法。toRaw方法源码如下,先判断你传入的参数是否有东西。然后继续调用toRaw,但是这次传入的值是从observed['__v_raw']但是显然我们的value只是一个简单的数字并没有这个属性。显然就是直接返回这个值了。
那最后执行完这个toRaw方法后就是简单把值返回一下
// export const enum ReactiveFlags {
// SKIP = '__v_skip',
// IS_REACTIVE = '__v_isReactive',
// IS_READONLY = '__v_isReadonly',
// RAW = '__v_raw'
// }
export function toRaw<T>(observed: T): T {
return (
(observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
)
}
然后执行这段代码:
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
//显然target不是一个数组。那么就执行 hasOwn(target, key)
//下面是hasOwn源码
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
//在target中显然有这个key属性。所以这里返回true
//即hadKey==true
然后接下来继续往下看set这段代码:
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
先进行一个判断。根据刚刚的分析。这里执行结果是true。上面看到hadKey的值为true。那么就进入了else分支trigger(target, TriggerOpTypes.SET, key, value, oldValue)
trigger函数就是派发更新的方法,下面是trigger函数的源码:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
...
}
if (type === TriggerOpTypes.CLEAR) {
...
} else if (key === 'length' && isArray(target)) {
...
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
...
case TriggerOpTypes.DELETE:
...
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
第一句话执行const depsMap = targetMap.get(target)从上面的分析知道targetMap是这样的:

那从Map中获取这个object的键值。所以const depsMap = Map(1) {"observe" => Set(1)}.
然后下面定义了一个set和一个add函数,暂且不看add函数,紧接着进入一系列的判断语句,我们传入的type参数是一个'set'字符串并且我们的key不是'length'。所以最后进入了else分支,裁剪出else分支代码如下:
{
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
...
case TriggerOpTypes.DELETE:
...
//这个case条件等同于case 'set
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
我们的key不是undefined。所以进入if语句,if语句执行add方法,看看add源码:
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
//const effects = new Set<ReactiveEffect>() 在set开头源码定义
if (effect !== activeEffect || effect.options.allowRecurse) {
effects.add(effect)
}
})
}
}
上面传入的参数是depsMap.get(key),这个执行结果就是获取到一个set(set的截图如下)。set里的内容就是function reactiveEffect()就是我们在get收集到的依赖。
//depsMap的样子
0: {"observe" => Set(1)}
//key就是observe

在add方法中,先判断参数是否存在。存在的话就遍历这个Set.并且添加到这个effects的Set中。
接下来进入switch语句,switch语句我们会进入最后那个:
switch (type) {
case TriggerOpTypes.ADD:
...
case TriggerOpTypes.DELETE:
...
//这个case条件等同于case 'set
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
target显示不是一个Map,是一个对象。直接break。
最后的一段代码如下:
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
最后定义了一个run方法。然后遍历effects Set里面的内容。逐个去执行run方法。看看run方法。
在执行run方法之前我们需要回顾一下上面定义的effect(非effects)是个什么东西,因为这个effect就是run方法的参数,同时也是effects的Set里面的元素,它长这样的:
const effect = function reactiveEffect(): unknown {
...
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = {
allowRecurse: true
onTrack: undefined
onTrigger: undefined
scheduler: ƒ queueJob(job)
}
根据它的长相,显然进入第一个分支:
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
effect.options.scheduler是一个叫做queueJob的函数,看看它的源码
//本文件开头定义的一个全局变量
const queue: (SchedulerJob | null)[] = []
export function queueJob(job: SchedulerJob) {
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
queue.push(job)
queueFlush()
}
}
显然这段代码先把effect推入这个数组里面。在执行queueFlush函数,queueFlush函数如下:
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
到这里其实和Vue 2.x已经很相似了。在 Vue 2x 中的 watcher 也是在下一个 tick 中执行,而 Vue 3.0 也是一样。而 flushJobs 中就会对 queue 队列中的 effect() 进行执行。后面就是最后执行准备渲染的逻辑。就不细说了。
把整个set整理为一个简单的流程图:

参考链接:https://segmentfault.com/a/1190000022198316
浙公网安备 33010602011771号