帶你完全搞懂Vue3的Proxy響應式原理!基於函數劫持實現Map和Set的響應式

前言

在本系列的上一篇文章前端

帶你完全搞懂Vue3的響應式原理!TypeScript從零實現基於Proxy的響應式庫。react

咱們詳細的講解了普通對象和數組實現響應式的原理,可是Proxy能夠作的遠不止於此,對於es6中新增的MapSetWeakMapWeakSet也同樣能夠實現響應式的支持。git

可是對於這部分的劫持,代碼中的邏輯是徹底獨立的一套,這篇文章就來看一下如何基於函數劫持實現實現這個需求。es6

閱讀本篇須要的一些前置知識:
Proxy
WeakMap
Reflect
Symbol.iterator (會講解)github

爲何特殊

在上一篇文章中,假設咱們經過data.a去讀取響應式數據data的屬性,則會觸發Proxy的劫持中的get(target, key)api

target就是data對應的原始對象,key就是a數組

咱們能夠在這時候給key: a註冊依賴,而後經過Reflect.get(data, key)去讀到原始數據返回出去。數據結構

回顧一下:app

/** 劫持get訪問 收集依賴 */
function get(target: Raw, key: Key, receiver: ReactiveProxy) {
  const result = Reflect.get(target, key, receiver)
  
  // 收集依賴
  registerRunningReaction({ target, key, receiver, type: "get" })

  return result
}
複製代碼

而當咱們的響應式對象是一個Map數據類型的時候,想象一下這個場景:函數

const data = reactive(new Map([['a', 1]]))

observe(() => data.get('a'))

data.set('a', 2)
複製代碼

讀取數據的方式變成了data.get('a')這種形式,若是仍是用上一篇文章中的get,會發生什麼狀況呢?

get(target, key)中的target是map原始對象,key是get

經過Reflect.get返回的是map.get這個方法,註冊的依賴也是經過get這個key註冊的,而咱們想要的效果是經過a這個key來註冊依賴。

因此這裏的辦法就是函數劫持,就是把對於MapSet的全部api的訪問(好比has, get, set, add)所有替換成咱們本身寫的方法,讓用戶無感知的使用這些api,可是內部卻已經被咱們本身的代碼劫持了。

實現

咱們把上篇文章中的目錄結構調整成這樣:

src/handlers
// 數組和對象的handlers
├── base.ts
// map和set的handlers
├── collections.ts
// 統一導出
└── index.ts
複製代碼

入口

首先看一下handlers/index.ts入口的改造

import { collectionHandlers } from "./collections"
import { baseHandlers } from "./base"
import { Raw } from "types"

// @ts-ignore
// 根據對象的類型 獲取Proxy的handlers
export const handlers = new Map([
  [Map, collectionHandlers],
  [Set, collectionHandlers],
  [WeakMap, collectionHandlers],
  [WeakSet, collectionHandlers],
  [Object, baseHandlers],
  [Array, baseHandlers],
  [Int8Array, baseHandlers],
  [Uint8Array, baseHandlers],
  [Uint8ClampedArray, baseHandlers],
  [Int16Array, baseHandlers],
  [Uint16Array, baseHandlers],
  [Int32Array, baseHandlers],
  [Uint32Array, baseHandlers],
  [Float32Array, baseHandlers],
  [Float64Array, baseHandlers],
])

/** 獲取Proxy的handlers */
export function getHandlers(obj: Raw) {
  return handlers.get(obj.constructor)
}

複製代碼

這裏定義了一個Map: handlers,導出了一個getHandlers方法,根據傳入數據的類型獲取Proxy的第二個參數handlers

baseHandlers在第一篇中已經進行了詳細講解。

這篇文章主要是講解collectionHandlers

collections

先看一下collections的入口:

// 真正交給Proxy第二個參數的handlers只有一個get
// 把用戶對於map的get、set這些api的訪問所有移交給上面的劫持函數
export const collectionHandlers = {
  get(target: Raw, key: Key, receiver: ReactiveProxy) {
    // 返回上面被劫持的api
    target = hasOwnProperty.call(instrumentations, key)
      ? instrumentations
      : target
    return Reflect.get(target, key, receiver)
  },
}
複製代碼

咱們全部的handlers只有一個get,也就是用戶對於map或者set上全部api的訪問(好比has, get, set, add),都會被轉移到咱們本身定義的api上,這其實就是函數劫持的一種應用。

