响应式系统的构建思路

2023 年 5 月 2 日 星期二
/ ,
14
摘要
该系统是参考vuejs2.7.14源码搭建的,旨在实现一个简单的响应式框架MiniVue。框架的搭建思路是先初始化数据和方法,然后将数据代理到实例的第一层,接着将数据设置为响应式对象,最后实例化订阅者Watcher并更新数据状态。订阅者的管理由Dep类完成,它负责依赖的收集和更新的派发。

响应式系统的构建思路

tips: 该系统搭建思路参考自vuejs2.7.14源码

Vue的响应式系统的构建是比较复杂的,在深入探究vue整体实现思路之前,我认为在尽可能保留源码设计逻辑下,自己动手实现一个基础的响应式系统是有必要的。这样对DepWatcherObserver等概念也会有初步的认识。

框架搭建

接下来我们将模拟Vue源码的实现思路,实现一个简单的类响应式框架MiniVue

参照vue的使用方法,我们在实例化MiniVue时可传递一个配置项。这个配置项需要尽可能简单化。定义入参只由挂载元素el,数据对象data和方法对象methods构成。模拟源码实现思路,MiniVue在实例化时会进行数据的初始化,数据初始化后开始真实dom的挂载。

<!-- index.html -->
<body>
  <div id="app">
    <h1>{{ count }}</h1>
  </div>
  <button id="addBtn">add</button>
  <button id="subBtn">sub</button>
  <script src="minivue.js"></script>
  <script>
    const app = new MiniVue({
      el: '#app',
      data() {
        return {
          count: 0,
        };
      },
      methods: {
        add() {
          this.count++;
        },
        sub() {
          this.count--;
        },
      },
    });
    const addBtn = document.getElementById('addBtn');
    const subBtn = document.getElementById('subBtn');
    addBtn.onclick = () => app.add();
    subBtn.onclick = () => app.sub();
  </script>
</body>
// minivue.js

class MiniVue {
  constructor(options) {
    // 初始化
    this.$options = options;
    this.$el = document.querySelector(el);
    this.init();
  }
  init() {
  // 初始化data
    this.initData();
  // 初始化methods
    this.initMethods(this);
  // 挂载真实dom
    this.$mount();
  }
  initData() {}

  initMethods() {}

  $mount() {}
}

初始化数据对象data

数据代理

将数据对象data响应式化之前,需要将数据存放至实例的_data属性中,使用proxy方法将_data内的数据代理到MiniVue实例的第一层。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: () => {},
  set: () => {},
};

initData() {
  let { data } = this.$options;
  data = this._data =
    typeof data === 'function' ? data.call(this, this) : data || {};
  const keys = Object.keys(data);
  let i = keys.length;
  while (i--) {
    // 将data属性代理至第一层
    this.proxy(this, '_data', keys[i]);
  }
  // 响应式化
  observer(data);
}
proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

设置响应式对象 - Observer

data会通过observer方法将自身修改为响应式对象,方法内部会实例化一个Observer类,最终data会利用这个类将数自身修改为响应式对象,这是所有流程的基础。

function observer(data) {
  if (Array.isArray(data) || _toString.call(data) === '[object Object]') {
    return new Observer(data);
  }
}
// 依赖
class Observer {
  constructor(value) {
    // 实例化依赖存储器
    this.dep = new Dep();
    // 为value添加__ob__属性
    Object.defineProperty(value, '__ob__', {
      value: this,
      enumerable: false,
      writable: true,
      configurable: true,
    });
    if (Array.isArray(value)) {
      // 略过,后续择机补充
    } else {
      // value是对象类型时,遍历对象的key
      const keys = Object.keys(value);
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        // 为所有属性重写getter setter
        defineReactive(value, key);
      }
    }
  }
}

订阅者 - Watcher

我们可以理解为一个Watcher实例就是一个依赖(订阅者),Watcher内部记录了这个订阅者监听的状态和更新操作的方法。 当某个Watcher实例的更新操作用于模板渲染时其被称为渲染Watcher

class Watcher {
  constructor(vm, expOrFn) {
    vm._watcher = this;
    this.vm = vm;
    this.uid = uid_watcher++;
    this.deps = [];
    this.depIds = new Set();
    this.getter = expOrFn;
    // Watcher.prototype.get的调用会进行状态的更新。
    this.get();
  }
  get() {
    // 执行getter方法 状态的更新
  }
  update() {
    this.run();
  }
  run() {
    const value = this.get();
  }
}

那么哪个时间点会实例化Watcher并更新数据状态呢?1. 在$mount方法调用时会实例化一个渲染Watcher,而实例化后最终会调用this.getter(),也就是vm._update(vm._render())去更新页面。2. 用户定义的computed等数据读取时也会实例化Watcher。这里我们只简单介绍第一情况——挂载真实dom时创建Watcher的流程,可以对vm._update(vm._render())代码做一下精简。省去创建虚拟dompatch打补丁的复杂操作。详见updateDom方法。

