手写Vue2源码(七)—— 侦听属性

710 阅读2分钟

前言

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

为何需要watch

第五节我们实现了数据的响应式,在渲染的时候收集依赖,在数据更新时响应式更新视图。但是只有在render中使用到的数据才会进行依赖收集,实际开发中,我们需要自己监听一些数据,进而执行一些操作。

实现思路

  1. 在initWatch中创建watcher
  2. 在watcher中执行get(),返回监听的数据;在此期间触发数据的getter,进行依赖收集
  3. 在数据改变时,触发数据的setter,派发更新,或在数组原型中派发更新
  4. 执行watcher.run(),再次执行this.getter(),获取到最新的newValue,执行回调函数

看一下Vue中watch的用法

new Vue({
    watch: {
        name(newVal, oldVal) {},
        'obj.name'(newVal, oldVal) {},
        age: [
            function(newVal, oldVal) {},
            function(newVal, oldVal) {},
        ],
        b: 'someMethod', // 直接接方法名
        id: {
            handler: (newVal, oldVal) => {},
            immediate: true
        }
    }
})

可以看到,watch中,key可能是单个直接的属性,也可能是层次很深的对象属性(例如obj.a.b.c);watch[key]的类型可能是函数、对象、数组、字符串等。

initWatch

stateMixin(Vue)中执行initState,在initState中执行initWatch(初始化watch)

// src/state.js
export function initState(vm) {
  const opts = vm.$options;
  // 初始化data
  if (opts.data) {
    initData(vm);
  }
  // 初始化watch
  if (opts.watch) {
    initWatch(vm);
  }
}

// 初始化watch
function initWatch(vm) {
  let watch = vm.$options.watch
  for (let k in watch) {
    const handler = watch[k]
    if (Array.isArray(handler)) {
      handler.forEach((handle) => {
        createWatcher(vm, k, handle)
      })
    } else {
      createWatcher(vm, k, handler)
    }
  }
}

// 创建watcher
function createWatcher(vm, key, handler, options = {}) {
  if (typeof handler === "object") {
    options = handler; //保存用户传入的对象
    handler = handler.handler; //是函数
  }
  // 如果handler是字符串,说明是一个方法名,直接从实例中调用
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  // watch 相当于调用了 vm.$watch()
  return vm.$watch(key, handler, options);
}

export function stateMixin(Vue) {
  Vue.prototype.$watch = function (exprOrFn, cb, options) {
    const vm = this;
    // user: true 表示创建的是一个用户watcher
    let watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true });  
    if (options.immediate) {
      cb(watcher.value); // 如果立刻执行
    }
  };
}

分析一下代码:

  1. initWatch其实最后执行了 return vm.$watch(key, handler, options),key为监听的数据,handler为回调函数,options为watch的配置(比如immediate、root)
  2. $watch 是挂载在vue原型上的方法,它创建了一个用户watcher,它与渲染watcher的区别就是:
    1. 用户watcher的options中增加属性user: true,而渲染watcher的options为true
    2. 用户watcher的第二个参数exprOrFn为监听的数据,而渲染watcher的exprOrFn为updateComponent方法

改造Watcher

用户自定义watcher需要解决的几个问题:

  1. 什么时候收集依赖
  2. 派发更新时如何获取到newVal和oldVal,并触发回调函数
import { pushTarget, popTarget } from "./dep";
import { queueWatcher } from "./scheduler";
import { isObject } from "../util/index";

// 全局变量id  每次new Watcher都会自增
let id = 0;
export default class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm;
    this.exprOrFn = exprOrFn;
    this.cb = cb;
    this.options = options;
    this.user = !!options.user; // 表示是不是用户watcher
    this.id = id++; // watcher的唯一标识
    this.deps = []; //存放dep的容器
    this.depsId = new Set(); //用来去重dep
    /**
     * 1. 渲染watcher中,exprOrFn为updateComponent(),是一个函数
     * 2. 在用户watcher中,exprOrFn为字符串(watch中的属性名,即监听地属性)
     */
    // 如果是渲染watcher(exprOrFn为vm._update(vm._render())) 或者 computed watcher(exprOrFn为计算属性里的getter)
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    } else {
      // 如果是user watcher,exprOrFn为监听的数据
      this.getter = function () {
        // watcher监听的数据可能是第一层 obj1,也可能是深层的某个属性 obj1.a.b,后者需要处理成 vm.obj1.a.b
        let path = exprOrFn.split('.')
        let obj = vm
        for (let i = 0; i < path.length; i++) {
          obj = obj[path[i]]
        }
        // 【*****】watch监听的数据一般定义在data中,当创建user watcher时会执行watcher里的get(),读取监听的数据;这个过程中就触发了数据的getter,会进行依赖收集(当前的Dep.target为user watcher(在get()中设置的),所以收集的是user watcher)
        return obj
      }
    }

    this.value = this.get();
  }

  // new Watcher时会执行get方法;之后数据更新时,直接手动调用get方法即可
  get() {
    pushTarget(this);
    const res = this.getter.call(this.vm); // 如果是用户watcher,则上一次执行getter得到的值即为oldValue
    popTarget();
    return res;
  }

  // ...

  run() {
    // 执行getter,更新视图/获取新值
    const newVal = this.getter(); // 新值
    const oldVal = this.value; //老值
    this.value = newVal; // newVal就成为了现在的值;为了保证下一次更新时,上一次的新值是下一次的老值

    // 如果是用户watcher
    if (this.user) {
      if (newVal !== oldVal || isObject(newVal)) {
        this.cb.call(this.vm, newVal, oldVal);
      }
    } else {
      // 渲染watcher
      this.cb.call(this.vm);
    }
  }
}

总结一下user watcher:

  • 收集依赖过程:在创建watcher时,执行this.get(),会返回监听的数据(设为this.value,即下一次更新时的oldVal),触发相关数据的依赖收集
  • 更新过程:当监听的数据改变时,会派发更新给相关的watcher;当之前收集的用户watcher被通知更新时,最终会执行run();在run里面执行this.getter()(【注意】:不要执行this.get(),因为get()会重复收集依赖)获取到newVal、以及oldVal=this.val,执行回调函数,传入新旧value。

问:如果监听的是$route.path,它并没有经过数据劫持,在创建user watcher实例,执行Watcher.get()时应该是无法触发数据的getter,那么如何收集依赖的?当$route.path改变时应该也不会触发数据setter,又何如派发更新?

系列文章