Vue3源码分析之compositionApi

TNTWeb - 全称腾讯新闻前端团队,组内小伙伴在Web前端、NodeJS开发、UI设计、移动APP等大前端领域都有所实践和积累。
目前团队主要支持腾讯新闻各业务的前端开发,业务开发之余也积累沉淀了一些前端基础设施,赋能业务提效和产品创新。
团队倡导开源共建,拥有各种技术大牛,团队Github地址:https://github.com/tnfe
本文作者dravenwu
Vue3源码分析之compositionApi
文章图片

本篇文章将会围绕Vue3的另外一个主要的文件夹reactivity来进行讲解,也就是Vue3中对外暴露的compositionApi的部分,,越来越有React Hooks的味道了。reactivity文件夹下面包含多个文件,主要功能在于computed、effect、reactive、ref;其他的文件是为其进行服务的,另外还有一个主入口文件index。reactivity下面对外暴露的所有api可见下图,我们本篇文件会结合使用对这些功能进行源码分析。
Vue3源码分析之compositionApi
文章图片

正文 正文在这里,正式开始。
computed computed的含义与Vue2中的含义是一样的,计算属性;使用方式也是和Vue2中差不多的,有两种使用方式:
computed使用
const {reactive, readonly, computed, ref} = Vue; const app = Vue.createApp({}); app.component('TestComponent', { setup(props) { // reactive const state = reactive({ count: 0, number: 10 }) // computed getter const computedCount = computed(() => { return state.count + 10 }) // computed set get const computedNumber = computed({ get: () => { return state.number + 100 }, set: (value) => { state.number = value - 50 } })const changeCount = function(){ state.count++; computedNumber.value = https://www.it610.com/article/200 } return { state, changeCount, computedCount, computedNumber } }, template: `init count:{{state.count}} computedCount:{{computedCount}} computedNumber:{{computedNumber}} ` })app.mount('#demo')

上面代码可以看到两次对computed的使用,第一次传递的是一个函数,第二次传递的是一个包含get和set的对象。
computed源码分析
接下来,咱们来看下computed的源码:
// @file packages/reactivity/src/computed.ts export function computed( getterOrOptions: ComputedGetter | WritableComputedOptions ) { let getter: ComputedGetter let setter: ComputedSetterif (isFunction(getterOrOptions)) { getter = getterOrOptions setter = __DEV__ ? () => { console.warn('Write operation failed: computed value is readonly') } : NOOP } else { getter = getterOrOptions.get setter = getterOrOptions.set }return new ComputedRefImpl( getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set ) as any }

上面是computed的入口的源码,此处和Vue2中的写法是一样的,都是对参数进行判断,生成getter和setter,这里最后调用的是ComputedRefImpl;
// packages/reactivity/src/computed.ts class ComputedRefImpl { private _value!: T private _dirty = truepublic readonly effect: ReactiveEffectpublic readonly __v_isRef = true; public readonly [ReactiveFlags.IS_READONLY]: booleanconstructor( getter: ComputedGetter, private readonly _setter: ComputedSetter, isReadonly: boolean ) { this.effect = effect(getter, { lazy: true, scheduler: () => { if (!this._dirty) { this._dirty = true trigger(toRaw(this), TriggerOpTypes.SET, 'value') } } })this[ReactiveFlags.IS_READONLY] = isReadonly }get value() { if (this._dirty) { this._value = https://www.it610.com/article/this.effect() this._dirty = false } track(toRaw(this), TrackOpTypes.GET,'value') return this._value }set value(newValue: T) { this._setter(newValue) } }

如上,是ComputedRefImpl的源码。ComputedRefImpl是一个class,内部包含_value、_dirty、effect、__v_isRef、ReactiveFlags.IS_READONLY等属性,还包括constructor和get、set等函数。了解的同学都知道,会首先调用构造函数也就是constructor;调用effect为effect属性赋值,把isReadonly赋值给ReactiveFlags.IS_READONLY属性,关于effect,咱们后面讲这块。此时ComputedRefImpl执行完成。
当获取当前computed的值的时候,如上面使用中computedCount在template中进行获取值的时候,会调用上面class内的get方法,get方法内部调用的是this.effect进行数据的获取,_dirty属性是为了数据的缓存,依赖未发生变化,则不会调用effect,使用之前的value进行返回。track是跟踪当前get调用的轨迹。
当为computed赋值的时候,如上面使用中computedNumber.value = https://www.it610.com/article/200的时候,,会调用上面class内的set方法,set内部还是调用了之前传递进来的函数。
reactive 接下来对reactive的讲解
reactive 使用
reactive官网给的解释是:返回对象的响应式副本。先来看下reactive的使用
const {reactive} = Vue; const app = Vue.createApp({}); app.component('TestComponent', { setup(props) { // reactive const state = reactive({ count: 0 }) const changeCount = function(){ state.count++; } return { state, changeCount } }, template: `reactive count:{{state.count}} ` }) app.mount('#demo')

当点击changeCount的时候,state.count会++,同时映射到h2-dom。
reactive 源码解读
// @file packages/reactivity/src/reactive.ts export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers ) }

如上源码可以看到,如果target有值并且target的[ReactiveFlags.IS_READONLY]属性,也就是__v_isReadonly为true的话,会直接返回当前对象,不做任何处理,后面对state.count的改变也不会映射到dom当中。如果不满足上面条件,则会调用createReactiveObject函数,传递4个参数:
  • target为原始对象;
  • 第二个是isReadonly,为false;
  • 第三个参数mutableHandlers是reactive对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的函数。
关于这个核心的函数,我们待会来进行讲解。
readonly 使用
现在我们来看下Vue3提供给我们的reactivity下面的第二个api:readonly。
官网给出的定义是:获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套 property 也是只读的
const {readonly} = Vue; const app = Vue.createApp({}); app.component('TestComponent', { setup(props) { const read = readonly({count: 1})const changeRead = function(){ read.count++; } return { read, changeRead } }, template: `readonly count:{{read.count}} ` })app.mount('#demo')

上面代码,是readonly的使用,在此试验了一下对readonly返回后的结果read,进行了改变的尝试,发现是改变不了的,属于只读,同时还会打印警告Set operation on key "count" failed: target is readonly.
readonly 源码解读
// @file packages/reactivity/src/reactive.ts export function readonly( target: T ): DeepReadonly> { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers ) }

上面就是readonly的源码入口,与reactive一样,都是调用的createReactiveObject函数:
  • 第一个参数还是target;
  • 第二个是isReadonly,为true;
  • 第三个参数readonlyHandlers是readonly对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的readonly所对应的函数。
shallowReactive 使用
官网文档给的解释:创建一个响应式代理,该代理跟踪其自身 property 的响应性,但不执行嵌套对象的深度响应式转换 (暴露原始值)。来看下shallowReactive的使用
const {shallowReactive} = Vue; const app = Vue.createApp({}); app.component('TestComponent', { setup(props) { const state = shallowReactive({ foo: 1, nested: { bar: 2 } }) const change = function(){ state.foo++ state.nested.bar++ } return { state, change } }, template: `foo:{{state.foo}} bar:{{state.nested.bar}} ` })app.mount('#demo')

上面代码基本是完全按照官网来写的,不过,试了下效果和官网上的效果不一样,并不是shallow类型,而是对内部的属性也进行了监听,bar的改变也会响应式的反映到dom当中去。也不知道是我姿势不对,还是Vue3的bug。
shallowReactive 源码解读
// @file packages/reactivity/src/reactive.ts export function shallowReactive(target: T): T { return createReactiveObject( target, false, shallowReactiveHandlers, shallowCollectionHandlers ) }

【Vue3源码分析之compositionApi】上面就是shallowReactive的源码入口,与reactive和readonly一样,都是调用的createReactiveObject函数:
  • 第一个参数还是target;
  • 第二个是isReadonly,为false;
  • 第三个参数shallowReactiveHandlers是shallowReactive对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的shallowReactive所对应的函数。
    shallowReadonly 使用
    官网给出的解释:创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。来看下使用:
    const {shallowReadonly} = Vue; const app = Vue.createApp({}); app.component('TestComponent', { setup(props) { const state = shallowReadonly({ foo: 1, nested: { bar: 2 } }) const change = function(){ state.foo++ state.nested.bar++ } return { state, change } }, template: `foo:{{state.foo}} bar:{{state.nested.bar}} ` })app.mount('#demo')

    上面代码基本是完全按照官网来写的,foo的改变不被允许,按照官网说明state.nested.bar是允许被改变的,在上面例子中,发现state.nested.bar的值是会改变的,但是不会响应到dom上。
    shallowReadonly 源码解读
    // @file packages/reactivity/src/reactive.ts export function shallowReadonly( target: T ): Readonly<{ [K in keyof T]: UnwrapNestedRefs }> { return createReactiveObject( target, true, shallowReadonlyHandlers, readonlyCollectionHandlers ) }

    上面就是shallowReadonly的源码入口,与reactive和readonly一样,都是调用的createReactiveObject函数:
  • 第一个参数还是target;
  • 第二个是isReadonly,为true;
  • 第三个参数shallowReadonlyHandlers是shallowReadonly对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的shallowReadonly所对应的函数。
isReadonly
isReadonly:检查对象是否是由readonly创建的只读代理。
使用如下:
const only = readonly({ count: 1 }) isOnly = isReadonly(only) // true

源码如下:
export function isReadonly(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.IS_READONLY]) }

ReactiveFlags.IS_READONLY是一个字符串,值为:__v_isReadonly,挂到对象上面就是属性,判断当前对象的__v_isReadonly属性是否是true,并返回。
isReactive
isReadonly:检查对象是否是 reactive创建的响应式 proxy。
使用如下:
const tive = reactive({ count: 1 }) isOnly = isReactive(tive) // true

源码如下:
export function isReactive(value: unknown): boolean { if (isReadonly(value)) { return isReactive((value as Target)[ReactiveFlags.RAW]) } return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE]) }

首先调用了上面提到的isReadonly方法判断是否是readonly创建的对象;如果是的话,则进一步使用当前对象的RAW属性调用isReactive来判断;如果不是则判断__v_isReactive是否为true;返回判断的结果。
ReactiveFlags.RAW是一个字符串,值为:__v_raw,挂到对象上面就是属性,也就是原始对象,判断是否是reactive代理的原始对象;
ReactiveFlags.IS_READONLY也是一个字符串,值为:__v_isReactive,挂到对象上面就是属性
isProxy
isProxy:检查对象是否是reactive 或 readonly创建的代理。
使用如下:
const tive = reactive({ count: 1 }) const only = readonly({ count: 1 }) is1 = isProxy(tive) // true is2 = isProxy(only) // true

源码如下:
export function isProxy(value: unknown): boolean { return isReactive(value) || isReadonly(value) }

调用上面提到的isReadonly方法和isReactive判断是否是proxy的对象。
markRaw
markRaw:标记一个对象,使其永远不会转换为代理。返回对象本身。
使用如下:
const foo = markRaw({}) console.log(isReactive(reactive(foo))) // falseconst bar = reactive({ foo }) console.log(isReactive(bar)) // true console.log(isReactive(bar.foo)) // false

从上面使用中可以看到,markRaw只对当前对象本身有效,被标记的对象作为属性的时候,大对象bar还是可以进行响应式处理的,但是bar里面的当前被标记的对象foo,还是一个非响应式对象,永远是foo对象本身。
export function markRaw(value: T): T { def(value, ReactiveFlags.SKIP, true) return value } export const def = (obj: object, key: string | symbol, value: any) => { Object.defineProperty(obj, key, { configurable: true, enumerable: false, value }) }

上面可以看到markRaw的源码,就是给要标记的对象增加了一个属性(ReactiveFlags.SKIP, 也就是__v_skip),并赋值true,所有要给当前对象进行响应式处理的时候,都会被忽略。
toRaw
toRaw:返回 reactive 或 readonly 代理的原始对象。这是一个转义口,可用于临时读取而不会引起代理访问/跟踪开销,也可用于写入而不会触发更改。不建议保留对原始对象的持久引用。请谨慎使用。
既然Vue让咱们谨慎使用,咱们还是在可以不使用的的时候不使用的好,这个就是把代理的原始对象进行返回。
const obj = { project: 'reactive' } const reactiveObj = reactive(obj)console.log(toRaw(reactiveObj) === obj) // true

源码:
export function toRaw(observed: T): T { return ( (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed ) }

返回当前对象的ReactiveFlags.RAW(也就是__v_raw)属性指向的对象,也就是对象本身,关于在什么地方给ReactiveFlags.RAW赋值的,后面会看到这部分。
createReactiveObject
上面的reactive、readonly、shallowReactive、shallowReadonly,都用到了createReactiveObject函数,现在咱们来看看这个函数的源码。
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) { return target } const proxyMap = isReadonly ? readonlyMap : reactiveMap const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy }

源码解读:
  • 首先,target得是一个对象,不是对象的话,直接返回当前值;当然Vue3也提供了对值的响应式的方法:ref,后面讲。
  • 判断有原始对象且,不是只读或者不是响应式的对象,则返回当前对象,这个地方真TM绕。
  • 根据是否是isReadonly,获取到代理存储的map,,如果之前代理过,已经存在,则把之前代理过的proxy返回。
  • 判断target的类型,getTargetType内部会对target对象进行判断,返回是common、collection或者invalid;如果不可用类型(invalid),则直接返回当前对象。此处会用到上面讲到的__v_skip。可用的类型就两个,一个是common,一个是collection;
  • 接下来就是没有代理过,获取代理的过程。new Proxy,如果是collection则使用传递进来的collectionHandlers,否则(也就是common)则使用baseHandlers;
  • 代理存储所使用的map,存储当前proxy;
  • 返回当前proxy。
通过上面reactive、readonly、shallowReactive、shallowReadonly的讲解,可以看到对于集合和common类型,提供了几种不同的处理对象,对象中所包含的内容也是不一样的,咱们在这里来对比着看下:
basehandler:
Vue3源码分析之compositionApi
文章图片

如上图,可以看到,basehandler里面所提供的函数,我们一一来看下。
deleteProperty
// @file packages/reactivity/src/baseHandlers.ts function deleteProperty(target: object, key: string | symbol): boolean { const hadKey = hasOwn(target, key) const oldValue = https://www.it610.com/article/(target as any)[key] const result = Reflect.deleteProperty(target, key) if (result && hadKey) { trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result }

  • 获取当前对象是否有当前key => hadKey;
  • 获取到当前的value存储为oldValue;
  • 调用Reflect.deleteProperty进行对当前对象target删除当前key的操作,返回结果为是否删除成功->result;
  • 删除成功,并且有当前key,则调用trigger,触发effect。
  • 返回删除是否成功的结果。
    ownKeys
    // @file packages/reactivity/src/baseHandlers.ts function ownKeys(target: object): (string | number | symbol)[] { track(target, TrackOpTypes.ITERATE, ITERATE_KEY) return Reflect.ownKeys(target) }

    这个函数很简单了就,获取target对象自己的属性key;跟踪获取的轨迹,然后调用Reflect.ownKeys获取结果。
    has
    // @file packages/reactivity/src/baseHandlers.ts function has(target: object, key: string | symbol): boolean { const result = Reflect.has(target, key) if (!isSymbol(key) || !builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key) } return result }

  • 调用Reflect.has获取当前对象是否有当前key;
  • 不是Symbol类型的key,或者不是Symbol本身的属性,调用track跟踪has调用的轨迹。
  • 返回结果,result。
    createSetter
    function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = https://www.it610.com/article/(target as any)[key] if (!shallow) { value = toRaw(value) if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else {}const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) 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 } }

    函数工厂,根据shallow生成set函数。set函数接受4个参数:target为目标对象;key为设置的属性;value为设置的值;receiver为Reflect的额外参数(如果遇到 setter,receiver则为setter调用时的this值)。
  • 首先获取到oldValue;
  • 如果非浅响应式,也就是正式情况的时候,获取到value的原始对象并赋值给value,如果target对象不是数组且oldValue是ref类型的响应式类型,并且新value不是ref类型的响应式,为oldValue赋值(ref类型的响应式对象,需要为对象的value赋值)。
  • 下面也就是深度响应式的代码逻辑了。
  • 如果是数组并且key是数字类型的,则直接判断下标,否则调用hasOwn获取,是否包含当前key => hadKey;
  • 调用Reflect.set进行设置值;
  • 如果目标对象和receiver的原始对象相等,则hadKey,调用trigger触发add操作;否则,调用trigger触发set操作。
  • 把set处理的结果返回,result。
    createGetter
    function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? readonlyMap : reactiveMap).get(target) ) { return target }const targetIsArray = isArray(target) if (targetIsArray && hasOwn(arrayInstrumentations, key)) { 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)) { const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res }if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) }return res } }

    函数工厂,根据shallow生成get函数。get函数接受3个参数:target为目标对象;key为设置的属性;receiver为Reflect的额外参数(如果遇到 setter,receiver则为setter调用时的this值)。
  • 如果key是__v_isReactive,则直接返回!isReadonly,通过上面的图可得知,reactive相关的调用createGetter,传递的是false,也就是会直接返回true;
  • 如果key是__v_isReadonly,则直接返回isReadonly,同样的通过上面的图可以得知,readonly相关的调用createGetter,传递的是true,也就是会直接返回true;
  • 如果key是__v_raw并且receiver等于proxyMap存储的target对象的proxy,也就是获取原始对象,则直接返回target;
  • 如果是数组的话,则会走自定义的方法,arrayInstrumentations;arrayInstrumentations是和Vue2中对数组的改写是一样的逻辑;
  • 下面会对key进行判断,如果Symbol对象并且是Set里面自定义的方法;或者key为__proto__或__v_isRef,则直接把Reflect.get(target, key, receiver)获取到的值直接返回;
  • 如果非只读情况下,调用track跟踪get轨迹;
  • 如果是shallow,非深度响应式,也是直接把上面获取到的res直接返回;
  • 如果是ref对象,则会调用.value获取值进行返回;
  • 剩下的情况下,如果得到的res是个对象,则根据isReadonly调用readonly或reactive获取值,进行返回;
  • 最后有一个res保底返回;
    collectionHandler:Vue3源码分析之compositionApi
    文章图片

    来看下createInstrumentationGetter的源码,上面图中三个都是调用此方法生成对应的处理对象。
    function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { const instrumentations = shallow ? shallowInstrumentations : isReadonly ? readonlyInstrumentations : mutableInstrumentationsreturn ( target: CollectionTypes, key: string | symbol, receiver: CollectionTypes ) => { if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if (key === ReactiveFlags.RAW) { return target }return Reflect.get( hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver ) } }

    上面createInstrumentationGetter函数根据isReadonly和shallow返回一个函数;
  • 根据isReadonly和shallow,获取到对应的instrumentations;此对象包含了对集合操作的所有方法;
  • 然后就把下面的函数进行了返回,createInstrumentationGetter相当于是一个闭包;
  • 返回的函数里面在执行调用的时候,会先对key进行判断,如果访问的是Vue的私有变量,也就是上面的__v_isReactive、__v_isReadonly、__v_raw等,会直接给出不同的返回;
  • 如果不是Vue的上面的三个私有变量,则会调用Reflect.get来获取对象的值;instrumentations,也就是重写的方法集合,不在此集合里面的,则会直接调用target自己的方法。
