Vue 相应式中心 observer 源码详解 + 实践

笔者日前进修了 Vue 的 Observer 部份,简朴地谷歌了一下,因为没有找到诠释地非常完整的中文资本,记下本身对其的邃晓并分享。

转载需申明出处 https://segmentfault.com/a/11… ,有协助请点赞。

本文援用的 Vue 版本为 v2.5.17-beta.0 。
不过 Vue 的 Observer 部份自2017年以来至今没什么大变化,v2.5.16 到 v2.5.17-beta.0 对 Observer 有个小小的 bugfix。

内容

本文引见 Vue 响应式道理的完成历程,并试图以之为参照改造出一个便于移植的库。这里笔者把 Vue 的 observer 部份提出来独登时讲,读者不须要对 Vue 其他部份非常熟习。

Vue 的响应式模子非常完美,完成地充足奇妙,私以为有进修的必要。本文预备从写一个简朴的模子动身,一步步添补功用,演化成 Vue 源码的形状,所以文章看起来好像巨长,但代码多有反复;我以为如许写,读者看起来会比较轻松,所以请没必要长文恐惊。卢瑟福说,“只要你能将一个理论讲得连女仆都懂了,你才算真正懂了”。虽然读者可以不是女仆(?),我也会写得只管邃晓的。

本文对 Observer 引见地很完整,对象和数组的差别处置惩罚,deep watching,以及异步行列都邑解说。固然,也不会完整整成源码那末贫苦,一些只和 Vue 有关的代码删除了,别的盘算属性(computed property)的部份只申明道理,省略了完成。

但平常的 JS 技能,ECMAScript 6,闭包的学问,Object.defineProperty 的学问照样须要具有的。

Vue 源码是用 Flow 写的,本文改成 TypeScript 了(同为范例注解,毕竟后者更盛行),未进修过的同砚只要把文中不像 JS 的部份去掉,当 JS 就好了。

JS 中数组是对象的一种,因为 Observer 部份对数组与一般对象的看待区分很大,所以下文说到对象,都是指 constructor 为 Object 的一般对象。

预备

可以先git clone git@github.com:vuejs/vue.git一份源码备看。observer 的部份在源码的 src/core/observer 目录下。

本文代码已放在 https://github.com/xyzingh/le… ,运转 npm i && npm run test 可以测试。

新建文件夹 learn-vue-observer,竖立几个文件。

util.ts

/* 一些经常运用函数的简写 */

export function def(obj: any, key: string, value: any, enumerable: boolean = false) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true,
  });
}

export function isObject(obj: any) {
  return obj !== null && typeof obj === "object";
}

export function hasOwn(obj: any, key: string): boolean {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

export function isPlainObject(obj: any): boolean {
  return Object.prototype.toString.call(obj) === "[object Object]";
}

export function isNative(ctor: any): boolean {
  return typeof ctor === "function" && /native code/.test(ctor.toString());
}

export function remove(arr: any[], item: any): any[] | void {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}

版本 0.1

假定我们要把下面这个对象转变成响应式的。

let obj = {
  a: {
    aa: {
      aaa: 123,
      bbb: 456,
    },
    bb: "obj.a.bb",
  },
  b: "obj.b",
};

如何算作是响应式的呢?假如将 obj 的任意键的值转变,都能实行一个响应的函数举行相干操纵(比方更新DOM),那末就算得上响应式了。为此,我们必将为 obj 的每一个键竖立代办,使对 obj 的直接操纵变成透过代办操纵。代办的体式格局有很多,Object.observeProxy,getter/setter。但 Object.observe 已被烧毁,Proxy 巨硬家从 Edge 才最先支撑,IE 全灭,所以可行的只要 getter/setter (IE9 最先支撑)。然则 getter/setter 依旧有很大的局限性,即只能转化已有属性,因而须要为用户供应迥殊的函数来设置新属性,这个函数我们末了再提。

obj 的值都转成 getter/setter 了,实在值存在哪呢?Vue 的做法是藏在闭包里。

下面我们定义3个函数/类,尝试递归地设置 obj 的 getter/setter。

index.ts

import { def, hasOwn, isObject, isPlainObject } from "./util";

/**
 * 尝试对 value 竖立 Observer 实例,
 * value 假如不是对象或数组,什么都不做。
 * @param value 须要尝试看管的目的
 */
export function observe(value: any) {
  if (!isObject(value)) {
    return;
  }

  let ob: Observer | void;
  if (typeof value.__ob__ !== "undefined") {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}

export class Observer {
  constructor(value: any) {
    def(value, "__ob__", this);
    this.walk(value);
  }
  public walk(value: any) {
    for (const key of Object.keys(value)) {
      defineReactive(value, key);
    }
  }
}

function defineReactive(obj: any, key: string, val?: any) {
  // 闭包中的 val 藏着 obj[key] 的实在值
  if (arguments.length === 2) {
    val = obj[key];
  }

  let childOb = observe(val); // val 假如不是对象的话,是返回 undefined 的。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      ////////////////
      console.log("you get " + val);
      ////////////////
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      ////////////////
      console.log("you set " + newVal);
      ////////////////
      val = newVal;
      childOb = observe(newVal);
    }
  });
}

