Vue2 Source

Vue2 Observer

deprecated warning

读源码前

  • 从 GitHub 克隆一份 vue@2 在 dev 上的分支
  • 使用 flow-remove-types 将 src 目录下的 flow 文件转换为普通的 JS
  • 使用 PowerShell 的 tree 指令输出 src 目录的树形结构,以理解源码结构

目录结构

可以大致根据目录将源码拆分为几个部分:

├─compiler // 模板编译相关
│  ├─codegen  // 代码生成器
│  ├─directives // 指令解析器
│  └─parser // 模板解析器
├─core  // 核心代码
│  ├─components // component 相关 API
│  ├─global-api // 全局 API
│  ├─instance
│  │  └─render-helpers
│  ├─observer // 响应式属性
│  ├─util // 工具函数
│  └─vdom // 虚拟节点
│      ├─helpers
│      └─modules
├─platforms // 平台相关代码
├─server  // 服务端渲染相关代码
│  ├─bundle-renderer
│  ├─optimizing-compiler
│  ├─template-renderer
│  └─webpack-plugin
├─sfc // SFC 结构解析器
└─shared  // 工具函数

变化侦测

去年读变化侦测的时候,借着项目的机会,实现过一个轻量的状态管理:state-vex,用到了 VueJS 响应式绑定相关的一些内容。

侦测类

Vue 通过 Observer 把对象的所有属性转化为带有能收集依赖并触发依赖更新的 setter/getter。setter/getter 调用时,分别收集依赖、触发依赖更新。Observer 还能遍历数组元素。但是更新依赖的方法放在了数组原型方法中,比如 Array.prototype.splice,用包装器将其增强,就能在调用时同时调用更新依赖的函数。

简而言之,Observer 使对象“可观测”,即“变化侦测”中的“侦测”。通过 observe 函数,我们能命令式地侦测一个对象。

function observe(value, asRootData) {
  ob = new Observer(value);
  return ob;
}

class Observer {
  constructor(value) {
    this.value = value;
    // 初始化依赖容器
    this.dep = new Dep();
    this.vmCount = 0;
    // 将侦测实例实例挂载到对象上方便访问
    def(value, "__ob__", this);
    // 如果是数组,则给它挂载带有依赖收集及触发依赖更新增强的原型函数,并遍历元素设置响应式属性;
    // 如果是对象,则遍历对象的每一个值并递归遍历子对象,设置响应式属性
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

监测数组变化

因为数组没有 setter/getter,所以使用数组时,没有办法做响应式更新。Vue 以猴子补丁的形式增强了数组元素的原型方法,使得这些方法被调用时,自动更新依赖。

// 需要增强的方法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    // 先执行方法获取结果,而不是先通知依赖更新,这个顺序要注意
    const result = original.apply(this, args);
    // observer 在设置响应式属性时就被挂载在了对象的 \_\_ob\_\_ 属性中
    const ob = this.__ob__;
    // inserted 用来保存数组中新增的元素,这些新增元素也需要遍历并设置响应式更新
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // 通知依赖更新
    ob.dep.notify();
    return result;
  });
});

响应式属性

设置响应式属性对应源码中的 definedProperty 这个函数,同时,还提供了 set、del 方法,分别用来给一个对象新增、删除属性及响应式属性。

function defineReactive(obj, key, val, customSetter, shallow) {
  // 每个属性都保持了一个依赖容器,
  // 所有依赖都会存放在这个依赖容器实例中
  const dep = new Dep();

  // 不给属性描述符不可变的对象设置 setter/getter
  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  // 保存属性原有的 setter/getter,不做破坏
  // cater for pre-defined getter/setters
  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;
      // 收集依赖
      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;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      // 非生产环境:自定义 setter 副作用
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return;
      // 使用原有的 setter 更新值或直接更新值
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      // 侦测子对象的变化
      childOb = !shallow && observe(newVal);
      // 通知依赖更新
      dep.notify();
    },
  });
}

依赖容器及依赖

接下来进入比较绕的部分。首先回答:依赖容器是什么?

刚刚看到了 dep.notify 的作用是“通知依赖更新”,所以 Dep 就是依赖容器。

我们上小节说到,每个属性都保持了一个依赖容器,所有依赖都会存放在这个依赖容器实例中。所以依赖容器还提供了统一更新依赖、删除依赖之类的方法。

