Vue3核心源碼解析 (二) : 響應式原理

響應式reactivity是Vue 3相對于Vue 2改動比較大的一個模塊,也是性能提升最多的一個模塊。其核心改變是,采用了ES 6的Proxy API來代替Vue 2中的Object.defineProperty方法來實現響應式。那么什么是Proxy API呢,Vue 3的響應式又是如何實現的?

1. Proxy API

Proxy API對應的Proxy對象是ES 6就已引入的一個原生對象,用于定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數調用等)。
從字面意思來理解,Proxy對象是目標對象的一個代理器,任何對目標對象的操作(實例化,添加/刪除/修改屬性等)都必須通過該代理器。因此,我們可以對來自外界的所有操作都進行攔截、過濾、修改等操作。
基于Proxy的這些特性常用于:
· 創建一個“響應式”的對象,例如Vue 3.0中的reactive方法。
· 創建可隔離的JavaScript“沙箱”。沙箱參考
定義proxy 的基本語法

  1. new Proxy
const p = new Proxy(target, handler)

target參數表示要使用Proxy包裝的目標對象(可以是任何類型的對象,包括原生數組、函數,甚至另一個代理);handler參數表示以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理p的行為。

  1. Proxy.revocable
const { proxy,revoke } = Proxy.revocable(target, handler)

該方法的返回值是一個對象,其結構為:{"proxy": proxy, "revoke": revoke}。其中,proxy表示新生成的代理對象本身,和用一般方式new Proxy(target, handler)創建的代理對象沒什么不同,只是它可以被撤銷掉;revoke表示撤銷方法,調用的時候不需要加任何參數就可以撤銷掉和它一起生成的那個代理對象。一旦某個代理對象被撤銷,它將變得幾乎完全不可調用,在它身上執行任何的可代理操作都會拋出TypeError異常。事例如下

let student = { name: 'zhagnsan', sex: 'male' }
let handler = {
    get: (target, key, receiver) => {
        console.info('proxy get')
        return key in target ? target[key] : undefined
    }
}
let { proxy, revoke } = Proxy.revocable(student,handler)

proxy.name //zhagnsan
revoke() //撤銷
//此時
proxy.name //proxy2.html:37 Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
revoke()后proxy

Proxy共有接近13個handler,也可以稱為鉤子,它們分別是:
· handler.getPrototypeOf():在讀取代理對象的原型時觸發該操作,比如在執行Object.getPrototypeOf(proxy)時。
· handler.setPrototypeOf():在設置代理對象的原型時觸發該操作,比如在執行Object.setPrototypeOf(proxy, null)時。
· handler.isExtensible():在判斷一個代理對象是否可擴展時觸發該操作,比如在執行Object.isExtensible(proxy)時。
· handler.preventExtensions():在讓一個代理對象不可擴展時觸發該操作,比如在執行Object.preventExtensions(proxy)時。
· handler.getOwnPropertyDescriptor():在獲取代理對象某個屬性的描述時觸發該操作,比如在執行Object.getOwnPropertyDescriptor(proxy, "foo")時。
· handler.defineProperty():在定義代理對象某個屬性的描述時觸發該操作,比如在執行Object.defineProperty(proxy, "foo", {})時。
· handler.has():在判斷代理對象是否擁有某個屬性時觸發該操作,比如在執行"foo" in proxy時。
· handler.get():在讀取代理對象的某個屬性時觸發該操作,比如在執行proxy.foo時。
· handler.set():在給代理對象的某個屬性賦值時觸發該操作,比如在執行行proxy.foo = 1時。
· handler.deleteProperty():在刪除代理對象的某個屬性時觸發該操作,即使用delete運算符,比如在執行delete proxy.foo時。
· handler.ownKeys():當執行Object.getOwnPropertyNames(proxy)和Object.getOwnProperty Symbols(proxy)時觸發。
· handler.apply():當代理對象是一個function函數,調用apply()方法時觸發,比如proxy.apply()。
· handler.construct():當代理對象是一個function函數,通過new關鍵字實例化時觸發,比如new proxy()。
結合這些handler,我們可以實現一些針對對象的限制操作; 禁止刪除和修改對象的某個屬性,代碼如下:

