「試着讀讀 Vue 源代碼」響應式系統是如何構建的 ❓

說明

  • 首先這篇文章是讀 vue.js 源代碼的梳理性文章,文章分塊梳理,記錄着本身的一些理解及大體過程;更重要的一點是但願在 vue.js 3.0 發佈前深刻的瞭解其原理。javascript

  • 若是你從未看過或者接觸過 vue.js 源代碼,建議你參考如下列出的 vue.js 解析的相關文章,由於這些文章更細緻的講解了這個工程,本文只是以一些 demo 演示某一功能點或 API 實現,力求簡要梳理過程。html

  • 若是搞清楚了工程目錄及入口,建議直接去看代碼,這樣比較高效 ( 遇到難以理解對應着回來看看別人的講解,加以理解便可 )vue

  • 文章所涉及到的代碼,基本都是縮減版,具體還請參閱 vue.js - 2.5.17java

  • 若有任何疏漏和錯誤之處歡迎指正、交流。node

構建前對 data 選項的預處理

在上文 「試着讀讀 Vue 源代碼」new Vue()發生了什麼 ❓, 着重梳理了 new Vue(() 其代碼執行的全過程,瞭解了 Vue 內部到底作了哪些工做,但就響應式系統的構建並無展開描述,Vue 在哪裏開始對Data選項進行響應式體系構建呢?沒錯,就是上文簡單提過的 _init() 內部執行的 initState 函數。react

注:這裏在對 data 選項初始化時,首先若存在 data 選項,則調用 initData 方法進行對 data 預處理,最終調用 observe(data, true /* asRootData */) 函數將 data 數據對象轉換成響應式的;若不存在,簡單初始化爲空對象處理便可。git

export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  /****** 初始化 data 選項 ******/
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  /****** 初始化 data 選項 ******/
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
複製代碼

initData 代碼實現

function initData(vm: Component) {
  /************************** data 提取並預處理 ***************************/
  // 說明: 1. 根據上文,data 選項最終將被合併成一個函數,該函數返回 data 的值。
  let data = vm.$options.data;
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
  // 檢驗 data 選項是不是一個純對象(注:在對data 選項合併處理以後走了一次 beforeCreate 鉤子函數,防止 data 在那裏被修改)
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== 'production' &&
      warn('數據函數應該返回一個對象', vm);
  }
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  // 爲了不選項屬性直接的覆蓋,將迭代 data 選項
  while (i--) {
    const key = keys[i];
    // 在非生產環境下 methods 存在:若是 methods 選項中的 key 在 data 中被定義將被警告
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`方法「${key}」已經被定義爲一個data屬性。`, vm);
      }
    }
    // 在非生產環境下 props 存在:若是 data 選項中的 key 在 props 中被定義了將被警告
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' &&
        warn(`data的屬性「${key}」已經被聲明爲一個props的屬性。`, vm);
    } else if (!isReserved(key)) {
      // isReserved 做用: 檢查字符串是否以$或_開頭; 剔除這些特徵字段,避免與 Vue 自身的屬性和方法相沖突。
      // 注: ① 若是你在data中定義了 `_message/$message` 你能夠試一下 `this._message / $message` 能不能訪問到?
      // proxy 做用: data 數據代理, 使你可以:this.message 而不是 this.data.message;
      // (`this.message <=> this.($data/_data/data).message`)。
      proxy(vm, `_data`, key);
    }
  }
  /************************** data 提取並預處理 ***************************/

  observe(data, true /* asRootData */); // observe 函數將 data 數據對象轉換成響應式
}
複製代碼

proxy 代碼實現