class Dep {
  constructor() {
    this.id = uid++;
    this.subs = [];
  }
  // 添加依赖
  addSub(sub) {
    this.subs.push(sub);
  }
  // 删除依赖
  removeSub(sub) {
    remove(this.subs, sub);
  }
  // 收集依赖
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  // 通知依赖更新
  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

依赖容器和依赖之间是存在耦合的,所以我们看到收集依赖的这个地方,仅仅调用了依赖的 addDep 方法,没有调用 addSub 把添加依赖。其实,addSub 是在依赖的 addDep 方法中调用的。这个和观察者模式离不开关系,在观察者模式中,观察者直接观测目标,并相应目标做出的通知。Watcher 直接观测响应式数据,当数据发生变更时,就能收到通知。但由于数据和观察者是多对多的关系,所以需要 Dep 依赖容器这么一个东西用来保存 Watcher 与数据的关系。

变化侦测

Dep.target 用来表示当前依赖,并且源码维护了一个依赖栈,通过提供的 pushTarget、popTarget 方法维护依赖栈及当前依赖(状态)。看以下代码:

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null;
const targetStack = [];
function pushTarget(target) {
  targetStack.push(target);
  Dep.target = target;
}
function popTarget() {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}

既然 Dep 用来存放 Watcher,是依赖容器,那么,依赖就是 Watcher。

Watcher 通过调用我们刚才提到的 pushTarget、popTarget 以维护全局的当前依赖(Dep.target)。全局的当前依赖只会有一个,因为 JS 是同步的,顺序执行的代码。我们先来看看 Watcher 怎么样维护当前依赖。

class Watcher {
  // 依赖实例通过 get 方法获取对象属性的值,以触发属性的 getter 以重收集依赖
  // 这里的“重收集”,意味着将当前依赖重新添加到不同对象的属性的依赖容器中
  // 所以在这里需要使用 pushTarget、popTarget 维护全局的当前依赖
  get() {
    pushTarget(this);
    let value;
    // vm 即该依赖对应的 Vue 实例,这个后续会介绍
    const vm = this.vm;
    // 触发 getter 更新
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // 如果依赖设置了 deep 选项,那会触发对应 value 的所有键的依赖更新
      // traverse 即递归读取对象的每一个元素
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      // 因为每次 get 之后都重收集了依赖,
      // 这里会把没有用的依赖给清除掉(调用依赖容器的 removeSub 移出依赖)
      this.cleanupDeps();
    }
    return value;
  }
}

Vue 实例中,data 方法会返回一个新的对象,这个对象能将值的变化响应式更新到模板中。其实就是,新的对象返回来后,使用了 observe 方法观测其变化,而其每一个属性的依赖容器中,都会保存这个 Vue 实例的 watcher。这样一来,只要属性发生了变化,依赖容器就会通知 Vue 实例的 watcher 进行更新。至于要更新什么,那当然是“执行回调函数”啦。想象一下 Vue 实例中的 watch 的写法:

new Vue({
  data() {
    return {
      b: null,
    };
  },
  watch: {
    b() {
      console.log("I'm callback");
    },
  },
});

这个 watch 不就是(尝试)给 data.b 设置响应式属性么... 所以说依赖就是 Vue 实例与响应式属性之间的对应关系。Watcher 保存了这种关系,还保存了响应式数据、回调函数等一些额外信息。我们看 Watcher 的构造器:

class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    // vm 即 Vue 实例
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    // Vue 实例可能对应多个 watcher,比如 vm.$data、vm.$watch
    vm._watchers.push(this);

    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();

    /* expOrFn 可以是函数或形如 'data.a.b.c' 的字符串
     * 若是字符串,对应的 getter 即读取 data.a.b.c 的值
     */
    this.expression =
      process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
    // parse expression for getter
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      // parsePath 即将 'data.a.b.c' 按照 '.' 拆分并依次读值
      this.getter = parsePath(expOrFn);
    }

    this.value = this.lazy ? undefined : this.get();
  }
}

Watcher 是组件级别的,如果状态发生了变化,只能通知到组件。再由组件对比内部的虚拟节点的变化。换句话说,Vue 对响应式状态变化侦测只通知到组件,这意味着只要某个属性变化了,相应的组件都会重渲染。


阅读更多


Copyright © 2024 Lionad - CC-BY-NC-CD-4.0