我们可以试一下

observe(obj);
console.log(obj.a.aa.aaa = 234);

输出应为

you get [object Object]
you get [object Object]
you set 234
234

然则,有个题目,我们不该假定 obj 的每一个键就是简朴的值,万一原本就是 getter/setter 呢?

let obj2 = {};
Object.defineProperty(obj2, "a", {
    configurable: true,
    enumerable: true,
    get() {
        return obj2._a;
    },
    set(val) {
        obj2._a = val;
    },
});
Object.defineProperty(obj2, "_a", {
    enumerable: false,
    value: 123,
    writable: true,
});

因而,须要修正 defineReactive ,继承用闭包保留 getter/setter 。

function defineReactive(obj: any, key: string, val?: any) {
  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  const getter = property!.get; // property! 的叹号是 TypeScript 语法,疏忽即可
  const setter = property!.set;

  // 为何写成 (!getter || setter) ?后面会议论。
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  let childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      const value = getter ? getter.call(obj) : val;
      ////////////////
      console.log("you get " + value);
      ////////////////
      return value;
    },
    set(newVal) {
      const value = getter ? getter.call(obj) : val;
      if (newVal === value) {
        return;
      }
      ////////////////
      console.log("you set " + newVal);
      ////////////////
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal);
    },
  });
}

如许就可以够成功地把 obj2 转变成响应式的。

笔者在邃晓 if ((!getter || setter) && arguments.length === 2) 时遇到过停滞,这实际上是讲:

  1. 假如 arguments 长为3,参数 val 存在,就以为是显式地设置了这个键的值,本来的值就不斟酌了
  2. 假如 getter setter 都存在,就以为这对 getter/setter 是在代办某个实在值,所以须要 val = obj[key],然后 let childOb = observe(val) 对这个实在值继承举行递归设置
  3. 不然 假如 getter 存在,setter 不存在,以为 getter 也许只是返回某个天生的值,不实行 val = obj[key],也就致使下面 let childOb = observe(undefined)
  4. getter 不存在,setter 存在,这类奇葩事变不在斟酌范围内(比方 document.cookie)

这是 v2.5.17-beta.0 的一个 bugfix ,有关的议论原文来自↓
issue/7280
issue/7302
pull/7981
issue/8494

版本 0.2: 加上对数组的支撑

并不是说之前的版本不支撑数组,而是平常开发者运用数组与运用对象的要领有区分。数组在 JS 中常被看成栈,行列,鸠合等数据结构的完成体式格局,会贮存批量的数据以待遍历。编译器对对象与数组的优化也有所差别。所以对数组的处置惩罚须要特化出来以进步机能。

起首,不能再对数组每一个键设置 getter/setter 了,而是修正掩盖数组的 push, pop, … 等要领。用户要修正数组只能运用这些要领,不然不会是响应式的(除了 Vue.set, Vue.delete)。

