前面我們學習了vue的響應式原理,我們知道了vue2底層是通過
Object.defineProperty
來實現資料響應式的,但是單有這個還不夠,我們在data中定義的資料可能沒有用於模版渲染,修改這些資料同樣會出發setter導致重新渲染,所以vue在這裡做了優化,通過收集依賴來判斷哪些資料的變更需要觸發檢視更新。
前言
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章~
我們先來考慮兩個問題:
- 1.我們如何知道哪裡用了data裡面的資料?
- 2.資料變更了,如何通知render更新檢視?
在檢視渲染過程中,被使用的資料需要被記錄下來,並且只針對這些資料的變化觸發檢視更新
這就需要做依賴收集,需要為屬性建立 dep 用來收集渲染 watcher
我們可以來看下官方介紹圖,這裡的collect as Dependency
就是原始碼中的dep.depend()
依賴收集,Notify
就是原始碼中的dep.notify()
通知訂閱者
依賴收集中的各個類
Vue原始碼中負責依賴收集的類有三個:
-
Observer:
可觀測類
,將陣列/物件轉成可觀測資料,每個Observer
的例項成員中都有一個Dep
的例項(上一篇文章實現過這個類) -
Dep:
觀察目標類
,每一個資料都會有一個Dep
類例項,它內部有個subs佇列,subs就是subscribers的意思,儲存著依賴本資料的觀察者
,當本資料變更時,呼叫dep.notify()
通知觀察者 -
Watcher:
觀察者類
,進行觀察者函式
的包裝處理。如render()
函式,會被進行包裝成一個Watcher
例項
依賴就是Watcher
,只有Watcher
觸發的getter
才會收集依賴,哪個Watcher
觸發了getter
,就把哪個watcher
收集到Dep
中。Dep使用釋出訂閱模式,當資料發生變化時,會迴圈依賴列表,把所有的watcher
都通知一遍,這裡我自己畫了一張更清晰的圖:
Observer類
這個類我們上一期已經實現過了,這一期我們主要增加的是defineReactive
在劫持資料gētter
時進行依賴收集,劫持資料setter
時進行通知依賴更新,這裡就是Vue收集依賴的入口
class Observer {
constructor(v){
// 每一個Observer例項身上都有一個Dep例項
this.dep = new Dep()
// 如果資料層次過多,需要遞迴去解析物件中的屬性,依次增加set和get方法
def(v,'__ob__',this) //給資料掛上__ob__屬性,表明已觀測
if(Array.isArray(v)) {
// 把重寫的陣列方法重新掛在陣列原型上
v.__proto__ = arrayMethods
// 如果陣列裡放的是物件,再進行監測
this.observerArray(v)
}else{
// 非陣列就直接呼叫defineReactive將資料定義成響應式物件
this.walk(v)
}
}
observerArray(value) {
for(let i=0; i<value.length;i++) {
observe(value[i])
}
}
walk(data) {
let keys = Object.keys(data); //獲取物件key
keys.forEach(key => {
defineReactive(data,key,data[key]) // 定義響應式物件
})
}
}
function defineReactive(data,key,value){
const dep = new Dep() //例項化dep,用於收集依賴,通知訂閱者更新
observe(value) // 遞迴實現深度監測,注意效能
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
//獲取值
// 如果現在處於依賴的手機階段
if(Dep.target) {
dep.depend()
}
// 依賴收集
return value
},
set(newV) {
//設定值
if(newV === value) return
observe(newV) //繼續劫持newV,使用者有可能設定的新值還是一個物件
value = newV
console.log('值變化了:',value)
// 釋出訂閱模式,通知
dep.notify()
// cb() //訂閱者收到訊息回撥
}
})
}
將Observer
類的例項掛在__ob__
屬性上,提供後期資料觀察時使用,例項化Dep
類例項,並且將物件/陣列
作為value屬性儲存下來 - 如果value是個物件,就執行walk()
過程,遍歷物件把每一項資料都變為可觀測資料(呼叫defineReactive
方法處理) - 如果value是個陣列,就執行observeArray()
過程,遞迴地對陣列元素呼叫observe()
。
Dep類(訂閱者)
Dep
類的角色是一個訂閱者
,它主要作用是用來存放Watcher
觀察者物件,每一個資料都有一個Dep
類例項,在一個專案中會有多個觀察者,但由於JavaScript是單執行緒的,所以在同一時刻,只能有一個觀察者
在執行,此刻正在執行的那個觀察者
所對應的Watcher
例項就會賦值給Dep.target
這個變數,從而只要訪問Dep.target
就能知道當前的觀察者是誰。
var uid = 0
export default class Dep {
constructor() {
this.id = uid++
this.subs = [] // subscribes訂閱者,儲存訂閱者,這裡放的是Watcher的例項
}
//收集觀察者
addSub(watcher) {
this.subs.push(watcher)
}
// 新增依賴
depend() {
// 自己指定的全域性位置,全域性唯一
//自己指定的全域性位置,全域性唯一,例項化Watcher時會賦值Dep.target = Watcher例項
if(Dep.target) {
this.addSub(Dep.target)
}
}
//通知觀察者去更新
notify() {
console.log('通知觀察者更新~')
const subs = this.subs.slice() // 複製一份
subs.forEach(w=>w.update())
}
}
Dep
實際上就是對Watcher
的管理,Dep
脫離Watcher
單獨存在是沒有意義的。
Dep
是一個釋出者,可以訂閱多個觀察者,依賴收集之後Dep
中會有一個subs
存放一個或多個觀察者,在資料變更的時候通知所有的watcher
。Dep
和Observer
的關係就是Observer
監聽整個data,遍歷data的每個屬性給每個屬性繫結defineReactive
方法劫持getter
和setter
, 在getter
的時候往Dep
類裡塞依賴(dep.depend)
,在setter
的時候通知所有watcher
進行update(dep.notify)
。
Watcher類(觀察者)
Watcher
類的角色是觀察者
,它關心的是資料,在資料變更之後獲得通知,通過回撥函式進行更新。
由上面的Dep
可知,Watcher
需要實現以下兩個功能:
dep.depend()
的時候往subs裡面新增自己dep.notify()
的時候呼叫watcher.update()
,進行更新檢視
同時要注意的是,watcher有三種:render watcher、 computed watcher、user watcher(就是vue方法中的那個watch)
var uid = 0
import {parsePath} from "../util/index"
import Dep from "./dep"
export default class Watcher{
constructor(vm,expr,cb,options){
this.vm = vm // 元件例項
this.expr = expr // 需要觀察的表示式
this.cb = cb // 當被觀察的表示式發生變化時的回撥函式
this.id = uid++ // 觀察者例項物件的唯一標識
this.options = options // 觀察者選項
this.getter = parsePath(expr)
this.value = this.get()
}
get(){
// 依賴收集,把全域性的Dep.target設定為Watcher本身
Dep.target = this
const obj = this.vm
let val
// 只要能找就一直找
try{
val = this.getter(obj)
} finally{
// 依賴收集完需要將Dep.target設為null,防止後面重複新增依賴。
Dep.target = null
}
return val
}
// 當依賴發生變化時,觸發更新
update() {
this.run()
}
run() {
this.getAndInvoke(this.cb)
}
getAndInvoke(cb) {
let val = this.get()
if(val !== this.value || typeof val == 'object') {
const oldVal = this.value
this.value = val
cb.call(this.target,val, oldVal)
}
}
}
要注意的是,watcher
中有個sync
屬性,絕大多數情況下,watcher
並不是同步更新的,而是採用非同步更新的方式,也就是呼叫queueWatcher(this)
推送到觀察者佇列當中,待nextTick
的時候進行呼叫。
這裡的parsePath
函式比較有意思,它是一個高階函式,用於把表示式解析成getter,也就是取值,我們可以試著寫寫看:
export function parsePath (str) {
const segments = str.split('.') // 先將表示式以.切割成一個資料
// 它會返回一個函式
return obj = > {
for(let i=0; i< segments.length; i++) {
if(!obj) return
// 遍歷表示式取出最終值
obj = obj[segments[i]]
}
return obj
}
}
Dep與Watcher的關係
watcher 中例項化了 dep 並向 dep.subs 中新增了訂閱者, dep 通過 notify 遍歷了 dep.subs 通知每個 watcher 更新。
總結
依賴收集
initState
時,對computed
屬性初始化時,觸發computed watcher
依賴收集initState
時,對偵聽屬性初始化時,觸發user watcher
依賴收集(這裡就是我們常寫的那個watch)render()
時,觸發render watcher
依賴收集re-render
時,render()
再次執行,會移除所有subs
中的watcer
的訂閱,重新賦值。
observe->walk->defineReactive->get->dep.depend()->
watcher.addDep(new Dep()) ->
watcher.newDeps.push(dep) ->
dep.addSub(new Watcher()) ->
dep.subs.push(watcher)
派發更新
- 元件中對響應的資料進行了修改,觸發
defineReactive
中的setter
的邏輯 - 然後呼叫
dep.notify()
- 最後遍歷所有的
subs(Watcher 例項)
,呼叫每一個watcher
的update
方法。
set ->
dep.notify() ->
subs[i].update() ->
watcher.run() || queueWatcher(this) ->
watcher.get() || watcher.cb ->
watcher.getter() ->
vm._update() ->
vm.__patch__()
推薦閱讀
原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」。
我是南玖,我們下一期見!!!