let human = {
    name: 'jack',
    age: 20
}
let handler = {
    get: (obj, key,receiver) => {
        console.info('proxy get')
        return key in obj ? obj[key] : undefined
    },
    set: (obj, key, value, receiver) => {
        console.info('proxy set',`receiver==proxy1:${receiver==proxy1}`,`receiver==male:${receiver==male}`)
        if (key == 'name') {
            throw new Error(`can not change property ${key}`)
        }
        obj[key] = value
        return true
    },
    deleteProperty: (obj, key,receiver) => {
        console.info('proxy delete');
        if (key == 'name') {
            throw new Error(`can not delete property ${key}`)
            delete obj[key]
            return true
        }
    }
}
let proxy1 = new Proxy(human, handler)
//賦值name屬性報錯
proxy1.name = 100;  //Uncaught Error: can not change property name at Object.set
// 刪除name屬性報錯
delete proxy1.name;  //Uncaught Error: can not delete property name at Object.deleteProperty

上面的代碼中,set方法多了一個receiver參數,這個參數通常是Proxy本身(即p),但一種場景除外:當有一段代碼執行male.moustache=true時,male不是一個proxy,且自身不含moustache屬性,但是它的原型鏈上有一個proxy,那么那個proxy的handler中的set方法會被調用,而此時male會作為receiver參數傳進來,如下事例:

let male = {}
Object.setPrototypeOf(male,proxy1)

//觸發handler的set方法
proxy1.age = 30; //set方法輸出 proxy set receiver==proxy1:true receiver==male:false
male.moustache = true; //set方法輸出 proxy set receiver==proxy1:false receiver==male:true

Proxy也能監聽到數組變化,代碼如下:

let arr = [1]
let handler = {
    set: (target, key, value, receiver) => {
       console.info('Proxy set')
       return Reflect.set(target,key,value);
    }
}
let p = new Proxy(arr,handler);
p.push(2);
console.info(p);

Reflect.set()用于修改數組的值,返回布爾類型,也可以用在修改數組原型鏈上的方法的場景,相當于target[key] = value。

2. Proxy和響應式對象reactive

Vue 3中使用響應式對象的方法如下

import {ref,reactive} from 'vue'
setup(){
   const name = ref('test')
   const state = reactive({
     list: []
   })
   return {name,state}
}

在Vue 3中,組合式API中經常會使用創建響應式對象的方法ref/reactive,其內部就是利用Proxy API來實現的,特別是借助handler的set方法可以實現雙向數據綁定相關的邏輯,這對于Vue 2中的Object.defineProperty()是很大的改變,主要提升如下:
· Object.defineProperty()只能單一地監聽已有屬性的修改或者變化,無法檢測到對象屬性的新增或刪除(Vue 2中采用$set()方法來解決),而Proxy則可以輕松實現。
· Object.defineProperty()無法監聽響應式數據類型是數組的變化(主要是數組長度的變化,Vue 2中采用重寫數組相關方法并添加鉤子來解決),而Proxy則可以輕松實現。
正是由于Proxy的特性,在原本使用Object.defineProperty()需要很復雜的方式才能實現的上面兩種能力,在Proxy無須任何配置,利用其原生的特性就可以輕松實現。

3. ref()方法運行原理

在Vue 3的源碼中,所有關于響應式的代碼都在core/package/reactivity下,其中reactivity/src/index.ts中暴露了所有可以使用的方法。我們以常用的ref()方法為例,來看看Vue 3是如何利用Proxy的。
ref()方法的主要邏輯在reactivity/src/ref.ts中,其代碼如下:

//入口文件
export function ref(value?: unknown) {
  return createRef(value, false)
}
// rawValue表示原始對象,shallow表示是否遞歸
// 如果本身已經是ref對象,則直接返回
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  // 創建一個新的RefImpl對象
  return new RefImpl(rawValue, shallow)
}

createRef這個方法接收的第二個參數是shallow,表示是否是遞歸監聽響應式(shallow = true表示淺層非遞歸,淺層 ref 的內部值將會原樣存儲和暴露,并且不會被深層遞歸地轉為響應式),這個和另一個響應式方法shallowRef()是對應的。在RefImpl構造函數中,有一個_value 屬性,這個屬性是由toReactive()方法返回的,toReactive()方法則在reactivity/src/reactive.ts文件中,代碼如下:

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
      // 如果是遞歸,則調用toReactive    
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