/** * 數據代理 * @param {Object} target 要在其上定義屬性的對象 * @param {string} sourceKey 資源屬性的名稱 * @param {string} key 要定義或修改的屬性的名稱 */
export function proxy(target: Object, sourceKey: string, key: string) {
  // getter 函數
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  // setter 函數
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
複製代碼
  • 上述代碼思路:
    • 提取 data 選項的值。
    • 判斷 data 選項內鍵名是否和 methods / props 內定義鍵名衝突。
    • data 選項內屬性作一層代理,且剔除特徵字符代理。
    • 調用 observe 函數將 data 數據對象轉換成響應式。

observe 觀察函數

/** * 在某些狀況下,咱們可能但願禁用組件更新計算中的觀察。 */
export let shouldObserve: boolean = true;
export function toggleObserving(value: boolean) {
  shouldObserve = value;
}

/** * 觀察函數 * @param {Any} value 觀測數據 * @param {Boolean} asRootData 被觀測的數據是不是根級數據 */
export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 值不是對象 或 值是虛擬DOM 直接退出
  if (!isObject(value) || value instanceof VNode) {
    return;
  }

  let ob: Observer | void;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() && // 判斷是不是服務端渲染
    (Array.isArray(value) || isPlainObject(value)) && // 判斷是不是數組 或 純對象
    Object.isExtensible(value) && // 判斷一個對象是不是可擴展的(是否能夠在它上面添加新的屬性)
    !value._isVue // 避免 Vue 實例對象被觀測
  ) {
    ob = new Observer(value);
  }

  if (asRootData && ob) {
    ob.vmCount++; // 根數據對象 target.__ob__.vmCount > 0
  }
  return ob;
}
複製代碼
  • 上述代碼思路:
    • data 選項的值進行類型判斷; 若合法,調用 Observer 類。
    • 如果根數據對象,執行 ob.vmCount++
    • 返回 Observer 實例。

Observer 觀察者基類

/** * 附加到每一個被觀察對象的觀察者類。 * 一旦附加,觀察者將目標對象的屬性鍵轉換爲 getter/setter,用於收集依賴項和分派更新。 */
export class Observer {
  value: any; // 觀察對象
  dep: Dep; // Dep 是一個可觀察的對象,能夠有多個指令訂閱它。
  vmCount: number; // 將此屬性做爲根 $data 的 vm 數量

  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;

    // 爲 value 添加 __ob__ 不可枚舉屬性, 值爲當前 `Observer` 實例
    def(value, '__ob__', this);

    // 後續的深度監測 data 數據下的二層級的數據多是數組、對象等...
    if (Array.isArray(value)) {
      // 攔截數組變異方法。
      // 判斷是否可使用 __proto__ 選擇不一樣的執行方法。
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      // 遞歸處理,解決嵌套數組。
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  /** * 遍歷每一個屬性並將它們轉換爲getter/setter。此方法只應在值類型爲Object時調用。 */
  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]);
    }
  }
}
複製代碼

數組處理

攔截數組變異方法實現github

// 建立一個新對象,使用 現有的對象(Array.prototype) 來提供新建立的對象的__proto__
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

// 須要攔截的數組變異方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

/** * 攔截突變方法併發出事件 */
methodsToPatch.forEach(function(method) {
  // 緩存數組原始變異方法
  const original = arrayProto[method];
  // 在 arrayMethods 上添加這些變異方法並作一些事情。
  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;
    }
    // 若存在將被插入的數組元素,將調用 observeArray 繼續進行處理
    if (inserted) ob.observeArray(inserted);

    ob.dep.notify(); // 觸發依賴

    return result; // 將值結果返回
  });
});
複製代碼
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

/** * 經過使用 _proto__ 攔截原型鏈來增長目標對象或數組 */
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]);
  }
}
複製代碼

下面是對數據變異攔截後的斷點截圖:算法

-

defineReactive

