【Vue.js】數(shù)據(jù)監(jiān)聽和3.0的Proxy API

從數(shù)據(jù)綁定開始

數(shù)據(jù)綁定是目前主流前端框架普及的一個重要原因,它們讓開發(fā)者專注于處理數(shù)據(jù)而非DOM的實現(xiàn)。Angular是基于scope的臟檢查機制,React是組件的state,Vue則是基于Object.defineProperty,今天我們將Vue的數(shù)據(jù)綁定原理和新API的特性和優(yōu)勢。

Vue是雙向綁定嗎?

不是,原則上Vue的子組件不能改變父組件傳下來的數(shù)據(jù)(prop),但可以通過v-model這樣的語法糖去實現(xiàn),事實上,Vue和React十分類似,都是采用了單向數(shù)據(jù)流,這樣做更有利于狀態(tài)的追蹤和管理。

Vue如何實現(xiàn)數(shù)據(jù)綁定

可以分成2部分:數(shù)據(jù)監(jiān)聽=>數(shù)據(jù)映射
映射很好理解,數(shù)據(jù)傳入模板或者render函數(shù)編譯成虛擬DOM,虛擬DOM保存了節(jié)點的標(biāo)簽(如div h3等)、綁定的數(shù)據(jù)(data、methods、props、computed等)以及子節(jié)點/關(guān)聯(lián)節(jié)點,再根據(jù)虛擬DOM的信息映射成真實DOM

數(shù)據(jù)監(jiān)聽基于Object.defineProperty,下面開始著重介紹

Object.defineProperty(以下簡稱OD)

這是JavaScript定義對象屬性的一個api,通過調(diào)用該方法,我們可以定義一個對象的屬性及屬性描述符,可以理解為屬性的屬性

Object.defineProperty(object, key, descriptor)

其中,descriptor可以定義如下內(nèi)容:
詳情參考mdn文檔

interface PropertyDescriptor {
    configurable?: boolean; // 可以對該屬性進行刪改
    enumerable?: boolean; // 是否可以被for in 或者 Object.key迭代獲取
    value?: any; // 屬性值,默認(rèn)為undefined
    writable?: boolean; //是否可以賦值
    get?(): any; // 如果定義了getter,當(dāng)獲取到這個屬性后,無視默認(rèn)值,讀取getter的返回值
    set?(v: any): void; // 對這個屬性賦值后觸發(fā)的回調(diào)
}

既然能夠通過劫持對象的獲取與設(shè)置,那么這里邊就可以做一些文章了 ,比如我想設(shè)計一個高溫預(yù)警系統(tǒng),當(dāng)溫度達(dá)到40度時發(fā)出警告:

const Temperature = {
    degree: 28
}

Object.defineProperty (Temperature, 'degree', {
   set (value) {
     if (value > 40) { alert('高溫紅色預(yù)警!') }
   }
})

Temperature.degree = 28 // 不會觸發(fā)預(yù)警
Temperature.degree = 41 // 觸發(fā)預(yù)警

Vue的監(jiān)聽機制同理,當(dāng)一個data對象定義時,Vue會對data所有的屬性設(shè)置setter和getter。假設(shè)我有一個組件:

export default {
    data () {
        foo: 1
    },
    template: `<h3>{{foo}}</h3>`
}

工作原理如下

無標(biāo)題.png

先對Data定義屬性'foo'并添加getter/setter和Dep(dependeny依賴)
當(dāng)訪問foo字段時,觸發(fā)了getter(1.),getter函數(shù)中先收集Data.foo依賴(2.),再返回返回初始值value(3.)。當(dāng)foo值改變后,觸發(fā)了setter(4.),setter函數(shù)中通知Dep(5.)進行更新,通過更新調(diào)度后最終返回更新后的結(jié)果(6.)。

Object.defineProperty的問題

1.對每一個key都要添加描述符:

在我之前的文章提到過,data中的每一個屬性都有監(jiān)聽,這樣做比較浪費JavaScript的開銷,無法監(jiān)聽到對象屬性的添加和刪除(需要通過Vue.set和Vue.delete處理)

2.無法響應(yīng)對象的增刪和數(shù)組的長度等方法:

如果直接往對象里添加一個屬性(如往o = {a:1}中添加o.b = 2),或者改變數(shù)組長度,Vue無法觸發(fā)監(jiān)聽

var a = [1,2,3,4,5]

a.forEach((v,k,a)=>{
    Object.defineProperty(a,k, {
      get: ()=>{console.log('你獲取了a'); return v},
      set: (newVal)=>{ alert(`你設(shè)置了${newVal}`)}
    })
})

a.push(6) // 沒有任何反應(yīng)
a.length = 2 // 沒有任何反應(yīng)

