赞
踩
前一章节,通过对prop,data所定义的属性建立观察类和发布器,但此时的发布类Dep中sub空空如也,如何实现watcher的注册,并在属性发生变化时,实现更新。本章节将继续介绍订阅Watcher。
在介绍前,我们先思考下,用户定义的prop,data在哪些方法或者表达式是需要实现响应式变化的?
首先用户自定义的watch,computed是需要的,另外我们的视图也会用到大量的属性表达式(如前面的实例{{item.id}}),也是需要的。method中虽然用到了这些数据,但是都是及时调用,所以不需要的。
事实上,源码中也是在这三个场景创建watcher,如果阅读过我前面的博客,在initWatcher,initComputer(第五篇),以及mount(第六篇)简单的介绍过,做过一些知识的铺垫。
这三种watcher分别为user watcher,compute watcher,以及render watcher,本篇就结合前面的知识,详细介绍这几种watcher实现原理。
我们以下面的的watch为运行实例:
- data:{
- msg:'this is msg',
- items:[
- {id:1},
- {id:2},
- {id:3}
- ]
- }
- watch: {
- msg: function (val, oldVal) {
- console.log('new: %s, old: %s', val, oldVal)
- },
- ....
- }
回顾下第五篇initWatcher的创建watcher过程,如下:
- Vue.prototype.$watch = function (
- expOrFn: string | Function,
- cb: any,
- options?: Object
- ): Function {
- ...
- //创建watcher对象,进行监听
- const watcher = new Watcher(vm, expOrFn, cb, options)
- ...
- }
其中主要入参,vm,即vue对象;expOrFn,待观察的表达式,即本例中属性msg对象;cb,回调函数,即本例中msg属性的对应的方法。watcher的定义位于src/core/observer/watcher.js。
- export default class Watcher {
- vm: Component;
- expression: string;
- cb: Function;
- id: number;
- deep: boolean;
- user: boolean;
- computed: boolean;
- sync: boolean;
- dirty: boolean;
- active: boolean;
- dep: Dep;
- deps: Array<Dep>;
- newDeps: Array<Dep>;
- depIds: SimpleSet;
- newDepIds: SimpleSet;
- before: ?Function;
- getter: Function;
- value: any;
-
- constructor (
- vm: Component,//组件对象
- expOrFn: string | Function,//待观察的表达式
- cb: Function,//回调函数,更新的时候调用
- options?: ?Object,
- isRenderWatcher?: boolean
- ) {
- //1、初始化变量
- this.vm = vm
- if (isRenderWatcher) {
- vm._watcher = this
- }
- vm._watchers.push(this)
- // options
- if (options) {
- this.deep = !!options.deep
- this.user = !!options.user
- this.computed = !!options.computed
- this.sync = !!options.sync
- this.before = options.before
- } else {
- this.deep = this.user = this.computed = this.sync = false
- }
- this.cb = cb
- this.id = ++uid // uid for batching
- this.active = true
- this.dirty = this.computed // for computed watchers
- this.deps = []
- this.newDeps = []
- this.depIds = new Set()
- this.newDepIds = new Set()
- this.expression = process.env.NODE_ENV !== 'production'
- ? expOrFn.toString()
- : ''
- // parse expression for getter
- //2、解析表达式,获取getter方法
- if (typeof expOrFn === 'function') {
- this.getter = expOrFn
- } else {
- 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
- )
- }
- }
- //3、依赖收集
- if (this.computed) {
- //对于计算属性watcher,此时没有立即进行依赖收集,在执行render函数时收集
- this.value = undefined
- this.dep = new Dep()
- } else {
- //对于普通watcher,调用get方法,进行依赖收集
- this.value = this.get()
- }
- }
- ...
- }

1、构造方法
我们先看下其构造方法。主要过程包括:
(1)、初始化变量。主要变量如下:
deep,表示是否要进行深度监听,即属性嵌套的变量进行监听。
computed,表示是否是计算属性。
sync,表示更新时,是否需要同步执行。
deps,newDeps,该watcher对应的维护的发布器数组。
(2)、解析表达式,获取getter方法,如果expOrFn是function类型,则直接设置为getter方法,否则调用parsePath方法返回属性对象作为getter方法,本例中则使用后一种,执行结果为vm[msg]。
- export function parsePath (path: string): any {
- ...
- return function (obj) {
- for (let i = 0; i < segments.length; i++) {
- if (!obj) return
- obj = obj[segments[i]]
- }
- return obj
- }
- }
(3)、依赖收集,这部分处理的很巧妙,对于非计算属性,直接调用了get方法(对于计算属性的watcher我们等会在表)。继续看下get方法。
2、get方法
- get () {
- //1、将当前的watcher压栈
- pushTarget(this)
- let value
- const vm = this.vm
- try {
- //2、核心代码,依赖收集
- 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
- //3、后置处理
- if (this.deep) {
- traverse(value)
- }
- popTarget()
- this.cleanupDeps()
- }
- return value
- }

