​淺談 Vue3 響應式原理與源碼解讀

一. 瞭解幾個概念

什麼是響應式

在開始響應式原理與源碼解析之前,需要先了解一下什麼是響應式?首先明確一個概念:響應式是一個過程,它有兩個參與方:

當數據發生改變時,引用數據的函數會自動重新執行,例如,視圖渲染中使用了數據,數據改變後,視圖也會自動更新,這就完成了一個響應的過程。

副作用函數

VueReact中都有副作用函數的概念,什麼是副作用函數?如果一個函數引用了外部的數據,這個函數會受到外部數據改變的影響,我們就說這個函數存在副作用,也就是我們所說的副作用函數。初聽這個名字不太好理解,其實 副作用函數就是引用了數據的函數或是與數據相關聯的函數。舉個例子:

<!DOCTYPE html>
<html lang="">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta >
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
    <div id="app"></div>
    <script>
        const obj = {
            name: 'John',
        }
        // 副作用函數 effect
        function effect() {
            app.innerHTML = obj.name
            console.log('effect', obj.name)
        }

        effect()

        setTimeout(() ={
            obj.name = 'ming'
            // 手動執行 effect 函數
            effect()
        }, 1000);
    </script>
</body>
</html>

在上面例子中,effect函數里面引用了外部的數據obj.name, 如果這個數據發生了改變,則會影響到這個函數,類似effect的這種函數就是副作用函數。

實現響應式的基本步驟

在上面的例子中, 當obj.name發生了改變,effect是我們手動執行的,如果能監聽到obj.name的變化,讓其自動執行副作用函數effect,那麼就實現了響應式的過程。其實無論是 Vue2 還是 Vue3 ,響應式的核心都是 數據劫持/代理、依賴收集、依賴更新,只不過由於實現數據劫持方式的差異從而導致具體實現的差異。

關於Vue2的響應式這裏不做重點講解,這篇文章主要關注Vue3響應式原理的實現。

二. Proxy 與 Reflect

在解析Vue3的響應式原理之前,首先需要了解兩個 ES6 新增的 API:PorxyReflect

Proxy

Proxy: 代理,顧名思義主要用於爲對象創建一個代理,從而實現對對象基本操作的攔截和自定義。可以理解成,在目標對象之前架設一層 “攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。基本語法:

let proxy = new Proxy(target, handler);
const obj = {
    name: 'John',
    age: 16
}

const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj與objProxy是否相等',obj === objProxy);
// 輸出
[Log] obj.age – 20
[Log] objProxy.age – 20 
[Log] obj與objProxy是否相等 – false

這裏objProxyhandler爲空,則直接指向被代理對象, 並且代理對象與數據源對象並不全等. 如果需要更加靈活的攔截對象的操作,就需要在handler中添加對應的屬性。例如:

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`獲取對象屬性${key}`)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log(`設置對象屬性${key}`)
        target[key] = value
    },
    deleteProperty(target, key) {
        console.log(`刪除對象屬性${key}`)
        return delete target[key]
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

// 輸出
[Log] 獲取對象屬性age值 (example01.html, line 22)
[Log] 16 (example01.html, line 36)
[Log] 設置對象屬性age值 (example01.html, line 26)
[Log] 刪除對象屬性age值 (example01.html, line 30)
[Log] true (example01.html, line 38)

上面的例子,我們在捕獲器中定義了set()get()deleteProperty()屬性,通過對proxy的操作實現了對obj的操作攔截。這些屬性的觸發方法有如下參數:

Reflect

Reflect: 反射,就是將代理的內容反射出去。ReflectProxy一樣,也是 ES6 爲了操作對象而提供的新 API。它提供攔截JavaScript操作的方法,這些方法與Proxy handlers 提供的的方法是一一對應的,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。且 Reflect 不是一個函數對象,即不能進行實例化,其所有屬性和方法都是靜態的。還是上面的例子

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`獲取對象屬性${key}`)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log(`設置對象屬性${key}`)
        return Reflect.set(target, key, value, receiver)
    },
    deleteProperty(target, key) {
        console.log(`刪除對象屬性${key}`)
        return Reflect.deleteProperty(target, key)
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

上面的例子中

Reflect.construct(target, args)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

三. reactive、ref 源碼解析

瞭解了ProxyReflect,看下Vue3是如何通過porxy實現響應式的。其核心是下面要介紹的兩個方法:reactiveref. 這裏依照 Vue3.2 版本的源碼進行解析。

reactive 的源碼實現

打開源文件,找到文件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,
    reactiveMap
  )
}
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    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
  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
  }
  // only a whitelist of value types can be observed.
  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
}
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

接下來就是根據不同的對象類型,傳入不同的handler的邏輯處理了,主要關注baseHandlers,裏面存在五個屬性操作方法,這重點解析getset方法。

源碼位置:packages/reactivity/src/baseHandlers.ts

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
get與依賴收集
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
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

那麼核心方法就是如果利用 track() 進行依賴收集的處理了, 源碼在 ``packages/reactivity/src/effect.ts`

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined

  trackEffects(dep, eventInfo)
}

以上就是整個 get() 捕獲器以及依賴收集的核心流程。

set與依賴更新

我們在回到baseHandlers中看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 (!shallow) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

以上就是整個baseHandlers關於依賴收集依賴更新的核心流程。

ref 的源碼實現

我們知道ref可以定義基本數據類型、引用數據類型的響應式。來看下它的源碼實現:packages/reactivity/src/ref.ts

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
  private _rawValue: T

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

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

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

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

五. 總結

一些參考:

https://github.com/vuejs/vue

https://zh.javascript.info/proxy#reflect

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ixBZhPic9otUv9x66cTxzw