代理模式Proxy 和 Vue3數據響應系統

1、代理模式

Proxy

Proxy 提供了強大的 Javascript 元編程,儘管他不像其餘 ES6 功能用的廣泛,但Proxy有許多功能,包括運算符重載,對象模擬,簡潔而靈活的API建立,對象變化事件,甚至Vue 3背後的內部響應系統提供動力。 javascript

Proxy用於修改某些操做的默認行爲,也能夠理解爲在目標對象以前架設一層攔截,外部全部訪問都先通過這層攔截,因此咱們叫它爲代理模式。 vue

ES6原生提供了Proxy構造函數,用來生成Proxy實例。java

var proxy = new Proxy(target, handler);

Proxy對象的全部用法,都是上面這種形式,不一樣的只是handle參數的寫法。其中new Proxy用來生成Proxy實例,target是表示所要攔截的對象,handle是用來定製攔截行爲的對象。
例子:react

const target = {}
const proxy = new Proxy(target, {
    get: (obj, prop) => {
        console.log('設置 get 操做')
        return obj[prop];
    },
    set: (obj, prop, value) => {
        console.log('set 操做')
        obj[prop] = value;
    }
});
proxy.a = 2  // set 操做
proxy.a  // 設置 get 操做

當給目標對象進行賦值或獲取屬性時,就會分別觸發getset方法,getset就是咱們設置的代理,覆蓋了默認的賦值或獲取行爲。
固然,除了getsetProxy還能夠攔截其餘共計13種操做編程

/* 
handler.get
handler.set
handler.has
handler.apply
handler.construct
handler.ownKeys
handler.deleteProperty
handler.defineProperty
handler.isExtensible
handler.preventExtensions
handler.getPrototypeOf
handler.setPrototypeOf
handler.getOwnPropertyDescriptor
*/
var target = function (a,b) { 
  return a + b;
 };
const proxy = new Proxy(target, {
    apply: (target, thisArg, argumentsList) => {
        console.log('apply function', argumentsList)
        return target(argumentsList[0], argumentsList[1]) * 10;
    }
});
proxy(1, 2)

Proxy 的用法

驗證屬性

let validator = {
  set: (obj, prop, value) => {
    if(prop === 'age') {
      if(!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer')
      }
      if(value > 200) {
        throw new TypeError('The age is seems invalid')
      }
    }
    obj[prop] = value;

    return true;
  }
};

let p = new Proxy({}, validator);
p.age = '11' // Uncaught TypeError: The age is not an integer
p.age = 2000 // Uncaught TypeError: The age is seems invalid
p.age = 18 // true

咱們有時候可能會對一個對象的某些屬性進行一些限制,好比年齡age,只能是字符串並且不超過 200 歲,當不知足這些要求時咱們就能夠經過代理拋出錯誤app

2、vue3 數據驅動: reactivity

10月出的時候,vue3公佈了源碼,其中數據響應式系統核心就是採用 Proxy 代理模式,咱們來看看它的源碼, reactivity的源碼位置在packages的文件內,
如下是簡化後的源碼。函數

// 代碼通過刪減
import { mutableHandlers, readonlyHandlers } from './baseHandlers'
// rawToReactive 和 reactiveToRaw 是兩個弱引用的 Map 結構
// 這兩個 Map 用來保存原始數據 和 可響應數據
// 建立完 Proxy 後須要把原始數據和 Proxy對象分別保存到這兩個Map結構
const rawToReactive = new WeakMap() // 鍵是原始數據,值是響應數據
const reactiveToRaw = new WeakMap() // 鍵是響應數據,值是原始數據

export const targetMap = new WeakMap<any, KeyToDepMap>()
// entry
function reactive(target) {
 // if trying to observe a readonly proxy, return the readonly version.
 // 若是是隻讀proxy,直接返回
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  // 若是目標被用戶標記爲只讀,那麼經過 readonly 建立一個只讀的Proxy
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
  )
}

