Vue2.x 源碼剖析之響應(yīng)式原理

Study Notes

本博主會持續(xù)更新各種前端的技術(shù)意狠,如果各位道友喜歡,可以關(guān)注泵琳、收藏摄职、點(diǎn)贊下本博主的文章誊役。

Vue.js 源碼剖析-響應(yīng)式原理

響應(yīng)式處理的入口

/* @flow */

import ...;

const sharedPropertyDefinition = {...};

export function proxy(target: Object, sourceKey: string, key: string) {...}

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);
  // 判斷options中是否存在data屬性
  if (opts.data) {
    // 如果存在data屬性蛔垢,則給其添加響應(yīng)式
    initData(vm);
  } else {
    // 如果不存在data屬性击孩,則給給vue對象,添加_data屬性鹏漆,初始化為空對象巩梢,并給其添加響應(yīng)式
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

function initProps(vm: Component, propsOptions: Object) {...}

function initData(vm: Component) {
  let data = vm.$options.data;
  // 判斷data是否是函數(shù)创泄,如果是一個函數(shù),則是組件中的data括蝠,將其this指向vue
  // 不是函數(shù)鞠抑,則判斷是否存在。不存在則賦值為空對象
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
  // 判斷data是否是[object Object]忌警,如果不是搁拙,則將data賦值為空對象
  // 并在非production環(huán)境下,拋出警告
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== 'production' &&
      warn(
        'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm,
      );
  }
  // proxy data on instance
  // 獲取data的所有key值
  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];
    // 在非production環(huán)境下法绵,判斷是否與methods里的方法存在同名箕速,存在則拋出警告
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm,
        );
      }
    }
    // 在非production環(huán)境下,判斷是否與props里的屬性存在同名朋譬,存在則拋出警告
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm,
        );
    } else if (!isReserved(key)) {
      // 如果key的第一個字符不是$或者_(dá)盐茎,為其設(shè)置代理
      proxy(vm, `_data`, key);
    }
  }
  // observe data
  // 將data轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
  // true:告訴observe函數(shù),其為根屬性data
  observe(data, true /* asRootData */);
}

function getData(data: Function, vm: Component): any {...}

const computedWatcherOptions = { lazy: true };

function initComputed(vm: Component, computed: Object) {...}

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function,
) {...}

function createComputedGetter(key) {...}

function initMethods(vm: Component, methods: Object) {...}

function initWatch(vm: Component, watch: Object) {...}

function createWatcher(
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object,
) {...}

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

src/core/observer/index.js

  • observe(value, asRootData)
  • 負(fù)責(zé)為每一個 Object 類型的 value 創(chuàng)建一個 observer 實(shí)例
/* @flow */

import ...;

const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export const observerState = {
  shouldConvert: true,
};
export class Observer {...}

function protoAugment(target, src: Object, keys: any) {...}

/* istanbul ignore next */
function copyAugment(target: Object, src: Object, keys: Array<string>) {...}

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
// 如果不存在觀察者實(shí)例徙赢,則創(chuàng)建觀察者實(shí)例字柠,并返回
// 存在,則直接返回現(xiàn)有的觀察者實(shí)例
export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 如果value不是一個對象或者value是一個虛擬dom狡赐,則不往下執(zhí)行募谎,即不需為其轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  // 如果value中存在__ob__屬性,并且__ob__是Observer阴汇,則直接將value.__ob__賦值給ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // observerState.shouldConvert為true
    // 并且當(dāng)前是瀏覽器環(huán)境
    // 并且value是一個數(shù)組或者是'[object Object]'
    // 并且value是可擴(kuò)展的(可以在它上面添加新的屬性)
    // 并且value不是Vue實(shí)例
    // 創(chuàng)建一個Observer對象数冬,并賦值給ob
    ob = new Observer(value);
  }
  // 如果處理的是根數(shù)據(jù)并且存在ob,則ob.vmCount++
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob;
}
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean,
) {...}

export function set(target: Array<any> | Object, key: any, val: any): any {...}
export function del(target: Array<any> | Object, key: any) {...}
function dependArray(value: Array<any>) {...}

Observer

src/core/observer/index.js

  • Observer 類
    • constructor()方法搀庶,初始化數(shù)據(jù)拐纱,并改變數(shù)組當(dāng)前對象的原型屬性
    • walk()方法為對象做響應(yīng)式處理
    • observeArray()方法為數(shù)組做響應(yīng)式處理
export class Observer {
  // 數(shù)據(jù)對象
  value: any;
  // 依賴對象
  dep: Dep;
  // 實(shí)例計(jì)數(shù)器
  // 以該對象在根$data的vms數(shù)
  vmCount: number; // number of vms that has this object as root $data

  // 構(gòu)造函數(shù),初始化數(shù)據(jù)
  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    // 將實(shí)例掛載到觀察對象的__ob__屬性
    def(value, '__ob__', this);
    // 如果觀察對象是一個數(shù)組哥倔,將數(shù)組轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment // 改變數(shù)組當(dāng)前對象的原型屬性
        : copyAugment; // 改變數(shù)組當(dāng)前對象的原型屬性
      // arrayKeys是對象的所有自身屬性的屬性名組成的數(shù)組
      augment(value, arrayMethods, arrayKeys);
      // 為數(shù)組中的每一個對象元素創(chuàng)建一個observer實(shí)例
      this.observeArray(value);
    } else {
      // 遍歷對象中的每一個屬性秸架,轉(zhuǎn)換成getter/setter
      this.walk(value);
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk(obj: Object) {
    // 獲取對象里的所有屬性
    const keys = Object.keys(obj);
    // 遍歷每一個屬性,設(shè)置為響應(yīng)式數(shù)據(jù)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]]);
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

對象響應(yīng)式處理

  • defineReactive
    • 為一個對象定義一個響應(yīng)式的屬性咆蒿,每一個屬性對應(yīng)一個 dep 對象
    • 如果該屬性的值是對象东抹,繼續(xù)調(diào)用 observe
    • 如果給屬性賦的新值是一個對象,繼續(xù)調(diào)用 observe
    • 如果數(shù)據(jù)更新發(fā)送通知
// 在對象上定義響應(yīng)式屬性
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean,
) {
  // 創(chuàng)建依賴對象實(shí)例沃测,其作用是為當(dāng)前屬性(key)收集依賴
  const dep = new Dep();

  // 獲取obj對象的屬性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key);
  // 當(dāng)且僅當(dāng)該屬性的 configurable 鍵值為 true 時
  // 該屬性的描述符才能夠被改變缭黔,同時該屬性也能從對應(yīng)的對象上被刪除。
  // 如果存在屬性描述符蒂破,并且configurable為false馏谨,即不可被轉(zhuǎn)換為響應(yīng)式數(shù)據(jù),則不繼續(xù)往下執(zhí)行
  if (property && property.configurable === false) {
    return;
  }

  // cater for pre-defined getter/setters
  // 提供預(yù)定義的存取器函數(shù)
  const getter = property && property.get;
  const setter = property && property.set;

  // 當(dāng)shallow為false或者不存在時
  // observe會判斷val是否是遞歸觀察子對象
  // 并將對象屬性都轉(zhuǎn)換為getter/setter附迷,返回子觀察對象
  let childOb = !shallow && observe(val);
  // 當(dāng)且僅當(dāng)該屬性的 enumerable 鍵值為 true 時惧互,該屬性才會出現(xiàn)在對象的枚舉屬性中哎媚。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 如果存在用戶設(shè)置的getter,將getter的this指向obj喊儡,并賦值給value
      // 否則直接將傳入的val賦值給value
      const value = getter ? getter.call(obj) : val;
      // 如果存在當(dāng)前依賴目標(biāo)拨与,即watcher對象,則建立依賴
      // 在src/core/observer/index.js中的mountComponent方法艾猜,創(chuàng)建了watcher對象
      // 在watcher對象的構(gòu)造函數(shù)中截珍,創(chuàng)建了Dep.target
      if (Dep.target) {
        // 為當(dāng)前屬性收集依賴
        dep.depend();
        // 如果存在子觀察對象,則建立子對象的依賴關(guān)系
        if (childOb) {
          // 為當(dāng)前子對象收集依賴
          childOb.dep.depend();
          // 如果value是數(shù)組箩朴,則特殊處理收集數(shù)組對象依賴
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      // 返回屬性值
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      // 如果新值等于舊值或者新值和舊值都為NaN岗喉,則不繼續(xù)往下執(zhí)行
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      // 如果傳入customSetter參數(shù)時,并且在非production環(huán)境時炸庞,調(diào)用customSetter方法
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      // 如果存在用戶設(shè)置的setter钱床,則將setter的this指向obj,并將newVal傳入
      // 否則直接newVal賦值給val
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      // 當(dāng)shallow為false或者不存在時
      // observe會判斷newVal是否是是遞歸觀察子對象埠居,返回子觀察對象
      childOb = !shallow && observe(newVal);
      // 派發(fā)更新(發(fā)布更新通知)
      dep.notify();
    },
  });
}

