圖解 Vue 響應式原理

最近部門分享,有同窗提到了 Vue 響應式原理,你們在討論時,發現一些同窗對這一知識理解還不夠深刻,不能造成一個閉環,爲了幫助你們理解這個問題,我從新過了一下 Vue 源碼,並整理了多張流程圖,便於你們理解。vue

  • Vue 初始化
  • 模板渲染
  • 組件渲染
本文 Vue 源碼版本:2.6.11,爲了便於理解,均有所刪減。

本文將從如下兩個方面進行探索:node

  • 從 Vue 初始化,到首次渲染生成 DOM 的流程。
  • 從 Vue 數據修改,到頁面更新 DOM 的流程。

Vue 初始化

先從最簡單的一段 Vue 代碼開始:dom

<template>
  <div>
    {{ message }}
  </div>
</template>
<script>
new Vue({
  data() {
    return {
      message: "hello world",
    };
  },
});
</script>

這段代碼很簡單,最終會在頁面上打印一個 hello world,它是如何實現的呢?異步

咱們從源頭:new Vue 的地方開始分析。函數

// 執行 new Vue 時會依次執行如下方法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)

// 5. 調用 walk 方法,遍歷 data 中的每個屬性,監聽數據的變化。
function walk(obj) {
  const keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
}

// 6. 執行 defineProperty 監聽數據讀取和設置。
function defineReactive(obj, key, val) {
  // 爲每一個屬性建立 Dep(依賴蒐集的容器,後文會講)
  const dep = new Dep();
  // 綁定 get、set
  Object.defineProperty(obj, key, {
    get() {
      const value = val;
      // 若是有 target 標識,則進行依賴蒐集
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set(newVal) {
      val = newVal;
      // 修改數據時,通知頁面從新渲染
      dep.notify();
    },
  });
}

數據描述符綁定完成後,咱們就能獲得如下的流程圖:源碼分析

圖中咱們能夠看到,Vue 初始化時,進行了數據的 get、set 綁定,並建立了一個 Dep 對象。this

對於數據的 get、set 綁定咱們並不陌生,可是 Dep 對象什麼呢?spa

Dep 對象用於依賴收集,它實現了一個發佈訂閱模式,完成了數據 Data 和渲染視圖 Watcher 的訂閱,咱們一塊兒來剖析一下。prototype

class Dep {
  // 根據 ts 類型提示,咱們能夠得出 Dep.target 是一個 Watcher 類型。
  static target: ?Watcher;
  // subs 存放蒐集到的 Watcher 對象集合
  subs: Array<Watcher>;
  constructor() {
    this.subs = [];
  }
  addSub(sub: Watcher) {
    // 蒐集全部使用到這個 data 的 Watcher 對象。
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      // 蒐集依賴,最終會調用上面的 addSub 方法
      Dep.target.addDep(this);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      // 調用對應的 Watcher,更新視圖
      subs[i].update();
    }
  }
}

根據對 Dep 的源碼分析,咱們獲得了下面這張邏輯圖:3d

瞭解 Data 和 Dep 以後,咱們來繼續揭開 Watcher 的面紗。

class Watcher {
  constructor(vm: Component, expOrFn: string | Function) {
    // 將 vm._render 方法賦值給 getter。
    // 這裏的 expOrFn 其實就是 vm._render,後文會講到。
    this.getter = expOrFn;
    this.value = this.get();
  }
  get() {
    // 給 Dep.target 賦值爲當前 Watcher 對象
    Dep.target = this;
    // this.getter 其實就是 vm._render
    // vm._render 用來生成虛擬 dom、執行 dom-diff、更新真實 dom。
    const value = this.getter.call(this.vm, this.vm);
    return value;
  }
  addDep(dep: Dep) {
    // 將當前的 Watcher 添加到 Dep 收集池中
    dep.addSub(this);
  }
  update() {
    // 開啓異步隊列,批量更新 Watcher
    queueWatcher(this);
  }
  run() {
    // 和初始化同樣,會調用 get 方法,更新視圖
    const value = this.get();
  }
}

源碼中咱們看到,Watcher 實現了渲染方法 _render 和 Dep 的關聯, 初始化 Watcher 的時候,打上 Dep.target 標識,而後調用 get 方法進行頁面渲染。加上上文的 Data,目前 Data、Dep、Watcher 三者的關係以下:

咱們再拉通串一下整個流程:Vue 經過 defineProperty 完成了 Data 中全部數據的代理,當數據觸發 get 查詢時,會將當前的 Watcher 對象加入到依賴收集池 Dep 中,當數據 Data 變化時,會觸發 set 通知全部使用到這個 Data 的 Watcher 對象去 update 視圖。

目前的總體流程以下:

上圖的流程中 Data 和 Dep 都是 Vue 初始化時建立的,但如今咱們並不知道 Wacher 是從哪裏建立的,帶着這個問題,咱們接着往下探索。

模板渲染

