watch与计算属性 (源码浅析)

注:本篇文章需要您先了解vue的响应式原理,入不了解,可以点击此处查看作者的vue的响应式原理相关文章
注:本文代码一律为简略版,只保留当前思路下的主要流程

watch

watch简单来讲是通过创建一个非组件的watcher(本文称为watch watcher)并传入一个回调函数cb,将watch watcher与要监听的属性对应的Dep进行绑定而实现的,当Dep通知watcher更新时,就会执行cb中的内容

下边用一个简单的例子来看下Vue中到底都走了什么逻辑:

new Vue({
    data(){
        return {
            aaa:1
        }
    },
    watch:{
        aaa(){
            console.log('我有改动啦~')
        }
    }
})

1

无论是上边例子中的调用方式还是Vue.$watch的方式,watch的处理最终都会走$watcher函数。
对该函数将创建watcher所需的options中标添加user:true属性,用来标识是用户创建的watcher
如果用户传入了immediate:true,在$watch函数中new watcher后会调用一遍用户传入的函数cb.call(vm, watcher.value)
然后在$watcher中调用new Watcher(vm, expOrFn, cb, options)创建watch watcher

vm: vm
expOrFn: ‘aaa’
cb: fun(){console.log(‘我有改动啦~’)}
options: {user:true,deep:boolean}

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
): Function {
    const vm: Component = this
    // 如果第二个参数传的是一个对象而不是函数的话,执行createWatcher去处理对象格式,下方有简略版演示
    if (isPlainObject(cb)) {
      // options中传入的watch在执行$watch前一定会执行过createWatcher函数
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true // 对该函数将创建`watcher`所需的`options`中标添加`user:true`属性
    // 核心:创建watch watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // immediate处理,直接执行函数function cb(){console.log('我有改动啦~')}
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () { // 返回移除watcher方法
      watcher.teardown()
    }
}

function createWatcher(){
    ...
    // $watch('aaa', fun(){console.log('我有改动啦~')}, {/*deep:true,immediate:true*/})
    return vm.$watch(expOrFn, handler, options)
}

2

watcher中,有一步如果是组件watcher会走getter = expOrFn ,在这里如果检测到expOrFn是字符串,就会将getter创建为一个取值函数(下边解释)
然后和正常组件watcher一样,走get()
在调用get()时,执行顺序为

调用pushTarget: 将Dep.target存入watcher
调用gettergetter的作用是返回this[expOrFn]属性(会找到并返回aaa属性的值)(因为初始化watch是在初始化data/computed之后执行的,所以这时候的this[expOrFn]已经可以取到data/computed中的内容了)
调用了getter,getter会返回"aaa"属性的值,因为"aaa"已经是响应式的了,所以会执行Object.defineProperty -> get,使this[expOrFn]dep和当前watch watcher相互收集依赖
调用popTarget:清除Dep.target

即可完成属性的dep与监听watcher的相互绑定,watch方法就实现了。
在更新时,会调用watcher update,执行cb函数

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    ...
    // 跳过初始化参数和非必要部分的代码
    this.deep = !!options.deep // 深度监听
    this.user = !!options.user // 是否是用户传入
    this.cb = cb  // 回调函数

    // if创建组件watcher的流程
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else { // else用于watch中执行的流程
      this.getter = parsePath(expOrFn) // 返回一个函数,函数调用后会返回expOrFn字符串对应的属性(走defineProperty的get)
      if (!this.getter) { // 没有就置为空并报错
        this.getter = noop
        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
        )
      }
    }
    this.value = this.get() // get执行结束后,value的值为"aaa"的值
  }
  get () {
    pushTarget(this) // 将该watcher(this)push到dep.target上
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // getter会返回"aaa"属性的值,因为"aaa"已经是响应式的了,所以会执行`Object.defineProperty -> get`,使`this[expOrFn]`的`dep`和当前`watch watcher`相互收集依赖  
    } catch (e) {
      throw e
    } finally {
      if (this.deep) { // 如果是深度监听
        // 对val进行遍历,(因为遍历就会访问其属性 -> get)使得里边所有的响应式属性都收集到该watch watcher
        traverse(value)
      }
      popTarget() // 将watcher pop出dep.target
      this.cleanupDeps()
    }
    return value
  }
}