因而,预备一个数组要领的替代品。哪些要领应当替代掉?那些不会过问原数组的要领不须要修正;删除数组元素的要领须要替代;增添或替代数组元素的要领须要替代,还要尝试把新的值变成响应式的。

array.ts

import { def } from './util';

const arrayProto = Array.prototype as any;
// 竖立以 Array.prototype 为原型的 arrayMethods 并导出
export const arrayMethods = Object.create(arrayProto);

// 会过问原数组的要领
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
];

methodsToPatch.forEach((method: string) => {
  // 原要领的缓存
  const original = arrayProto[method];

  // 在 arrayMethods 上定义替代要领
  def(arrayMethods, method, function (this: any, ...args: any[]) {
    const result = original.apply(this, args);
    const ob = this.__ob__;

    // 新增的元素
    let inserted: any[] | void;

    switch (method) {
      // 会增添或替代元素的要领
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    if (inserted){
      ob.observeArray(inserted); // Observer 上新增的要领
    }
    ///////////////////////////////
    console.log("array is modified.");
    ///////////////////////////////
    return result;
  });
});

然后修正 Observer,区分看待数组。

export class Observer {
  constructor(value: any) {
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      // 替代原型(Object.setPrototype 这个要领实行地比较慢,而且支撑状况堪忧)
      Object.setPrototypeOf(value, arrayMethods);
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  public walk(value: any) {
    for (const key of Object.keys(value)) {
      defineReactive(value, key);
    }
  }
  public observeArray(items: any[]) {
    // 设置 l = items.length 防备遍历历程当中 items 长度变化
    for (let i = 0, l = items.length; i < l; i++) {
      // 直接视察数组元素,省略在键上设置 getter/setter 的步骤
      observe(items[i]);
    }
  }
}

版本 1.0: 增添 Dep, Watcher

从 API 动身思索写法

vm.$watch( expressionOrFunction, callback [, options] ) 是 Vue 最基本的视察本身 data 的体式格局。我们参考这个函数,提出实用本文的一个函数:

watch( target, expression, callback )

视察 target 这个对象的表达式 exp 的值,一旦发作变化时实行 callback (同步地)。callback 的第一个参数为新的值,第二个参数为旧的值,this 为 target。

比方 watch(obj, "a.aa.bbb", val => console.log(val)) ,当 obj.a.aa.bbb 转变时,控制台会打印新的值。注重 obj 应当已经由 observe(obj) 转化过了。

之前版本我们在 getter/setter 处留下了

/////////////////
console.log(XXX)
/////////////////

只要把这些替代成响应代码,就可以完成 watch 要领了。

如今来定义一下哪些状况应实行 callback 。

假定 obj.a.aa.bbb = 456 ,我们对这个键举行了 watch :

  1. obj.a.aa.bbb = 456 值没变,不须要
  2. obj.a.aa.bbb = 999 应实行 callback
  3. obj.a.aa = { bbb: 456 } 值没变,不实行 callback
  4. obj.a.aa = { bbb: 999 } 应实行 callback
  5. obj = {a:{aa:{bbb:999}}} 对象都被替代成新的了,想实行 callback 也不可以

假定我们还对 obj.a.aa 举行了 watch :

  1. obj.a.aa.bbb = 999 虽然 obj.a.aa 发作了变异(mutation),但 obj.a.aa 照样它本身,不实行 callback
  2. obj.a.aa = { bbb: 456 } 应实行 callback

简而言之,假如 target 沿着 expression 剖析到的值与之前的不全等,就以为须要实行 callback 。关于基本范例来讲,就是值的不全等。关于一般对象,就是援用不雷同。但数组比较特别,对数组元素举行了操纵,就应实行 callback 。

怎样组织代码呢?Evan You (Vue 作者) 的要领比较奇妙。

Observer, Dep, Watcher

竖立两个新的类,Dep, Watcher 。Dep 是 Dependency 的简称,每一个 Observer 的实例,成员中都有一个 Dep 的实例。

这个 Dep 的本质是个数组,安排着监听这个 Observer 的 Watcher ,当这个 Observer 对应的值变化时,就关照 Dep 中的一切 Watcher 实行 callback 。

export class Observer {
  constructor(value: any) {
    this.dep = new Dep(); // 新增
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
// .........................

Watcher 是挪用 watch 函数发作的,它保留着 callback 而且保护了一个数组,数组寄存了一切 存有这个 Watcher 的 Dep 。如许当这个 Watcher 须要被删除时,可以遍历数组,从各个 Dep 中删去本身,也就是 unwatch 的历程。

Watcher 什么时刻被放入 Dep 中的先不谈。先说说 Dep 都在什么地方。

以上说得并不全对,应当说,原始的 Dep 是竖立在 defineReactive 的闭包中,Observer 的 dep 成员只是这个原始的 Dep 的备份,一向一同被保护,保持一致。别的,Observer 只会竖立在对象或数组的 __ob__ 上,假如键的值不是对象或数组,只会有闭包中的 Dep 保留这个键的 Watcher 。

function defineReactive(obj: any, key: string, val?: any) {
  const dep = new Dep(); // 新增
  const property = Object.getOwnPropertyDescriptor(obj, key);
// ...........................

举例来讲,

let obj = {
  // obj.__ob__.dep: 保留 obj 的 dep

  a: { // 闭包中有 obj.a 的 dep
    // obj.a.__ob__.dep: 保留 obj.a 的 dep

    aa: { // 闭包中有 obj.a.aa 的 dep
      // obj.a.aa.__ob__.dep: 保留 obj.a.aa 的 dep

      aaa: 123, // 闭包中有 obj.a.aa.aaa 的 dep
      bbb: 456, // 闭包中有 obj.a.aa.bbb 的 dep
    },
    bb: "obj.a.bb", // 闭包中有 obj.a.bb 的 dep
  },
  b: "obj.b", // 闭包中有 obj.b 的 dep
};
observe(obj);

数组特别看待,不对数组的成员举行 defineReactive ,

let obj = {
  arr: [ // 闭包中 obj.arr 的 dep
    // obj.arr.__ob__.dep

    2, // 没有 dep ,没有闭包
    3,
    5,
    7,
    11,
    { // 没有闭包
      // obj.arr[6].__ob__.dep 存在
    },
    [ // 没有闭包
      // obj.arr[7].__ob__.dep 存在
    ],
  ],
};
observe(obj);

温习一下,dep 本质是个数组,放着监听这个键的 Watcher 。

当这个键的值被修正时,就应当关照响应 dep 的一切 Watcher ,我们在 Dep 上设置 notify 要领,用来完成这个功用。

为此,修正 setter 的部份。

function defineReactive(obj: any, key: string, val?: any) {
  const dep = new Dep();
  // .................................
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // .............................
    },
    set(newVal) {
      // .............................
      }
      childOb = observe(newVal);
      dep.notify();
    },
  });
}

数组的部份,

array.ts

  // ................
  def(arrayMethods, method, function (this: any, ...args: any[]) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    // .....................
    if (inserted){
      ob.observeArray(inserted);
    }
    ob.dep.notify();
    return result;
  });