上文中,咱們分析了初始化 Vue 過程當中處理數據的部分,接下來,咱們分析一下數據渲染的部分。

其實 new Vue 執行到最後,會調用 mount 方法,將 Vue 實例渲染成 dom 。

// new Vue 執行流程。
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,編譯 Vue 中的 template 模板,生成 render 方法。
// 4. Vue.prototype.$mount 調用上面的 render 方法掛載 dom。
// 5. mountComponent

// 6. 建立 Watcher 實例
const updateComponent = () => {
  vm._update(vm._render());
};
// 結合上文,咱們就能得出,updateComponent 就是傳入 Watcher 內部的 getter 方法。
new Watcher(vm, updateComponent);

// 7. new Watcher 會執行 Watcher.get 方法
// 8. Watcher.get 會執行 this.getter.call(vm, vm) ,也就是執行 updateComponent 方法
// 9. updateComponent 會執行 vm._update(vm._render())

// 10. 調用 vm._render 生成虛擬 dom
Vue.prototype._render = function (): VNode {
  const vm: Component = this;
  const { render } = vm.$options;
  let vnode = render.call(vm._renderProxy, vm.$createElement);
  return vnode;
};
// 11. 調用 vm._update(vnode) 渲染虛擬 dom
Vue.prototype._update = function (vnode: VNode) {
  const vm: Component = this;
  if (!prevVnode) {
    // 初次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
  } else {
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
};
// 12. vm.__patch__ 方法就是作的 dom diff 比較,而後更新 dom,這裏就不展開了。

看完 Vue 模板渲染的過程,咱們能夠獲得以下的流程圖:

到這裏,咱們就知道了 Watcher 實際上是在 Vue 初始化的階段建立的,屬於生命週期中 beforeMount 的位置建立的,建立 Watcher 時會執行 render 方法,最終將 Vue 代碼渲染成真實的 DOM。

咱們再將以前的流程整合一下,就能獲得如下的流程:

上圖分析了 Vue 初始化到渲染 DOM 的整個過程,最後咱們再分析一下,當數據變化時,Vue 又是怎麼進行更新的?

其實,在上圖也能看出,在 Data 變化時,會調用 Dep.notify 方法,隨即調用 Watcher 內部的 update 方法,此方法會將全部使用到這個 Data 的 Watcher 加入一個隊列,並開啓一個異步隊列進行更新,最終執行 _render 方法完成頁面更新。

總體的流程以下:

好了,探索到這裏,Vue 的響應式原理,已經被咱們分析透徹了,若是你尚未明白,不妨再細品一下上圖。

組件渲染

原本探索到上面的流程圖就結束了,但好奇的我又想到了一個問題 😂。

Vue 組件又是怎麼渲染的呢?

帶着這個問題,我繼續查閱了源碼。

// 從模板編譯開始,當發現一個自定義組件時,會執行如下函數
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement

// 6. createComponent
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // $options._base 其實就是全局 Vue 構造函數,在初始化時 initGlobalAPI 中定義的:Vue.options._base = Vue
  const baseCtor = context.$options._base;
  // Ctor 就是 Vue 組件中 <script> 標籤下 export 出的對象
  if (isObject(Ctor)) {
    // 將組件中 export 出的對象,繼承自 Vue,獲得一個構造函數
    // 至關於 Vue.extend(YourComponent)
    Ctor = baseCtor.extend(Ctor);
  }
  const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
  return vnode;
}

// 7. 實現組件繼承 Vue,並調用 Vue._init 方法,進行初始化
Vue.extend = function (extendOptions: Object): Function {
  const Super = this;
  const Sub = function VueComponent(options) {
    // 調用 Vue.prototype._init,以後的流程就和首次加載保持一致
    this._init(options);
  };
  // 原型繼承,至關於:Component extends Vue
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  return Sub;
};

看完組件渲染的源碼後,結合上文,從新整理了一張流程圖,圖中的藍色部分就是渲染組件的過程。

好了,如今是真的結束了,最終的流程圖就是上面的這一張圖。

問個問題,如今你理解 Vue 響應式原理了嗎?

若是仍以爲很差理解,我這裏還準備了一張帶標註的簡圖 😂

思考與總結

本文從源碼的角度,介紹了 Vue 響應式原理,來簡單回顧一下吧。

  1. 從 new Vue 開始,首先經過 get、set 監聽 Data 中的數據變化,同時建立 Dep 用來蒐集使用該 Data 的 Watcher。
  2. 編譯模板,建立 Watcher,並將 Dep.target 標識爲當前 Watcher。
  3. 編譯模板時,若是使用到了 Data 中的數據,就會觸發 Data 的 get 方法,而後調用 Dep.addSub 將 Watcher 蒐集起來。
  4. 數據更新時,會觸發 Data 的 set 方法,而後調用 Dep.notify 通知全部使用到該 Data 的 Watcher 去更新 DOM。

最後,若是你對此有任何想法,歡迎留言評論!

相關文章
相關標籤/搜索