reactive完结
至此,reactive文件里面的这些方法咱们都梳理了一遍,简单的做了源码的分析和解读,感兴趣的读者可以深入源码研究下Vue中为何这样实现。
Refs 接下来我们将开始对ref及其附属方法的使用和讲解。
ref
首先,咱们对ref进行讲解,官网给出的解释是:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。
先来看下ref的使用。
const {ref} = Vue; const app = Vue.createApp({}); app.component('TestComponent', { setup(props) { const count = ref(0) const obj = ref({number: 10}) const change = function(){ count.value++; obj.value.number++ }return { count, obj, change } }, template: `count:{{count}} number:{{obj.number}} ` }) app.mount('#demo')

上面是ref的使用,可以看到ref接受的是一个普通类型的值或者是一个对象,Vue官网给出的例子是不包含传递对象的,其实这也就是Vue不提倡使用ref来响应式一个对象,如果是对对象的响应式,Vue还是提倡使用上面reactive来实现;第二个需要注意点在于template中对ref对象的引用是不需要加上value属性来获取值,如上ref对象count在js中需要count.value,但是在template种只需count即可;
来看下ref的源码实现
// @file packages/reactivity/src/ref.ts export function ref( value: T ): T extends Ref ? T : Ref> export function ref(value: T): Ref> export function ref(): Ref export function ref(value?: unknown) { return createRef(value) }function createRef(rawValue: unknown, shallow = false) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) }class RefImpl { private _value: Tpublic readonly __v_isRef = trueconstructor(private _rawValue: T, private readonly _shallow = false) { this._value = https://www.it610.com/article/_shallow ? _rawValue : convert(_rawValue) }get value() { track(toRaw(this), TrackOpTypes.GET,'value') return this._value }set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = https://www.it610.com/article/newVal this._value = this._shallow ? newVal : convert(newVal) trigger(toRaw(this), TriggerOpTypes.SET,'value', newVal) } } }const convert = (val: T): T => isObject(val) ? reactive(val) : valexport const hasChanged = (value: any, oldValue: any): boolean => value !== oldValue && (value =https://www.it610.com/article/== value || oldValue === oldValue)