计算属性 computed

计算属性是创建一个计算属性watcher,然后让计算属性中用到的属性的dep同时和组件watcher + 计算属性watcher绑定,当属性被修改时,将计算属性watcher标记为脏值,在再次使用计算属性时,重新调用函数获取新的computed

还是用一个例子来解释下代码逻辑

new Vue({
    data() {
        return {
            aaa: '123',
        }
    },
    computed:{
        bbb:{ // 以对象格式可以更好的说明源码
            get () {
                return 'bbb的' + this.aaa
            },
            set(e) {
                console.log('set',e)
            }
        }
    }
})

0

初始化时,计算属性处理的入口函数是initComputed,在内部首先创建了计算属性watcher来实现计算属性的功能并将watcher放入到vm._computedWatchers对象中(vm._computedWatchers.bbb = 当前watcher实例),然后调用defineComputed(vm, key, userDef)get/set方法代理到vm

function initComputed (vm: Component, computed: Object) {
  // 在vm上创建vm._computedWatchers空对象
  const watchers = vm._computedWatchers = Object.create(null)

  //为每个computed的属性都创建一个内部watcher,并存在vm._computedWatchers里
  for (const key in computed) {
    // 获取opttion.computed.bbb(可能是函数也可能是对象{get:fun,set:fun})
    const userDef = computed[key] 
    // 获取get函数(fun () { return 'bbb的' + this.aaa })
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 为计算属性创建一个内部watcher,与普通watcher不同的是,options里的lazy为true(computedWatcherOptions里有lazy属性)
    watchers[key] = new Watcher(
      vm,getter || (() => {}),
      () => {},
      { lazy: true }
    )

    // 处理get/set函数,并将其Object.defineProperty代理到vm上
    defineComputed(vm, key, userDef)
  }
}

1

创建计算属性watcher: new Watcher(vm,getter||noop,noop,computedWatcherOptions)

vm:vm
getter: fun () { return 'bbb的' + this.aaa }
noop: noop = function () {}
computedWatcherOptions: { lazy: true }
计算属性watcher中有一个dirty:boolean属性,用于标识该watchervalue是否需要更新,默认为true(需要更新)
watcherget方会返回valuegetter方法的返回值,也就是return 'bbb的'+this.aaa),计算属性不会每次获取时都执行内部的函数,只有在内部使用的属性改变时才会调用获取新的返回值;因此,和其他watcher不同,计算属性watcher创建时不会立即执行getter函数去求值,求值的时机是defineComputed函数代理的defineProperty -> get方法(组件中使用this.bbb)时调用watcher.evaluate()来执行get方法获取新的valuedefineProperty -> get下边会解释)

export default class Watcher {
    constructor(
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
    ) {
        this.vm = vm
        // options:配置项
        this.lazy = !!options.lazy // 表示为计算属性
        this.cb = cb  // 回调函数(计算属性中为空函数【() => {}】)
        this.dirty = this.lazy // 该值是否为脏值(用于计算属性)
        // 用于存储dep
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.getter = expOrFn

        // 注:计算属性中这块不会调用get方法
        // 如果是计算属性,则不需要在这里就求值,在外界get这个计算属性时再求值
        // 如果非计算属性,调用get方法收集依赖
        this.value = this.lazy
            ? undefined
            : this.get()
    }

    // get在watch中已经说过,此处就不细说了,反正主要目的就是收集依赖+返回value
    get() {
        pushTarget(this) // 将该watcher(this)push到dep.target上
        let value
        const vm = this.vm
        try {
            value = this.getter.call(vm, vm) // 组件watcher在执行getter方法时,会走组件中所有用到的属性的defineProperty.get
        } catch (e) {
            if (this.user) {
                handleError(e, vm, `getter for watcher "${this.expression}"`)
            } else {
                throw e
            }
        } finally {
            popTarget() // 将watcher pop出dep.target
        }
        return value
    }

