原文連接vue
其實在一年前我已經寫過一篇關於 vue響應式原理的文章,可是最近我翻開看看發現講的內容和我如今內心想的有些不太同樣,因此我打算從新寫一篇更通俗易懂的文章。git
個人目標是能讓讀者讀完我寫的文章能學到知識,有一部分文章標題都以深刻淺出開頭,目的是把一個複雜的東西排除掉干擾學習的因素後剩下的核心原理經過很簡單的描述來讓讀者學習到知識。github
關於vue的內部原理其實有不少個重要的部分,變化偵測,模板編譯,virtualDOM,總體運行流程等。swift
今天主要把變化偵測這部分單獨拿出來說一講。api
關於變化偵測首先要問一個問題,在 js 中,如何偵測一個對象的變化,其實這個問題仍是比較簡單的,學過js的都能知道,js中有兩種方法能夠偵測到變化,Object.defineProperty
和 ES6 的proxy
。數組
到目前爲止vue仍是用的 Object.defineProperty
,因此咱們拿 Object.defineProperty
來舉例子說明這個原理。app
這裏我想說的是,無論之後vue是否會用 proxy
重寫這部分,我講的是原理,並非api,因此不論之後vue會怎樣改,這個原理是不會變的,哪怕vue用了其餘徹底不一樣的原理實現了變化偵測,可是本篇文章講的原理同樣能夠實現變化偵測,原理這個東西是不會過期的。函數
以前我寫文章有一個毛病就是喜歡對着源碼翻譯,結果過了半年一年人家源碼改了,我寫的文章就一毛錢都不值了,並且對着源碼翻譯還有一個缺點是對讀者的要求有點偏高,讀者若是沒看過源碼或者看的和我不是一個版本,那根本就不知道我在說什麼。工具
好了不說廢話了,繼續講剛纔的內容。學習
知道 Object.defineProperty
能夠偵測到對象的變化,那麼咱們瞬間能夠寫出這樣的代碼:
function defineReactive (data, key, val) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { return val }, set: function (newVal) { if(val === newVal){ return } val = newVal } }) }
寫一個函數封裝一下 Object.defineProperty
,畢竟 Object.defineProperty
的用法這麼複雜,封裝一下我只須要傳遞一個 data,和 key,val 就好了。
如今封裝好了以後每當 data
的 key
讀取數據 get
這個函數能夠被觸發,設置數據的時候 set
這個函數能夠被觸發,可是,,,,,,,,,,,,,,,,,,發現好像並沒什麼鳥用?
如今我要問第二個問題,「怎麼觀察?」
思考一下,咱們之因此要觀察一個數據,目的是爲了當數據的屬性發生變化時,能夠通知那些使用了這個 key
的地方。
舉個?:
<template> <div>{{ key }}</div> <p>{{ key }}</p> </template>
模板中有兩處使用了 key
,因此當數據發生變化時,要把這兩處都通知到。
因此上面的問題,個人回答是,先收集依賴,把這些使用到 key
的地方先收集起來,而後等屬性發生變化時,把收集好的依賴循環觸發一遍就行了~
總結起來其實就一句話,getter中,收集依賴,setter中,觸發依賴。
如今咱們已經有了很明確的目標,就是要在getter中收集依賴,那麼咱們的依賴收集到哪裏去呢??
思考一下,首先想到的是每一個 key
都有一個數組,用來存儲當前 key
的依賴,假設依賴是一個函數存在 window.target
上,先把 defineReactive
稍微改造一下:
function defineReactive (data, key, val) { let dep = [] // 新增 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.push(window.target) // 新增 return val }, set: function (newVal) { if(val === newVal){ return } // 新增 for (let i = 0; i < dep.length; i++) { dep[i](newVal, val) } val = newVal } }) }
在 defineReactive
中新增了數組 dep,用來存儲被收集的依賴。
而後在觸發 set 觸發時,循環dep把收集到的依賴觸發。
可是這樣寫有點耦合,咱們把依賴收集這部分代碼封裝起來,寫成下面的樣子:
export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { this.addSub(Dep.target) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
而後在改造一下 defineReactive
:
function defineReactive (data, key, val) { let dep = new Dep() // 修改 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend() // 修改 return val }, set: function (newVal) { if(val === newVal){ return } dep.notify() // 新增 val = newVal } }) }
這一次代碼看起來清晰多了,順便回答一下上面問的問題,依賴收集到哪?收集到Dep中,Dep是專門用來存儲依賴的。
上面咱們僞裝 window.target
是須要被收集的依賴,細心的同窗可能已經看到,上面的代碼 window.target
已經改爲了 Dep.target
,那 Dep.target
是什麼?咱們究竟要收集誰呢??
收集誰,換句話說是當屬性發生變化後,通知誰。
咱們要通知那個使用到數據的地方,而使用這個數據的地方有不少,並且類型還不同,有多是模板,有多是用戶寫的一個 watch,因此這個時候咱們須要抽象出一個能集中處理這些不一樣狀況的類,而後咱們在依賴收集的階段只收集這個封裝好的類的實例進來,通知也只通知它一個,而後它在負責通知其餘地方,因此咱們要抽象的這個東西須要先起一個好聽的名字,嗯,就叫它watcher吧~
因此如今能夠回答上面的問題,收集誰??收集 Watcher。
watcher 是一箇中介的角色,數據發生變化通知給 watcher,而後watcher在通知給其餘地方。
關於watcher咱們先看一個經典的使用方式:
// keypath vm.$watch('a.b.c', function (newVal, oldVal) { // do something })
這段代碼表示當 data.a.b.c
這個屬性發生變化時,觸發第二個參數這個函數。
思考一下怎麼實現這個功能呢?
好像只要把這個 watcher 實例添加到 data.a.b.c
這個屬性的 Dep 中去就好了,而後 data.a.b.c
觸發時,會通知到watcher,而後watcher在執行參數中的這個回調函數。
好,思考完畢,開工,寫出以下代碼:
class Watch { constructor (expOrFn, cb) { // 執行 this.getter() 就能夠拿到 data.a.b.c this.getter = parsePath(expOrFn) this.cb = cb this.value = this.get() } get () { Dep.target = this value = this.getter.call(vm, vm) Dep.target = undefined } update () { const oldValue = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldValue) } }
這段代碼能夠把本身主動 push
到 data.a.b.c
的 Dep 中去。
由於我在 get
這個方法中,先把 Dep.traget 設置成了 this
,也就是當前watcher實例,而後在讀一下 data.a.b.c
的值。
由於讀了 data.a.b.c
的值,因此確定會觸發 getter
。
觸發了 getter
上面咱們封裝的 defineReactive
函數中有一段邏輯就會從 Dep.target
裏讀一個依賴 push
到 Dep
中。
因此就致使,我只要先在 Dep.target 賦一個 this
,而後我在讀一下值,去觸發一下 getter
,就能夠把 this
主動 push
到 keypath
的依賴中,有沒有很神奇~
依賴注入到 Dep
中去以後,當這個 data.a.b.c
的值發生變化,就把全部的依賴循環觸發 update 方法,也就是上面代碼中 update 那個方法。
update
方法會觸發參數中的回調函數,將value 和 oldValue 傳到參數中。
因此其實不論是用戶執行的 vm.$watch('a.b.c', (value, oldValue) => {})
仍是模板中用到的data,都是經過 watcher 來通知本身是否須要發生變化的。
如今其實已經能夠實現變化偵測的功能了,可是咱們以前寫的代碼只能偵測數據中的一個 key,因此咱們要加工一下 defineReactive
這個函數:
// 新增 function walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } function defineReactive (data, key, val) { walk(val) // 新增 let dep = new Dep() Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend() return val }, set: function (newVal) { if(val === newVal){ return } dep.notify() val = newVal } }) }
這樣咱們就能夠經過執行 walk(data)
,把 data
中的全部 key
都加工成能夠被偵測的,由於是一個遞歸的過程,因此 key
中的 value
若是是一個對象,那這個對象的全部key也會被偵測。
如今又發現了新的問題,data
中不是全部的 value
都是對象和基本類型,若是是一個數組怎麼辦??數組是沒有辦法經過 Object.defineProperty
來偵測到行爲的。
vue 中對這個數組問題的解決方案很是的簡單粗暴,我說說vue是如何實現的,大致上分三步:
第一步:先把原生 Array
的原型方法繼承下來。
第二步:對繼承後的對象使用 Object.defineProperty
作一些攔截操做。
第三步:把加工後能夠被攔截的原型,賦值到須要被攔截的 Array
類型的數據的原型上。
vue的實現
第一步:
const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto)
第二步:
;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method const original = arrayProto[method] Object.defineProperty(arrayMethods, method, { value: function mutator (...args) { console.log(methods) // 打印數組方法 return original.apply(this, args) }, enumerable: false, writable: true, configurable: true }) })
如今能夠看到,每當被偵測的 array
執行方法操做數組時,我均可以知道他執行的方法是什麼,而且打印到 console
中。
如今我要對這個數組方法類型進行判斷,若是操做數組的方法是 push unshift splice (這種能夠新增數組元素的方法),須要把新增的元素用上面封裝的 walk
來進行變化檢測。
而且不論操做數組的是什麼方法,我都要觸發消息,通知依賴列表中的依賴數據發生了變化。
那如今怎麼訪問依賴列表呢,可能咱們須要把上面封裝的 walk
加工一下:
// 工具函數 function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() // 新增 this.vmCount = 0 def(value, '__ob__', this) // 新增 // 新增 if (Array.isArray(value)) { this.observeArray(value) } else { this.walk(value) } } /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { new Observer(items[i]) } } }
咱們定義了一個 Observer
space######space類,他的職責是將 data
轉換成能夠被偵測到變化的 data
,而且新增了對類型的判斷,若是是 value
的類型是 Array
循環 Array將每個元素丟到 Observer 中。
而且在 value 上作了一個標記 __ob__
,這樣咱們就能夠經過 value
的 __ob__
拿到Observer實例,而後使用 __ob__
上的 dep.notify()
就能夠發送通知啦。
而後咱們在改進一下Array原型的攔截器:
;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })
能夠看到寫了一個 switch
對 method
進行判斷,若是是 push
,unshift
,splice
這種能夠新增數組元素的方法就使用 ob.observeArray(inserted)
把新增的元素也丟到 Observer
中去轉換成能夠被偵測到變化的數據。
在最後不論操做數組的方法是什麼,都會調用 ob.dep.notify()
去通知 watcher
數據發生了改變。
如今咱們有一個 arrayMenthods
是被加工後的 Array.prototype
,那麼怎麼讓這個對象應用到Array
上面呢?
思考一下,咱們不能直接修改 Array.prototype
由於這樣會污染全局的Array,咱們但願 arrayMenthods
只對 data
中的Array
生效。
因此咱們只須要把 arrayMenthods
賦值給 value
的 __proto__
上就行了。
咱們改造一下 Observer
:
export class Observer { constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { value.__proto__ = arrayMethods // 新增 this.observeArray(value) } else { this.walk(value) } } }
若是不能使用 __proto__
,就直接循環 arrayMethods
把它身上的這些方法直接裝到 value
身上好了。
什麼狀況不能使用 __proto__
我也不知道,各位大佬誰知道可否給我留個言?跪謝~
因此咱們的代碼又要改造一下:
// can we use __proto__? const hasProto = '__proto__' in {} // 新增 export class Observer { constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { // 修改 const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } } function protoAugment (target, src: Object, keys: any) { target.__proto__ = src } function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }
關於vue對Array的攔截實現上面剛說完,正由於這種實現方式,其實有些數組操做vue是攔截不到的,例如:
this.list[0] = 2
修改數組第一個元素的值,沒法偵測到數組的變化,因此並不會觸發 re-render
或 watch
等。
在例如:
this.list.length = 0
清空數組操做,沒法偵測到數組的變化,因此也不會觸發 re-render
或 watch
等。
由於vue的實現方式就決定了沒法對上面舉得兩個例子作攔截,也就沒有辦法作到響應,ES6是有能力作到的,在ES6以前是沒法作到模擬數組的原生行爲的,如今 ES6 的 Proxy 能夠模擬數組的原生行爲,也能夠經過 ES6 的繼承來繼承數組原生行爲,從而進行攔截。
最後掏出vue官網上的一張圖,這張圖其實很是清晰,就是一個變化偵測的原理圖。
getter
到 watcher
有一條線,上面寫着收集依賴,意思是說 getter
裏收集 watcher
,也就是說當數據發生 get
動做時開始收集 watcher
。
setter
到 watcher
有一條線,寫着 Notify
意思是說在 setter
中觸發消息,也就是當數據發生 set
動做時,通知 watcher
。
Watcher
到 ComponentRenderFunction 有一條線,寫着 Trigger re-render
意思很明顯了。