對此,Vue對數(shù)組的方法如pop、push、sort等提供了響應(yīng)補丁,還提供了Vue.set方法做兼容處理。

Proxy

既然OD方法存在這些方面的缺陷,那么使用Proxy無疑是很好的替代品:

Proxy(target, handler)

區(qū)別于OD,我們可以對整個對象進行監(jiān)聽操作,且看MDN文檔示例代碼

let handler = {
    get: function(target, name){
        return name in target ? target[name] : 37;
    }
};

let p = new Proxy({}, handler);

p.a = 1;
p.b = undefined;

console.log(p.a, p.b);    // 1, undefined
console.log('c' in p, p.c);    // false, 37

而且對數(shù)組也能劫持:

var proxyArr = new Proxy(arr, {
  get (target, key) {
    alert(`你獲取了${target}.${key}`)
    return target[key]
  },
  set (target, key, value) {
    alert(`你設(shè)置了${target}.${key}->${value}`)
  }
})

proxyArr[0] // 觸發(fā)訪問元素下標(biāo)getter
proxyArr.sort //  觸發(fā)訪問數(shù)組方法的getter
proxyArr.length = 2 // 觸發(fā)setter
proxyArr.push(1) // 觸發(fā)setter和每個數(shù)組遍歷的getter

另外,Proxy跟Reflect時相輔相成的,參見https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

其中,Reflect.get(foo, 'key')等價于foo.key,Reflect.set(foo,'key','value')等價于foo.key = 'value'Reflect.has(foo, 'key')等價于'key' in foo
一般來說,需要在Proxy的Hanlder中使用Reflect的API,那么上面的handler.get應(yīng)該改為:

function(target, name){
  return Reflect.has(target, name) ? Reflect.get(target, name) : 37;
}

Vue3.0中的Proxy

上個月(19年10月),Vue3.0 - vue-next在github開放,根據(jù)上文介紹的Proxy和Reflect,我們來看看3.0如何使用Proxy做響應(yīng)式數(shù)據(jù)的:
響應(yīng)式的代碼在packages/reactivity/src/reactive.ts中,我省略了邊界判斷的代碼,直接上主線:
首先導(dǎo)入Proxy需要的Hanlder (這里先不講,我們在后面解釋)

import {
  mutableHandlers, // 可變代理Handlers
  /* 省略其他Handlers */
} from './baseHandlers'
import {
  mutableCollectionHandlers, // 專門針對Set/Map/WeakSet/WeakMap的Hanlers
} from './collectionHandlers'

創(chuàng)建2個Map,用來存儲原始數(shù)據(jù)與響應(yīng)式數(shù)據(jù)的相互映射

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // 原始對應(yīng)響應(yīng)式
const reactiveToRaw = new WeakMap<any, any>() // 響應(yīng)式對應(yīng)原始

這樣一來,原始與響應(yīng)之間可以雙向映射;
接下來,利用這2種映射表,創(chuàng)建一個入口函數(shù),傳入原始值、映射表和Hanlders

export function reactive(target: object) {
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
function createReactiveObject(
  target: unknown, // 原數(shù)據(jù)
  toProxy: WeakMap<any, any>, // rawToReactive
  toRaw: WeakMap<any, any>, // reactiveToRaw
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>, 
) {
  // 省略原數(shù)據(jù)邊界檢查代碼
  //
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)  // 響應(yīng)map中添加響應(yīng)與原始映射
  toRaw.set(observed, target)  // 原始map中添加原始與響應(yīng)映射
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

回過頭來看Handlers代碼

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

分析其中的getter和Setter

function createGetter(isReadonly: boolean, unwrap = true) {
  return function get(target: object, key: string | symbol, receiver: object) {
    let res = Reflect.get(target, key, receiver)
    if (unwrap && isRef(res)) {
      res = res.value
    } else {
      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
  }
}

getter中先執(zhí)行了track函數(shù)(對應(yīng)了2.x的Dep.depend),再根據(jù)值的類型返回原始值/只讀類型和遞歸響應(yīng)式的值。

function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  value = toRaw(value)
  const oldValue = (target as any)[key]
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  const hadKey = 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, OperationTypes.ADD, key)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, OperationTypes.SET, key)
    }
  }
  return result
}

setter中的trigger對應(yīng)了2.xDep.notify,并且因為再setter里能夠獲取到目標(biāo)對象,所以也自然能知道到底是添加還是修改了。

可以看到,Proxy對于對象劫持要靈活且有用得多,最主要的是相對于OD,Proxy額外生成的Getter和Setter更少,更節(jié)約內(nèi)存(當(dāng)然,嵌套的Object還得遞歸監(jiān)聽這點沒變)。這也就是為什么Vue3.0會使用Proxy替代Object.defineProperty的原因了(同時也是我為什么在前文中說“僅限2.0”了)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容