sayHi(friend)
function sayHi(friend) {
if(friend.status === '不太理解響應(yīng)式且還沒有看過上一篇') {
console.log(`
建議看下上一篇脓鹃,因為算是響應(yīng)式的基礎(chǔ)了啃匿,
不然可能這篇看起來會費點勁吟逝。
`)
} else if(friend.status === '之前看過上一篇了') {
console.log(`
也可以瞟一眼瘦赫,為了和這一篇相契合丽啡,部分地方做了增刪改辱匿。
`)
} else if(friend.status === '我是大牛刃永,就來看看你理解的怎么樣') {
console.log(`
大佬货矮!里邊請~
`)
}
}
我們首先來看下改變數(shù)組的兩種方式:
export default {
data() {
list: [1, 2, 3]
},
methods: {
changeArr1() { // 方式一:重新賦值
this.list = [4, 5, 6]
},
changeArr2() { // 方式二:方法改變
this.list.push(7)
}
}
}
對于這兩種改變數(shù)據(jù)的方式,vue
內(nèi)部的實現(xiàn)并不相同斯够。
方式一:重新賦值
- 實現(xiàn)原理和對象是一樣的囚玫,再
vm._render()
時有用到list
,就將依賴收集起來雳刺,重新賦值后走對象派發(fā)更新的那一套劫灶。
方式二:方法改變
- 走對象的那一套就不行了,因為并不是重新賦值掖桦,雖然改變了數(shù)組自身但并不會觸發(fā)
set
本昏,原有的響應(yīng)式系統(tǒng)根本感知不到,所以我們接下來就分析枪汪,vue
是如何解決使用數(shù)組方法改變自身觸發(fā)視圖的涌穆。
Dep收集依賴的位置
上一篇它的聲音并不大怔昨,現(xiàn)在我們來重新認識它。Dep
類的主要作用就是管理依賴宿稀,在響應(yīng)式系統(tǒng)中會有兩個地方要實例化它趁舀,當然它們都會進行依賴的收集,首先是之前具體包裝的時候:
function defineReactive(obj, key, val) {
const dep = new Dep() // 自動依賴管理器
...
Object.defineProperty(obj, key, {
get() {...},
set() {...}
})
}
這里它會對每個讀取到的key
都進行依賴收集祝沸,無論是對象/數(shù)組/原始類型矮烹,如果是通過重新賦值觸發(fā)set
就會使用這里收集到的依賴進行更新,筆者這里就把它命名為自動依賴管理器罩锐,方便和之后的區(qū)分奉狈。
還有一個地方也會對它進行實例化就是Observer
類中:
class Observer {
constructor(value) {
this.dep = new Dep() // 手動依賴管理器
...
}
}
這個依賴管理器并不能通過set
觸發(fā),而且是只會收集對象/數(shù)組的依賴涩惑。也就是說對象的依賴會被收集兩次仁期,一次在自動依賴管理器內(nèi),一次在這里竭恬,為什么要收集兩次跛蛋,本章之后說明。而最重要的是數(shù)組使用方法改變自身去觸發(fā)更新的依賴就是再這收集的痊硕,這個前提還是很有必要交代下的赊级。
數(shù)組的響應(yīng)式原理
數(shù)組響應(yīng)式數(shù)據(jù)的創(chuàng)建
數(shù)組示例:
export default {
data() {
return {
list: [{
name: 'cc',
sex: 'man'
}, {
name: 'ww',
sex: 'woman'
}]
}
}
}
流程開始還是執(zhí)行observe
方法,接下來我們更加詳細分析響應(yīng)式系統(tǒng):
function observe(value) {
if (!isObject(value) { //不是數(shù)組或?qū)ο蟛沓瘢僖? return
}
let ob
if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 避免重復(fù)包裝
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
只要是響應(yīng)式的數(shù)據(jù)都會有一個__ob__
的屬性此衅,它是在Observer
類中掛載的,如果已經(jīng)有__ob__
屬性就直接賦值給ob
亭螟,不會再次去創(chuàng)建Observer
實例,避免重復(fù)包裝骑歹。首次肯定沒__ob__
屬性了预烙,所以再重新看下Observer
類的定義:
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep() // 手動依賴管理器
def(value, '__ob__', this) // 掛載__ob__屬性,三個參數(shù)
...
}
}
現(xiàn)在看Observer
類會豐富很多道媚,首先定義一個手動依賴管理器扁掸,然后掛載一個不可枚舉的__ob__
屬性到傳入的參數(shù)下,表示它的一個響應(yīng)式的數(shù)據(jù)最域,而且__ob__
的值就是當前Observer
類的實例谴分,它擁有實例上的所有屬性和方法,這很重要镀脂,我們接下來看下def
是如何完成屬性掛載的:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
其實就是一個簡單的封裝牺蹄,第四個參數(shù)不傳,enumerable
項就是不可枚舉的了薄翅。接著看Observer
類的定義:
class Observer {
constructor(value) {
...
if (Array.isArray(value)) { // 數(shù)組
...
} else { // 對象
this.walk(value) // {list: [{...}, {...}]}
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
首次傳入還是對象的格式沙兰,所以會執(zhí)行walk
遍歷的將對象每個屬性包裝為響應(yīng)式的氓奈,再來看下defineReactive
方法:
function defineReactive(obj, key, val) {
const dep = new Dep() // 自動依賴管理器
val = obj[key] // val為數(shù)組 [{...}, {...}]
let childOb = observe(val) // 返回Observer類實例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 依賴收集
if (Dep.target) {
dep.depend() // 自動依賴管理器收集依賴
if (childOb) { // 只有對象或數(shù)組才有返回值
childOb.dep.depend() // 手動依賴管理器收集依賴
if (Array.isArray(val)) { 如果是數(shù)組
dependArray(val) // 將數(shù)組每一項包裝為響應(yīng)式
}
}
}
return value
},
set(newVal) {
...
}
}
}
首先遞歸執(zhí)行observe(val)
會有一個返回值了,如果是對象或數(shù)組的話鼎天,childOb
就是Observer
類的實例舀奶。所以在get
內(nèi)的childOb.dep.depend()
執(zhí)行的就是Observer
類里定義的dep
進行依賴收集,收集的render watcher
跟自動依賴管理器是一樣的斋射。接下來如果是數(shù)組就執(zhí)行dependArray
方法:
function dependArray (value) {
for (let e, i = 0, i < value.length; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend() // 是響應(yīng)式數(shù)據(jù)
if (Array.isArray(e)) { // 如果是嵌套數(shù)組
dependArray(e) // 遞歸調(diào)用自己
}
}
}
這個方法的作用就是遞歸的為每一項收集依賴育勺,這里每一項都必須要有__ob__
屬性,然后執(zhí)行Observer
類里的dep
手動依賴收集器進行依賴收集罗岖。我們現(xiàn)在知道數(shù)組的依賴放哪了涧至,現(xiàn)在關(guān)心的是在哪里去更新這個收集到的依賴。
數(shù)組方法更新依賴
回到defineReactive
方法呀闻,看看let childOb = observe(val)
這句代碼:
function defineReactive(obj, key, val) {
...
val = obj[key] // val為數(shù)組 [{...}, {...}]
let childOb = observe(val) // 看這句
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {...},
set(newVal) {...}
}
}
通過求值化借,val
現(xiàn)在就是具體的數(shù)組,傳入到observe
內(nèi)以數(shù)組的形式執(zhí)行捡多,我們又回到Observer
類中:
class Observer {
constructor(value) {
...
if (Array.isArray(value)) { // 數(shù)組
const augment = hasProto // 第一句
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys) // 第二句
this.observeArray(value) // 第三句
} else { // 對象
...
}
}
observeArray(items) {
for (let i = 0, i < items.length; i++) {
observe(items[i])
}
}
}
數(shù)組方法改變自身觸發(fā)視圖原理:首先覆蓋數(shù)組的
__proto__
隱式原型蓖康,借用數(shù)組原生的方法,定義vue
內(nèi)部自定義的數(shù)組異變方法攔截原生方法垒手,再調(diào)用異變方法改變自身之后手動觸發(fā)依賴蒜焊。
有了這只指向月亮的手,我們現(xiàn)在就一起去往心中的月亮科贬。首先分析第一句:
const augment = hasProto ? protoAugment : copyAugment
--------------------------------------------------------
const hasProto = '__proto__' in {}
function protoAugment (target, src) { // src為攔截器
target.__proto__ = src
}
function copyAugment (target, src, keys) { // src為攔截器
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
__proto__
這個屬性并不是所有瀏覽器都有的泳梆,筆者之前也一直以為這是一個通用屬性,原來IE11
才開始有這個屬性榜掌,通過'__protp__' in {}
也可以快速判斷當前瀏覽瀏覽器是否IE10
以上优妙?確實用過,好用憎账!
是否有__proto__
屬性處理方法也不相同套硼,如果有的的話,直接在protoAugment
方法內(nèi)使用攔截器覆蓋胞皱;如果沒有__proto__
屬性邪意,那就在當前調(diào)用數(shù)組下掛載攔截器里的異變數(shù)組方法。
實現(xiàn)原理都是根據(jù)原型鏈的特性反砌,再數(shù)組使用原生方法之前加一個攔截器雾鬼,攔截器內(nèi)定義的都是可以改變數(shù)組自身的異變方法,如果攔截器內(nèi)沒有就向一層去找宴树。
接下來分析第二句策菜,也是整個數(shù)組方法實現(xiàn)的核心:
augment(value, arrayMethods, arrayKeys)
----------------------------------------------------------------------------
const arrayProto = Array.prototype // 數(shù)組原型,有所有數(shù)組原生方法
const arrayMethods = Object.create(arrayProto) // 創(chuàng)建空對象攔截器
const methodsToPatch = [ // 七個數(shù)組使用會改變自身的方法
'push','pop','shift','unshift','splice','sort','reverse'
]
methodsToPatch.forEach(function (method) { // 往攔截器下掛載異變方法
const original = arrayProto[method] // 過濾出七個數(shù)組原生原始方法
def(arrayMethods, method, function mutator (...args) { // 不定參數(shù)
const result = original.apply(this, args) // 借用原生方法,this就是調(diào)用的數(shù)組
const ob = this.__ob__ // 之前Observer類下掛載的__ob__
let inserted // 臨時保存數(shù)組新增的值
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) {
ob.observeArray(inserted) // 執(zhí)行Observer類中的observeArray方法
}
ob.dep.notify() // 觸發(fā)手動依賴收集器內(nèi)的依賴
return result // 返回數(shù)組執(zhí)行結(jié)果
})
})
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// 獲取攔截器內(nèi)掛載好的七個方法key的數(shù)組集合做入,用于沒有__proto__的情況
首先獲取數(shù)組的所有原生方法冒晰,從中過濾出七個調(diào)用可以改變自身的方法,然后創(chuàng)建攔截器在它下面掛載七個經(jīng)過異變的方法竟块,這個異變方法的使用效果和原生方法是一致的壶运,因為就是使用apply
借用的,將執(zhí)行后的結(jié)果保存給result
浪秘,比如:
const arr = [1, 2, 3]
const result = arr.push(4)
這個時候arr
就變成了[1,2,3,4]
蒋情,result
保存的就是新數(shù)組的長度,既然模仿就模仿的像一點耸携。
接下來的賦值const ob = this.__ob__
棵癣,之前定義的__ob__
不僅僅是標記位,保存的也是Observer
類的實例夺衍。
有三個操作數(shù)組的方法是會添加新值的狈谊,使用inserted
變量保存新添的值。如果是使用splice
方法沟沙,就將前面兩個表示位置的參數(shù)截取掉河劝。然后使用observeArray
方法將新添加的參數(shù)包裝為響應(yīng)式的。
最后通知手動依賴管理器內(nèi)收集到的依賴派發(fā)更新矛紫,返回數(shù)組執(zhí)行后的結(jié)果赎瞎。
最后執(zhí)行第三句:
this.observeArray(value)
將數(shù)組內(nèi)的是數(shù)組或?qū)ο蟮拿恳豁椂及b成響應(yīng)式的。所以當數(shù)組再使用方法時颊咬,首先會去arrayMethods
攔截器內(nèi)查找是否是異變方法盹憎,不是的話才去調(diào)用數(shù)組原生方法:
export default {
data() {
return {
list: [1, 2, 3]
}
},
methods: {
changeArr1() {
this.list.push(4) // 調(diào)用攔截器里的異變方法
},
changeArr2() {
this.list = this.list.concat(5)
// 調(diào)用原生方法犬庇,因為攔截器里沒有度迂,必須重新賦值因為不會改變自身
}
}
}
至此數(shù)組響應(yīng)式系統(tǒng)相關(guān)的也講解完畢节腐,整個響應(yīng)式系統(tǒng)也分析完了。我們來總結(jié)下吧麸澜,數(shù)組和對象它們收集依賴都是在get
方法里哟绊,但是依賴存放位置并不同,對象是在defineReactive
方法的dep
內(nèi)痰憎,數(shù)組是Observer
類中的dep
里;依賴的觸發(fā)對象可以直接在set
方法中派發(fā)更新攀涵,而數(shù)組是在自己定義的異變數(shù)組方法最后手動觸發(fā)的铣耘。
同樣數(shù)組響應(yīng)式也是不是完美的,它也有缺點:
export default {
data() {
return {
list: [1, 2, 3]
}
},
methods: {
changeListItem() { // 改變數(shù)組某一項
this.list[1] = 5
},
changeListLength() { // 改變數(shù)組長度
this.list.length = 0
}
}
}
以上兩種方式都改變了數(shù)組以故,但響應(yīng)式是無法監(jiān)聽到的蜗细,因為不會觸發(fā)set
也沒用使用數(shù)組方法去改變。不過大家還記得我們之前介紹的手動依賴管理器么?我們只要手動去通知它更新依賴就可以觸發(fā)視圖變更~
export default {
data() {
return {
list: [1, 2, 3],
info: { name: 'cc' }
}
},
methods: {
changeListItem() { // 改變數(shù)組某一項
this.list[1] = 5
this.list.__ob__.dep.notify() // 手動通知
},
changeListLength() { // 改變數(shù)組長度
this.list.length = 0
this.list.__ob__.dep.notify() // 手動通知
},
changeInfo() {
this.info.sex = 'man'
this.info.__ob__.dep.notify() // 對象也可以
}
}
}
常規(guī)的對象增加屬性是不會被感知到的炉媒,也可以使用手動通知的形式觸發(fā)依賴踪区,知道這個原理還是很cool
的~
官方填坑
上面的奇技淫巧并不被推薦使用,我們還是介紹下官方推薦的彌補響應(yīng)式不足的兩個API
吊骤,$set
和$delete
缎岗,其實它們只是處理一些情況,都不滿足的最后還是調(diào)了一下手動依賴管理器來實現(xiàn)白粉,只是進行了簡單的二次封裝传泊。
this.$set || Vue.set
function set(target, key, val) {
if(Array.isArray(target)) { // 數(shù)組
target.length = Math.max(target.length, key) // 最大值為長度
target.splice(key, 1, val) // 移除一位,異變方法派發(fā)更新
return val
}
if(key in target && !(key in Object.prototype)) { // key屬于target
target[key] = val // 賦值操作觸發(fā)set
return val
}
if(!target.__ob__) { // 普通對象賦值操作
target[key] = val
return val
}
defineReactive(target.__ob__.value, key, val) // 將新值包裝為響應(yīng)式
target.__ob__.dep.notify() // 手動觸發(fā)通知
return val
}
首先判斷target
是否是數(shù)組鸭巴,是數(shù)組的話第二個參數(shù)就是長度了眷细,設(shè)置數(shù)組的長度,然后使用splice
這個異變方法插入val
鹃祖。
然后是判斷key
是否屬于target
溪椎,屬于的話就是賦值操作了,這個會觸發(fā)set
去派發(fā)更新恬口。接下來如果target
并不是響應(yīng)式數(shù)據(jù)校读,那就是普通對象,那就設(shè)置一個對應(yīng)key
吧楷兽。最后以上情況都不滿足地熄,說明是在響應(yīng)式數(shù)據(jù)上新增了一個屬性,把新增的屬性轉(zhuǎn)為響應(yīng)式數(shù)據(jù)芯杀,然后通知手動依賴管理器派發(fā)更新端考。
this.$delete || Vue.delete
function del (target, key) {
if (Array.isArray(target)) { // 數(shù)組
target.splice(key, 1) // 移除指定下表
return
}
if (!hasOwn(target, key)) { // key不屬于target,再見
return
}
delete target[key] // 刪除對象指定key
if (!target.__ob__) { // 普通對象揭厚,再見
return
}
target.__ob__.dep.notify() // 手動派發(fā)更新
}
this.$delete
就更加簡單了却特,首先如果是數(shù)組就使用異變方法splice
移除指定下標值。如果target
是對象但key
不屬于它筛圆,再見裂明。然后刪除制定key
的值,如果target
不是響應(yīng)式對象太援,刪除的就是普通對象一個值闽晦,刪了就刪了。否則通知手動依賴管理器派發(fā)更新視圖提岔。
最后按照慣例我們還是以一道vue
可能會被問到的面試題作為本章的結(jié)束~
面試官微笑而又不失禮貌的問道:
- 請簡單描述下
vue
響應(yīng)式系統(tǒng)仙蛉?
懟回去:
- 簡單來說就是使用
Object.defineProperty
這個API
為數(shù)據(jù)設(shè)置get
和set
。當讀取到某個屬性時碱蒙,觸發(fā)get
將讀取它的組件對應(yīng)的render watcher
收集起來荠瘪;當重置賦值時夯巷,觸發(fā)set
通知組件重新渲染頁面。如果數(shù)據(jù)的類型是數(shù)組的話哀墓,還做了單獨的處理趁餐,對可以改變數(shù)組自身的方法進行重寫,因為這些方法不是通過重新賦值改變的數(shù)組篮绰,不會觸發(fā)set
后雷,所以要單獨處理。響應(yīng)系統(tǒng)也有自身的不足阶牍,所以官方給出了$set
和$delete
來彌補喷面。
順手點個贊或關(guān)注唄,找起來也方便~
分享一個筆者自己寫的組件庫走孽,哪天可能會用的上了 ~ ↓