xingbofeng.github.io icon indicating copy to clipboard operation
xingbofeng.github.io copied to clipboard

Vue源码学习笔记之Dep和Watcher

Open xingbofeng opened this issue 6 years ago • 0 comments

之前在解析Vue源码的过程中专门提到了Observe模块的变异方法与相应式原理。但是对于相应式的通知视图更新这一块儿只是专门提了通过DepWatcher这样一个发布-订阅模式来进行通知。

恰好最近刚刚学习了发布-订阅模式的原理,正巧利用这样一段时间来进行Vue的专门的发布-订阅模块的源码学习。

先看一下Vue文档对于深入相应式原理的剖析:

在初始化的时候,首先通过 Object.defineProperty 改写 getter/setterData 注入观察者能力,在数据被调用的时候,getter 函数触发,调用方(会为调用方创建一个 Watcher)将会被加入到数据的订阅者序列,当数据被改写的时候,setter 函数触发,变更将会通知到订阅者(Watcher)序列中,并由 Watcher 触发 re-render,后续的事情就是通过 render function code 生成虚拟 DOM,进行 diff 比对,将不同反应到真实的 DOM 中。

发布-订阅模式

最近在阅读《JavaScript设计模式与开发实践》是专门对发布-订阅模式这一块进行了精读,精读部分内容可见我写的精读部分的文档:发布-订阅模式

一个简单的发布-订阅模式的实现主要是以下三点内容:

  • 指定好发布者;
  • 发布者有一个缓存列表,里面存放了回调函数,以便发布后通知订阅者;
  • 发布消息的时候遍历缓存列表,依次触发订阅者的回调;

以下为一个售楼处-短信通知的简版发布订阅模式的实现:

var event = {
  clientList: [],
  listen: function(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    // 订阅的消息添加进缓存列表 
    this.clientList[key].push(fn);
  },
  trigger: function() {
    var key = Array.prototype.shift.call(arguments);
    var fns = this.clientList[key];
    if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
      return false;
    }
    for (var i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments); // (2) // arguments 是 trigger 时带上的参数
    }
  }
};

// **再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能**
var installEvent = function(obj) {
  for (var i in event) {
    obj[i] = event[i];
  }
};

var salesOffices = {}; // 定义发布者(售楼处)
installEvent(salesOffices);

// 小明订阅消息
salesOffices.listen('squareMeter88', function(price) {
  console.log('价格= ' + price);
});

// 小红订阅消息
salesOffices.listen('squareMeter100', function(price) {
  console.log('价格= ' + price);
});
salesOffices.trigger('squareMeter88', 2000000); // 输出:2000000
salesOffices.trigger('squareMeter100', 3000000); // 输出:3000000

我们在这里使用了一个全局的Event对象来代理我们所有的发布-订阅模型

Dep模块

Dep模块的位置在src/core/observer/dep.js,主要作用是收集订阅者的容器。

看以下代码:

/* @flow */

// 首先引入watcher模块,用于通知观察者
import type Watcher from './watcher'
import { remove } from '../util/index'