function createReactiveObject(target, toProxy, toRaw, baseHandlers) {
  let observed = toProxy.get(target)
  // 原數據已經有相應的可響應數據, 返回可響應數據
  if (observed !== void 0) {
    return observed
  }
  // 原數據已是可響應數據
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, baseHandlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  // 把原數據當作key保存在targetMap,value值是一個 Map 類型
  // 
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

reactive 方法就是暴露給外面的入口方法,方法裏面只作了一件事情,就是判斷是否要生成只讀的Proxy對象,若是是則調用readonly建立,不是則直接使用createReactiveObject來生成響應是數據。 性能

createReactiveObject 裏面第一步嘗試在toProxy中獲取是否已經有這個target的響應式數據,若是有則直接把獲取到的返回出去,第二步判斷target裏面是否已是可響應數據,第三步就是經過new Proxy建立可響應數據,其中baseHandlers./baseHandlers.ts這個文件下定義。建立完成後,把數據保存到toProxytoRaw,這樣方便下次建立時使用。 this

咱們知道響應式數據是如何建立,接下來咱們看一下baseHandlers.ts裏面定義的handler實現代理

get

先看一段代碼,

let handler = {
  get: (obj, prop) => {
      console.log('get 操做')
      return obj[prop];
  },
  set: (obj, prop, value) => {
    console.log('set 操做')
    return true;
  }
};

let p = new Proxy({
  a: {}
}, handler);
p.a.c = 1  // get 操做

這時候咱們對target裏面的a對象進行賦值,可是咱們的set裏面是不能觸發深度的數據賦值,可是這時候是會觸發get,那麼這裏就會出現一個問題,較深層次的數據就沒法被代理到了。解決辦法很簡單,就是經過get判斷值是否爲對象,若是是則把值再走一遍Proxy

function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    const res = Reflect.get(target, key, receiver)
    // 
    track(target, OperationTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

let handler = {
  get: createGetter(false),
  set: (obj, prop, value) => {
    console.log('set 操做')
    return true;
  }
};

let p = new Proxy({
  a: {}
}, handler);
p.a.c = 1  // get 操做

vue3中使用createGetter方法來返回getcreateGetter裏面判斷經過Reflect.get獲取到的數據若是是Object,則繼續調用reactive生成Proxy對象,從而得到了對對象內部的偵測。而且,每一次的 proxy 數據,都會保存在 WeakMap 中,訪問時會直接從中查找,從而提升性能。 track方法和effect有關,咱們下文再說。

set

function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean {
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // 是否新增 key
  // trigger 是用來觸發回調
  if (!hadKey) {
    trigger(target, OperationTypes.ADD, key)
  } else if (value !== oldValue) {
    trigger(target, OperationTypes.SET, key)
  }  
  return result
}

對於 set 函數來講,有主要兩個做用,第一個就是設置值,第二個是調用 trigger,這也是 effect 中的內容。
簡單來講,若是某個 effect 回調中有使用到 value.num,那麼這個回調會經過track方法被收集起來,並在調用 value.num = 2 時經過trigger觸發。

那麼怎麼收集這些內容呢?這就要說說 targetMap 這個對象了。targetMap是在reactive裏面建立的WeakMap類型,
它用於存儲依賴關係。

// effect.ts
import { targetMap } from './reactive'

// track用來把回調保存在 targetMap 中
export function track(
  target: any,
  type: OperationTypes,
  key?: string | symbol
) {
  if (!shouldTrack) {
    return
  }
  // activeReactiveEffectStack 的用處是保持依賴函數的存在
  const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
  if (effect) {
    // 這個函數作的事情就是塞依賴到 map 中,用於下次尋找是否有這個依賴
    // 另外就是把 effect 的回調保存起來
    // 經過獲取targetMap上保存的 Map 類型數據
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      // 什麼都沒有,設置空的map給它
      targetMap.set(target, (depsMap = new Map()))
    }
    // 獲取target中的依賴
    let dep = depsMap.get(key!)
    if (dep === void 0) {
      depsMap.set(key!, (dep = new Set()))
    }
    if (!dep.has(effect)) {
      dep.add(effect)
      effect.deps.push(dep)
    }
  }
}

咱們再瞭解一下effect的組成

function createReactiveEffect(
  fn: Function,
  options: ReactiveEffectOptions
): ReactiveEffect {
  // 一系列賦值操做,重點看 run 的實現
  const effect = function effect(...args): any {
    return run(effect as ReactiveEffect, fn, args)
  } as ReactiveEffect
  effect.isEffect = true
  effect.active = true
  effect.raw = fn
  effect.scheduler = options.scheduler
  effect.onTrack = options.onTrack
  effect.onTrigger = options.onTrigger
  effect.onStop = options.onStop
  effect.computed = options.computed
  // 用於收集依賴函數
  effect.deps = []
  return effect
}

function run(effect: ReactiveEffect, fn: Function, args: any[]): any {
  if (!effect.active) {
    return fn(...args)
  }
  if (activeReactiveEffectStack.indexOf(effect) === -1) {
    cleanup(effect)
    // 執行回調 push,回調執行結束 pop
    // activeReactiveEffectStack 的用處是保持依賴函數的存在
    // 舉個例子:
    // const counter = reactive({ num: 0 })
    // effect(() => {
    //   console.log(counter.num)
    // })
    // counter.num = 7
    // effect 回調在執行的過程當中會觸發 counter 的 get 函數
    // get 函數會觸發 track,在 track 函數調用的過程當中會執行 effect.deps.push(dep) 而且將
    // 也就是把回調 push 到了回調的 deps 屬性上
    // 這樣在下次 counter.num = 7 的時候會觸發 counter 的 set 函數
    // set 函數會觸發 trigger,在 trigger 函數中會 effects.forEach(run),把須要執行的回調都執行一遍
    try {
      activeReactiveEffectStack.push(effect)
      return fn(...args)
    } finally {
      activeReactiveEffectStack.pop()
    }
  }
}

最後

咱們最後把流程再回顧一下,首先經過createReactiveObject建立Proxy對象,建立完成後把這個Proxy對象看成key保存在targetMap中。當觸發get方法時調用 track 函數,把依賴函數保存到targetMap中。觸發set的時候在調用trigger運行回調。

相關文章
相關標籤/搜索