如此一来,每当修正值时,响应的 Watcher 都邑被关照了。

如今的题目是,什么时刻怎样把 Watcher 放入 dep 中。下面我们先来尝试完成 Dep 。

dep.ts

import { remove } from "./util";
import { Watcher } from "./watcher";

let uid = 0;

export default class Dep {
  public id: number;
  public subs: Watcher[];

  constructor() {
    this.id = uid++;
    this.subs = [];
  }

  public addSub(sub: Watcher) {
    this.subs.push(sub);
  }

  public removeSub(sub: Watcher) {
    remove(this.subs, sub);
  }

  public notify() {
    // 先复制一份,应对关照 Watcher 历程当中,this.subs 可以变化的状况
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      // Watcher 上定义了 update 要领,用来被关照
      subs[i].update();
    }
  }
}

(重点)用 Touch 的要领,网络依靠

假定用 watch(obj, "a.aa.bbb", val => console.log(val)) ,竖立了一个 Watcher ,这个 Watcher 应被放进哪些 Dep 中呢?

因为 obj.a, obj.a.aa 转变时,obj.a.aa.bbb 的值可以转变,所以答案是 obj.a, obj.a.aa, obj.a.aa.bbb 的闭包中的 Dep, 前二者是对象,所以在 __ob__.dep 中再放一份。