/** * 在對象上定義反應性屬性。 */
export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  const dep = new Dep(); // 訂閱池

  /******************** 剔除不可配置屬性 *********************/
  const property = Object.getOwnPropertyDescriptor(obj, key); // 返回指定對象上一個自有屬性對應的屬性描述符
  if (property && property.configurable === false) {
    return;
  }

  /******************** 知足預約義的 getter / setter *********************/
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]; // 觸發取值函數 - 收集依賴
  }

  let childOb = !shallow && observe(val); // 默認深度觀測

  /******************** 劫持屬性並配置 *********************/
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    /******************** 返回正確的屬性值並收集依賴 *********************/
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;

      // target 保存着要被收集的依賴(觀察者)
      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; // 緩存舊值
      // NaN 或 值 相等不處理
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter(); // 用來打印輔助信息
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal); // 深度觀測
      dep.notify(); // 觸發依賴
    }
  });
}
複製代碼

Dep

import type Watcher from './watcher';
import { remove } from '../util/index';

let uid = 0;

/** * 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) {
      Dep.target.addDep(this);
    }
  }

  notify() {
    // 首先穩定訂閱者列表
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

// 正在評估的當前目標監視程序。這是全局唯一的,由於在任什麼時候候都只能評估一個監視程序。
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();
}
複製代碼
  • 上述陳述了響應式系統構建的部份內容,知道如何爲屬性構建響應式屬性,即構造 getter/setter;知道了在 getter 時收集依賴,在 setter 觸發依賴。 同時對 Dep 這個基類作了相應的分析。express

  • 就之前文例子,斷點圖簡單展現了構建以後的結果:

-

Watcher

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError
} from '../util/index';

import { traverse } from './traverse';
import { queueWatcher } from './scheduler';
import Dep, { pushTarget, popTarget } from './dep';

import type { SimpleSet } from '../util/index';

let uid = 0;

/** * 一個觀察者解析一個表達式,收集依賴關係,當表達式值改變時觸發回調。這用於$watch() api和指令。 * 經過對「被觀測目標」的求值,觸發數據屬性的 get 攔截器函數從而收集依賴 */
export default class Watcher {
  vm: Component; // 組件實例
  expression: string; // 被觀察的目標表達式
  cb: Function; // 當被觀察的表達式的值變化時的回調函數
  id: number; // 觀察者實例對象的惟一標識
  deep: boolean; // 當前觀察者實例對象是不是深度觀測
  user: boolean; // 標識當前觀察者實例對象是 開發者定義的 仍是 內部定義的
  computed: boolean; // 標識當前觀察者實例對象是不是計算屬性的觀察者
  sync: boolean; // 告訴觀察者當數據變化時是否同步求值並執行回調 默認 將須要從新求值並執行回調的觀察者放到一個異步隊列中,當全部數據的變化結束以後統一求值並執行回調
  dirty: boolean; // for computed watchers, true 表明着尚未求值
  active: boolean; // 觀察者是否處於激活狀態,或者可用狀態
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet; // 用來在 屢次求值(當數據變化時從新求值的過程) 中避免收集重複依賴
  newDepIds: SimpleSet; // 用來避免在 一次求值 的過程當中收集重複的依賴
  before: ?Function; // Watcher 實例的鉤子
  getter: Function;
  value: any;

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, // 當前觀察者對象的選項
    isRenderWatcher?: boolean // isRenderWatcher 用來標識該觀察者實例是不是渲染函數的觀察者
  ) {
    /****************** 初始化一些實例屬性 ******************/
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);

    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.computed = !!options.computed;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.computed = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid;
    this.active = true;
    this.dirty = this.computed;
    /****************** 初始化一些實例屬性 ******************/

    /****************** 實現避免收集重複依賴 ******************/
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    /****************** 實現避免收集重複依賴 ******************/

    /****************** 解析路徑 ******************/
    this.expression =
      process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '';
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = function() {};
        process.env.NODE_ENV !== 'production' &&
          warn(
            `監視路徑失敗:「${exports}」監視程序只接受簡單的點分隔路徑。要實現徹底控制,可使用函數。`,
            vm
          );
      }
    }
    /****************** 解析路徑 ******************/

    /****************** 求值 - 計算屬性的觀察者 與 普通屬性觀察者 處理方式 ******************/
    if (this.computed) {
      this.value = undefined;
      this.dep = new Dep();
    } else {
      this.value = this.get();
    }
  }

  /** * 求值:觸發訪問器屬性的 get 攔截器函數,並從新收集依賴項。 */
  get() {
    pushTarget(this); // 給 Dep.target 賦值
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // 「觸發」每個屬性,所以它們都做爲依賴項被跟蹤,以便進行深度監視
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }

  /** * 向該指令添加一個依賴項。 */
  addDep(dep: Dep) {
    const id = dep.id;
    if (!this.newDepIds.has(id)) {
      // 避免收集重複依賴
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this); // 移除已經沒有關聯關係的觀察者
      }
    }
  }

  /** * 清理依賴項集合. */
  cleanupDeps() {
    let i = this.deps.length;
    while (i--) {
      const dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    // 引用類型變量交換值的過程
    let tmp = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  }

  /** * 用戶界面。將在依賴項更改時調用。 */
  update() {
    if (this.computed) {
      // 計算屬性監視程序有兩種模式: 延遲模式 和 激活模式。
      // 默認狀況下,它初始化爲lazy,只有當至少有一個訂閱者(一般是另外一個計算屬性或組件的呈現函數)依賴於它時纔會被激活。
      if (this.dep.subs.length === 0) {
        // 在延遲模式下,除非必要,不然咱們不想執行計算,所以咱們只需將監視程序標記爲dirty。
        // 實際計算是在訪問計算屬性時在this.evaluate()中即時執行的。
        this.dirty = true;
      } else {
        // 在激活模式下,咱們但願主動執行計算,但只在值確實發生更改時通知訂閱者。
        this.getAndInvoke(() => {
          this.dep.notify();
        });
      }
    } else if (this.sync) {
      this.run();
    } else {
      // 處於性能考量,異步更新隊列,但最終都會執行 watcher.run(),此處再也不細說。
      queueWatcher(this);
    }
  }

  /** * 調度器的工做界面。將由調度程序調用。 */
  run() {
    if (this.active) {
      this.getAndInvoke(this.cb);
    }
  }

  getAndInvoke(cb: Function) {
    const value = this.get();
    if (
      value !== this.value ||
      // 即便值是相同的,深度觀察者和對象/數組上的觀察者也應該觸發,由於值可能發生了突變。
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value;
      this.value = value;
      this.dirty = false;
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`);
        }
      } else {
        cb.call(this.vm, value, oldValue);
      }
    }
  }

  /** * 計算並返回監視程序的值。這隻對計算過的屬性觀察者調用。 */
  evaluate() {
    if (this.dirty) {
      this.value = this.get();
      this.dirty = false;
    }
    return this.value;
  }

  /** * Depend on this watcher. Only for computed property watchers. */
  depend() {
    if (this.dep && Dep.target) {
      this.dep.depend();
    }
  }

  /** * 從全部依賴項的訂閱服務器列表中刪除self。 */
  teardown() {
    if (this.active) {
      // 從vm的監視者列表中刪除self這是一個有點昂貴的操做,因此若是正在銷燬vm,咱們就跳過它。
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      let i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  }
}
複製代碼

上文已分析了構建響應式所有的內容,下面就 $watch 函數 渲染函數 的觀察者 簡單演示整個響應流過程。

渲染函數

上文在談 new Vue() 最終程序走的是掛載函數,接下來,就看看掛載函數作了哪些處理。(注意:這裏的掛載函數在初始化時已經被重寫,給運行時版的 $mount 函數增長編譯模板的能力)

import { mountComponent } from 'core/instance/lifecycle';

/** * 公用的掛載方法 * * @param {String | Element} el 掛載元素 * @param {Boolean} hydrating 用於 Virtual DOM 的補丁算法 * @returns {Function} 真正的掛載組件的方法 */
Vue.prototype.$mount = function( el?: string | Element, hydrating?: boolean ): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};
複製代碼

mountComponent

/** * 組件掛載函數 */
export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component {
  vm.$el = el;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    if (process.env.NODE_ENV !== 'production') {
      if (
        (vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el ||
        el
      ) {
        warn(
          '您正在使用Vue的僅運行時構建,其中模板編譯器不可用。要麼將模板預編譯爲呈現函數,要麼使用編譯器包含的構建。',
          vm
        );
      } else {
        warn('加載組件失敗:模板或呈現函數未定義。', vm);
      }
    }
  }
  callHook(vm, 'beforeMount');

  /******************* 把虛擬DOM渲染成真正的DOM ********************/
  let updateComponent;
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name;
      const id = vm._uid;
      const startTag = `vue-perf-start:${id}`;
      const endTag = `vue-perf-end:${id}`;

      mark(startTag);
      const vnode = vm._render(); // 調用 vm.$options.render 函數並返回生成的虛擬節點(vnode)
      mark(endTag);
      measure(`vue ${name} render`, startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating); // 渲染虛擬節點爲真正的 DOM
      mark(endTag);
      measure(`vue ${name} patch`, startTag, endTag);
    };
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating);
    };
  }
  /******************* 把虛擬DOM渲染成真正的DOM ********************/

  /******************* 實例化觀察者 ********************/
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate');
        }
      }
    },
    true
  );

  hydrating = false;

  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
  return vm;
}
複製代碼

$watch 函數

$watch: 觀察 Vue 實例變化的一個表達式或計算屬性函數。回調函數獲得的參數爲新值和舊值。表達式只接受監督的鍵路徑。對於更復雜的表達式,用一個函數取代。

在上文初始化過程,談到 $watch 的初始化,下面是代碼實現。

export function stateMixin(Vue: Class<Component>) {

  ...

  Vue.prototype.$watch = function( expOrFn: string | Function, cb: any, options?: Object ): Function {
    const vm: Component = this;
    // 這個裏就是爲了規範化 watch 參數,這裏不細說。
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options);
    }
    options = options || {};
    options.user = true; // 用戶自定義回調
    const watcher = new Watcher(vm, expOrFn, cb, options); // 實例化觀察者
    // 若是當即觸發,則當即執行回調。不然放入異步隊列中
    if (options.immediate) {
      cb.call(vm, watcher.value); // 這裏注意,第二個參數(newVal)未傳,因此你在回調拿不到
    }
    // 返回一個取消觀察函數,用來中止觸發回調
    return function unwatchFn() {
      watcher.teardown();
    };
  };

  ...
}

複製代碼

演示 demo

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>vue.js DEMO</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p>數據屬性:{{ message }}</p>
      <button @click="update">更新</button>
    </div>

    <script> new Vue({ el: '#app', data: { message: 'hello vue.js' }, mounted() { this.$watch('message', function(newVal, oldVal) { console.log(`message: __新值__${newVal}___舊值___${oldVal}`); }); }, methods: { update() { this.message = `${this.message} ---- ${Math.random()}`; } } }); </script>
  </body>
</html>
複製代碼

演示效果 及步驟梳理

  • 點擊更新按鈕,設置 message 屬性,觸發 message 更新(setter
    • -
  • dep.notify() 觸發依賴,依次執行 update (這裏包含渲染函數的觀察者: 渲染函數 => watch )
    • -
  • 通過異步隊列處理,統一調用更新程序 run
    • 渲染函數(通過其處理,最新更新的值已經被渲染到 DOM 上):
      • -
    • $watch:
      • -
      • get 求值,執行回調,更新新值
        • -
      • 返回新值,緩存舊值,調用回調,傳入新舊值。
        • -
相關文章
相關標籤/搜索