前端 mvvm 框架底层学习(八、双向绑定加入发布订阅模式)

前言

上一篇《前端 mvvm 框架底层学习(七、双向绑定优化)》介绍了我们如何解决了双向绑定中的局部更新的问题,或者解决更小颗粒度更新的问题,但代码比较混乱,所有的 Dom 操作及数据绑定操作都耦合到了一起。这里我们来学习一下 Vue 是如何结合合经典的发布/订阅模式进行双向绑定优化的。

发布/订阅模式
发布/订阅模式

Vue的observer依赖关系图[本站原创]

如果直接去刨 Vue 的 observer 的源码,整个流程其实是非常复杂的,下面就把流程图画一下便于全局理解

Vue的observer依赖关系图
Vue的observer依赖关系图

由图上可以看到一处很巧妙的地方 Observer 通过 observe(val) 递归遍历实现了对对象进行深度 observe / watch。这里面其实出现了双向的互相依赖并不是十分完美,理想的状态是不使用双向的互相依赖。立个flag,后续将尝试按自己的方式实现 Vue 的 Observer。:)

来自于简书用户 “流动码文” 的简化版本 Vue 双向绑定源码

本来想自己去把作者源码中的 flow 代码精简,后面发现网上有牛人流动码文已经整理出非常精辟的代码,而且注释非常详尽;在这里,我参照 Vue 源码对注释修改了一下,下面一起来学习学习

引用自:https://www.jianshu.com/p/2df6dcddb0d7