因为在对表达式 obj.a.aa.bbb 求值时,会顺次实行 obj.a, (obj.a).aa, ((obj.a).aa).bbb 的 getter ,这也恰好对应了应被放入 Watcher 的键,所以很天然的一个主意是,

划定一个全局变量,平常是 null ,当在决议某个 Watcher 该放入哪些 Dep 的时刻(即 依靠网络 阶段),让这个全局变量指向这个 Watcher 。然后 touch 被看管的谁人键,换言之,对谁人键求值。途中会挪用一连串的 getter ,往那些 getter 所对应的 Dep 里放入这个 Watcher 就对了。以后再将全局变量改回 null 。

这个做法的妙处,还在于它可以同时实用 deep watching盘算属性(computed property)。deep watching 后面会再说,关于盘算属性,这使得用户直接写函数就行,无需显式申明这个盘算属性所依靠的其他属性,非常文雅,因为在运算这个函数时,用到其他属性就会触发 getter ,可以的依靠都邑被网络起来。

我们来尝试完成,

export default class Dep {
  // Dep.target 即前文所谓的全局变量
  public static target: Watcher | null = null;

  public id: number;
  public subs: Watcher[];

  public depend() {
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }
// ...................................................
function defineReactive(obj: any, key: string, val?: any) {
  const dep = new Dep();
  // ...................................................
  let childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      const value = getter ? getter.call(obj) : val;

      // 假如处在依靠网络阶段
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
      }

      return value;
    },
  // ....................................................
}

如今也该把一向议论的 Watcher 给完成了。依据前面说的,它应当有个 update 要领。

watcher.ts

import Dep from "./dep";

let uid = 0;

export class Watcher {
  public id: number;
  public value: any;
  public target: any;
  public getter: (target: any) => any;
  public callback: (newVal: any, oldVal: any) => void;

  constructor(
    target: any,
    expression: string,
    callback: (newVal: any, oldVal: any) => void,
  ) {
    this.id = uid++;
    this.target = target;
    this.getter = parsePath(expression);
    this.callback = callback;
    this.value = this.get();
  }

  public get() {
    // 进入依靠网络阶段
    Dep.target = this;

    let value: any;
    const obj = this.target;
    try {
      // 挪用了一连串 getter ,对应的键的 dep 中放入了这个 watcher
      value = this.getter(obj);
    } finally {
      // 退出依靠网络阶段
      Dep.target = null;
    }
    return value;
  }

  public update() {
    this.run();
  }
  public run() {
    this.getAndInvoke(this.callback);
  }
  public getAndInvoke(cb: (newVal: any, oldVal: any) => void) {
    const value = this.get();
    if (value !== this.value || isObject(value) /* 看管目的为对象或数组的话,仍应实行回调,因为值可以变异了 */) {
      const oldVal = this.value;
      this.value = value;
      cb.call(this.target, value, oldVal);
    }
  }
}

const bailRE = /[^\w.$]/;
function parsePath(path: string): any {
  if (bailRE.test(path)) {
    return;
  }
  const segments = path.split(".");
  return (obj: any) => {
    for (const segment of segments) {
      if (!obj) { return; }
      obj = obj[segment];
    }
    return obj;
  };
}

版本 1.1: 特化数组的依靠网络

function defineReactive(obj: any, key: string, val?: any) {
// .....................................
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
// ......................................
}

function dependArray(value: any[]) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];

    // 若为多维数组,继承递归看管
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

议论的原文来自 issue/3883 ,举例而言,

let obj = {
  matrix: [
    [2, 3, 5, 7, 11],
    [13, 17, 19, 23, 29],
  ],
};
observe(obj);
watch(obj, "matrix", val => console.log(val));
obj.matrix[0].push(1);
// 致使 matrix[0].__ob__.dep.notify() ,因为递归看管,这个 dep 里也有上面的 Watcher