    // evaluate: 作用是调用get,绑定依赖并更新value
    // 在计算属性被响应式代理get时才会执行该函数
    // 执行完get后,会把dirty设置false,表明已经求过值了。
    // 当这个watcher的依赖有变化后,会通过执行watcher的update方法(在observer/dep.js里的notify方法里,subs[i].update(),这里subs[i]其实就是watcher),再次把dirty变为true
    evaluate() {
        this.value = this.get()
        this.dirty = false
    }
}

1.1 第一遍执行时的更新时机:

初始化时,创建计算属性watcher,虽然计算属性watcher不会立即执行getter方法获取value,但只要页面中用到了该计算属性(bbb),那么就会走计算属性的defineProperty -> get,这时getter就一定会执行

class Watcher {
    constructor () {
        ...
        // 计算属性watcher的lazy肯定为true,不会走this.get()方法
        this.value = this.lazy ? undefined : this.get()
    }
    get() {
        ...
    }
}

<template>
    这是页面,我有一个参数是{{bbb}} ←在这里调用了,走了bbb的getter,getter会收集依赖
</template>

2

defineComputedObject.definePropertycomputed的方法代理到vm上,Object.defineProperty -> get方法中会通过watcher中的dirty属性判断computed返回值是否需要更新,如果需要,就调用watcher.evaluate更新,最后返回value值,不需要更新就直接返回value(此处跳过收集依赖部分内容,下边单独说)

const noop = function () { } // noop是个空函数(在下方会常用到)
const sharedPropertyDefinition = { // Object.defineProperty用到的对象
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed(
    target: any,
    key: string,
    userDef: Object | Function
) {
    // 为sharedPropertyDefinition赋值get、set
    if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = createComputedGetter(key)
        sharedPropertyDefinition.set = noop
    } else {
        sharedPropertyDefinition.get = userDef.get ? createComputedGetter(key) : noop
        sharedPropertyDefinition.set = userDef.set || noop
    }
    // 代理
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter(key) {
    // 这个函数才是我们在this.bbb时调用执行的getter
    return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) { // if能在_computedWatchers中找到bbb
            // dirty:该值是否为脏值,如果是脏值就需要重新计算,计算是通过watcher.evaluate函数,计算后会把dirty改为false
            if (watcher.dirty) {
                watcher.evaluate() // evaluate的功能就是调用getter然后dirty=true
            }
            // 收集依赖  在下边会说
            if (Dep.target) {
                watcher.depend()
            }
            return watcher.value
        }
    }
}

??? 依赖收集与更新时机

上边也说过,计算属性watcher中有一个dirty属性,这个值是用来判断该计算属性是否是“脏值”,创建后默认为true,但是,dirty属性除了初始化时,还在什么时候变成false的呢?

在开始之前,我们要先重新认识下处理Dep.target的两个方法:

Vuetarget做了一个堆栈的存储:在pushTarget时,会同时将当前target放入targetStack数组中,如果连续两次pushTarget,那么当第一次调用popTarget,会将Dep.target置为 第一次pushTarget时的target

Dep.target = null
const targetStack = []

function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

/*
 *  ************ 举例说明 ************
 */
pushTarget(A) // target:A  targetStack:[A]
pushTarget(B) // target:B  targetStack:[A,B]
popTarget()   // target:A  targetStack:[A]

让我们按照时间顺序来看一下:

初始化时候生成的计算属性watcher不会执行get,也就不会进行依赖收集,
渲染时,首先会创建一个组件watcher,并放入Dep.target中,然后调用update、renderthis.getter.call(vm, vm))函数,在这个过程中会get计算属性aaa

class Watcher {
    constructor () {
        ...
        // 计算属性watcher:此处没有走get,就不会收集依赖
        // 组件watcher,走了get并收集依赖
        this.value = this.lazy ? undefined : this.get()
    }
    get() { // 收集依赖并return value
        pushTarget(this) // 放入Dep.target中
        ... 
        value = this.getter.call(vm, vm) // 渲染函数
        // 渲染时目前执行到此处-----------
        ...
        popTarget() // 移除Dep.target中
        return value
    }
}