// 闭包定义一个唯一的ID,这里在上边也说了,我们需要保持每次都要通知到指定的通知者,因此用唯一ID标示
let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher; // 一个订阅者
  id: number; // 定义Dep的唯一id作为标示
  subs: Array<Watcher>; // 维护一个观察者队列,一旦数据发生改变通知所有观察者

  constructor () {
    this.id = uid++ // 定义发布者的唯一ID
    this.subs = [] // 观察者队列
  }

  // 添加观察者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // 移除观察者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // Dep.target变量存的是一个订阅者对象
  // 一旦其发布者发布过数据,通知订阅者!
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 通知订阅者,数据更新啦(调用订阅者的update方法)
  notify () {
    // stabilize the subscriber list first
    // 保证是一个Array,这里就是订阅者的队列
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.

// targetStack定义一个栈,用于收集依赖
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

其中Dep.target.addDep(this)Watcher模块,作用为添加依赖:

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

  • Dep定义了发布者的模型,在整个应用中使用唯一的id对其实例进行标识。

  • Dep的订阅者独自形成一个订阅者队列subsDep通过addSubremoveSub方法添加和移除订阅者。

  • Dep通过notify通知订阅者数据更新。这个更新对于对象来说是通过setter完成,对于数组,因为其length属性不可configurable并且不可enumerable 以及 writable。因此Vue使用变异方法更新数据以确保能正常notify

  • 当数据的getter触发后,会收集依赖,但也不是所有的触发方式都会收集依赖,只有通过watcher触发的getter会收集依赖,而所谓的被收集的依赖就是当前watcherDOM中的数据必须通过watcher来绑定,只通过watcher来读取。

如何收集依赖?

看以下代码:

new Vue({
  template: 'computed',
  data: {
    raw: 1
  },
  computed: {
    model: function() {
      return this.raw + 1;
    },
  },
  watch: {
    model: function() {
      console.log('the computed');
    },
  },
});

在计算属性处理完成后,会发现在vm下挂载了一个keymodel的属性。

vm.model = function() {
  return this.raw + 1;
}

vm挂载的过程中就已经触发了一次getter便收集了一次依赖!

收集依赖的理解

  • Dep其实是dependence依赖的缩写,举个例子,我们的一个模板{{ a + b }},我们会说他的依赖有ab,其实就是依赖了dataab属性,更精确的说是依赖了a属性中闭包的dep实例b属性中闭包的那个dep实例

  • 详细来说:我们的这个{{ a + b }}DOM里最终会被a + b表达式的真实值所取代,所以存在一个求出这个a+b的表达式的过程,求值的过程就会自然的分别触发abgetter,而在getter中,我们看到执行了dep.depend(),这个函数实际上会做dep.addSub(Dep.target),即在dep的订阅者数组中存放了Dep.target,让Dep.target订阅dep

  • Dep.target是什么?他就是我们后面介绍的Watcher实例,为什么要放在Dep.target里呢?是因为getter函数并不能传参,dep可以通过闭包的形式放进去,那watcher可就不行了,watcher内部存放了a + b这个表达式,也是由watcher计算a + b的值,在计算前他会把自己放在一个公开的地方(Dep.target),然后计算a + b,从而触发表达式中所有遇到的依赖的getter,这些getter执行过程中会把Dep.target加到自己的订阅列表中。等整个表达式计算成功,Dep.target又恢复为null.这样就成功的让watcher分发到了对应的依赖的订阅者列表中,订阅到了自己的所有依赖。

还是不理解Dep?看Watcher

Vue 2.4.2版本中,Watcher模块位于src/core/observer/watcher.js

Watcher可以先暂时理解为房产中介用户买房子找中介,中介帮忙找房主,房主卖房子找中介,中介帮房主把房子卖给用户。

setter触发消息到Watcherwatcher帮忙告诉Directive更新DOMDOM中修改了数据也会通知给WatcherWatcher帮忙修改数据。

先来看Watcher类构造器:

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: Object
) {
  this.vm = vm
  vm._watchers.push(this)
  // options
  if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
  } else {
    this.deep = this.user = this.lazy = this.sync = false
  }
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  // lazy watcher是在计算属性里用到的,Vue在初始化时会封装你的计算属性的getter,
  // 并在里面闭包了一个新创建的lazy watcher
  // 而指令bind函数中创建的那个并不是lazy watcher,即使这个指令是绑定到一个计算属性上的,请注意区分
  // lazy不会像一般的指令的watcher那样在这个watcher构造函数里计算初始值(this.value)
  // 而计算属性的getter里写有了逻辑,如果他的lazy watcher的dirty是false,
  // 就拿出之前计算过的值返回给你(dirty的意思表示是数据的依赖有变化,你需要重新计算)
  // 否则就会使用Watcher.prototype.evaluate完成求值,
  // 一旦指定lazy为true,那么这个数据就肯定是dirty的
  // 因此初始化时,是从没有计算过的,数据是undefined,并非正确的值,因此肯定需要计算,所以this.dirty = this.lazy
  this.dirty = this.lazy // for lazy watchers
  // 用deps存储当前的依赖,而新一轮的依赖收集过程中收集到的依赖则会放到newDeps中
  // 之所以要用一个新的数组存放新的依赖是因为当依赖变动之后,
  // 比如由依赖a和b变成依赖a和c
  // 那么需要把原先的依赖订阅清除掉,也就是从b的subs数组中移除当前watcher,因为我已经不想监听b的变动
  // 所以我需要比对deps和newDeps,找出那些不再依赖的dep,然后dep.removeSub(当前watcher),这一步在afterGet中完成
  this.deps = []
  this.newDeps = []
  // 这两个set是用来提升比对过程的效率,不用set的话,判断deps中的一个dep是否在newDeps中的复杂度是O(n)
  // 改用set来判断的话,就是O(1)
  this.depIds = new Set()
  this.newDepIds = new Set()
  this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : ''
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    // 对于计算属性computed来说,就会进入到这里
    this.getter = expOrFn
  } else {
    // 使用parsePath初始化getter
    // 把expression解析为一个对象,对象的get/set属性存放了获取/设置的函数
    // 比如hello解析的get函数为function(scope) {return scope.hello;}
    this.getter = parsePath(expOrFn)
    // 表达式解析错误的错误处理
    if (!this.getter) {
      this.getter = function () {}
      process.env.NODE_ENV !== 'production' && warn(
        `Failed watching path: "${expOrFn}" ` +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      )
    }
  }
  // 如果设定lazy,不会立刻调用
  this.value = this.lazy
    ? undefined
    : this.get()
}