版本 1.2: 完美 Watcher 的生命周期

只要 watch 没有 unwatch 天然是不合理的。前面提到,Watcher 也保护了一个数组 deps,寄存一切 放了这个 Watcher 的 Dep ,当这个 Watcher 析构时,可以从这些 Dep 中删去本身。

我们给 Watcher 增添 active, deps, depIds, newDeps, newDepIds 属性,addDep, cleanupDeps, teardown 要领,个中 teardown 要领起的是析构的作用,active 标志 Watcher 是不是可用,其他的都是围绕着保护 deps 。

export class Watcher {
  // ..............................
  public active = true;
  public deps: Dep[] = [];
  public depIds = new Set<number>();
  public newDeps: Dep[] = [];
  public newDepIds = new Set<number>();

  public run() {
    if (this.active) {
      this.getAndInvoke(this.callback);
    }
  }

  // newDeps 是新一轮网络的依靠,deps 是之前一轮网络的依靠
  public 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);
      }
    }
  }

  public get() {
    Dep.target = this;

    let value: any;
    const obj = this.target;
    try {
      value = this.getter(obj);
    } finally {
      Dep.target = null;
      this.cleanupDeps();
    }
    return value;
  }

  // 清算依靠
  // 之前网络的依靠 假如不出如今新一轮网络的依靠中,就清撤除
  // 再交流 deps/newDeps, depIds/newDepIds
  public cleanupDeps() {
    let i = this.deps.length;
    while (i--) {
      const dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    const tmpIds = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmpIds;
    this.newDepIds.clear();

    const tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  }

  public teardown() {
    if (this.active) {
      let i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  }
}

修正之前的 dep.ts

export default class Dep {
  public depend() {
    if (Dep.target) {
      // this.addSub(Dep.target);
      Dep.target.addDep(this);
    }
  }
}

版本 2.0: deep watching

Deep watching 的道理很简朴,就是在用 touch 网络依靠的基本上,递归遍历并 touch 一切子元素,如此一来,一切子元素都被网络到依靠中。个中只要防备对象援用成环须要轻微注重一下,这个用一个鸠合纪录遍历到的元夙来处理。

我们给 Watcher 组织函数增添一个 deep 选项。

直接贴代码,

export class Watcher {
  public deep: boolean;
  constructor(
    target: any,
    expression: string,
    callback: (newVal: any, oldVal: any) => void,
    {
      deep = false,
    },
  ) {
    this.deep = deep;
    // ................................
  }
  public get() {
    Dep.target = this;

    let value: any;
    const obj = this.target;
    try {
      value = this.getter(obj);
    } finally {
      if (this.deep) {
        // touch 一切子元素,网络到依靠中
        traverse(value);
      }
      Dep.target = null;
      this.cleanupDeps();
    }
    return value;
  }

  public getAndInvoke(cb: (newVal: any, oldVal: any) => void) {
    const value = this.get();
    if (value !== this.value ||
      isObject(value) ||
      this.deep /* deep watcher 一向实行 */
    ) {
      const oldVal = this.value;
      this.value = value;
      cb.call(this.target, value, oldVal);
    }
  }
}

traverse.ts

import { isObject } from "./util";

const seenObjects = new Set();

export function traverse(val: any) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}

function _traverse(val: any, seen: Set<any>) {
  let i;
  let keys;
  const isA = Array.isArray(val);
  if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
    return;
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id;
    if (seen.has(depId)) {
      return;
    }
    seen.add(depId);
  }
  if (isA) {
    i = val.length;
    while (i--) { _traverse(val[i] /* touch */, seen); }
  } else {
    keys = Object.keys(val);
    i = keys.length;
    while (i--) { _traverse(val[keys[i]] /* touch */, seen); }
  }
}

版本 2.1: 异步 Watcher, 异步行列

运用异步 Watcher 可以缓冲在统一次事宜轮回中发作的一切数据转变。假如在本次实行栈中统一个 Watcher 被屡次触发,只会被推入到行列中一次。如许在缓冲时去除反复数据,可以防止没必要要的盘算,进步机能。

