vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社群湧現了一大票vue原始碼閱讀類的文章,在下借這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀原始碼時的想法進行總結,出產一些文章,作為自己思考的總結,本人水平有限,歡迎留言討論~
目標Vue版本:2.5.17-beta.0
vue原始碼註釋:github.com/SHERlocked9…
宣告:文章中原始碼的語法都使用 Flow,並且原始碼根據需要都有刪節(為了不被迷糊 @_@),如果要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~
1. 響應式系統
通過官網的介紹我們知道 Vue.js 是一個MVVM框架,它並不關心檢視變化,而通過資料驅動檢視更新,這讓我們的狀態管理非常簡單,而這是怎麼實現的呢。盜用官網一張圖
每個元件例項都有相應的 Watcher
例項物件,它會在元件渲染的過程中把屬性記錄為依賴,之後當依賴項的 setter
被呼叫時,會通知 watcher
重新計算,從而致使它關聯的元件得以更新。
這裡有三個重要的概念 Observe
、Dep
、Watcher
,分別位於src/core/observer/index.js
、src/core/observer/dep.js
、src/core/observer/watcher.js
Observe
類主要給響應式物件的屬性新增getter/setter
用於依賴收集與派發更新Dep
類用於收集當前響應式物件的依賴關係Watcher
類是觀察者,例項分為渲染 watcher、計算屬性 watcher、偵聽器 watcher三種
2. 程式碼實現
2.1 initState
響應式化的入口位於 src/core/instance/init.js 的 initState
中:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/core/instance/state.js export function initState(vm: Component) { const opts = vm.$options if (opts.props) initProps(vm, opts.props) // 初始化props if (opts.methods) initMethods(vm, opts.methods) // 初始化methods if (opts.data) initData(vm) // 初始化data if (opts.computed) initComputed(vm, opts.computed) // 初始化computed if (opts.watch) initWatch(vm, opts.watch) // 初始化watch } } |
它非常規律的定義了幾個方法來初始化 props
、methods
、data
、computed
、wathcer
,這裡看一下 initData
方法,來窺一豹
1 2 3 4 5 6 7 8 9 10 |
// src/core/instance/state.js function initData(vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} observe(data, true /* asRootData */) // 給data做響應式處理 } 複製程式碼 |
首先判斷了下 data 是不是函式,是則取返回值不是則取自身,之後有一個 observe
方法對 data
進行處理,這個方法嘗試給建立一個Observer例項 __ob__
,如果成功建立則返回新的Observer例項,如果已有Observer例項則返回現有的Observer例項
2.2 Observer/defineReactive
1 2 3 4 5 6 7 8 |
// src/core/observer/index.js export function observe (value: any, asRootData: ?boolean): Observer | void { let ob: Observer | void ob = new Observer(value) return ob } |
這個方法主要用 data
作為引數去例項化一個 Observer 物件例項,Observer 是一個 Class,用於依賴收集和 notify
更新,Observer 的建構函式使用 defineReactive
方法給物件的鍵響應式化,給物件的屬性遞迴新增 getter/setter
,當data被取值的時候觸發 getter
並蒐集依賴,當被修改值的時候先觸發 getter
再觸發 setter
並派發更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// src/core/observer/index.js export class Observer { value: any; dep: Dep; constructor (value: any) { value: any; this.dep = new Dep() def(value, '__ob__', this) // def方法保證不可列舉 this.walk(value) } // 遍歷物件的每一個屬性並將它們轉換為getter/setter walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { // 把所有可遍歷的物件響應式化 defineReactive(obj, keys[i]) } } } export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) { const dep = new Dep() // 在每個響應式鍵值的閉包中定義一個dep物件 // 如果之前該物件已經預設了getter/setter則將其快取,新定義的getter/setter中會將其執行 const getter = property && property.get const setter = property && property.set let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 如果原本物件擁有getter方法則執行 if (Dep.target) { // 如果當前有watcher在讀取當前值 dep.depend() // 那麼進行依賴收集,dep.addSub } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 先getter if (newVal === value || (newVal !== newVal && value !== value)) { // 如果跟原來值一樣則不管 return } if (setter) { setter.call(obj, newVal) } // 如果原本物件擁有setter方法則執行 else { val = newVal } dep.notify() // 如果發生變更,則通知更新,呼叫watcher.update() } }) } |
getter
的時候進行依賴的收集,注意這裡,只有在 Dep.target
中有值的時候才會進行依賴收集,這個 Dep.target
是在Watcher例項的 get
方法呼叫的時候 pushTarget
會把當前取值的watcher推入 Dep.target
,原先的watcher壓棧到 targetStack
棧中,當前取值的watcher取值結束後出棧並把原先的watcher值賦給 Dep.target
,cleanupDeps
最後把新的 newDeps
裡已經沒有的watcher清空,以防止檢視上已經不需要的無用watcher觸發
setter
的時候首先 getter
,並且比對舊值沒有變化則return,如果發生變更,則dep通知所有subs中存放的依賴本資料的Watcher例項 update
進行更新,這裡 update
中會 queueWatcher( )
非同步推送到排程者觀察者佇列 queue
中,在nextTick時 flushSchedulerQueue( )
把佇列中的watcher取出來執行 watcher.run
且執行相關鉤子函式
2.3 Dep
上面多次提到了一個關鍵詞 Dep
,他是依賴收集的容器,或者稱為依賴蒐集器,他記錄了哪些Watcher依賴自己的變化,或者說,哪些Watcher訂閱了自己的變化;這裡引用一個網友的發言:
@liuhongyi0101 :簡單點說就是引用計數 ,誰借了我的錢,我就把那個人記下來,以後我的錢少了 我就通知他們說我沒錢了
而把借錢的人記下來的小本本就是這裡 Dep
例項裡的subs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/core/observer/dep.js let uid = 0 // Dep例項的id,為了方便去重 export default class Dep { static target: ?Watcher // 當前是誰在進行依賴的收集 id: number subs: Array<Watcher> // 觀察者集合 constructor() { this.id = uid++ // Dep例項的id,為了方便去重 this.subs = [] // 儲存收集器中需要通知的Watcher } addSub(sub: Watcher) { ... } /* 新增一個觀察者物件 */ removeSub(sub: Watcher) { ... } /* 移除一個觀察者物件 */ depend() { ... } /* 依賴收集,當存在Dep.target的時候把自己新增觀察者的依賴中 */ notify() { ... } /* 通知所有訂閱者 */ } const targetStack = [] // watcher棧 export function pushTarget(_target: ?Watcher) { ... } /* 將watcher觀察者例項設定給Dep.target,用以依賴收集。同時將該例項存入target棧中 */ export function popTarget() { ... } /* 將觀察者例項從target棧中取出並設定給Dep.target */ |
這裡 Dep
的例項中的 subs
蒐集的依賴就是 watcher 了,它是 Watcher
的例項,將來用來通知更新
2.4 Watcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// src/core/observer/watcher.js /* 一個解析表示式,進行依賴收集的觀察者,同時在表示式資料變更時觸發回撥函式。它被用於$watch api以及指令 */ export default class Watcher { constructor( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean // 是否是渲染watcher的標誌位 ) { this.getter = expOrFn // 在get方法中執行 if (this.computed) { // 是否是 計算屬性 this.value = undefined this.dep = new Dep() // 計算屬性建立過程中並未求值 } else { // 不是計算屬性會立刻求值 this.value = this.get() } } /* 獲得getter的值並且重新進行依賴收集 */ get() { pushTarget(this) // 設定Dep.target = this let value value = this.getter.call(vm, vm) popTarget() // 將觀察者例項從target棧中取出並設定給Dep.target this.cleanupDeps() return value } addDep(dep: Dep) { ... } /* 新增一個依賴關係到Deps集合中 */ cleanupDeps() { ... } /* 清理newDeps裡沒有的無用watcher依賴 */ update() { ... } /* 排程者介面,當依賴發生改變的時候進行回撥 */ run() { ... } /* 排程者工作介面,將被排程者回撥 */ getAndInvoke(cb: Function) { ... } evaluate() { ... } /* 收集該watcher的所有deps依賴 */ depend() { ... } /* 收集該watcher的所有deps依賴,只有計算屬性使用 */ teardown() { ... } /* 將自身從所有依賴收集訂閱列表刪除 */ } |
get
方法中執行的 getter
就是在一開始new渲染watcher時傳入的 updateComponent = () => { vm._update(vm._render(), hydrating) }
,這個方法首先 vm._render()
生成渲染VNode樹,在這個過程中完成對當前Vue例項 vm
上的資料訪問,觸發相應一眾響應式物件的 getter
,然後 vm._update()
去 patch
注意這裡的 get
方法最後執行了 getAndInvoke
,這個方法首先遍歷watcher中存的 deps
,移除 newDep
中已經沒有的訂閱,然後 depIds = newDepIds; deps = newDeps
,把 newDepIds
和 newDeps
清空。每次新增完新的訂閱後移除舊的已經不需要的訂閱,這樣在某些情況,比如 v-if
已不需要的模板依賴的資料發生變化時就不會通知watcher去 update
了
2.5 小結
整個收集的流程大約是這樣的,可以對照著上面的流程看一下
watcher 有下面幾種使用場景:
render watcher
渲染 watcher,渲染檢視用的 watchercomputed watcher
計算屬性 watcher,因為計算屬性即依賴別人也被人依賴,因此也會持有一個Dep
例項watch watcher
偵聽器 watcher
只要會被別的觀察者 (watchers
) 依賴,比如data、data的屬性、計算屬性、props,就會在閉包裡生成一個 Dep 的例項 dep
並在被呼叫 getter
的時候 dep.depend
收集它被誰依賴了,並把被依賴的watcher存放到自己的subs中 this.subs.push(sub)
,以便在自身改變的時候通知 notify
存放在 dep.subs
陣列中依賴自己的 watchers
自己改變了,請及時 update
~
只要依賴別的響應式化物件的物件,都會生成一個觀察者 watcher
,用來統計這個 watcher
依賴了哪些響應式物件,在這個 watcher
求值前把當前 watcher
設定到全域性 Dep.target
,並在自己依賴的響應式物件發生改變的時候及時 update
本文是系列文章,隨後會更新後面的部分,共同進步~
網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~