(1)将自身的watcher对象压入栈,设置全局的变量Dep.target为当前的watcher对象。
- export function pushTarget (_target: ?Watcher) {
- if (Dep.target) targetStack.push(Dep.target)
- Dep.target = _target
- }
(2)执行getter方法,触发该属性的get劫持,我们回顾前一章节定义的劫持方法。
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter () {
- const value = getter ? getter.call(obj) : val
- //依赖收集
- if (Dep.target) {
- dep.depend()
- if (childOb) {
- childOb.dep.depend()
- if (Array.isArray(value)) {
- dependArray(value)
- }
- }
- }
- return value
- }
- ...

全局变量Dep.target表示的就是当前的watcher对象,非空,继续调用Dep类的depend方法
- depend () {
- if (Dep.target) {
- Dep.target.addDep(this)
- }
- }
最终又回调了watcher中的addDep方法,
- addDep (dep: Dep) {
- const id = dep.id
- if (!this.newDepIds.has(id)) {
- //加入到watcher的dep数组
- this.newDepIds.add(id)
- this.newDeps.push(dep)
- if (!this.depIds.has(id)) {
- //加入到dep的sub数组
- dep.addSub(this)
- }
- }
- }
该方法,首先将dep保存到watcher的newDeps数组中,然后调用Dep的addSub,将watcher对象加入到sub数组中。
- addSub (sub: Watcher) {
- this.subs.push(sub)
- }
整个调用过程比较绕,这样做的目的,就是为了建立dep和watcher间的双向链表。
(3)后置处理,包括deep处理,清除当前的watcher对象等。
整个过程完成后,msg属性的Dep对象的sub中就添加了watcher对象。msg的依赖模型如下:
以下面的计算属性computeMsg为运行实例:
- <div id="app">
- {{computeMsg}}
- ...
- </div>
-
- var vm = new Vue({
- data:{
- msg:'this is msg',
- items:[
- {id:1},
- {id:2},
- {id:3}
- ]
- }
- watch: {
- msg: function (val, oldVal) {
- console.log('new: %s, old: %s', val, oldVal)
- },
- computed:{
- computeMsg:function(){
- return "this is computed msg:"+this.msg
- }
- }
- ....
- }

该实例中,创建computeMsg计算属性,并在模板(template)中使用。computeMsg计算属性表达式中调用了msg属性。
我们来回顾下第五章节的initComputed方法。
- function initComputed (vm: Component, computed: Object) {
- // $flow-disable-line
- const watchers = vm._computedWatchers = Object.create(null)
- // computed properties are just getters during SSR
- const isSSR = isServerRendering()
- //1、循环计算属性
- for (const key in computed) {
- const userDef = computed[key]
- const getter = typeof userDef === 'function' ? userDef : userDef.get
- ...
-
- if (!isSSR) {
- // create internal watcher for the computed property.
- //2、为每个属性创建watcher
- watchers[key] = new Watcher(
- vm,
- getter || noop,
- noop,
- computedWatcherOptions
- )
- }
-
- ...
- //3、劫持数据变化,创建监听方法
- defineComputed(vm, key, userDef)
- }
- }
-
- export function defineComputed (
- target: any,
- key: string,
- userDef: Object | Function
- ) {
- const shouldCache = !isServerRendering()
- if (typeof userDef === 'function') {
- sharedPropertyDefinition.get = shouldCache
- ? createComputedGetter(key)
- : userDef
- sharedPropertyDefinition.set = noop
- } else {
- ....
- }
- //4、并对计算属性的getter和setter进行劫持
- Object.defineProperty(target, key, sharedPropertyDefinition)
- }
- }
-
- //getter方法劫持
- function createComputedGetter (key) {
- return function computedGetter () {
- const watcher = this._computedWatchers && this._computedWatchers[key]
- if (watcher) {
- //依赖收集,将订阅类添加到
- watcher.depend()
- //返回值
- return watcher.evaluate()
- }
- }
- }
-

在该方法中,循环所定义的计算属性,为每个计算属性创建watcher,并设置getter方法,监听数据变化(计算属性setter方法用的较少)。watcher的主要入参:vm,即vue对象;getter,计算属性的表达式,即本例中computeMsg对应的表达式。继续看下watcher的构造方法。
1、构造函数
- constructor (
- vm: Component,//组件对象
- expOrFn: string | Function,//待观察的表达式
- cb: Function,//回调函数
- options?: ?Object,
- isRenderWatcher?: boolean
- ) {
- ...
- // parse expression for getter
- //1、对于计算属性,表达式即为getter
- if (typeof expOrFn === 'function') {
- this.getter = expOrFn
- } else {
- 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
- )
- }
- }
-
- if (this.computed) {
- //2、对于计算属性,创建了dep,此时没有立即进行依赖收集,
- this.value = undefined
- this.dep = new Dep()
- } else {
- this.value = this.get()
- }
- }

