【前端詞典】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',{

  value: value,

  set: function( newVal ){

    document.getElementById('app').value = newVal;

  }

});

input.addEventListener('input', function(){

  data.num = 2;

});

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

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

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

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

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

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

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

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

Vue 是如何實現響應式的

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

先看一張很直觀的流程圖:
【前端詞典】Vue 響應式原理其實很好懂

initData 方法

在 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 的主要過程也是作兩件事:

  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.defineProperty 的 getter 和 setter方法中調用 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 中定義了許多原型方法,這裏我只粗略的講 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 響應式原理其實很好懂

Vue 相關文章輸出計劃

最近總有朋友問我 Vue 相關的問題,所以接下來我會輸出 9 篇 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 該如何選擇

建議你關注個人公衆號,第一時間就能夠接收最新的文章。
【前端詞典】Vue 響應式原理其實很好懂
若是你想加羣交流,也能夠添加有點智能的機器人,自動拉你進羣:

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

相關文章
相關標籤/搜索