【前端詞典】 Vue 響應式原理其實很好懂

前言

這是十篇 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

若是不使用 Vue,咱們應該怎麼實現?

個人第一想法是像下面這樣實現:數組

let data = {
  num: 1
};
Object.defineProperty(data, 'num',{
  set: function( newVal ){
    document.getElementById('app').value = newVal;
  }
});
input.addEventListener('input', function(){
  data.num = 2;
});
複製代碼

這樣能夠粗略的實現點擊元素,自動更新視圖。bash

這裏咱們須要經過 Object.defineProperty 來操做對象的訪問器屬性。監聽到數據變化的時候,操做相關 DOM。app

而這裏用到了一個常見模式 —— 發佈/訂閱模式。異步

我畫了一個大概的流程圖,用來講明觀察者模式和發佈/訂閱模式。以下:函數

仔細的同窗會發現,我這個粗略的過程和使用 Vue 的不一樣的地方就是須要我本身操做 DOM 從新渲染。post

若是咱們使用 Vue 的話,這一步就是 Vue 內部的代碼來處理的。這也是咱們爲何在使用 Vue 的時候無需手動操做 DOM 的緣由。

關於 Object.defineProperty 我在上一篇文章已經說起,這裏就再也不復述。

Vue 是如何實現響應式的

咱們知道對象能夠經過 Object.defineProperty 操做其訪問器屬性,即對象擁有了 gettersetter 方法。這就是實現響應式的基石。

先看一張很直觀的流程圖:

initData 方法

在 Vue 的初始化的時候,其 _init() 方法會調用執行 initState(vm) 方法。initState 方法主要是對 propsmethodsdatacomputedwathcer 等屬性作了初始化操做。

這裏咱們就對 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 的主要過程也是作兩件事:

  1. 經過 proxy 把每個值 vm._data.[key] 都代理到 vm.[key] 上;
  2. 調用 observe 方法觀測整個 data 的變化,把 data 也變成響應式(可觀察),能夠經過 vm._data.[key] 訪問到定義 data 返回函數中對應的屬性。

數據劫持 — Observe

經過這個方法將 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__ 的屬性。

defineReactive 方法遍歷全部屬性

// 定義一個響應式對象的具體實現
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.definePropertygettersetter 方法中調用 dep 的相關方法。

即:

  1. getter 方法完成的工做就是依賴收集 —— dep.depend()
  2. setter 方法完成的工做就是發佈更新 —— dep.notify()

咱們發現這裏都和 Dep 對象有着不可忽略的關係。接下來咱們就看看 Dep 對象。這個 Dep

調度中心做用的 Dep

前文中咱們提到發佈/訂閱模式,在發佈者和訂閱者以前有一個調度中心。這裏的 Dep 扮演的角色就是調度中心,主要的做用就是:

  1. 收集訂閱者 Watcher 並添加到觀察者列表 subs
  2. 接收發布者的事件
  3. 通知訂閱者目標更新,讓訂閱者執行本身的 update 方法

詳細代碼以下:

// 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

Watcher 中定義了許多原型方法,這裏我只粗略的講 updateget 這三個方法。

// 爲了方便理解,部分兼容代碼已被我省去
  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 相關文章輸出計劃

最近總有朋友問我 Vue 相關的問題,所以接下來我會輸出 10 篇 Vue 相關的文章,但願對你們有必定的幫助。我會保持在 7 到 10 天更新一篇。

  1. 【前端詞典】Vuex 注入 Vue 生命週期的過程(完成)
  2. 【前端詞典】學習 Vue 源碼的必要知識儲備(完成)
  3. 【前端詞典】淺析 Vue 響應式原理(完成)
  4. 【前端詞典】新老 VNode 進行 patch 的過程
  5. 【前端詞典】如何開發功能組件並上傳 npm
  6. 【前端詞典】從這幾個方面優化你的 Vue 項目
  7. 【前端詞典】從 Vue-Router 設計講前端路由發展
  8. 【前端詞典】在項目中如何正確的使用 Webpack
  9. 【前端詞典】Vue 服務端渲染
  10. 【前端詞典】Axios 與 Fetch 該如何選擇

建議你關注個人公衆號,第一時間就能夠接收最新的文章。

若是你想加羣交流,也能夠添加有點智能的機器人,自動拉你進羣:

相關文章
相關標籤/搜索