從數(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>`
}
工作原理如下
先對(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”了)瑞驱。