數(shù)組響應(yīng)式處理

在 observer 構(gòu)造函數(shù)中

// 如果觀察對象是一個數(shù)組查牌,將數(shù)組轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
if (Array.isArray(value)) {
  const augment = hasProto
    ? protoAugment // 改變數(shù)組當(dāng)前對象的原型屬性
    : copyAugment; // 改變數(shù)組當(dāng)前對象的原型屬性
  // arrayKeys是對象的所有自身屬性的屬性名組成的數(shù)組
  augment(value, arrayMethods, arrayKeys);
  // 為數(shù)組中的每一個對象元素創(chuàng)建一個observer實(shí)例
  this.observeArray(value);
} else {
  // 遍歷對象中的每一個屬性,轉(zhuǎn)換成getter/setter
  this.walk(value);
}
  • 改變數(shù)組當(dāng)前對象的原型屬性

src/core/observer/array.js

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index';

const arrayProto = Array.prototype;
// 使用數(shù)組的原型創(chuàng)建一個新的對象
export const arrayMethods = Object.create(arrayProto);

/**
 * Intercept mutating methods and emit events
 */
// 修改數(shù)組元素的方法
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(
  function (method) {
    // cache original method
    // 緩存數(shù)組的原方法
    const original = arrayProto[method];
    // 調(diào)用Object.defineProperty()方法滥壕,重寫數(shù)組的方法
    def(arrayMethods, method, function mutator(...args) {
      // 執(zhí)行原數(shù)組的原方法纸颜,并改變其this指向
      const result = original.apply(this, args);
      // 獲取數(shù)組對象的__ob__屬性
      const ob = this.__ob__;
      // 獲取數(shù)組新增的元素
      let inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break;
        case 'splice':
          inserted = args.slice(2);
          break;
      }
      // 如果存在新增元素,重新遍歷數(shù)組元素設(shè)置為響應(yīng)式數(shù)據(jù)
      if (inserted) ob.observeArray(inserted);
      // notify change
      // 調(diào)用數(shù)組的ob對象發(fā)送更新通知
      ob.dep.notify();
      return result;
    });
  },
);