那關鍵就在於instrumentations這個對象上,咱們對於這些api的本身的實現。

劫持api的實現

get和set

export const instrumentations = {
  get(key: Key) {
    // 獲取原始數據
    const target = proxyToRaw.get(this)
    // 獲取原始數據的__proto__ 拿到原型鏈上的方法
    const proto: any = Reflect.getPrototypeOf(this)
    // 註冊get類型的依賴
    registerRunningReaction({ target, key, type: "get" })
    // 調用原型鏈上的get方法求值 而後對於複雜類型繼續定義成響應式
    return findReactive(proto.get.apply(target, arguments))
  },
  set(key: Key, value: any) {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    // 是不是新增的key
    const hadKey = proto.has.call(target, key)
    // 拿到舊值
    const oldValue = proto.get.call(target, key)
    // 求出結果
    const result = proto.set.apply(target, arguments)
    if (!hadKey) {
      // 新增key值時以type: add觸發觀察函數
      queueReactionsForOperation({ target, key, value, type: "add" })
    } else if (value !== oldValue) {
      // 已存在的key的值發生變化時以type: set觸發觀察函數
      queueReactionsForOperation({ target, key, value, oldValue, type: "set" })
    }
    return result
  },
}

/** 對於返回值 若是是複雜類型 再進一步的定義爲響應式 */
function findReactive(obj: Raw) {
  const reactiveObj = rawToProxy.get(obj)
  // 只有正在運行觀察函數的時候纔去定義響應式
  if (hasRunningReaction() && isObject(obj)) {
    if (reactiveObj) {
      return reactiveObj
    }
    return reactive(obj)
  }
  return reactiveObj || obj
}
複製代碼

核心的getset方法和上一篇文章中的實現就幾乎同樣了,get返回的值經過findReactive確保進一步定義響應式數據,從而實現深度響應。

至此,這樣的用例就能夠跑通了:

const data = reactive(new Map([['a', 1]]))
observe(() => console.log('a', data.get('a')))

data.set('a', 5)
// 從新打印出a 5
複製代碼

接下來再針對一些特有的api進行實現:

has

has (key) {
    const target = proxyToRaw.get(this)
    const proto = Reflect.getPrototypeOf(this)
    registerRunningReactionForOperation({ target, key, type: 'has' })
    return proto.has.apply(target, arguments)
  },
複製代碼

add

add就是典型的新增key的流程,會觸發循環相關的觀察函數。

add (key: Key) {
    const target = proxyToRaw.get(this)
    const proto: any  = Reflect.getPrototypeOf(this)
    const hadKey = proto.has.call(target, key)
    const result = proto.add.apply(target, arguments)
    if (!hadKey) {
      queueReactionsForOperation({ target, key, value: key, type: 'add' })
    }
    return result
  },
複製代碼

delete

delete也和上一篇中的deleteProperty的實現大體相同,會觸發循環相關的觀察函數。

delete (key: Key) {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    const hadKey = proto.has.call(target, key)
    const result = proto.delete.apply(target, arguments)
    if (hadKey) {
      queueReactionsForOperation({ target, key, type: 'delete' })
    }
    return result
  },
複製代碼

clear

clear () {
    const target: any = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    const hadItems = target.size !== 0
    const result = proto.clear.apply(target, arguments)
    if (hadItems) {
      queueReactionsForOperation({ target, type: 'clear' })
    }
    return result
  },
複製代碼

在觸發觀察函數的時候,針對clear這個type作了一些特殊處理,也是觸發循環相關的觀察函數。

export function getReactionsForOperation ({ target, key, type }) {
  const reactionsForTarget = connectionStore.get(target)
  const reactionsForKey = new Set()

+ if (type === 'clear') {
+ reactionsForTarget.forEach((_, key) => {
+ addReactionsForKey(reactionsForKey, reactionsForTarget, key)
+ })
  } else {
    addReactionsForKey(reactionsForKey, reactionsForTarget, key)
  }

 if (
    type === 'add' 
    || type === 'delete' 
+ || type === 'clear'
) {
    const iterationKey = Array.isArray(target) ? 'length' : ITERATION_KEY
    addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey)
  }

  return reactionsForKey
}
複製代碼

clear的時候,把每個key收集到的觀察函數都給拿到,而且把循環的觀察函數也拿到,能夠說是觸發最全的了。