在reactive.ts中,開始真正創建一個響應式對象,代碼如下:

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target, //原始對象
    false, //是否readonly 
    mutableHandlers, //proxy的handler對象的baseHandlers(get,set,has,deleteProperty,ownKeys)
    mutableCollectionHandlers, // proxy的handler對象collectionHandlers (get: /*#__PURE__*/ createInstrumentationGetter(false, false))
    reactiveMap // proxy對象映射
  )
}

再看下function createReactiveObject 的源碼:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {  //如果不是object(是null,undefined),則不能創建響應式
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
 // 如果已經是proxy對象或者只讀,則直接返回
  if ( 
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy ,已經創建過也直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  //只有復合類型的target才能被創建響應式
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
 // 調用Proxy Api
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  //標記該對象已經創建響應式
  proxyMap.set(target, proxy)
  return proxy
}

createReactiveObject()方法傳遞了兩種handler,分別是baseHandlers和collectionHandlers。如果target的類型是Map、Set、WeakMap、WeakSet,這些特殊對象則會使用collectionHandlers;如果target的類型是Object、Array,則會使用baseHandlers;如果是一個原始對象,則不會創建Proxy對象,reactiveMap會存儲所有響應式對象的映射關系,用來避免同一個對象重復創建響應式。
createReactiveObject 主要邏輯為:

  • 防止只讀和重復創建響應式。
  • 根據不同的target類型選擇不同的handler。
  • 創建Proxy對象。
    我們在看下mutableHandlers的源碼,具體要實現哪幾個handler (packages\reactivity\src\baseHandlers.ts)
const get = /*#__PURE__*/ createGetter()

const set = /*#__PURE__*/ createSetter()

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

以handler.get為例,我們看下具體實現邏輯,當讀取對象的某個屬性時候就為調用get()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) { // 如果訪問對象的key是__v_isReactive, 則直接返回常量
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) { // 如果訪問對象的key是  __v_isReadonly,則直接返回常量
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) { // 如果訪問對象的key是  __v_isShallow,則直接返回常量
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {// 如果訪問對象的key是__v_raw,或者原始對象,只讀對象等直接返回target
      return target
    }
 // 如果target是數組類型
    const targetIsArray = isArray(target)

    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {  // 訪問的key值是數組的原生方法,那么直接返回調用結果
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }
    //反射求值
    const res = Reflect.get(target, key, receiver)
   // 判斷訪問的key是否是Symbol或者不需要響應式的key,例如__proto__,__v_isRef,__isVue
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
// 收集響應式,為了后面的effect方法可以檢測到
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
// 如果是非遞歸綁定,則直接返回結果
    if (shallow) {
      return res
    }
// 如果結果已經是響應式的,則先判斷類型,再返回
    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
 // 如果當前key的結果也是一個對象,那么就要遞歸調用reactive方法對該對象再次執行響應式綁定邏輯
    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
  }
}

createGetter 實現比較復雜,主要邏輯如下:

  • 對于handler.get方法來說,最終都會返回當前對象對應key的結果,即obj[key],所以這段代碼最終會return結果。
  • 對于非響應式key、只讀key等,直接返回對應的結果。
  • 對于數組類型的target,key值如果是原型上的方法,例如includes、push、pop等,則采用Reflect.get直接返回。
  • 在effect添加收集監聽track,為響應式監聽服務。
  • 當前key對應的結果是一個對象時,為了保證set方法能夠被觸發,需要循環遞歸地對這個對象進行響應式綁定,即遞歸調用reactive()方法。

我們再看下set的源碼:

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,// 即將被設置的新值
    receiver: object
  ): boolean {
 // 緩存舊值
    let oldValue = (target as any)[key]
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
// 新舊值轉換原始對象
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      // 如果舊值已經是一個RefImpl對象且新值不是RefImpl對象
      // 例如var v = Vue.reactive({a:1,b:Vue.ref({c:3})})場景的set
      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
    }
  // 用來判斷是新增key還是更新key的值
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
 // 設置set結果,并添加監聽effect邏輯
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
// 判斷target有沒有動過,包括在原型上添加或者刪除某些項
    if (target === toRaw(receiver)) {
      if (!hadKey) {// 新增key的觸發監聽
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {// 更新key的觸發監聽
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
      // 返回set的結果 true/false
    return result
  }
}

handler.set方法的核心功能是設置key對應的值,即obj[key] = value,同時對新舊值進行邏輯判斷和處理,最后添加trigger觸發監聽track邏輯,以便于觸發effect。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容