Dep 類

  1. 在 defineReactive() 的 getter 中創(chuàng)建 dep 對象绎橘,當(dāng)存在 Dep.target , 調(diào)用 dep.depend()
  2. dep.depend() 內(nèi)部調(diào)用 Dep.target.addDep(this)胁孙,也就是 watcher 的 addDep() 方法,它內(nèi)部最終調(diào)用 dep.addSub(this)称鳞,把訂閱者(Watcher)添加到 dep 的 subs 數(shù)組中涮较,當(dāng)數(shù)據(jù)變化的時候調(diào)用 watcher 對象的 update() 方法
  3. 什么時候設(shè)置的 Dep.target? 通過簡單的案例調(diào)試觀察。調(diào)用 mountComponent() 方法的時候冈止,創(chuàng)建了渲染 watcher 對象狂票,執(zhí)行 watcher 中的 get() 方法
  4. get() 方法內(nèi)部調(diào)用 pushTarget(this),把當(dāng)前 Dep.target = watcher熙暴,同時把當(dāng)前 watcher 入棧闺属,因?yàn)橛懈缸咏M件嵌套的時候先把父組件對應(yīng)的 watcher 入棧,再去處理子組件的 watcher周霉,子組件的處理完畢后掂器,再把父組件對應(yīng)的 watcher 出棧,繼續(xù)操作
  5. Dep.target 用來存放目前正在使用的 watcher诗眨。全局唯一唉匾,并且一次也只能有一個 watcher 被使用
/* @flow */

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

