我想用大白话讲清楚watch和computed

背景

一直以来我对vue中的watchcomputed都只知其一;不知其二的,知道一点(例如:watchcomputed的本质都是new Watcher,computed有缓存,只有调用的时候才会执行,也只有当依赖的数据变化了,才会再次触发...),而后就没有而后了。javascript

也看了不少大佬写的文章,一大段一大段的源码列出来,着实让我这个菜鸡看的头大,天然也就不想看了。最近,我又开始学习vue源码,才真正理解了它们的实现原理。vue

data() {
  return {
    msg: 'hello guys',
    info: {age:'18'},
    name: 'FinGet'
  }
}

watcher

watcher 是什么?侦听器?它就是个类class!java

class Watcher{
  constructor(vm,exprOrFn,callback,options,isRenderWatcher){
  }
}
  • vm vue实例
  • exprOrFn 多是字符串或者回调函数(有点懵就日后看,如今它不重要)
  • options 各类配置项(配置啥,日后看)
  • isRenderWatcher 是不是渲染Wathcer

initState

Vue 初始化中 会执行一个 initState方法,其中有你们最熟悉的initData,就是Object.defineProperty数据劫持。缓存

export function initState(vm) {
  const opts = vm.$options;
  // vue 的数据来源 属性 方法 数据 计算属性 watch
  if(opts.props) {
    initProps(vm);
  }
  if(opts.methods) {
    initMethod(vm);
  }
  if(opts.data) {
    initData(vm);
  }
  if(opts.computed){
    initComputed(vm);
  }
  if(opts.watch) {
    initWatch(vm, opts.watch);
  }
}

在数据劫持中,Watcher的好基友Dep出现了,Dep就是为了把Watcher存起来。函数

function defineReactive(data, key, val) {
  let dep = new Dep(); 
  Object.defineProperty(data, key, {
    get(){
      if(Dep.target) {
        dep.depend(); // 收集依赖
      }
      return val;
    },
    set(newVal) {
      if(newVal === val) return;
      val = newVal;
      dep.notify(); // 通知执行
    }
  })
}
initData的时候, Dep.target啥也不是,因此收集了个寂寞。 target是绑在Dep这个类上的(静态属性),不是实例上的。

可是当$mount以后,就不同了。至于$mount中执行的什么compilegeneraterenderpatchdiff都不是本文关注的,不重要,绕过!学习

你只须要知道一件事:会执行下面的代码this

new Watcher(vm, updateComponent, () => {}, {}, true); // true 表示他是一个渲染watcher

updateComponent就是更新哈,不计较具体执行,它如今就是个会更新页面的回调函数,它会被存在Watchergetter中。它对应的就是最开始那个exprOrFn参数。lua

嘿嘿嘿,这个时候就不同了:spa

  1. 渲染页面就是调用了你定义的数据(别杠,定义了没调用),就会走get
  2. new Watcher 就会调用一个方法把这个实例放到Dep.target上。
pushTarget(watcher) {
  Dep.target = watcher;
}

这两件事正好凑到一块儿,那么 dep.depend()就干活了。prototype

因此到这里能够明白一件事,全部的 data中定义的数据,只要被调用,它都会收集一个渲染 watcher,也就是数据改变,执行 set中的 dep.notify就会执行渲染 watcher

下图就是定义了msginfoname三个数据,它们都有个渲染Watcher

眼尖的小伙伴应该看到了msg中还有两个watcher,一个是用户定义的watch,另外一个也是用户定义的watch。啊,固然不是啦,vue是作了去重的,不会有重复的watcher,正如你所料,另外一个是computed watcher

用户watch

咱们通常是这样使用watch的:

watch: {
  msg(newVal, oldVal){
    console.log('my watch',newVal, oldVal)
  }
  // or
  msg: {
    handler(newVal, oldVal) {
      console.log('my watch',newVal, oldVal)
    },
    immediate: true
  }
}

这里会执行一个initWatch,一顿操做以后,就是提取出exprOrFn(这个时候它就是个字符串了)、handleroptions,这就和Watcher莫名的契合了,而后就瓜熟蒂落的调用了vm.$watch方法。

Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
    options.user = true; // 标记为用户watcher
    // 核心就是建立个watcher
    const watcher = new Watcher(this, exprOrFn, cb, options);
    if(options.immediate){
      cb.call(vm,watcher.value)
    }
 }

来吧,避免不了看看这段代码(原本粘贴了好长一段,但说了大白话,我就把和这段关系不大的给删减了):