get计算属性aaa会执行computedGetter(2.中代理的函数),在内部会调动watcher.evaluate(),也就是会执行计算属性watcherget

class Watcher {
    constructor () {...}
    evaluate() {
        this.value = this.get() // 此处调用了get
        this.dirty = false // 将dirty设为false
    }
    ...
}

计算属性watcher的在get中,因为执行了pushTarget(this),会将计算属性watcher放入Dep.target中来顶替组件watcher,然后下边走了this.getter.call(vm, vm)(当前getterfun() {return 'bbb的' + this.aaa})将getter中用到的属性(this.aaa)和当前计算属性watcher相互收集依赖,最后走popTarget()后,Dep.target又变成了组件watcher

// 当前执行的为计算属性watcher
get() {
    pushTarget(this) // Dep.target:计算属性watcher  targetStack:[组件watcher, 计算属性watcher]
    ... 
    value = this.getter.call(vm, vm) // fun() {return 'bbb的' + this.aaa}
    // this.aaa本身就是响应式属性,他的get中会执行dep.depend方法,将dep和Dep.target中的watcher相互绑定
    ...
    popTarget() // Dep.target:组件watcher  targetStack:[组件watcher]
    return value
}


// 备注 dep的depend方法
depend () {
    if (Dep.target) {
        Dep.target.addDep(this) // 调用Dep.target这个watcher中的addDep将当前dep和watcher相互绑定
    }
}

到了这里,我们刚刚抛出的疑问已经解决了,计算属性中用到的参数(aaa)的dep已经和计算属性watcher相互收集,当属性aaa更新时,就会调用watcherupdatedirty置位true
但是还有一个问题,就是计算属性本身是个watcher,当他内部使用的属性(如当前例子中的aaa)没有在页面中使用时,aaadep只会和当前计算属性watcher进行了绑定,并没有何组件watcher进行绑定,这时,当aaa改变导致计算属性bbb改变,虽然将dirty置位true,但组件并没有更新,这要怎么办呢?(答:因为computedGetter还没有走完…),让我们继续往下看~

让我们再来看一下computedGetter方法(获取计算属性时执行的Object.defineProperty -> get方法)

return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) { // if能在_computedWatchers中找到bbb
        // dirty:该值是否为脏值,如果是脏值就需要重新计算,计算是通过watcher.evaluate函数,计算后会把dirty改为false
        if (watcher.dirty) {
            watcher.evaluate() // evaluate的功能就是调用getter然后dirty=true
        }
        // *********************重点***************************
        if (Dep.target) {
            watcher.depend()
        }
        return watcher.value
    }
}

还记不记得,这个函数才刚刚执行到watcher.evaluate()这块,还没有完全执行完毕
watcher.evaluate()执行完毕后,代码走到了上边写的"重点"的位置,下一步是判断Dep.target有没有值
计算属性中执行完popTarget()后,将计算属性watcherpop出去了,所以当前Dep.target的值为组件watcher,所以肯定会进入if

// 当前执行的所有和该流程有关的pushTarget/popTarget:
// 创建组件时
popTarget(组件watcher)
// Dep.target:组件watcher  targetStack:[组件watcher]
----------------
// 计算属性watcher中 evaluate() -> get()
pushTarget(计算属性watcher)
// Dep.target:计算属性watcher  targetStack:[组件watcher, 计算属性watcher]
...
popTarget()
// Dep.target:组件watcher  targetStack:[组件watcher]

如果有值,继续执行watcher.depend()收集依赖,watcher.depend()中的watcher指的是计算属性watcher,因为他在上一步时刚刚执行完getter,与dep相互收集完依赖,所以该计算属性watcher内部包含当前计算属性watcher所用到的所有dep,那我们只需要将这些dep组件watcher进行依赖收集,就可以当这些dep对应的属性改变时,调用组件watcher进行组件的更新操作啦~

class Watcher {
    constructor () {...}
    depend () { // 循环执行内部所有dep的depend方法,使得这些dep与Dep.target这个watcher相互收集依赖
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }
}

你可能感兴趣的:(JS,vue源码,vue,javascript,前端,vue.js)