let uid = 0;

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  // 靜態(tài)屬性,Watcher對象
  static target: ?Watcher;
  // dep實(shí)例id
  id: number;
  // dep實(shí)例對應(yīng)的Watcher對象/訂閱者數(shù)組
  subs: Array<Watcher>;

  // 構(gòu)造函數(shù)匠楚,初始化數(shù)據(jù)
  constructor() {
    this.id = uid++;
    this.subs = [];
  }

  // 添加新的訂閱者Watcher對象
  addSub(sub: Watcher) {
    this.subs.push(sub);
  }

  // 移除訂閱者Watcher對象
  removeSub(sub: Watcher) {
    remove(this.subs, sub);
  }

  // 將觀察對象和Watcher對象建立依賴
  depend() {
    // 如果存在target巍膘,則把dep對象添加到watcher的依賴中
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

  // 發(fā)布通知
  notify() {
    // stabilize the subscriber list first
    // 克隆數(shù)組
    const subs = this.subs.slice();
    // 調(diào)用每個訂閱者的update方法實(shí)現(xiàn)更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

// Dep.target 用來存放目前正在使用的watcher
// 全局唯一,并且一次也只能有一個watcher被使用
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null;
const targetStack = [];

// 入棧并將當(dāng)前 watcher 賦值給Dep.target
export function pushTarget(_target: Watcher) {
  // 每一個組件都有一個mountComponent函數(shù)
  // mountComponent函數(shù)創(chuàng)建了watcher對象
  // 所以每一個組件對應(yīng)一個watcher對象
  // 如果A組件嵌套B組件芋簿,當(dāng)我們渲染A組件時峡懈,發(fā)現(xiàn)存在子組件,則先渲染子組件与斤,將A組件渲染掛起
  // 所以這里需要將A組件的Dep.target入棧
  // 當(dāng)子組件渲染結(jié)束后肪康,將其彈出棧,并繼續(xù)執(zhí)行A組件渲染
  // 總結(jié):
  // 父子組件嵌套的時候先把父組件對應(yīng)的watcher入棧撩穿,再去處理子組件的watcher磷支。
  // 子組件處理完畢后,再把父組件對應(yīng)的watcher出棧食寡,繼續(xù)操作
  if (Dep.target) targetStack.push(Dep.target);
  Dep.target = _target;
}

// 出棧操作
export function popTarget() {
  Dep.target = targetStack.pop();
}

Watcher 類

  • Watcher 分為三種雾狈,Computed Watcher、用戶 Watcher (偵聽器)抵皱、渲染 Watcher
  • 什么時候渲染 Watcher善榛?在 src/core/instance/lifecycle.js 中的 mountComponent 方法中創(chuàng)建
  • Watcher 的構(gòu)造函數(shù)初始化,處理 expOrFn (渲染 watcher 和偵聽器處理不同)
  • 調(diào)用 this.get() 呻畸,它里面調(diào)用 pushTarget() 方法移盆,接下來執(zhí)行 this.getter.call(vm, vm) (對于渲染 watcher 調(diào)用 updateComponent),如果是用戶 watcher 會獲取屬性的值(觸發(fā) get 操作)
  • 當(dāng)數(shù)據(jù)更新的時候伤为,dep 中調(diào)用 notify() 方法咒循,notify() 中調(diào)用 watcher 的 update() 方法
  • update() 中調(diào)用 queueWatcher()
  • queueWatcher() 是一個核心方法,去除重復(fù)操作绞愚,調(diào)用 flushSchedulerQueue() 刷新隊(duì)列并執(zhí)行 watcher
  • flushSchedulerQueue() 中對 watcher 排序剑鞍,遍歷所有 watcher ,如果存在 before爽醋,觸發(fā)生命周期的鉤子函數(shù) beforeUpdate蚁署,執(zhí)行 watcher.run(),它內(nèi)部調(diào)用 this.get()蚂四,然后調(diào)用 this.cb() (渲染 watcher 的 cb 是 noop)

哪些數(shù)組操作可以觸發(fā)視圖更新

const vm = new Vue({
  el: '#app',
  data: {
    arr: [2, 3, 5],
  },
});

// 在瀏覽器控制臺分別執(zhí)行下面的代碼
// 會更新視圖
// vm.arr.push(8)
// 數(shù)據(jù)已改變光戈,但不會更新視圖
// vm.arr[0] = 100
// 將數(shù)組清空,也不會更新視圖
// vm.arr.length = 0
// 將數(shù)組第一位刪除遂赠,并重新賦值為100久妆,這時會更新視圖
// vm.arr.splice(0,1,100)

總結(jié)

實(shí)例方法/數(shù)據(jù)

Vue.set(target, propertyName/index, value)

  • 參數(shù):

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • 返回值:設(shè)置的值。

  • 用法:

    向響應(yīng)式對象中添加一個 property跷睦,并確保這個新 property 同樣是響應(yīng)式的筷弦,且觸發(fā)視圖更新。它必須用于向響應(yīng)式對象上添加新 property,因?yàn)?Vue 無法探測普通的新增 property (比如 this.myObject.newProperty = 'hi')

注意對象不能是 Vue 實(shí)例,或者 Vue 實(shí)例的根數(shù)據(jù)對象勉盅。

  • 示例
Vue.set(obj, 'foo', 'test');

vm.$set(target, propertyName/index, value)

  • 參數(shù):

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • 返回值:設(shè)置的值譬挚。

  • 用法:

    這是全局 Vue.set 的別名。

  • 示例

vm.$set(obj, 'foo', 'test');

源碼分析

export function set(target: Array<any> | Object, key: any, val: any): any {
  // 如果目標(biāo)是數(shù)組,并且key 是合法的索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 通過Math.max返回最大值号醉,并賦值給target.length
    target.length = Math.max(target.length, key);
    // 通過 splice 對key位置的元素進(jìn)行替換
    // splice 在 src/core/observer/array.js中進(jìn)行了響應(yīng)式的處理
    target.splice(key, 1, val);
    return val;
  }
  // 如果target中已存在key屬性反症,則直接賦值
  if (hasOwn(target, key)) {
    target[key] = val;
    return val;
  }
  // 獲取 target 中的 observer 對象
  const ob = (target: any).__ob__;
  // 如果target是vue實(shí)例或者是$data,則直接返回
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.',
      );
    return val;
  }
  // 如果不存在ob畔派,那么target 不是一個響應(yīng)式對象铅碍,則直接賦值并返回
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 把 key 設(shè)置為響應(yīng)式屬性
  defineReactive(ob.value, key, val);
  // 發(fā)送更新通知
  ob.dep.notify();
  return val;
}