getter

vm初始化时getter

get () {
  pushTarget(this) // 做依赖收集
  let value
  const vm = this.vm
  try {
    // 调用其getter方法,初始化就调用
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 表达式错误
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value) // 如果深度订阅,递归观察对象变化
    }
    // watcher的值计算完成后,新的依赖将被设置,旧的依赖会被删除,依赖收集完成。
    popTarget()
    this.cleanupDeps()
  }
  return value
}

traverse表示深度订阅,设置VM.$watch第三个参数为{ deep: true }

const seenObjects = new Set()
function traverse (val: any) {
  seenObjects.clear()
  _traverse(val, seenObjects)
}

function _traverse (val: any, seen: ISet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
    return
  }
  // 如果当前值有Observer
  if (val.__ob__) {
    // 拿到当前值的Observer的订阅者管理员的id
    const depId = val.__ob__.dep.id
    // 如果seen中已经有这个id了(已经被订阅),直接返回
    if (seen.has(depId)) {
      return
    }
    // 否则添加到seen中(订阅它)
    seen.add(depId)
  }
  // 数组递归
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else { // 对象递归
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

更新数据

update () {
  /* istanbul ignore else */
  // lazy模式下,标记下当前是脏的就可以了
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    // 同步模式下直接更新
    this.run()
  } else {
    // 否则异步更新(暂时不展开,可见scheduler模块)
    queueWatcher(this)
  }
}

更深的理解?

  • Vue实例初始化过程中,将所有计算属性包装为lazy watcher
  • 首次访问计算属性时,watcherdirty,此时开始计算此watcher的值(dirty表示数据是脏的,必须计算一次);
  • 计算开始之前,此watcher将被设置为依赖目标(Dep.target.addDep(this)),开始依赖收集;
  • 计算watcher值的过程中,被访问到属性的getter中会是检查是否存在依赖目标,若存在依赖目标就创建依赖关系;
  • watcher的值计算完成后,新的依赖将被设置,旧的依赖会被删除,依赖收集完成。
  • 当依赖属性更新时,会通知自身的依赖目标,watcher被设置为dirty(提醒watcher又该更新了);
  • 再次访问该计算属性,重复计算及依赖收集步骤。

xingbofeng avatar Jul 24 '17 06:07 xingbofeng