开篇
我们以一段代码为例子:
<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