邏輯也很容易理解,clear的行爲每個key都須要關心,只要在observe函數中讀取了任意的key,clear的時候也須要從新執行這個observe的函數。

forEach

forEach (cb, ...args) {
    const target = proxyToRaw.get(this)
    const proto = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const wrappedCb = (value, ...rest) => cb(findObservable(value), ...rest)
    return proto.forEach.call(target, wrappedCb, ...args)
  },
複製代碼

到了forEach的劫持 就稍微有點難度了。

首先registerRunningReaction註冊依賴的時候,用的key是iterate,這個很容易理解,由於這是遍歷的操做。

這樣用戶後續對集合數據進行新增或者刪除、或者使用clear操做的時候,會從新觸發內部調用了forEach的觀察函數

重點看下接下來這兩段代碼:

const wrappedCb = (value, ...rest) => cb(findObservable(value), ...rest)
return proto.forEach.call(target, wrappedCb, ...args)
複製代碼

wrappedCb包裹了用戶本身傳給forEach的cb函數,而後傳給了集合對象原型鏈上的forEach,這又是一個函數劫持。用戶傳入的是map.forEach(cb),而咱們最終調用的是map.forEach(wrappedCb)。

在這個wrappedCb中,咱們把cb中本應該得到的原始值value經過findObservable定義成響應式數據交給用戶,這樣用戶在forEach中進行的響應式操做同樣能夠收集到依賴了,不得不讚嘆這個設計的巧妙。

keys && size

get size () {
    const target = proxyToRaw.get(this)
    const proto = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    return Reflect.get(proto, 'size', target)
  },
  keys () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    return proto.keys.apply(target, arguments)
  },
複製代碼

因爲keyssize返回的值不須要定義成響應式,因此直接返回原值就能夠了。

values

再來看一個須要作特殊處理的典型

values () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const iterator = proto.values.apply(target, arguments)
    return patchIterator(iterator, false)
  },
複製代碼

這裏有一個知識點須要注意一下,就是集合對象的values方法返回的是一個迭代器對象Map.values

這個迭代器對象每一次調用next()都會返回Map中的下一個值

,爲了讓next()獲得的值也能夠變成響應式proxy,咱們須要用patchIterator劫持iterator

// 把iterator劫持成響應式的iterator
function patchIterator (iterator) {
  const originalNext = iterator.next
  iterator.next = () => {
    let { done, value } = originalNext.call(iterator)
    if (!done) {
      value = findReactive(value)
    }
    return { done, value }
  }
  return iterator
}
複製代碼

也是經典的函數劫持邏輯,把原有的{ done, value }值拿到,把value值定義成響應式proxy

理解了這個概念之後,剩下相關幾個handler也好理解了

entries

entries () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const iterator = proto.entries.apply(target, arguments)
    return patchIterator(iterator, true)
  },
複製代碼

對應entries也有特殊處理,把迭代器傳給patchIterator的時候須要特殊標記一下這是entries,看一下patchIterator的改動:

/** 把iterator劫持成響應式的iterator */ 
function patchIterator (iterator, isEntries) {
  const originalNext = iterator.next
  iterator.next = () => {
    let { done, value } = originalNext.call(iterator)
    if (!done) {
+ if (isEntries) {
+ value[1] = findReactive(value[1])
      } else {
        value = findReactive(value)
      }
    }
    return { done, value }
  }
  return iterator
}
複製代碼

entries操做的每一項是一個[key, val]的數組,因此經過下標[1],只把值定義成響應式,key不須要特殊處理。

Symbol.iterator

[Symbol.iterator] () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const iterator = proto[Symbol.iterator].apply(target, arguments)
    return patchIterator(iterator, target instanceof Map)
  },
複製代碼

這裏又是一個比較特殊的處理了,[Symbol.iterator]這個內置對象會在for of操做的時候被觸發,具體能夠看本文開頭給出的mdn文檔。因此也要用上面的迭代器劫持的思路。

patchIterator的第二個參數,是由於對Map數據結構使用for of操做的時候,返回的是entries結構,因此也須要進行特殊處理。

總結

本文的代碼都在這個倉庫裏
github.com/sl1673495/p…

函數劫持的思路在各類各樣的前端庫中都有出現,這幾乎是進階必學的一種技巧了,但願經過本文的學習,你能夠理解函數劫持的一些強大的做用。也能夠想象Vue3裏用proxy來實現響應式能力有多麼強。

相關文章
相關標籤/搜索