Vue.delete(target, propertyName/index)

  • 參數(shù):

    • {Object | Array} target
    • {string | number} propertyName/index

僅在 2.2.0+ 版本中支持 Array + index 用法。

  • 用法:

    刪除對象的 property线椰。如果對象是響應(yīng)式的胞谈,確保刪除能觸發(fā)更新視圖。這個方法主要用于避開 Vue 不能檢測到 property 被刪除的限制士嚎,但是你應(yīng)該很少會使用它呜魄。

在 2.2.0+ 中同樣支持在數(shù)組上工作。

目標(biāo)對象不能是一個 Vue 實(shí)例或 Vue 實(shí)例的根數(shù)據(jù)對象莱衩。

vm.$delete(target, propertyName/index)

  • 參數(shù):

    • {Object | Array} target
    • {string | number} propertyName/index
  • 用法:

    這是全局 Vue.delete 的別名爵嗅。

源碼分析

export function del(target: Array<any> | Object, key: any) {
  // 如果目標(biāo)是數(shù)組笨蚁,并且key 是合法的索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 通過 splice 對key位置的元素進(jìn)行刪除
    // splice 在 src/core/observer/array.js中進(jìn)行了響應(yīng)式的處理
    target.splice(key, 1);
    return;
  }
  // 獲取 target 中的 observer 對象
  const ob = (target: any).__ob__;
  // 如果target是vue實(shí)例或者是$data睹晒,則直接返回
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.',
      );
    return;
  }
  // 如果target不存在key屬性,則直接返回
  if (!hasOwn(target, key)) {
    return;
  }
  // 刪除target的key屬性
  delete target[key];
  // 如果不存在ob括细,那么target 不是一個響應(yīng)式對象伪很,則直接返回
  if (!ob) {
    return;
  }
  // 發(fā)送更新通知
  ob.dep.notify();
}

vm.$watch(expOrFn, callback, [options])

  • 功能

    • 觀察 Vue 實(shí)例變化的一個表達(dá)式或計(jì)算屬性函數(shù)》艿ィ回調(diào)函數(shù)得到的參數(shù)為新值和舊值锉试。表達(dá)式只接受監(jiān)督的鍵路徑。對于更復(fù)雜的表達(dá)式览濒,用一個函數(shù)取代呆盖。
  • 參數(shù)

    • {string | Function} expOrFn:要監(jiān)視的 $data 中的屬性,可以是表達(dá)式或函數(shù)

    • {Function | Object} callback:數(shù)據(jù)變化后執(zhí)行的函數(shù)

      • 函數(shù):回調(diào)函數(shù)

      • 對象:具有 handler 屬性(字符串或者函數(shù))贷笛,如果該屬性為字符串則 methods 中相應(yīng)的定義

    • {Object} [options]:可選的選項(xiàng)

      • {boolean} deep:布爾類型应又,深度監(jiān)聽

      • {boolean} immediate:布爾類型,是否立即執(zhí)行一次回調(diào)函數(shù)

    • 返回值:{Function} unwatch

    注意:在變更 (不是替換) 對象或數(shù)組時乏苦,舊值將與新值相同株扛,因?yàn)樗鼈兊囊弥赶蛲粋€對象/數(shù)組。Vue 不會保留變更之前值的副本。

  • 示例

const vm = new Vue({
  el: '#app',
  data: {
    a: '1',
    b: '2',
    msg: 'Hello Vue',
    user: {
      firstName: '諸葛',
      lastName: '亮',
    },
  },
});
// expOrFn 是表達(dá)式
vm.$watch('msg', function (newVal, oldVal) {
  console.log(newVal, oldVal);
});
vm.$watch('user.firstName', function (newVal, oldVal) {
  console.log(newVal);
});
// expOrFn 是函數(shù)
vm.$watch(
  function () {
    return this.a + this.b;
  },
  function (newVal, oldVal) {
    console.log(newVal);
  },
);
// deep 是 true洞就,消耗性能
vm.$watch(
  'user',
  function (newVal, oldVal) {
    // 此時的 newVal 是 user 對象
    console.log(newVal === vm.user);
  },
  {
    deep: true,
  },
);
// immediate 是 true
vm.$watch(
  'msg',
  function (newVal, oldVal) {
    console.log(newVal);
  },
  {
    immediate: true,
  },
);