上面是按照运行轨迹来看的Vue3中ref的源码部分;根据ref的声明可以看到ref接受任何参数,返回类型为Ref对象,内部调用的是createRef;
  • createRef函数内部会先对value进行判断,如果已经是ref对象的话,直接返回当前value,否则就调用new RefImpl来生成ref对象进行返回。
  • constructor里面会判断是否是浅响应_shallow,浅的话,直接返回_rawValue,否则调用convert来返回;可以看到除了私有属性_value外,还有一个__v_isRef的只读属性为true;
  • convert里面则会对val进行判断了,对象则调用reactive,否则直接返回val,此处也就可以看到上面ref也可以接受对象作为参数的缘由了。
  • get里面会跟踪调用轨迹,track;返回当前value;
  • set里面会调用hasChanged判断是否发生了改变,此处会对NaN进行check,因为NaN与啥都不相等;设置新的值,同时调用trigger触发set调用。
    isRef
    isRef很明显就是判断是否是ref对象的方法。使用如下:
    const count = ref(0) const is = isRef(count) const is2 = isRef(10)

    来看下源码,源码也很简单:
    export function isRef(r: Ref | unknown): r is Ref export function isRef(r: any): r is Ref { return Boolean(r && r.__v_isRef === true) }

    此处就使用到了RefImpl里面那个只读属性了,判断__v_isRef是否为true就可以了。
    shallowRef
    官网给出的解释:创建一个 ref,它跟踪自己的 .value 更改,但不会使其值成为响应式的。
    shallowRef的源码如下:
    export function shallowRef( value: T ): T extends Ref ? T : Ref export function shallowRef(value: T): Ref export function shallowRef(): Ref export function shallowRef(value?: unknown) { return createRef(value, true) }

    shallowRef与ref的调用流程是一样的,不过是多了个参数,导致_shallow为true,就在RefImpl里面调用时,直接返回了当前value,而不会进行到convert函数。
    unRef
    官网解释:如果参数为 ref,则返回内部值,否则返回参数本身。 源码如下:
    export function unref(ref: T): T extends Ref ? V : T { return isRef(ref) ? (ref.value as any) : ref }

    确实如官网所说,就一行代码,ref对象则返回其value,否则直接返回ref。
    triggerRef
    官网给出的解释:手动执行与 shallowRef 关联的任何效果。 ,比较模糊,通俗点就是手动触发一次effect的调用;
    看下使用:
    const count = ref(0) const change = function(){ count.value++; triggerRef(count) } const shallow = shallowRef({ greet: 'Hello, world' }) watchEffect(() => { console.log(count.value) console.log(shallow.value.greet) }) shallow.value.greet = 'Hello, universe'

    源码如下:
    export function triggerRef(ref: Ref) { trigger(ref, TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0) }