watcher的入参,vm表示vue对象,getter为待观察的表达式,即计算属性函数。
与user watcher相比,有两个不同的地方。
(1)、由于expOrFn 是计算属性的表达式,类型是function,所以走一个分支,将getter设置为计算属性表达式。
(2)、并没有调用get方法进行依赖收集,只是创建了dep对象,该对象保存依赖该计算属性的watcher。
计算属性的依赖关系不同于普通的属性,它即依赖于其表达式包含的普通属性,比如说本例中的属性msg,同时又被其他调用者依赖,如本例中调用computeMsg的模板表达式({{computeMsg}})。本例的依赖模型如下:
那么问题来了,既然这些依赖关系没有在定义的时候进行收集,那是什么时候做的呢?答案就是在模板调用的时候。
计算属性的设计初衷是简化模板的表达式,避免太多的逻辑导致模板的复杂。所以,只有在模板调用的情况下才会触发依赖,如果只定义不调用,进行依赖就会造成浪费。
下面我们来介绍与模板render相关的watcher(render watcher),并看下如何完成红色标注的依赖收集。
我们先回顾下第六章节介绍挂载时的mountComponent方法。
- export function mountComponent (
- vm: Component,
- el: ?Element,
- hydrating?: boolean
- ): Component {
- ....
- //2、定义updateComponent,vm._render将render表达式转化为vnode,vm._update将vnode渲染成实际的dom节点
- updateComponent = () => {
- vm._update(vm._render(), hydrating)
- }
- //3、首次渲染,并监听数据变化,并实现dom的更新
- new Watcher(vm, updateComponent, noop, {
- before () {
- if (vm._isMounted) {
- callHook(vm, 'beforeUpdate')
- }
- }
- }, true /* isRenderWatcher */)
- ....
- }

当时我们说这个watcher有两个作用,1、实现首次dom渲染,并完成依赖收集。2、监听模板所包含表达式的变化,实现update。
该watcher是数据"render"模板的"桥梁",称之为render watcher,其核心部分体现在入参expression(第二个参数,即updateComponent)。我们看下该watcher的初始化的主要过程:
- constructor (
- vm: Component,//组件对象
- expOrFn : string | Function,//待观察的表达式
- cb: Function,//回调函数
- options?: ?Object,
- isRenderWatcher?: boolean
- ) {
- ....
- if (typeof expOrFn === 'function') {//1、设置getter方法为表达式
- this.getter = expOrFn
- } else {
- .....
- }
-
- if (this.computed) {
- ....
- } else {
- //2、执行get方法,进行依赖收集
- this.value = this.get()
- }
- }

1、expOrFn为function,所以设置getter方法为表达式,即为updateComponent方法。
2、由于不是计算属性,与user watcher一样,执行get方法,实际就是执行updateComponent方法。
- updateComponent = () => {
- vm._update(vm._render(), hydrating)
- }
该方法包含vm._render(),vm._update两个执行阶段,我们在第六章重点分析过,我们不再做详细介绍。今天我们重点回答前面提出的问题,如何实现依赖关系的收集。
以上面的计算属性为例,在模板中,我们使用了"{{computeMsg}}"。
- <div id="app">
- {{computeMsg}}
- ...
- </div>
该模板经过编译(参见前面的编译部分)后,该部分的render表达式"_s(computeMsg)",在执行vm._render时,就会触发computeMsg所设置的getter方法。
- function createComputedGetter (key) {
- return function computedGetter () {
- const watcher = this._computedWatchers && this._computedWatchers[key]
- if (watcher) {
- //1、依赖收集,将订阅类添加到dep中
- watcher.depend()
- //2、返回值
- return watcher.evaluate()
- }
- }
- }
1、调用watcher.depend,将当前的render watcher对象添加到compteMsg的dep中,完成依赖关系的收集。
- depend () {
- if (this.dep && Dep.target) {
- this.dep.depend()
- }
- }
注意这里的Dep.target指的是render watcher。依赖模型如下:
2、调用watcher.evaluate()。
- evaluate () {
- //如果有更新,则重新计算,否则返回缓存值
- if (this.dirty) {
- this.value = this.get()
- this.dirty = false
- }
- return this.value
- }
这是this指的是compute watcher。其get方法就是属性表达式。
- function(){
- return "this is computed msg:"+this.msg
- }
由于表达式中包含了msg属性,在执行过程中,又触发msg的get监听方法,将该compute watcher 添加到msg的dep中,完成被依赖关系的收集。最终的依赖模型如下:
至此,computeMsg的依赖和被依赖关系收集完成。大家也可以尝试分析下模板中调用普通属性的依赖关系收集过程。
目前msg属性的dep发布器中收集了两个依赖的watcher对象,当我们重新设置msg值,会发生什呢?
当重新设置msg值,就会触发set方法。
- Object.defineProperty(obj, key, {
- ...
- set: function reactiveSetter (newVal) {
- const value = getter ? getter.call(obj) : val
- /* eslint-disable no-self-compare */
- if (newVal === value || (newVal !== newVal && value !== value)) {
- return
- }
- /* eslint-enable no-self-compare */
- if (process.env.NODE_ENV !== 'production' && customSetter) {
- customSetter()
- }
- if (setter) {
- setter.call(obj, newVal)
- } else {
- val = newVal
- }
- //核心部分,通知更新
- childOb = !shallow && observe(newVal)
- dep.notify()
- }
- })

调用其发布器的notify方法。
- //通知相关的watcher类更新
- notify () {
- // stabilize the subscriber list first
- const subs = this.subs.slice()
- for (let i = 0, l = subs.length; i < l; i++) {
- subs[i].update()
- }
- }
循环属性关联的watcher类,由前面可知,此时dep的sub集合中有user watcher和computed watcher两个对象。分别调用其update方法。
- update () {
- /* istanbul ignore else */
- if (this.computed) {//计算属性watcher处理
- // A computed property watcher has two modes: lazy and activated.
- // It initializes as lazy by default, and only becomes activated when
- // it is depended on by at least one subscriber, which is typically
- // another computed property or a component's render function.
- //当没有订阅计算属性时,不需要计算,仅仅设置dirty为true,表示下次重新计算
- if (this.dep.subs.length === 0) {
- // In lazy mode, we don't want to perform computations until necessary,
- // so we simply mark the watcher as dirty. The actual computation is
- // performed just-in-time in this.evaluate() when the computed property
- // is accessed.
- this.dirty = true
- } else {
- // In activated mode, we want to proactively perform the computation
- // but only notify our subscribers when the value has indeed changed.
- this.getAndInvoke(() => {
- this.dep.notify()
- })
- }
- } else if (this.sync) {//同步处理,立即执行
- this.run()
- } else {//异步处理,进入堆栈
- queueWatcher(this)
- }
- }

update分为三个分支处理,我们分别对三种情况进行分析。
(1)、计算属性处理。
(2)、同步处理,立即执行。
(3)、异步处理,加入到堆栈中。
1、计算属性
对于计算属性的watcher对象,判断是否有订阅过该计算属性,如果没有(即该计算属性的dep的sub集合是否为空),则设置dirty为true,下次将重新计算。
如本例中,computeMsg是被render watcher依赖,所以会进入else分支,执行getAndInvoke方法。
- this.getAndInvoke(() => {
- this.dep.notify()
- })
-
- getAndInvoke (cb: Function) {
- //获取最新的值
- const value = this.get()
- if (
- //
- value !== this.value ||//值发生变化
- // Deep watchers and watchers on Object/Arrays should fire even
- // when the value is the same, because the value may
- // have mutated.
- isObject(value) ||//object对象
- this.deep//深度watcher
- ) {
- // set new value
- //更新值
- const oldValue = this.value
- this.value = value
- this.dirty = false
- //执行回调函数
- if (this.user) {
- try {
- cb.call(this.vm, value, oldValue)
- } catch (e) {
- handleError(e, this.vm, `callback for watcher "${this.expression}"`)
- }
- } else {
- cb.call(this.vm, value, oldValue)
- }
- }
- }