三種類型的 Watcher 對象

  • 沒有靜態(tài)方法盆繁,因?yàn)?$watch 方法中要使用 Vue 的實(shí)例

  • Watcher 分三種:計(jì)算屬性 Watcher、用戶 Watcher (偵聽器)奖磁、渲染 Watcher

  • 創(chuàng)建順序:計(jì)算屬性 Watcher改基、用戶 Watcher (偵聽器)繁疤、渲染 Watcher

  • 源碼分析

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    // 獲取 Vue 實(shí)例 this
    const vm: Component = this
    if (isPlainObject(cb)) {
      // 如果 cb 是對象咖为,則執(zhí)行 createWatcher
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 標(biāo)記為用戶 watcher
    options.user = true
    // 創(chuàng)建用戶 watcher 對象
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      // immediate 如果為 true,則立即執(zhí)行一次 cb 回調(diào)稠腊,把this指向vue躁染,并且當(dāng)前值傳入
      cb.call(vm, watcher.value)
    }
    // 返回取消監(jiān)聽的方法
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

Vue.nextTick([callback, context])

  • 參數(shù):

  • {Function} [callback]

  • {Object} [context]

  • 用法:

    在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)。在修改數(shù)據(jù)之后立即使用這個方法架忌,獲取更新后的 DOM吞彤。

// 修改數(shù)據(jù)
vm.msg = 'Hello';
// DOM 還沒有更新
Vue.nextTick(function () {
  // DOM 更新了
});

// 作為一個 Promise 使用 (2.1.0 起新增,詳見接下來的提示)
Vue.nextTick().then(function () {
  // DOM 更新了
});

2.1.0 起新增:如果沒有提供回調(diào)且在支持 Promise 的環(huán)境中叹放,則返回一個 Promise饰恕。請注意 Vue 不自帶 Promise 的 polyfill,所以如果你的目標(biāo)瀏覽器不原生支持 Promise (IE:你們都看我干嘛)井仰,你得自己提供 polyfill埋嵌。

vm.$nextTick([callback])

  • 參數(shù):

    • {Function} [callback]
  • 用法:

    將回調(diào)延遲到下次 DOM 更新循環(huán)之后執(zhí)行。在修改數(shù)據(jù)之后立即使用它俱恶,然后等待 DOM 更新雹嗦。它跟全局方法 Vue.nextTick 一樣,不同的是回調(diào)的 this 自動綁定到調(diào)用它的實(shí)例上合是。

    2.1.0 起新增:如果沒有提供回調(diào)且在支持 Promise 的環(huán)境中了罪,則返回一個 Promise。請注意 Vue 不自帶 Promise 的 polyfill聪全,所以如果你的目標(biāo)瀏覽器不是原生支持 Promise (IE:你們都看我干嘛)泊藕,你得自行 polyfill。

  • 示例

new Vue({
  // ...
  methods: {
    // ...
    example: function () {
      // 修改數(shù)據(jù)
      this.message = 'changed';
      // DOM 還沒有更新
      this.$nextTick(function () {
        // DOM 現(xiàn)在更新了
        // `this` 綁定到當(dāng)前實(shí)例
        this.doSomethingElse();
      });
    },
  },
});

源碼分析

src/core/instance/index.js 中調(diào)用 src/core/instance/render.js 中的 renderMixin 方法

  • 手動調(diào)用 vm.$nextTick()
  • 在 Watcher 的 queueWatcher 中執(zhí)行 nextTick()
  • src/core/util/env.js