const Vue = (function() {
  let uid = 0;
  // 用于储存订阅者并发布消息
  /**[新增]
   * A dep is an observable that can have multiple 一个Dep就是一个可被监听的对象(依赖)
   * directives subscribing to it. 并且可以供多个指令进行订阅(发布/订阅模式)
   */
  class Dep {
    constructor() {
      // 设置id,用于区分新Watcher和只改变属性值后新产生的Watcher
      this.id = uid++; // [新增]uid for batching,保证所有依赖的id都是唯一的
      // 储存订阅者的数组
      this.subs = [];
    }
    // 触发target上的Watcher中的addDep方法,参数为dep的实例本身
    depend() {
      Dep.target.addDep(this);
    }
    // 添加订阅者
    addSub(sub) {
      this.subs.push(sub);
    }
    notify() {
      // 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
      this.subs.forEach(sub => sub.update());
    }
  }
  // 为Dep类设置一个静态属性,默认为null,工作时指向当前的Watcher
  // [新增] The current target watcher being evaluated. 当前的目标watcher被访问
  // This is globally unique because only one watcher 每次全局一次只能有一个watcher在被访问
  // can be evaluated at a time.
  Dep.target = null;
  // 监听者,监听对象属性值的变化
  /**[新增]
   * Observer class that is attached to each observed 这里表达得很清楚,Observer类是绑定到每个被观察的对象
   * object. Once attached, the observer converts the target 一旦被绑定上,observer就会给目标对象的属性值keys添加
   * object's property keys into getter/setters that 上用于收集依赖集dependencies的getter/setters的勾子并且分发
   * collect dependencies and dispatch updates. 更新事件
   */
  class Observer {
    constructor(value) {
      this.value = value;
      this.walk(value);
    }
    // 遍历属性值并监听
    /**[新增]
     * Walk through each property and convert them into
     * getter/setters. This method should only be called when
     * value type is Object. 这里限定value只能是对象时才会触发walk函数
     */
    walk(value) {
      Object.keys(value).forEach(key => this.convert(key, value[key]));
    }
    // 执行监听的具体方法
    convert(key, val) {
      defineReactive(this.value, key, val);
    }
  }

  function defineReactive(obj, key, val) {
    const dep = new Dep();
    // 给当前属性的值添加监听
    let chlidOb = observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: () => {
        // 如果Dep类存在target属性,将其添加到dep实例的subs数组中
        // target指向一个Watcher实例,每个Watcher都是一个订阅者
        // Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
        if (Dep.target) {
          dep.depend();
        }
        return val;
      },
      set: newVal => {
        if (val === newVal) return;
        val = newVal;
        // 对新值进行监听
        chlidOb = observe(newVal);
        // 通知所有订阅者,数值被改变了
        dep.notify();
      },
    });
  }
  /** [新增]尝试为一个值创建一个观察者实例,如果成功就返回一个Observer,或者如果value已经有就返回已有的
   * 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.
   */
  function observe(value) {
    // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
    if (!value || typeof value !== 'object') {
      return;
    }
    return new Observer(value);
  }
  /** [新增]
   * A watcher parses an expression, collects dependencies, 一个watcher会解释一个表达式,收集所有依赖
   * and fires callback when the expression value changes. 接着当表达式的值发生改变时会触发回调。
   * This is used for both the $watch() api and directives. 会在$watch的api和directives指令中用到
   */
  class Watcher {
    constructor(vm, expOrFn, cb) {
      this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者
      this.vm = vm; // 被订阅的数据一定来自于当前Vue实例
      this.cb = cb; // 当数据更新时想要做的事情
      this.expOrFn = expOrFn; // 被订阅的数据
      this.val = this.get(); // 维护更新之前的数据
    }
    // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用,
    update() {
      this.run();
    }
    /**[新增]
     * Add a dependency to this directive.给这个指令添加一个依赖dep
     */
    addDep(dep) {
      // 如果在depIds的hash中没有当前的id,可以判断是新Watcher,因此可以添加到dep的数组中储存
      // 此判断是避免同id的Watcher被多次储存
      if (!this.depIds.hasOwnProperty(dep.id)) {
        dep.addSub(this); // [新增]给dep增加本watcher为订阅者
        this.depIds[dep.id] = dep; // [新增]本watcher的依赖id数据depIds相应加上dep
      }
    }
    run() {
      const val = this.get();
      console.log(val);
      if (val !== this.val) {
        this.val = val;
        this.cb.call(this.vm, val); // [新增]数据变化,执行相应的回调
      }
    }
    /**[新增]
     * Evaluate the getter, and re-collect dependencies.取新的getter里的值,和重新收集相关的依赖
     */
    get() {
      // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
      Dep.target = this; // [新增]给全局的Dep的target赋值,因为每次只能有一个watcher被访问
      const val = this.vm._data[this.expOrFn];
      // 置空,用于下一个Watcher使用
      // [新增]之前说过一次只能有一个target被访问
      Dep.target = null;
      console.log(Dep.target, 2);
      return val;
    }
  }

  class Vue {
    constructor(options = {}) {
      // 简化了$options的处理
      this.$options = options;
      // 简化了对data的处理
      let data = (this._data = this.$options.data);
      // 将所有data最外层属性代理到Vue实例上
      Object.keys(data).forEach(key => this._proxy(key));
      // 监听数据
      observe(data);
    }
    // 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者
    $watch(expOrFn, cb) {
      new Watcher(this, expOrFn, cb);
    }
    _proxy(key) {
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get: () => this._data[key],
        set: val => {
          this._data[key] = val;
        },
      });
    }
  }

  return Vue;
})();

let demo = new Vue({
  data: {
    text: '',
  },
});

const p = document.getElementById('p');
const input = document.getElementById('input');

input.addEventListener('keyup', function(e) {
  demo.text = e.target.value;
});

demo.$watch('text', str => p.innerHTML = str);

在线 Demo

注:Dep 是 dependence 的缩写,中文就是 “依赖” 的意思。因为 Watcher 订阅者需要依赖 Dep 才能了解数据的变化,没有 DepWatcher 根本不可能知道数据发生了变化,当有数据变化发生时,Dep 会通知 WatcherDep 相当于是杂志社,Watcher 作为订阅者,首先需要向杂志社订阅杂志,这样当有新的杂志(消息)产生时,Dep 才会通知 Watcher,所以 Watcher 强烈依赖 Dep,他们之间是这样的一种关系。

总结

其实完成双向绑定的本质就对组件中 statedata 数据进行深度 watch,而完成深度 watch 要使用的解决方案可以是使用 ES5 中的 Object.defineProperty 递归遍历 data 对象数据进行深度 watch,也可以使用 ES6 的Proxy 结合 Reflect 进行同样的处理。

作者: 博主

Talk is cheap, show me the code!

《前端 mvvm 框架底层学习(八、双向绑定加入发布订阅模式)》有2个想法

发表评论

电子邮件地址不会被公开。

Captcha Code