该方法核心部分是执行传入的回调cb方法,即this.dep.notify(),此时递归调用render watcher的update,又回到了update方法。
2、同步处理
如果不是计算属性(本例中的user watcher,render watcher),并设置了同步处理,则调用run方法。如:
- run () {
- if (this.active) {
- this.getAndInvoke(this.cb)
- }
- }
run方法调用getAndInvoke,该方法核心部分是:
(1)执行get方法获取value,对于render watcher ,执行updateComponent方法,重新生成Vnode,并patch,实现dom的更新(下一章节将详细说明)。
(2)回调cb方法,如本例中的msg的watch表达式(注意:render watcher的cb是noon)
3、异步处理
对于非同步,则调用queueWatcher将watcher压入堆栈中。在将这个方法之前,我们先看如果更新queue中的watcher,即flushSchedulerQueue方法,在src/core/observer/scheduler.js中
- function flushSchedulerQueue () {
- //1、设置标识位flush为true,标识正在刷新中
- flushing = true
- let watcher, id
-
- // Sort queue before flush.
- // This ensures that:
- // 1. Components are updated from parent to child. (because parent is always
- // created before the child)
- // 2. A component's user watchers are run before its render watcher (because
- // user watchers are created before the render watcher)
- // 3. If a component is destroyed during a parent component's watcher run,
- // its watchers can be skipped.
- //2、将queue数组从小到大排序
- queue.sort((a, b) => a.id - b.id)
-
- // do not cache length because more watchers might be pushed
- // as we run existing watchers
- //3、循环队栈queue,执行watcher.run方法,实现更新。
- for (index = 0; index < queue.length; index++) {
- watcher = queue[index]
- if (watcher.before) {
- watcher.before()
- }
- id = watcher.id
- has[id] = null
- watcher.run()
- // in dev build, check and stop circular updates.
- if (process.env.NODE_ENV !== 'production' && has[id] != null) {
- circular[id] = (circular[id] || 0) + 1
- if (circular[id] > MAX_UPDATE_COUNT) {
- warn(
- 'You may have an infinite update loop ' + (
- watcher.user
- ? `in watcher with expression "${watcher.expression}"`
- : `in a component render function.`
- ),
- watcher.vm
- )
- break
- }
- }
- }
-
- // keep copies of post queues before resetting state
- const activatedQueue = activatedChildren.slice()
- const updatedQueue = queue.slice()
- //4、重置相关的状态
- resetSchedulerState()
-
- // call component updated and activated hooks
- callActivatedHooks(activatedQueue)
- callUpdatedHooks(updatedQueue)
-
- // devtool hook
- /* istanbul ignore if */
- if (devtools && config.devtools) {
- devtools.emit('flush')
- }
- }

我们看下核心的步骤:
(1)设置标识位flush,表示queue正在处理中。
(2)根据queue中的watcher的id,从小到大进行排序(也就是创建的先后)
(3)循环queque中的watcher,执行run方法,实现更新。
(4)queue中的执行完成后,则重置相关的状态,包括flush,wait等,等待下一次执行。
整个处理过程还是比较清晰的,这里注意一点,在第3步中,queue是可能动态变化的。
现在回过头来,继续看queueWatcher,是如何将watcher加入到queue中,又是如何触发flushSchedulerQueue执行的。
- export function queueWatcher (watcher: Watcher) {
- const id = watcher.id
- if (has[id] == null) {//对于同一个watcher,不会重复加入到queue,避免多次触发
- has[id] = true
- //1、将watcher加入队列中
- if (!flushing) {//尚未刷新,则加入队栈,待执行
- queue.push(watcher)
- } else {//2、正在刷新中,则动态的插入到到对应位置。
- // if already flushing, splice the watcher based on its id
- // if already past its id, it will be run next immediately.
- let i = queue.length - 1
- //从后往前,查找对应位置
- while (i > index && queue[i].id > watcher.id) {
- i--
- }
- queue.splice(i + 1, 0, watcher)
- }
- // queue the flush
- //3、通过nexttick执行queue中的watcher
- if (!waiting) {
- waiting = true
- nextTick(flushSchedulerQueue)
- }
- }
- }

将watcher加入queue队栈中,分两种情况,
(1) flushSchedulerQueue未执行,则将watcher加入到queue即可,待下一次执行。
(2)flushSchedulerQueue执行中,则从后往前查找对应的位置,然后插入到queue。
(3)通过nextTick,调用flushSchedulerQueue,实现queue中watcher的更新。vue的DOM是异步更新的,nextTick确保了在DOM更新后再执行,在这里可以认为下一个事件循环的"tick"。nextTick机制实现了"批量"的更新,效率更高。
这里要注意,对于同一个watcher,不能重复的加入到queue中,避免多次触发。
本章节的逻辑还是比较复杂。我们将各个方法间的调用关系总结下,便于大家理解。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。