這是十篇 Vue 系列文章的第三篇,這篇文章咱們講講 Vue 最核心的功能之一:響應式原理。前端
能夠這樣理解:當一個狀態改變以後,與這個狀態相關的事務也當即隨之改變,從前端來看就是數據狀態改變後相關 DOM 也隨之改變。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。react
咱們先看看咱們在 Vue 中常見的寫法:ios
<div id="app" @click="changeNum"> {{ num }} </div> var app = new Vue({ el: '#app', data: { num: 1 }, methods: { changeNum() { this.num = 2 } } })
這種寫法很常見,不過你考慮過當爲何執行 this.num=2 後視圖爲何會更新呢?經過這篇文章我力爭把這個點講清楚。npm
個人第一想法是像下面這樣實現:數組
let data = { num: 1 }; Object.defineProperty(data, 'num',{ value: value, set: function( newVal ){ document.getElementById('app').value = newVal; } }); input.addEventListener('input', function(){ data.num = 2; });
這樣能夠粗略的實現點擊元素,自動更新視圖。app
這裏咱們須要經過 Object.defineProperty 來操做對象的訪問器屬性。監聽到數據變化的時候,操做相關 DOM。異步
而這裏用到了一個常見模式 —— 發佈/訂閱模式。ide
我畫了一個大概的流程圖,用來講明觀察者模式和發佈/訂閱模式。以下:函數
仔細的同窗會發現,我這個粗略的過程和使用 Vue 的不一樣的地方就是須要我本身操做 DOM 從新渲染。學習
若是咱們使用 Vue 的話,這一步就是 Vue 內部的代碼來處理的。這也是咱們爲何在使用 Vue 的時候無需手動操做 DOM 的緣由。
關於 Object.defineProperty 我在上一篇文章已經說起,這裏就再也不復述。
咱們知道對象能夠經過 Object.defineProperty 操做其訪問器屬性,即對象擁有了 getter 和 setter 方法。這就是實現響應式的基石。
先看一張很直觀的流程圖:
在 Vue 的初始化的時候,其 _init() 方法會調用執行 initState(vm) 方法。 initState 方法主要是對 props、 methods、 data、 computed 和 wathcer 等屬性作了初始化操做。
這裏咱們就對 data 初始化的過程作一個比較詳細的分析。
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { ...... } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] ...... // 省略部分兼容代碼,但不影響理解 if (props && hasOwn(props, key)) { ...... } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data observe(data, true /* asRootData */) }
initData初始化 data 的主要過程也是作兩件事:
經過 proxy 把每個值 vm._data.[key] 都代理到 vm.[key] 上;
經過這個方法將 data 下面的全部屬性變成響應式(可觀察)。
// 給對象的屬性添加 getter 和 setter,用於依賴收集和發佈更新 export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value // 實例化 Dep 對象 this.dep = new Dep() this.vmCount = 0 // 把自身實例添加到數據對象 value 的 __ob__ 屬性上 def(value, '__ob__', this) // value 是否爲數組的不一樣調用 if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } // 取出全部屬性遍歷 walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
def 函數內封裝了 Object.defineProperty ,因此你 console.log(data) ,會發現多了一個 ob 的屬性。
// 定義一個響應式對象的具體實現 export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() ..... // 省略部分兼容代碼,但不影響理解 let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 進行依賴收集 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val ..... // 省略部分兼容代碼,但不影響理解 if (setter) { setter.call(obj, newVal) } else { val = newVal } // 對新的值進行監聽 childOb = !shallow && observe(newVal) // 通知全部訂閱者,內部調用 watcher 的 update 方法 dep.notify() } }) }
defineReactive 方法最開始初始化 Dep 對象的實例,而後經過對子對象遞歸調用 observe 方法,使全部子屬性也能變成響應式的對象。而且在 Object.defineProperty 的 getter 和 setter方法中調用 dep 的相關方法。
即:
getter 方法完成的工做就是依賴收集 —— dep.depend()
咱們發現這裏都和 Dep 對象有着不可忽略的關係。接下來咱們就看看 Dep 對象。這個 Dep
前文中咱們提到發佈/訂閱模式,在發佈者和訂閱者以前有一個調度中心。這裏的 Dep 扮演的角色就是調度中心,主要的做用就是:
收集訂閱者 Watcher 並添加到觀察者列表 subs
接收發布者的事件
詳細代碼以下:
// Dep 構造函數 export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } // 向 dep 的觀察者列表 subs 添加 Watcher addSub (sub: Watcher) { this.subs.push(sub) } // 從 dep 的觀察者列表 subs 移除 Watcher removeSub (sub: Watcher) { remove(this.subs, sub) } // 進行依賴收集 depend () { if (Dep.target) { Dep.target.addDep(this) } } // 通知全部訂閱者,內部調用 watcher 的 update 方法 notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } // Dep.target 是全局惟一的觀察者,由於在任什麼時候候只有一個觀察者被處理。 Dep.target = null // 待處理的觀察者隊列 const targetStack = [] export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() }
Dep 能夠理解成是對 Watcher 的一種管理,Dep 和 Watcher 是緊密相關的。因此咱們必須看一看 Watcher 的實現。
Watcher 中定義了許多原型方法,這裏我只粗略的講 update 和 get 這三個方法。
// 爲了方便理解,部分兼容代碼已被我省去 get () { // 設置須要處理的觀察者 pushTarget(this) const vm = this.vm let value = this.getter.call(vm, vm) // deep 是否爲 true 的處理邏輯 if (this.deep) { traverse(value) } // 將 Dep.target 指向棧頂的觀察者,並將他從待處理的觀察者隊列中移除 popTarget() // 執行依賴清空動做 this.cleanupDeps() return value } update () { if (this.computed) { ... } else if (this.sync) { // 標記爲同步 this.run() } else { // 通常都是走這裏,即異步批量更新:nextTick queueWatcher(this) } }
Vue 的響應式過程大概就是這樣了。感興趣的能夠看看源碼。
最後咱們在經過這個流程圖來複習一遍:
最近總有朋友問我 Vue 相關的問題,所以接下來我會輸出 9 篇 Vue 相關的文章,但願對你們有必定的幫助。我會保持在 7 到 10 天更新一篇。
【前端詞典】Vuex 注入 Vue 生命週期的過程(完成)
【前端詞典】學習 Vue 源碼的必要知識儲備(完成)
【前端詞典】 Vue 響應式原理其實很好懂(完成)
【前端詞典】新老 VNode 進行 patch 的過程
【前端詞典】如何開發功能組件並上傳 npm
【前端詞典】從這幾個方面優化你的 Vue 項目
【前端詞典】從 Vue-Router 設計講前端路由發展
【前端詞典】在項目中如何正確的使用 Webpack
【前端詞典】Vue 服務端渲染
建議你關注個人公衆號,第一時間就能夠接收最新的文章。
若是你想加羣交流,也能夠添加有點智能的機器人,自動拉你進羣: