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

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

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

Vue是雙向綁定嗎秩彤?

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

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

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

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

Object.defineProperty(以下簡(jiǎn)稱OD)

這是JavaScript定義對(duì)象屬性的一個(gè)api嵌屎,通過(guò)調(diào)用該方法推正,我們可以定義一個(gè)對(duì)象的屬性及屬性描述符,可以理解為屬性的屬性

Object.defineProperty(object, key, descriptor)

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

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

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

const Temperature = {
    degree: 28
}

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

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

Vue的監(jiān)聽(tīng)機(jī)制同理粘勒,當(dāng)一個(gè)data對(duì)象定義時(shí),Vue會(huì)對(duì)data所有的屬性設(shè)置setter和getter屎即。假設(shè)我有一個(gè)組件:

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

工作原理如下

無(wú)標(biāo)題.png

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

Object.defineProperty的問(wèn)題

1.對(duì)每一個(gè)key都要添加描述符:

在我之前的文章提到過(guò)井赌,data中的每一個(gè)屬性都有監(jiān)聽(tīng)谤逼,這樣做比較浪費(fèi)JavaScript的開(kāi)銷贵扰,無(wú)法監(jiān)聽(tīng)到對(duì)象屬性的添加和刪除(需要通過(guò)Vue.set和Vue.delete處理)

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

如果直接往對(duì)象里添加一個(gè)屬性(如往o = {a:1}中添加o.b = 2),或者改變數(shù)組長(zhǎng)度流部,Vue無(wú)法觸發(fā)監(jiān)聽(tīng)

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) // 沒(méi)有任何反應(yīng)
a.length = 2 // 沒(méi)有任何反應(yīng)

對(duì)此戚绕,Vue對(duì)數(shù)組的方法如pop、push枝冀、sort等提供了響應(yīng)補(bǔ)丁舞丛,還提供了Vue.set方法做兼容處理。

Proxy

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

Proxy(target, handler)

區(qū)別于OD球切,我們可以對(duì)整個(gè)對(duì)象進(jìn)行監(jiān)聽(tīng)操作,且看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

而且對(duì)數(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ā)訪問(wèn)元素下標(biāo)getter
proxyArr.sort //  觸發(fā)訪問(wèn)數(shù)組方法的getter
proxyArr.length = 2 // 觸發(fā)setter
proxyArr.push(1) // 觸發(fā)setter和每個(gè)數(shù)組遍歷的getter

另外跨晴,Proxy跟Reflect時(shí)相輔相成的欧聘,參見(jiàn)https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

其中,Reflect.get(foo, 'key')等價(jià)于foo.key端盆,Reflect.set(foo,'key','value')等價(jià)于foo.key = 'value'怀骤,Reflect.has(foo, 'key')等價(jià)于'key' in foo
一般來(lái)說(shuō),需要在Proxy的Hanlder中使用Reflect的API焕妙,那么上面的handler.get應(yīng)該改為:

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

Vue3.0中的Proxy

上個(gè)月(19年10月)蒋伦,Vue3.0 - vue-next在github開(kāi)放,根據(jù)上文介紹的Proxy和Reflect焚鹊,我們來(lái)看看3.0如何使用Proxy做響應(yīng)式數(shù)據(jù)的:
響應(yīng)式的代碼在packages/reactivity/src/reactive.ts中痕届,我省略了邊界判斷的代碼,直接上主線:
首先導(dǎo)入Proxy需要的Hanlder (這里先不講末患,我們?cè)诤竺娼忉專?/p>

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

創(chuàng)建2個(gè)Map研叫,用來(lái)存儲(chǔ)原始數(shù)據(jù)與響應(yīng)式數(shù)據(jù)的相互映射

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

這樣一來(lái),原始與響應(yīng)之間可以雙向映射璧针;
接下來(lái)嚷炉,利用這2種映射表,創(chuàng)建一個(gè)入口函數(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
}

回過(guò)頭來(lái)看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ù)(對(duì)應(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對(duì)應(yīng)了2.xDep.notify哗讥,并且因?yàn)樵賡etter里能夠獲取到目標(biāo)對(duì)象,所以也自然能知道到底是添加還是修改了胞枕。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末娘摔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子唤反,更是在濱河造成了極大的恐慌凳寺,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彤侍,死亡現(xiàn)場(chǎng)離奇詭異肠缨,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)盏阶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門晒奕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人名斟,你說(shuō)我怎么就攤上這事脑慧。” “怎么了砰盐?”我有些...
    開(kāi)封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵闷袒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我岩梳,道長(zhǎng)囊骤,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任冀值,我火速辦了婚禮也物,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘列疗。我一直安慰自己焦除,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布作彤。 她就那樣靜靜地躺著,像睡著了一般乌逐。 火紅的嫁衣襯著肌膚如雪竭讳。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天浙踢,我揣著相機(jī)與錄音绢慢,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛胰舆,可吹牛的內(nèi)容都是我干的骚露。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼缚窿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼棘幸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起倦零,我...
    開(kāi)封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤误续,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后扫茅,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蹋嵌,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年葫隙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了栽烂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恋脚,死狀恐怖腺办,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情慧起,我是刑警寧澤菇晃,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站蚓挤,受9級(jí)特大地震影響磺送,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灿意,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一估灿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缤剧,春花似錦馅袁、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至抵窒,卻和暖如春弛针,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背李皇。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工削茁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓茧跋,卻偏偏與公主長(zhǎng)得像慰丛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瘾杭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容