Vue3原始碼分析之compositionApi

TNTWEB發表於2021-10-26

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獲取結果。

    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 = (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中為何這樣實現。

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: `
        <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

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

相關文章