toRef
官网给出的解释是:可以用来为源响应式对象上的 property 属性创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。 简单描述就是为对象的一个属性增加一个引用,这个引用可以随意使用,响应式不变。来看下源码:
export function toRef( object: T, key: K ): Ref { return isRef(object[key]) ? object[key] : (new ObjectRefImpl(object, key) as any) }class ObjectRefImpl { public readonly __v_isRef = trueconstructor(private readonly _object: T, private readonly _key: K) {}get value() { return this._object[this._key] }set value(newVal) { this._object[this._key] = newVal } }

这部分的代码比较简单,也比较容易读懂,和上面RefImpl一样的是都增加了一个只读的__v_isRef属性。
toRefs
官网对toRefs给出的解释是:将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。 通俗点描述就是把响应式对象的每个属性,都变成ref对象。来看下源码:
export function toRefs(object: T): ToRefs { if (__DEV__ && !isProxy(object)) { console.warn(`toRefs() expects a reactive object but received a plain one.`) } const ret: any = isArray(object) ? new Array(object.length) : {} for (const key in object) { ret[key] = toRef(object, key) } return ret }

这里尤为要求是一个响应式的对象,非响应式对象还会打印警告。for循环调用上面讲到的toRef函数,把对象里面的每个属性都变为ref对象。
customRef
官网给出的解释是:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数 来看下customRef的源码:
class CustomRefImpl { private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set']public readonly __v_isRef = trueconstructor(factory: CustomRefFactory) { const { get, set } = factory( () => track(this, TrackOpTypes.GET, 'value'), () => trigger(this, TriggerOpTypes.SET, 'value') ) this._get = get this._set = set }get value() { return this._get() }set value(newVal) { this._set(newVal) } }export function customRef(factory: CustomRefFactory): Ref { return new CustomRefImpl(factory) as any }

相对应的,使用的时候,接受的是一个factory,factory是一个函数,参数为track和trigger,同时factory的返回须包含两个函数,一个为get,一个为set。track就是effect的track,trigger也是effect的trigger;来看下使用:
const {customRef} = Vue; const app = Vue.createApp({}); function useDebouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => { return { get() { track() return value }, set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = https://www.it610.com/article/newValue trigger() }, delay) } } }) }app.component('TestComponent', { setup(props) { return { text: useDebouncedRef('hello') } }, template: `` })app.mount('#demo')

上面是customRef的使用的例子,和官网的例子是一样的,能够实现防抖,同时也能够显式的控制什么时候调用track来跟踪和什么时候来调用trigger来触发改变。
Refs完结
上面我们对refs里面的几种方法做了源码的解读和部分的api是如何使用的,关于Vue3为何提供了两种响应式的方案:reactive和Refs,这其实就和代码风格有关系了,有的同学习惯使用对象,而有的同学习惯使用变量,Vue3为这两种方案都提供了,想用哪个用哪个。
effect 其实可以看到上面好多地方都用到了这个方法,包括effect、track、trigger等都是effect里面提供的方法,effect里面提供的方法属于Vue的内部方法,不对外暴露。下面我们挨个来看看这部分的源码,
isEffect
isEffect是为判断是否是有副作用的函数。来看下源码:
export function isEffect(fn: any): fn is ReactiveEffect { return fn && fn._isEffect === true }

可以看到上面的判断,就是对函数的_isEffect进行判断,非常简单。
effect
effect作为Vue2和Vue3中核心的部分,都有这个的概念,重中之重,来看下这部分的源码:
export function effect( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }function createReactiveEffect( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect { 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 }let shouldTrack = true const trackStack: boolean[] = []export function pauseTracking() { trackStack.push(shouldTrack) shouldTrack = false }export function enableTracking() { trackStack.push(shouldTrack) shouldTrack = true }export function resetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last }

如上,就是effect部分的源码。顺着执行顺序一步步走下来。
  • 调用方调用effect函数,参数为函数fn,options(默认为{});
  • 判断是否已经是effect过的函数,如果是的话,则直接把原函数返回。
  • 调用createReactiveEffect生成当前fn对应的effect函数,把上面的参数fn和options直接传进去;
  • 判断options里面的lazy是否是false,如果不是懒处理,就直接调用下对应的effect函数;
  • 返回生成的effect函数。
接下来看下createReactiveEffect函数的调用过程。
  • 为effect函数赋值,暂时先不考虑reactiveEffect函数内部到底干了什么,只要明白创建了个函数,并赋值给了effect变量。
  • 然后为effect函数添加属性:id, _isEffect, active, raw, deps, options
  • 把effect返回了。
下面我们回到上面非lazy情况下,调用effect,此时就会执行reactiveEffect函数。
  • 首先判断了是否是active状态,如果不是,说明当前effect函数已经处于失效状态,直接返回return options.scheduler ? undefined : fn()
  • 查看调用栈effectStack里面是否有当前effect,如果无当前effect,接着执行下面的代码。
  • 先调用cleanup,把当前所有依赖此effect的全部清掉,deps是个数组,元素为Set,Set里面放的则是ReactiveEffect,也就是effect;
  • 把当前effect入栈,并将当前effect置为当前活跃effect->activeEffect;后执行fn函数;
  • finally,把effect出栈,执行完成了,把activeEffect还原到之前的状态;
  • 其中涉及到调用轨迹栈的记录。和shouldTrack是否需要跟踪轨迹的处理。
stop
stop方法是用来停止当前effect的。属于Vue3内部方法,来看下源码:
export function stop(effect: ReactiveEffect) { if (effect.active) { cleanup(effect) if (effect.options.onStop) { effect.options.onStop() } effect.active = false } }

  • 调用cleanup清空掉,和上面调用cleanup一样。
  • 执行当前effect.options.onnStop钩子函数。
  • 把当前effect的active状态置为false。
结言 本篇文章主要围绕reactivity文件夹里面提供给大家使用的compositionApi的部分进行了相对应的使用和源码解读,大家感兴趣的还是去读下这部分的源码,毕竟这是Vue3新出的功能,越来越react的一步......
欢迎大家一起来讨论Vue3,刚出的版本,带来了新的同时,肯定也会带着意想不到的惊喜(bug),让我们发现它,解决掉它,也是一种进步,也是防止自己踩坑的好方法。
Vue3源码分析之compositionApi
文章图片

    推荐阅读