手写Vue2源码(五)—— 观察者模式

432 阅读9分钟

前言

通过手写Vue2源码,更深入了解Vue; 在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅; 另外我会编写一些开发文档,阐述编码细节及实现思路; 源码地址:手写Vue2源码

问题分析

上一节,页面初次渲染,我们调用了vm._update(vm._render())将VNode渲染到页面上;但是有一个问题:如果有一个异步方法修改了data,页面是不会响应式更新的,与数据驱动视图思想不符。本文将采用观察者模式,定义Watcher和Dep,完成依赖收集和派发更新,从而实现渲染更新。 思考一下怎么实现页面响应式更新?

  1. 在修改数据时,我们需要知道哪些组件使用到该数据,并触发所有相关组件的视图更新;
  2. 在执行vm._render(),获取数据时,我们需要给数据收集所有使用到该数据的组件;

流程分析

大概实现流程如下:

  1. 在渲染组件时,实例化一个渲染watcher(后续利用watcher触发视图更新,以及绑定watcher与数据的关系)
  2. 在触发数据的getter时,收集依赖(一个数据可能对应多个watcher,同时一个watcher可能对应多个数据)
  3. 在触发数据的setter时,通知watcher更新视图(需要进行异步渲染优化)

在收集依赖时我们要做两件事:

  • 给数据收集相关watcher,目的是当数据变化时,通知watcher更新视图
  • 给watcher收集相关数据,目的是为了在组件更新的时候进行【依赖清除】,对没有用到的deps进行清除,避免该dep修改时进行无意义的重新渲染

为了实现这个功能,我们增加了一个中间者——dep:

  1. dep和数据一一对应
  2. dep必须唯一(一个watcher中可存放的多个dep,但dep必须唯一)
  3. dep作为一个中间者,具体实现流程为:做数据劫持时实例化一个dep ——> 触发getter时通知dep收集watcher(dep.depend(data))——> depend中通知watcher收集dep(Dep.target.addDep(this)Dep.target在创建watcher时指向当前watcher)——> 在watcher中收集dep,并通知dep收集watcher(dep.addSub(this)

Vue观察者模式:

  • watcher就是观察者,它需要订阅数据的变化,当数据变化时,通知它去更新视图
  • dep就是被观察者,dep作为一个中间者的身份,实现了:在数据getter中进行依赖收集,在Dep中通知watcher收集相关dep,在Watcher中通知Dep收集相关watcher
  • 当数据变化时,dep相关watcher【观察者】自动更新视图

创建渲染watcher

暂时只分析渲染watchercomputed watcheruser watcher后续章节再分析)。 组件渲染时会执行mountComponent(),在该方法中实例化一个渲染watcher。

// src/lifecycle.js
export function mountComponent(vm, el) {
  let updateComponent = () => {
    vm._update(vm._render());
  };
  // 每个组件渲染的时候,都会创建一个watcher,并执行updateComponent;true表示是渲染Watcher
  new Watcher(
    vm,
    updateComponent,
    () => {
      console.log('视图更新了')
      callHook(vm, "beforeUpdate");
    },
    true
  );
}

实例化dep、收集依赖、通知更新

在数据劫持时,实例化一个独一无二的dep,在setter中收集依赖,在setter中派发更新。 直接看代码:

// src/observer/index.js
function defineReactive(data, key, value) {
  observe(value);

  let dep = new Dep() // 为每个属性创建一个独一无二的dep
  Object.defineProperty(data, key, {
    get() {
      // Dep.target指向当前渲染watcher,只在渲染时存在,渲染完删除
      // 这里的判断是为了保证只在渲染时才进行依赖收集
      if(Dep.target) { 
        dep.depend() // 收集依赖
      }
      return value;
    },
    set(newVal) {
      if (newVal === value) return;
      observe(newVal);
      value = newVal;

      dep.notify(); // 通知dep存放的watchers去更新--派发更新
    },
  });
}

实现Dep

