4

Vue3源码分析之compositionApi

 2 years ago
source link: https://segmentfault.com/a/1190000040848951
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Vue3源码分析之compositionApi

TNTWeb - 全称腾讯新闻前端团队,组内小伙伴在Web前端、NodeJS开发、UI设计、移动APP等大前端领域都有所实践和积累。

目前团队主要支持腾讯新闻各业务的前端开发,业务开发之余也积累沉淀了一些前端基础设施,赋能业务提效和产品创新。

团队倡导开源共建,拥有各种技术大牛,团队Github地址:https://github.com/tnfe

本文作者dravenwu

image.png

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

正文在这里,正式开始。

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 = 200
        }
        return {
            state,
            changeCount,
            computedCount,
            computedNumber
        }
    },
    template: `
        <div>
            <h2>init count:<i>{{state.count}}</i></h2>
            <h2>computedCount:<i>{{computedCount}}</i></h2>
            <h2>computedNumber:<i>{{computedNumber}}</i></h2>
            <button @click="changeCount">changeCount</button>
        </div>
    `
})

app.mount('#demo')

上面代码可以看到两次对computed的使用,第一次传递的是一个函数,第二次传递的是一个包含get和set的对象。

computed源码分析

接下来,咱们来看下computed的源码:

// @file packages/reactivity/src/computed.ts
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (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<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    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 = 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 = 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: `
        <div>
            <h2>reactive count:<i>{{state.count}}</i></h2>
            <button @click="changeCount">changeCount</button>
        </div>
    `
})
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: `
        <div>
            <h2>readonly count:<i>{{read.count}}</i></h2>
            <button @click="changeRead">changeRead</button>
        </div>
    `
})

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<T extends object>(
  target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
  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: `
        <div>
            <h2>foo:<i>{{state.foo}}</i></h2>
            <h2>bar:<i>{{state.nested.bar}}</i></h2>
            <button @click="change">change</button>
        </div>
    `
})

app.mount('#demo')

上面代码基本是完全按照官网来写的,不过,试了下效果和官网上的效果不一样,并不是shallow类型,而是对内部的属性也进行了监听,bar的改变也会响应式的反映到dom当中去。也不知道是我姿势不对,还是Vue3的bug。

shallowReactive 源码解读

// @file packages/reactivity/src/reactive.ts
export function shallowReactive<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers
  )
}

上面就是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: `
          <div>
              <h2>foo:<i>{{state.foo}}</i></h2>
              <h2>bar:<i>{{state.nested.bar}}</i></h2>
              <button @click="change">change</button>
          </div>
      `
    })
    
    app.mount('#demo')

    上面代码基本是完全按照官网来写的,foo的改变不被允许,按照官网说明state.nested.bar是允许被改变的,在上面例子中,发现state.nested.bar的值是会改变的,但是不会响应到dom上。

    shallowReadonly 源码解读

    // @file packages/reactivity/src/reactive.ts
    export function shallowReadonly<T extends object>(
    target: T
    ): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
    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))) // false

const bar = reactive({ foo })
console.log(isReactive(bar)) // true
console.log(isReactive(bar.foo)) // false

从上面使用中可以看到,markRaw只对当前对象本身有效,被标记的对象作为属性的时候,大对象bar还是可以进行响应式处理的,但是bar里面的当前被标记的对象foo,还是一个非响应式对象,永远是foo对象本身。

export function markRaw<T extends object>(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<T>(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<any>, collectionHandlers: ProxyHandler<any>) {
    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:

如上图,可以看到,basehandler里面所提供的函数,我们一一来看下。

deleteProperty

// @file packages/reactivity/src/baseHandlers.ts
function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (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获取结果。

    // @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 = (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:

    来看下createInstrumentationGetter的源码,上面图中三个都是调用此方法生成对应的处理对象。

    function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
    const instrumentations = shallow
      ? shallowInstrumentations
      : isReadonly
        ? readonlyInstrumentations
        : mutableInstrumentations
    
    return (
      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中为何这样实现。

接下来我们将开始对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: `
        <div>
            <h2>count:<i>{{count}}</i></h2>
            <h2>number:<i>{{obj.number}}</i></h2>
            <button @click="change">change</button>
        </div>
    `
})
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<T extends object>(
  value: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
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<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, private readonly _shallow = false) {
    this._value = _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 = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
  
export const hasChanged = (value: any, oldValue: any): boolean =>
  value !== oldValue && (value === 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<T>(r: Ref<T> | unknown): r is Ref<T>
    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<T extends object>(
    value: T
    ): T extends Ref ? T : Ref<T>
    export function shallowRef<T>(value: T): Ref<T>
    export function shallowRef<T = any>(): Ref<T | undefined>
    export function shallowRef(value?: unknown) {
    return createRef(value, true)
    }

    shallowRef与ref的调用流程是一样的,不过是多了个参数,导致_shallow为true,就在RefImpl里面调用时,直接返回了当前value,而不会进行到convert函数。

    unRef

    官网解释:如果参数为 ref,则返回内部值,否则返回参数本身。 源码如下:

    export function unref<T>(ref: T): T extends Ref<infer V> ? 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<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return isRef(object[key])
    ? object[key]
    : (new ObjectRefImpl(object, key) as any)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(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<T extends object>(object: T): ToRefs<T> {
  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<T> {
  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
  private readonly _set: ReturnType<CustomRefFactory<T>>['set']

  public readonly __v_isRef = true

  constructor(factory: CustomRefFactory<T>) {
    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<T>(factory: CustomRefFactory<T>): Ref<T> {
  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 = newValue
                    trigger()
                }, delay)
            }
        }
    })
}

app.component('TestComponent', {
    setup(props) {
        return {
            text: useDebouncedRef('hello')
        }
    },
    template: `
        <div>
            <input v-model="text" />
        </div>
    `
})

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<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

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
}

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方法是用来停止当前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),让我们发现它,解决掉它,也是一种进步,也是防止自己踩坑的好方法。

image.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK