從數(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>`
}
工作原理如下

先對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”了)。