Vue.js
中,將數(shù)據(jù)對象轉(zhuǎn)化為響應(yīng)式數(shù)據(jù)的是 Observer
構(gòu)造函數(shù)葬毫。我準(zhǔn)備結(jié)合前面幾篇已經(jīng)整理出來的思路梗醇,實(shí)現(xiàn)一個(gè)自己的 Observer
捉捅。
為了讓代碼結(jié)構(gòu)更加清晰冶匹,同時(shí)考慮到可復(fù)用性习劫,我先從前面幾篇已有的實(shí)現(xiàn)中抽一些功能較為獨(dú)立的代碼出來:
-
defineReactive
方法
function defineReactive(obj, key) {
const dep = []
let value = obj[key]
Object.defineProperty(obj, key, {
get () {
dep.push(target)
return value
},
set (newVal) {
if (newVal === value) return
value = newVal
dep.forEach(f => {
f()
})
}
})
}
該方法用來將數(shù)據(jù)對象 obj
上的數(shù)據(jù)屬性 key
轉(zhuǎn)化為響應(yīng)式屬性。
dep
是“依賴收集器”嚼隘,屬性 key
的 getter setter
都通過閉包引用著自己的 dep
诽里。target
仍然作為全局變量存在,中轉(zhuǎn)依賴以幫助 getter
收集依賴飞蛹。setter
會(huì)執(zhí)行對應(yīng) getter
收集到的所有依賴谤狡,但如果發(fā)現(xiàn)設(shè)置的值與原值無異,則直接 return
桩皿,什么也不做豌汇。
這是直接從 Vue數(shù)據(jù)響應(yīng)原理(一)—— 簡單實(shí)現(xiàn) 里拿過來的代碼,但如果要封裝一個(gè)功能完善泄隔、可復(fù)用性高的方法的話,肯定還要考慮一些邊界條件與異常場景宛徊,比如佛嬉,如果傳遞進(jìn)來的屬性本來就是不可配置的?這時(shí)就得加個(gè)判斷:
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && !property.configurable) {
return
}
首先獲取到對象 obj
上屬性 key
的屬性描述符對象闸天,然后進(jìn)行判斷暖呕,如果屬性描述符對象存在,并且該屬性本來就不可配置苞氮,那么直接 return
湾揽。
再比如,如果傳進(jìn)來的屬性本來就有 getter setter
函數(shù)對 笼吟?那就要把原來的 getter setter
緩存起來库物,在新定義的 getter
里除卻收集依賴這項(xiàng)工作以外,還要將緩存起來的 getter
執(zhí)行并將結(jié)果返回贷帮。同樣戚揭,在新定義的 setter
里,除去執(zhí)行依賴的工作以外撵枢,還要將設(shè)置的新值 newVal
與緩存的 getter
執(zhí)行之后得到的值比較民晒,如果相等則直接 return
精居,什么都不做。并且要將緩存起來的 setter
執(zhí)行一遍潜必,以替代原來的賦值操作 value = newVal
靴姿。
反映至代碼即:
function defineReactive(obj, key) {
const dep = []
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && !property.configurable) {
return
}
const getter = property && property.get
const setter = property && property.set
let value = obj[key]
Object.defineProperty(obj, key, {
get () {
getter && (value = getter.call(obj))
dep.push(target)
return value
},
set (newVal) {
getter && (value = getter.call(obj))
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
value = newVal
}
dep.forEach(f => {
f()
})
}
})
}
上面有這么一句:
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
其實(shí)本來是這樣的:
if (newVal === value) {
return
}
但是考慮到 NaN
的情況:
NaN === NaN // false
這會(huì)導(dǎo)致:
newVal === value // false
所以應(yīng)該在判斷條件中加上:
newVal !== newVal && value !== value
利用 NaN
與自身不相等的特性判斷出 NaN
,最后就成了:
newVal === value || (newVal !== newVal && value !== value)
值得注意的是:
Infinity === Infinity // true
-Infinity === -Infinity // true
1 / 0 === 2 / 0 // true
-
walk
方法
function walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
該方法用于遍歷數(shù)據(jù)對象 obj
的每一個(gè)屬性磁滚,同時(shí)調(diào)用之前定義的 defineReactive
方法佛吓,將遍歷到的屬性轉(zhuǎn)化為響應(yīng)式屬性。
hasProto
const hasProto = '__proto__' in {}
該變量用于判斷瀏覽器是否支持 __proto__
屬性恨旱。
-
arrayMethods
對象
const mutationMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
arrayMethods[method] = function (...args) {
const result = arrayProto[method].apply(this, args)
console.log(`我截獲了對數(shù)組的${method}操作`)
return result
}
})
該對象用于代理數(shù)組的變異方法以實(shí)現(xiàn)攔截辈毯。
-
def
方法
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
該方法是 Object.defineProperty
的簡單封裝,用于定義一個(gè)屬性搜贤,可以控制該屬性是否可枚舉谆沃。
-
protoAugment
方法
function protoAugment(target, src) {
target.__proto__ = src
}
該方法用于在瀏覽器支持 __proto__
屬性時(shí),通過修改原型鏈仪芒,讓 __proto__
指向 src
唁影,來增強(qiáng)目標(biāo)對象或數(shù)組。
-
copyAugment
方法
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
該方法用來遍歷 keys
掂名,并在目標(biāo)對象 target
上定義不可枚舉的屬性据沈,該屬性的鍵為 keys
中的元素饺蔑,值為該元素在 src
中對應(yīng)的屬性值猾警。
-
isPlainObject
方法
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
該方法用于判斷給定的變量是否為純對象发皿。
有了以上這些方法和屬性之后穴墅,Observer
類也就應(yīng)運(yùn)而生了:
class Observer {
constructor (value) {
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, mutationMethods)
} else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
但現(xiàn)在有兩個(gè)問題玄货,一個(gè)是這個(gè)類沒有實(shí)現(xiàn)深度觀測鹅士,再一個(gè)是沒有對調(diào)用 Observer
時(shí)傳進(jìn)來的參數(shù)做檢測惩坑,以防止傳進(jìn)來 undefined null 100 'kobe'
等等不能被觀測的數(shù)據(jù)類型。并且我希望調(diào)用 Observer
的時(shí)候傳進(jìn)來的只能是數(shù)組或者純對象慢哈。綜合這些因素,再封裝一層出來會(huì)比較好:
function observe(value) {
if (Array.isArray(value) || isPlainObject(value)) {
return new Observer(value)
}
}
observe
會(huì)判斷給定的 value
如果是數(shù)組或者純對象的話再去 new
出來 Observer
键俱,并將結(jié)果返回编振。
有了 observe
踪央,深度觀測就可以這樣來實(shí)現(xiàn):在 defineReactive
方法中畅蹂,對給定的 obj[key]
以及 setter
中的 newVal
調(diào)用 observe
方法進(jìn)行觀測液斜,因?yàn)檫@兩者都可能是數(shù)組或者純對象,如果不是,observe
方法內(nèi)部已經(jīng)統(tǒng)一做了判斷祷嘶,外部調(diào)用時(shí)無需特殊處理论巍。即:
function defineReactive(obj, key) {
const dep = []
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && !property.configurable) {
return
}
const getter = property && property.get
const setter = property && property.set
let value = obj[key]
// 這里
observe(value)
Object.defineProperty(obj, key, {
get () {
getter && (value = getter.call(obj))
dep.push(target)
return value
},
set (newVal) {
getter && (value = getter.call(obj))
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
value = newVal
}
// 這里
observe(newVal)
dep.forEach(f => {
f()
})
}
})
}
但其實(shí)發(fā)現(xiàn)還有一個(gè)問題,現(xiàn)在數(shù)組鞋怀、純對象以及純對象內(nèi)嵌套數(shù)組密似、純對象內(nèi)嵌套純對象這幾種情形都已經(jīng)實(shí)現(xiàn)了(深度)觀測村斟,但數(shù)組內(nèi)嵌套純對象以及數(shù)組內(nèi)嵌套數(shù)組還沒有實(shí)現(xiàn)蟆盹,所以要再寫這么一個(gè)方法:
function observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
該方法用來遍歷給定的數(shù)組逾滥,即 items
匣距,再分別對每一個(gè)元素 items[i]
執(zhí)行 observe
方法毅待,即可對數(shù)組里面的嵌套情形進(jìn)行深度觀測。同時(shí) Observer
類要做以下改造:
class Observer {
constructor (value) {
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, mutationMethods)
// 二
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 一
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
注釋一的地方外里,給 Observer
類添加一個(gè)實(shí)例方法,也就是我剛寫的 observeArray
墩莫。
注釋二的地方狂秦,調(diào)用 observeArray 方法,并將數(shù)組 value 作為參數(shù)傳入堪簿。
那么最終戴甩,代碼就是這個(gè)樣子:
const mutationMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
const hasProto = '__proto__' in {}
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
function defineReactive(obj, key) {
const dep = []
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && !property.configurable) {
return
}
const getter = property && property.get
const setter = property && property.set
let value = obj[key]
observe(value)
Object.defineProperty(obj, key, {
get () {
getter && (value = getter.call(obj))
dep.push(target)
return value
},
set (newVal) {
getter && (value = getter.call(obj))
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
value = newVal
}
observe(newVal)
dep.forEach(f => {
f()
})
}
})
}
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
arrayMethods[method] = function (...args) {
const result = arrayProto[method].apply(this, args)
console.log(`我截獲了對數(shù)組的${method}操作`)
return result
}
})
function observe(value) {
if (Array.isArray(value) || isPlainObject(value)) {
return new Observer(value)
}
}
class Observer {
constructor (value) {
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, mutationMethods)
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
function protoAugment(target, src) {
target.__proto__ = src
}
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
function myWatch(exp, fn) {
target = fn
if (typeof exp === 'function') {
exp()
return
}
let pathArr,
obj = data
if (/\./.test(exp)) {
pathArr = exp.split('.')
pathArr.forEach(p => {
target = fn
obj = obj[p]
})
return
}
data[exp]
}
添加以下測試代碼:
const data = {
name: 'kobe bryant',
otherInfo: {
height: 198,
numbers: [8, 24]
},
teammates: [
'paul gasol',
{
name: 'shaq',
numbers: [32, 34, 33]
}
]
}
function render() {
document.body.innerText = `我最喜歡的NBA球員是${data.name}茉稠,他身高${data.otherInfo.height}cm而线,穿過${data.otherInfo.numbers.length}個(gè)球衣號(hào)碼,${data.otherInfo.numbers[0]}和${data.otherInfo.numbers[1]}誓竿,他的隊(duì)友有${data.teammates[0]}和${data.teammates[1].name}筷屡,其中毙死,${data.teammates[1].name}在湖人時(shí)期穿的球衣號(hào)碼為${data.teammates[1].numbers[1]}號(hào)`
}
observe(data)
myWatch(render, render)
data.name = 'michael'
data.otherInfo.height = 198.1
data.otherInfo.numbers.push(23)
data.teammates[1].name = 'scott pippen'
data.teammates[1].numbers.push(33)
執(zhí)行以后發(fā)現(xiàn),無論嵌套關(guān)系如何對屬性的賦值操作均觸發(fā)了 render
函數(shù),對兩個(gè)數(shù)組data.otherInfo.numbers
和 data.teammates[1].numbers
的 push
操作也執(zhí)行了擴(kuò)展的功能即打印 '我截獲了對數(shù)組的push操作'
這句信息。但是數(shù)組的 push
操作沒有觸發(fā)頁面重新渲染,這是因?yàn)閷?shù)組變異方法的整個(gè)代理過程中沒有收集依賴也沒有觸發(fā)依賴候址,這個(gè)問題先留下匹耕,等我寫到 Dep
類的時(shí)候再回過頭來寫這個(gè)問題稳其。但其實(shí)站在這篇博客的角度來看,Observer
類的封裝就算是初步完成了嘱蛋。