export const nextTick = (function () {
  const callbacks = [];
  let pending = false;
  let timerFunc;

  function nextTickHandler() {
    pending = false;
    // 克隆回調(diào)函數(shù)數(shù)組
    const copies = callbacks.slice(0);
    // 將callbacks數(shù)組置為空數(shù)組
    callbacks.length = 0;
    // 遍歷執(zhí)行回調(diào)函數(shù)數(shù)組中的每一個回調(diào)函數(shù)
    for (let i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }

  // An asynchronous deferring mechanism.
  // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
  // but microtasks actually has too high a priority and fires in between
  // supposedly sequential events (e.g. #4521, #6690) or even between
  // bubbling of the same event (#6566). Technically setImmediate should be
  // the ideal choice, but it's not available everywhere; and the only polyfill
  // that consistently queues the callback after all DOM events triggered in the
  // same loop is by using MessageChannel.
  /* istanbul ignore if */
  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
      setImmediate(nextTickHandler);
    };
  } else if (
    typeof MessageChannel !== 'undefined' &&
    (isNative(MessageChannel) ||
      // PhantomJS
      MessageChannel.toString() === '[object MessageChannelConstructor]')
  ) {
    const channel = new MessageChannel();
    const port = channel.port2;
    channel.port1.onmessage = nextTickHandler;
    timerFunc = () => {
      port.postMessage(1);
    };
  } else if (typeof Promise !== 'undefined' && isNative(Promise)) {
  /* istanbul ignore next */
    // use microtask in non-DOM environments, e.g. Weex
    const p = Promise.resolve();
    timerFunc = () => {
      p.then(nextTickHandler);
    };
  } else {
    // fallback to setTimeout
    timerFunc = () => {
      setTimeout(nextTickHandler, 0);
    };
  }

  return function queueNextTick(cb?: Function, ctx?: Object) {
    let _resolve;
    // 把 cb 加上異常處理存入 callbacks 數(shù)組中
    callbacks.push(() => {
      if (cb) {
        try {
          // 如果存在cb
          // 將cb的this指向ctx难礼,并調(diào)用 cb()
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      // 返回 promise 對象
      return new Promise((resolve, reject) => {
        _resolve = resolve;
      });
    }
  };
})();
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末娃圆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鹤竭,更是在濱河造成了極大的恐慌踊餐,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臀稚,死亡現(xiàn)場離奇詭異吝岭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門窜管,熙熙樓的掌柜王于貴愁眉苦臉地迎上來散劫,“玉大人,你說我怎么就攤上這事幕帆』癫” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵失乾,是天一觀的道長常熙。 經(jīng)常有香客問我,道長碱茁,這世上最難降的妖魔是什么裸卫? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮纽竣,結(jié)果婚禮上墓贿,老公的妹妹穿的比我還像新娘。我一直安慰自己蜓氨,他們只是感情好聋袋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著穴吹,像睡著了一般幽勒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上刀荒,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天代嗤,我揣著相機(jī)與錄音,去河邊找鬼缠借。 笑死干毅,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的泼返。 我是一名探鬼主播硝逢,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绅喉!你這毒婦竟也來了渠鸽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤柴罐,失蹤者是張志新(化名)和其女友劉穎徽缚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體革屠,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凿试,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年排宰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片那婉。...
    茶點(diǎn)故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡板甘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出详炬,到底是詐尸還是另有隱情盐类,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布呛谜,位于F島的核電站在跳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏呻率。R本人自食惡果不足惜硬毕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一呻引、第九天 我趴在偏房一處隱蔽的房頂上張望礼仗。 院中可真熱鬧,春花似錦逻悠、人聲如沸元践。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽单旁。三九已至,卻和暖如春饥伊,著一層夾襖步出監(jiān)牢的瞬間象浑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工琅豆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留愉豺,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓茫因,卻偏偏與公主長得像蚪拦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子冻押,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容