Dep实现的功能:

  1. 唯一性
  2. subs数组存放watchers
  3. 通知watchers更新 —— notify()
  4. 通知watcher收集dep —— depend()
  5. 在dep中收集watcher —— addSub(watcher)
  6. 创建自身的target属性,用来保存当前watcher

具体实现:

// src/observer/dep.js
/**
 * 1. 每个属性我都给他分配一个dep(一对一的关系),一个dep可以存放多个watcher(一个属性可能对应多个watcher)
 * 2. 一个watcher中还可以存放多个dep(一个watcher可能对应多个属性,而dep与属性一一对应)
 * 3. dep具有唯一性
 */
let id = 0; // 给dep添加一个标识,保证它的唯一性
export default class Dep {
  constructor() {
    this.id = id++;
    this.subs = [];  // 用来存放watcher
  }

  // 将dep实例放到watcher中
  depend() {
    // 如果当前存在watcher
    if (Dep.target) {
      // Dep.target即当前watcher,是在new Watcher时设置的
      Dep.target.addDep(this); // this为dep实例(与属性一一对应),即把自身dep实例存放在watcher里面
    }
  }

  // 依次执行subs里面的watcher更新方法
  notify() {
    this.subs.forEach((watcher) => watcher.update());
  }

  // 把watcher加入到dep实例的subs容器(因为一个dep可能对应多个watcher)
  addSub(watcher) {
    this.subs.push(watcher);
  }
}

/**
 * targetStack定义在全局,为整个项目所有watcher
 * Dep.target定义在Dep自身而非prototype上,无法被实例继承,标识当前的watcher,具有唯一性
 */
// 栈结构用来存众多watcher
const targetStack = [];
// Dep.target 为 dep 当前所对应的watcher(即栈顶的watcher),默认为null
Dep.target = null;

export function pushTarget(watcher) {
  targetStack.push(watcher);
  Dep.target = watcher; // Dep.target指向当前watcher
}

export function popTarget() {
  targetStack.pop(); // 当前watcher出栈 拿到上一个watcher
  Dep.target = targetStack[targetStack.length - 1];
}

实现watcher

Dep实现的功能:

  1. 实现页面渲染 —— get(),在get()调用this.getter(),即调用第二个形参(即vm._update(vm._render()));在get()中配置Dep.target,进行入栈和出栈操作,保证渲染时的Dep.target指向当前watcher;
  2. 页面更新 —— update(),需要进行异步渲染优化,后续再完善,暂时直接调用this.get()
  3. 收集dep —— addDep(dep),要确保dep的唯一性,另外在该方法中还要通知dep收集watcher

具体实现:

// src/observer/watcher
export default class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm;
    this.exprOrFn = exprOrFn;
    this.cb = cb;
    this.options = options;

    this.deps = []; //存放dep的容器
    this.depsId = new Set(); //用来去重dep

    this.getter = exprOrFn;
    this.get();
  }

  // new Watcher时会执行get方法;之后数据更新时,直接手动调用get方法即可
  get() {
    // 把当前watcher放到全局栈,并设置Dep.target(无法继承,具唯一性)为当前watcher
    pushTarget(this);
    /**
     * 执行exprOrFn,如果watcher是渲染watcher,则exprOrFn为vm._update(vm._render())
     * 在执行render函数的时候,获取变量会触发属性的getter(定义在对象数据劫持中),在getter中进行依赖收集
     */
    const res = this.getter.call(this.vm);
    // 执行完getter就把当前watcher删掉,以防止用户在methods/生命周期中访问data属性时进行依赖收集(数据劫持时会判断Dep.target是否存在)
    popTarget(); // 在调用方法之后把当前watcher实例从全局watcher栈中移除,设置Dep.target为新的栈顶watcher
    return res;
  }

  /**
   * 1. 保证dep唯一,因为在render过程中,同一属性可能被多次调用,只需收集一次依赖即可;另外初始渲染收集过的dep,在更新时也不用再次收集(通过dep的id来判断)
   * 2. 将dep放到watcher中的deps数组中
   * 3. 在dep实例中添加watcher
   */
  addDep(dep) {
    let id = dep.id;
    if (!this.depsId.has(id)) {
      this.depsId.add(id);
      // 将dep放到watcher中的deps数组中
      this.deps.push(dep);
      console.log('watcher.deps------------', this.deps)
      // 直接调用dep的addSub方法  把自己watcher实例添加到dep的subs容器里面
      dep.addSub(this);
    }
  }

  // 更新当前watcher相关的视图
  update() {
    console.log('watcher.update:更新视图')
    this.get()
    // toDO... 如果短时间内同一watcher执行了多次update,我们希望先将watcher缓存下来,等一会儿一起更新
  }
}