Vue 源码中的异步行列模子比下文中的庞杂,因为 Vue 要保证

  1. 从父组件到子组件的更新递次
  2. 用户定义的 watcher 在 担任衬着的 watcher 之前运转
  3. 若在父组件的 watcher 运转时摧毁了子组件,子组件的 watcher 应被跳过
  4. 被盘算属性依靠的另一盘算属性先运转

假如这些是你的兴致,请直接转战源码 src/core/observer/scheduler.js 。

如今修正 watcher.ts ,

export class Watcher {
  public deep: boolean;
  public sync: boolean;
  constructor(
    target: any,
    expression: string,
    callback: (newVal: any, oldVal: any) => void,
    {
      deep = false,
      sync = false, // 增添同步选项
    },
  ) {
    this.deep = deep;
    this.sync = sync;
    // ............................
  }
  public update() {
    if (this.sync) {
      this.run();
    } else {
      queueWatcher(this); // 推入行列
    }
  }
}

竖立 scheduler.ts

/// <reference path="next-tick.d.ts" />
import { nextTick } from "./next-tick";
import { Watcher } from "./watcher";

const queue: Watcher[] = [];
let has: { [key: number]: true | null } = {};
let waiting = false;
let flushing = false;
let index = 0;

/**
 * 重置 scheduler 的状况.
 */
function resetSchedulerState() {
  index = queue.length = 0;
  has = {};
  waiting = flushing = false;
}

/**
 * 革新行列,并运转 watcher
 */
function flushSchedulerQueue() {
  flushing = true;
  let watcher;
  let id;

  queue.sort((a, b) => a.id - b.id);

  
  for (index = 0; index < queue.length /* 不缓存行列长度,因为新的 watcher 可以在实行行列时加进来 */; index++) {
    watcher = queue[index];
    id = watcher.id;
    has[id] = null;
    watcher.run();
  }

  resetSchedulerState();
}

/**
 * 将一个 watcher 推入行列
 * 雷同 ID 的 watcher 会被跳过
 * 除非行列中之前的雷同ID的 watcher 已被处置惩罚掉
 */
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      let i = queue.length - 1;

      // 放到行列中响应 ID 的位置
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    if (!waiting) {
      waiting = true;
      
      // 放入微使命行列
      nextTick(flushSchedulerQueue);
    }
  }
}

假如不清楚微使命行列是什么,可以浏览下 邃晓浏览器和node.js中的Event loop事宜轮回

下面贴一下 Vue 的 nextTick 完成。
next-tick.d.ts

// 本身给 next-tick 写了下接口
export declare function nextTick(cb: () => void, ctx?: any): Promise<any> | void;

next-tick.js (注重这是 JS)

import { isNative } from "./util";

const inBrowser = typeof window !== "undefined";
const inWeex = typeof WXEnvironment == "undefined" && !!WXEnvironment.platform;
const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();
const UA = inBrowser && window.navigator.userAgent.toLowerCase();
const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === "ios");

function noop() {}
function handleError() {}

const callbacks = [];
let pending = false;

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc;
let macroTimerFunc;
let useMacroTask = false;

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. 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)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else if (typeof MessageChannel !== "undefined" && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === "[object MessageChannelConstructor]"
)) {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = flushCallbacks;
  macroTimerFunc = () => {
    port.postMessage(1);
  };
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== "undefined" && isNative(Promise)) {
  const p = Promise.resolve();
  microTimerFunc = () => {
    p.then(flushCallbacks);
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) { setTimeout(noop); }
  };
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc;
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
export function withMacroTask(fn) {
  return fn._withTask || (fn._withTask = function() {
    useMacroTask = true;
    const res = fn.apply(null, arguments);
    useMacroTask = false;
    return res;
  });
}

export function nextTick(cb, ctx) {
  let _resolve;
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    if (useMacroTask) {
      macroTimerFunc();
    } else {
      microTimerFunc();
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}

总结

本文代码已放在 https://github.com/xyzingh/le… ,运转 npm i && npm run test 可以测试。

    原文作者:xyzinsf
    原文地址: https://segmentfault.com/a/1190000015709022
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