class Watcher{
  constructor(vm,exprOrFn,callback,options,isRenderWatcher){
    this.vm = vm;
    this.callback = callback;
    this.options = options;
    if(options) {
      this.user = !!options.user;
    }
    this.id = id ++;
    if (typeof exprOrFn == 'function') {
      this.getter = exprOrFn; // 将内部传过来的回调函数 放到getter属性上
    } else {
      this.getter = parsePath(exprOrFn);
      if (!this.getter) {
        this.getter = (() => {});
      }
    }
    this.value = this.get();
  }
  get(){
    pushTarget(this); // 把当前watcher 存入dep中
    let result = this.getter.call(this.vm, this.vm); // 渲染watcher的执行 这里会走到observe的get方法,而后存下这个watcher
    popTarget(); // 再置空 当执行到这一步的时候 因此的依赖收集都完成了,都是同一个watcher
    return result;
  }
}
// 这个就是拿来把msg的值取到,取到的就是oldVal
function parsePath(path) {
  if (!path) {
    return
  }
  var segments = path.split('.');
  return function(obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}

你们能够看到,new Watcher会执行一下get方法,当是渲染Watcher就会渲染页面,执行一次updateComponent,当它是用户Watcher就是执行parsePath中的返回的方法,而后获得一个值this.value也就是oldVal

嘿嘿嘿,既然取值了,那又走到了msgget里面,这个时候dep.depend()又干活了,用户Watcher就存进去了。

msg改变的时候,这过程当中还有一些骚操做,不重要哈,最后会执行一个run方法,调用回调函数,把newValueoldValue传进去:

run(){
    let oldValue = this.value;
    // 再执行一次就拿到了如今的值,会去重哈,watcher不会重复添加
    let newValue = this.get();
    this.value = newValue;
    if(this.user && oldValue != newValue) { 
      // 是用户watcher, 就调用callback 也就是 handler
      this.callback(newValue, oldValue)
    }
  }

computed

computed: {
  c_msg() {
    return this.msg + 'computed'
  }
  // or
  c_msg: {
    get() {
      return this.msg + 'computed'
    },
    set() {}
  }
},

computed有什么特色:

  1. 调用的时候才会执行
  2. 有缓存
  3. 依赖改变时会从新计算

调用的时候执行,我怎么知道它在调用?嘿嘿嘿,Object.defineProperty不就是干这事的嘛,巧了不是。

依赖的数据改变时会从新计算,那就须要收集依赖了。仍是那个逻辑,调用了this.msg -> get -> dep.depend()

function initComputed(vm) {
  let computed = vm.$options.computed;
  const watchers = vm._computedWatchers = {};
  for(let key in computed) {
    const userDef = computed[key];
    // 获取get方法
    const getter = typeof userDef === 'function' ? userDef : userDef.get;
    // 建立计算属性watcher lazy就是第一次不调用
    watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
    defineComputed(vm, key, userDef)
  }
}
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: () => {},
  set: () => {}
}
function defineComputed(target, key, userDef) {
  if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = createComputedGetter(key)
  } else {
      sharedPropertyDefinition.get = createComputedGetter(userDef.get);
      sharedPropertyDefinition.set = userDef.set;
  }
  // 使用defineProperty定义 这样才能作到使用才计算
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

下面这一段最重要,上面的看一眼就好,上面作的就是把get方法找出来,用Object.defineProperty绑定一下。

class Watcher{
  constructor(vm,exprOrFn,callback,options,isRenderWatcher){
      ... 
    this.dirty = this.lazy;
    // lazy 第一次不执行
    this.value = this.lazy ? undefined : this.get();
    ...
  }
  
  update(){
    if (this.lazy) {
      // 计算属性 须要更新
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this); // 这就是个衬托 如今无论它
    }
  }
  evaluate() {
    this.value = this.get();
    this.dirty = false;
  }
}

缓存就在这里,执行get方法会拿到一个返回值this.value就是缓存的值,在用户Watcher中,它就是oldValue,写到这里的时候,对尤大神的佩服,又加深一层。🐂🍺plus!

function createComputedGetter(key) {
  return function computedGetter() {
    // this 指向vue 实例
    const watcher = this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) { // 若是dirty为true
        watcher.evaluate();// 计算出新值,并将dirty 更新为false
      }
      // 若是依赖的值不发生变化,则返回上次计算的结果
      return watcher.value
    }
  }
}

watcherupdate是何时调用的?也就是数据更新调用dep.notify()dirty就须要变成true,可是计算属性仍是不能立刻计算,仍是须要在调用的时候才计算,因此在update的时候只是改了dirty的状态!而后下次调用的时候就会从新计算。

class Dep {
  constructor() {
    this.id = id ++;
    this.subs = [];
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  depend() {
    Dep.target.addDep(this);
  }
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

总结

  1. watchcomputed 本质都是Watcher,都被存放在Dep中,当数据改变时,就执行dep.notify把当前对应Dep实例中存的Watcherrun一下,这样执行了渲染Watcher 页面就刷新了;
  2. 每个数据都有本身的Dep,若是他在模版中被调用,那它必定有一个渲染Watcher
  3. initData时,是没有 Watcher 能够收集的;
  4. 发现没有,渲染WatcherComputed 中,exprOrFn都是函数,用户Watcher 中都是字符串。

文章中的代码是简略版的,还有不少细枝末节的东西没说,不重要也只是针对本文不重要,你们能够去阅读源码更深刻的理解。

相关文章
相关标签/搜索