class MiniVue {
  $mount() {
    function _createUpdateDomFn(root) {
      // 缓存模板
      const cacheTemplate = root.$el.innerHTML;
      function update(innerHtml) {
        root.$el.innerHTML = innerHtml;
      }
      return function (vm) {
        let innerHtml = cacheTemplate;
        const matched = cacheTemplate.matchAll(/\{\{([^}]+)\}\}/g);
        let current;
        while ((current = matched.next().value)) {
          // 这里访问到了响应式数据,将进行依赖收集
          const cur = current[1].trim();

          const val = vm[cur] || '';
          innerHtml = innerHtml.replace(current[0], val);
        }
        update(innerHtml);
      };
    }
    const updateDom = _createUpdateDomFn(this);
    // 传入更新方法
    new Watcher(this, updateDom);
  }
}

订阅者(依赖)管理 - Dep

Watcher如果被理解为每个数据的订阅者(依赖),那么Dep就可以理解为订阅者(依赖)的管理器。所有响应式数据都可以在渲染时使用,也可以在计算属性中使用,相对的每个数据的Watcher也会很多,那么如何在数据更新时能够通知到每个Watcher进行更新呢?这时候就要用到Dep来进行通知管理了。而Dep这个类只需要做两件事情,依赖的收集,派发依赖的更新。

let uid = 0;
// 依赖收集器
class Dep {
  constructor() {
    // 唯一标识,为防止相同dep重复收集
    this.id = uid++;
    // sub 存储该Dep收集器所属数据的依赖watcher
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      // 使用当前watcher进行收集
      Dep.target.addDep(this);
    }
  }
  notify() {
    // 派发更新
    const subs = this.subs.slice(0);
    for (let i = 0; i < subs.length; i++) {
       // 遍历dep中的依赖,对每个依赖执行更新操作
      const sub = subs[i];
      sub.update();
    }
  }
}

响应式化过程 defineReactive

我们来看看数据拦截的过程,前面Observer实例化最终会调用defineReactive方法重写传入属性的getter、setter,方法内首先实例化了一个依赖管理器Dep,重写的getter方法中对依赖进行了收集,也就是调用dep.depend()方法。setter阶段,在新旧值比较出不同后会调用dep.notify()方法让依赖管理器派发更新。

function defineReactive(obj, key) {
  // 实例化该属性的依赖管理器
  const dep = new Dep();
  let val = obj[key];
  let childObj = observer(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      if (Dep.target) {
        // 如果依赖存储器原型的target(渲染watcher)有值,则进行依赖收集
        dep.depend();
        if (childObj) {
          childOb.dep.depend();
        }
      }
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      val = newVal;
      childObj = observer(newVal);
      // 派发更新
      dep.notify();
    },
  });
}

回过头看watcher,实例化watcher时会执行get将自身实例push到全局数组targetStack中,并执行getter更新状态。此时如果是渲染watcher,源码中则会执行updateComponents函数,即调用vm._update(vm._render())进行真实dom的更新,在执行vm._render()创建VNode tree过程中便有可能会访问到响应式数据,响应式数据重写的getter方法便会执行,并开始进行依赖收集。此时targetStack中存放了当前的watcher依赖(订阅者),Dep便能将之收集并相互建立关系。在响应式数据改变后派发watcher执行更新操作。

let targetStack = [];
Dep.target = null;
function pushTarget(target) {
  targetStack.push(target);
  Dep.target = target;
}
function popTarget() {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}
let uid_watcher = 0;
class Watcher {
  constructor(vm, expOrFn) {
    vm._watcher = this;
    this.vm = vm;
    this.uid = uid_watcher++;
    this.deps = [];
    this.depIds = new Set();
    this.getter = expOrFn;
    this.get();
  }
  get() {
// 将自身实例push到全局数组targetStack
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      // 依赖收集发生在这里
      value = this.getter.call(vm, vm);
    } catch (err) {
      throw err;
    } finally {
      // this.cleanupDeps();
      popTarget();
    }
  }
  addDep(dep) {
    // 根据dep的id 防止当前watcher被同一Dep多次收集
    if (!this.depIds.has(dep.id)) {
    // watcher收集dep
      this.deps.push(dep);
      this.depIds.add(dep.id);
    // dep收集watcher
      dep.addSub(this);
    }
  }
  cleanupDeps() {
    let i = this.deps.length;
    while (i--) {
      const dep = this.deps[i];
      dep.removeSub(this);
    }
  }
  update() {
    this.run();
  }
  run() {
    const value = this.get();
  }
}

初始化methods方法对象

methods对象初始化就非常简单了,和data初始化时一样的思路,唯一不同的是我们需要将vm实例绑定作为methods的this上下文,这样在methods内部访问this时能够拿到MiniVue实例

class MiniVue {
  initMethods(vm) {
    let { methods } = this.$options;
    this._methods = methods;
    const keys = Object.keys(methods);
    let i = keys.length;
    while (i--) {
      this._methods[keys[i]] = this._methods[keys[i]].bind(vm);
      this.proxy(this, '_methods', keys[i]);
    }
  }
  proxy(target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter() {
      return this[sourceKey][key];
    };
    sharedPropertyDefinition.set = function proxySetter(val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }
}

源码详见

vuejs2.7.14源码地址

本文完整源码

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...