数组的依赖收集

// src/observer/index.js
import { arrayMethods } from "./array";
import Dep from "./dep";
class Observer {
  // 通过new命令生成class实例时,会自动调用constructor(),即会执行this.walk(data)方法
  constructor(data) {
    this.value = data
    this.dep = new Dep(); // 给data添加一个dep,收集data整体的一个dep(主要用于数组的依赖收集)

    if (Array.isArray(data)) {
      // 数组响应式处理
      // 重写数组的原型方法,将data原型指向重写后的对象
      data.__proto__ = arrayMethods;
      // 如果数组中的数据是对象,需要监控对象的变化
      this.observeArray(data);
    } else {
      // 对象响应式处理
      this.walk(data);
    }
  }
  observeArray(data) {
    data.forEach((item) => {
      observe(item);
    });
  }
}

function defineReactive(data, key, value) {
  let childOb =  observe(value); // 【关键】递归,劫持对象中所有层级的所有属性
  let dep = new Dep() // 为每个属性创建一个独一无二的dep
  Object.defineProperty(data, key, {
    get() {
      if(Dep.target) {
        dep.depend()
        // 如果属性的值依然是一个数组/对象,则对该 数组/对象 整体进行依赖收集
        if(childOb) {
          childOb.dep.depend(); // 让对象和数组也记录watcher
          // 如果数据结构类似 {a:[1,2,[3,4,[5,6]]]} 这种数组多层嵌套,数组包含数组的情况,那么我们访问a的时候,只是对第一层的数组进行了依赖收集
          // 里面的数组因为没访问到,所以无法收集依赖,但是如果我们改变了a里面的第二层数组的值,是需要更新页面的,所以需要对数组递归进行依赖收集
          if (Array.isArray(value)) {
            // 如果内部还是数组
            dependArray(value); // 遍历 + 递归数组,对数组不同层级的所有数组元素进行依赖收集
          }
        }
      }
      return value;
    },
  });
}

// 遍历递归收集数组依赖
function dependArray(value) {
  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);
    }
  }
}

总结:

  1. 如果一个对象的属性值为对象/数组,通过childOb.dep.depend()对该 对象/数组 整体进行收集依赖(childOb.dep是在new Observer(value)时添加的)
  2. 如果数组的子元素依然有数组,即多层数组嵌套的情况,则采用遍历加递归的方式,对所有数组及子元素进行依赖收集(递归observe数组元素时,无法对深层数组进行依赖收集,因为深层数组无法走到defineReactive这一步)
  3. 执行initData() ——> return Observer(data),只是进行数据劫持,依赖收集是在触发getter时进行的

数组派发更新

数组的响应式主要是通过重写原型方法,所以数组的派发更新也在原型中进行:

// src/observer/array.js

methods.forEach(method => {
    arrayMethods[method] = function(...args) {
        const result = oldArrayPrototype[method].call(this,...args) // this指向调用该方法的data(即经过响应式处理的数组)

        // 对于数组中新增的元素,也需要进行监控
        const ob = this.__ob__;
        let inserted;
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case "splice":
                inserted = args.slice(2);
            default:
                break;
        }
        // inserted是个数组,需要调用observeArray来监测
        if (inserted) ob.observeArray(inserted);

        // 数组派发更新;dep是在new Observer(data)时添加的
        ob.dep.notify()
        return result
    }
})

核心思想:在new Observer(data)时,往data添加 dep__ob__属性以及收集依赖,在原型上通过 this.__ob__.dep.notify()派发更新。

系列文章