背景

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

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

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

watcher

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

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都不是本文关注的,不重要,绕过!

你只须要晓得一件事:会执行上面的代码

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

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

嘿嘿嘿,这个时候就不一样了:

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

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

所以到这里能够明确一件事,所有的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的值取到,取到的就是oldValfunction 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 中都是字符串。

文章中的代码是简略版的,还有很多细枝末节的货色没说,不重要也只是针对本文不重要,大家能够去浏览源码更深刻的了解。