7.6 极简风的响应式系统

Vue的响应式系统构建是比较复杂的,直接进入源码分析构建的每一个流程会让理解变得困难,因此我觉得在尽可能保留源码的设计逻辑下,用最小的代码构建一个最基础的响应式系统是有必要的。对Dep,Watcher,Observer概念的初步认识,也有助于下一篇对响应式系统设计细节的分析。

7.6.1 框架搭建

我们以MyVue作为类响应式框架,框架的搭建不做赘述。我们模拟Vue源码的实现思路,实例化MyVue时会传递一个选项配置,精简的代码只有一个id挂载元素和一个数据对象data。模拟源码的思路,我们在实例化时会先进行数据的初始化,这一步就是响应式的构建,我们稍后分析。数据初始化后开始进行真实DOM的挂载。

  1. var vm = new MyVue({
  2. id: '#app',
  3. data: {
  4. test: 12
  5. }
  6. })
  7. // myVue.js
  8. (function(global) {
  9. class MyVue {
  10. constructor(options) {
  11. this.options = options;
  12. // 数据的初始化
  13. this.initData(options);
  14. let el = this.options.id;
  15. // 实例的挂载
  16. this.$mount(el);
  17. }
  18. initData(options) {
  19. }
  20. $mount(el) {
  21. }
  22. }
  23. }(window))

7.6.2 设置响应式对象 - Observer

首先引入一个类Observer,这个类的目的是将数据变成响应式对象,利用Object.defineProperty对数据的getter,setter方法进行改写。在数据读取getter阶段我们会进行依赖的收集,在数据的修改setter阶段,我们会进行依赖的更新(这两个概念的介绍放在后面)。因此在数据初始化阶段,我们会利用Observer这个类将数据对象修改为相应式对象,而这是所有流程的基础。

  1. class MyVue {
  2. initData(options) {
  3. if(!options.data) return;
  4. this.data = options.data;
  5. // 将数据重置getter,setter方法
  6. new Observer(options.data);
  7. }
  8. }
  9. // Observer类的定义
  10. class Observer {
  11. constructor(data) {
  12. // 实例化时执行walk方法对每个数据属性重写getter,setter方法
  13. this.walk(data)
  14. }
  15. walk(obj) {
  16. const keys = Object.keys(obj);
  17. for(let i = 0;i< keys.length; i++) {
  18. // Object.defineProperty的处理逻辑
  19. defineReactive(obj, keys[i])
  20. }
  21. }
  22. }

7.6.3 依赖本身 - Watcher

我们可以这样理解,一个Watcher实例就是一个依赖,数据不管是在渲染模板时使用还是在用户计算时使用,都可以算做一个需要监听的依赖,watcher中记录着这个依赖监听的状态,以及如何更新操作的方法。

  1. // 监听的依赖
  2. class Watcher {
  3. constructor(expOrFn, isRenderWatcher) {
  4. this.getter = expOrFn;
  5. // Watcher.prototype.get的调用会进行状态的更新。
  6. this.get();
  7. }
  8. get() {}
  9. }

那么哪个时间点会实例化watcher并更新数据状态呢?显然在渲染数据到真实DOM时可以创建watcher$mount流程前面章节介绍过,会经历模板生成render函数和render函数渲染真实DOM的过程。我们对代码做了精简,updateView浓缩了这一过程。

  1. class MyVue {
  2. $mount(el) {
  3. // 直接改写innerHTML
  4. const updateView = _ => {
  5. let innerHtml = document.querySelector(el).innerHTML;
  6. let key = innerHtml.match(/{(\w+)}/)[1];
  7. document.querySelector(el).innerHTML = this.options.data[key]
  8. }
  9. // 创建一个渲染的依赖。
  10. new Watcher(updateView, true)
  11. }
  12. }

7.6.4 依赖管理 - Dep

watcher如果理解为每个数据需要监听的依赖,那么Dep 可以理解为对依赖的一种管理。数据可以在渲染中使用,也可以在计算属性中使用。相应的每个数据对应的watcher也有很多。而我们在更新数据时,如何通知到数据相关的每一个依赖,这就需要Dep进行通知管理了。并且浏览器同一时间只能更新一个watcher,所以也需要一个属性去记录当前更新的watcher。而Dep这个类只需要做两件事情,将依赖进行收集,派发依赖进行更新。

  1. let uid = 0;
  2. class Dep {
  3. constructor() {
  4. this.id = uid++;
  5. this.subs = []
  6. }
  7. // 依赖收集
  8. depend() {
  9. if(Dep.target) {
  10. // Dep.target是当前的watcher,将当前的依赖推到subs中
  11. this.subs.push(Dep.target)
  12. }
  13. }
  14. // 派发更新
  15. notify() {
  16. const subs = this.subs.slice();
  17. for (var i = 0, l = subs.length; i < l; i++) {
  18. // 遍历dep中的依赖,对每个依赖执行更新操作
  19. subs[i].update();
  20. }
  21. }
  22. }
  23. Dep.target = null;

7.6.5 依赖管理过程 - defineReactive

我们看看数据拦截的过程。前面的Observer实例化最终会调用defineReactive重写getter,setter方法。这个方法开始会实例化一个Dep,也就是创建一个数据的依赖管理。在重写的getter方法中会进行依赖的收集,也就是调用dep.depend的方法。在setter阶段,比较两个数不同后,会调用依赖的派发更新。即dep.notify

  1. const defineReactive = (obj, key) => {
  2. const dep = new Dep();
  3. const property = Object.getOwnPropertyDescriptor(obj);
  4. let val = obj[key]
  5. if(property && property.configurable === false) return;
  6. Object.defineProperty(obj, key, {
  7. configurable: true,
  8. enumerable: true,
  9. get() {
  10. // 做依赖的收集
  11. if(Dep.target) {
  12. dep.depend()
  13. }
  14. return val
  15. },
  16. set(nval) {
  17. if(nval === val) return
  18. // 派发更新
  19. val = nval
  20. dep.notify();
  21. }
  22. })
  23. }

回过头来看watcher,实例化watcher时会将Dep.target设置为当前的watcher,执行完状态更新函数之后,再将Dep.target置空。这样在收集依赖时只要将Dep.target当前的watcher pushDepsubs数组即可。而在派发更新阶段也只需要重新更新状态即可。

  1. class Watcher {
  2. constructor(expOrFn, isRenderWatcher) {
  3. this.getter = expOrFn;
  4. // Watcher.prototype.get的调用会进行状态的更新。
  5. this.get();
  6. }
  7. get() {
  8. // 当前执行的watcher
  9. Dep.target = this
  10. this.getter()
  11. Dep.target = null;
  12. }
  13. update() {
  14. this.get()
  15. }
  16. }

7.6.6 结果

一个极简的响应式系统搭建完成。在精简代码的同时,保持了源码设计的思想和逻辑。有了这一步的基础,接下来深入分析源码中每个环节的实现细节会更加简单。