vue收集依赖

vue收集依赖机制

  • 通过Object.definedproperty来设置观察属性的setter和getter
  • 通过getter收集依赖,通过setter触发依赖更新

与收集依赖相关的三个类

  • Observer,观察对象,其变化时会通知观察者
  • Watcher,观察者,观察观察对象,依赖收集对象,观察对象变化时会被通知,同时调用其对象上的更新方法
  • Dep,专门用于管理依赖的对象,其上面有添加删除以及通知依赖的方法,并保存一个依赖队列

Observer

  • 以vue中的data为例,Object.definedproperty发生在new Vue => Vue => initState => initData => observe => new Observer
  • 简略源码如下:
class Observer {
    constructor (value) {
        this.value = value
        this.dep = new Dep()
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods)
            } else {
                copyAugment(value, arrayMethods, arrayKeys)
            }
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }

    walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }

    observeArray (items: Array) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
}
  • def函数即在实例化Observer时,将他绑定到入参value上的__ob__上面,这一步是为了后面可以直接通过__ob__访问到对应的
function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    });
}
  • 由于数组和对象不同,数组无法通过Object.definedproperty设置getter和setter,所以数组要通过别的方法,即重写原型方法的方式去拦截对数组改动的方法,实现响应,数组的处理放到后面讨论
  • walk方法,用于对对象中的每个属性进行处理,defineReactive方法中将会使用Object.definedproperty,将value变成响应式的
function defineReactive (obj, key, val, shallow) {
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    let childOb = !shallow && observe(val)
    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
        },
        set: function reactiveSetter (newVal) {
            const value = getter ? getter.call(obj) : val
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if (getter && !setter) return
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = !shallow && observe(newVal)
            dep.notify()
        }
    })
}
  • 如果当前key对应的属性是不可配置的,则退出
  • shallow用于判断是否将子属性也转换成响应式的,一般都是false
  • 在设置key对应属性的getter中,如果当前Dep类下面target属性有值,则调用实例化后的dep上的depend方法,他会把target的值保留下来,同时如果子属性也是对象,且已经被Observer化后,由于Observer时会在其实例上保留dep属性,故这里可以直接调用childOb.dep.depend(),这样一来假设当前key对应的属性是一个obj,其子属性也会同时被观察,即子属性发生变化时也会通知依赖更新;
  • 而如果当前key对应的值是一个数组,为了也能实现深度监听,调用了dependArray方法:
function dependArray (value: Array) {
    for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
            dependArray(e)
        }
    }
}
  • 这里dependArray会递归遍历每一个元素,由于之前已经observe了这个val,所以这里的元素理论上已经有__ob__属性,可以通过他调用dep的depend进行依赖;
  • 回到Observer中,在设置key对应属性的stter时,判断了一遍其新旧值是否不同,不同的时候才通知依赖更新,减少无谓更新;另外将新设的值用ovserve方法处理,这样假设新设的值是一个对象,其子属性也能被观察,触发依赖更新;最后调用dep的notify方法通知依赖更新;
  • 数组的依赖放在后面讨论
  • 另外,只有getter没有setter的时候收集依赖无效,见下面代码
const data = {}
let _a = 1
Object.defineProperty(data, 'getterProp', {
    enumerable: true,
    configurable: true,
    get: () => {
        return {
            a: _a
        }
    }
})
  • 由于data只有getter,其被defineReactive处理后,getter出来的永远是{a: _a},而setter中假设没有if (getter && !setter) return判断,则通过data.getterProp = { a: 2 }将会触发依赖更新,同时将{a: 2}通过observe处理,但由于setter中并不会改变_a的值,所以getter出来的依旧是{a: 1},即依赖更新实际并没有意义,同时新产生的{a: 2}虽然被观察了,但永远不会被依赖,所以加了仅有getter时跳出处理的逻辑判断

Dep

let uid = 0
class Dep {
    constructor () {
        this.id = uid++
        this.subs = []
    }
    
    addSub (sub) {
        this.subs.push(sub)
    }

    removeSub (sub) {
        remove(this.subs, sub)
    }

    depend () {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

    notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}
  • Dep类中保存着subs数组,这个subs数组其实就是watcher,观察者队列,后面分析watcher会说到;同时通过this.id标记当前Dep实例;
  • removeSub用于从数组中移除对应元素,实际通过array.splice进行操作;
  • Observer中调用的dep.depend方法实际调用的是Dep.target上面的addDep方法,并将当前实例的上下文作为参数传了进去,这里的Dep.target实际是Watcher实例,将在后面分析watcher时提及;
  • notify,就是用来通知所有收集到的依赖,通过遍历subs数组实现,这里也可以看出,subs中的每个元素,即watcher中,都有一个update方法;

Watcher

let uid = 0
class Watcher {
    constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
    ) {
        this.vm = vm
        if (isRenderWatcher) {
            vm._watcher = this
        }
        vm._watchers.push(this)
        if (options) {
            this.deep = !!options.deep
            this.user = !!options.user
            this.lazy = !!options.lazy
            this.sync = !!options.sync
            this.before = options.before
        } else {
            this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set() 
        
        // parse expression for getter
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
            this.getter = parsePath(expOrFn)
            if (!this.getter) {
                this.getter = noop
            }
        }
        this.value = this.lazy ? undefined : this.get()
    }
    
    // Evaluate the getter, and re-collect dependencies.
    get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
            value = this.getter.call(vm, vm)
        } catch (e) {
            throw e
        } finally {
            if (this.deep) {
                traverse(value)
            }
            popTarget()
            this.cleanupDeps()
        }
        return value
    }

    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)
            }
        }
    }

    cleanupDeps () {
        let i = this.deps.length
        while (i--) {
            const dep = this.deps[i]
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this)
            }
        }
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear()
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
    }

    update () {
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            this.run()
        } else {
            queueWatcher(this)
        }
    }

    run () {
        if (this.active) {
            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) ||
                this.deep
            ) {
                const oldValue = this.value
                this.value = value
                if (this.user) {
                    try {
                        this.cb.call(this.vm, value, oldValue)
                    } catch (e) {
                        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                    }
                } else {
                    this.cb.call(this.vm, value, oldValue)
                }
            }
        }
    }

    // Evaluate the value of the watcher.
    // This only gets called for lazy watchers.
    evaluate () {
        this.value = this.get()
        this.dirty = false
    }

    depend () {
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }

    teardown () {
        if (this.active) {
        // remove self from vm's watcher list
        // this is a somewhat expensive operation so we skip it
        // if the vm is being destroyed.
            if (!this.vm._isBeingDestroyed) {
                remove(this.vm._watchers, this)
            }
            let i = this.deps.length
            while (i--) {
                this.deps[i].removeSub(this)
            }
            this.active = false
        }
    }
}
  • watcher实例化时入参有:vm,组件实例;expOrFn,表达式,即watcher中的表达式,或computed里的getter,或渲染组件的渲染方法;cb,回调函数,在watcher被通知变化,执行update之后执行;options,各种设置;isRenderWatcher,是否是render watcher,即是否是模版产生的watcher,由于模版中也会有数据依赖,故模版也有对应的watcher,在mounteComponent阶段生成,同时isRenderWatcher为true的时候,会把watcher实例挂在vm的_watcher上;
  • options中的各种设置有:deep,深度监听,如watch中传入deep为true,则表达式中的对象属性或数组元素都会被监听,而不仅仅是监听其引用变化;sync,同步更新,监听到变化马上变化,否则采取batch update策略(batch update,通过事件循环实现,使用微或宏任务,将update方法放到事件循环末尾执行,之前所有的变化都通过一个队列收集起来);user,标记该watcher是否是用户自定义的,为true时会执行cb,系统自动标记了,无需用户手动设置;before,render watcher时会传进来,会在watcher的更新前调用;lazy,懒监听,系统自动添加在computed中,即只要其依赖的变量没发现变化的话,都不会重新计算该computed值,而是一直使用缓存的值;
  • 实例化的最后一步是触发get函数,如果是lazy的话,就不执行,value=undefined
  • get函数中一头一尾有两个方法pushTarget和popTarget,其是Dep文件里定义的两个方法:
const targetStack = []

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

function popTarget () {
    Dep.target = targetStack.pop()
}
  • 这里即把当前的watcher实例添加到了Dep.target中,其后执行this.getter.call的时候,会读取别的变量,触发之前observe的时候定义的get拦截器,这时依赖收集就被触发了;
  • 假设在pushTarget触发的时候,Dep.target已经保存了别的值,先把该值压入stack中,并重置Dep.target,在popTarget再把之前压入stack的watcher拿出来,重新赋给Dep.target,从而保证在同一时间,有且只有一个watcher在Dep.target上
  • 如果设置了deep,则会执行traverse方法:
const seenObjects = new Set()

function traverse (val) {
    _traverse(val, seenObjects)
    seenObjects.clear()
}

function _traverse (val, seen) {
    let i, keys
    const isA = Array.isArray(val)
    if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
        return
    }

    if (val.__ob__) {
        const depId = val.__ob__.dep.id
        if (seen.has(depId)) {
            return
        }
        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)
    }
}
  • traverse方法首先进行了一些判断,把无需深度监听的情况排除了,然后根据__ob__判断是否是响应式的数据,如果是则判断是否已经处理过依赖了,如果有就跳出逻辑
  • 接着判断是否是数组,数组的话,递归每个元素;不是数组的话,递归每个key,注意这里通过val[i]或val[keys[i]],实际上是调用了响应数据的get方法,也就是已经触发了依赖收集
  • 回到Watcher类里,get的最后一步是执行cleanupDeps,这是因为变化过程中,可能有些依赖已经不存在了,那就没必要再去观察这些变量,故用一个newDeps和newDepIds的队列记录最新的依赖集合,然后对比现有的dpes队列,进行一遍清洗;
  • dep.removeSub(this)将Observer里的依赖关系删除,this.deps = this.newDeps修改Watcher中的依赖关系
  • addDep方法,实际即Observer中定义的get方法中调用的dep.depend最终调用的方法,他会把watcher的实例push进入参(即Observer上的dep)队列中,同时也把observer的dep添加到自己的newdeps队列中,从而双方上面都有双方的实例引用,依赖关系从而建立;
  • depend方法,搜了下源码只有在初始化computed里面用到,相关代码如下:
function createComputedGetter (key) {
    return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate()
            }
            if (Dep.target) {
                watcher.depend()
            }
            return watcher.value
        }
    }
}
  • createComputedGetter会用于劫持computed属性的get方法,这里的watcher是computed的watcher,但Dep.target并不是computed的watcher,而是调用了computed属性的watcher,打个比方可能是渲染函数的watcher,如上所说,pushTarget会将watcher压入栈,computed压入栈的时候是调用get的时候,而由于computed属性是懒更新的,new watcher的时候并不会调用get方法,其get被调用是在evaluate方法里,那么按照上面代码逻辑,evaluate调用完毕或直接跳过了evaluate方法,这时候的Dep.target都不是computed的watcher,而是调用他的watcher,所以这里的逻辑是在computed依赖的Observer的dep上面添加调用computed的watcher,这样当依赖属性变化,被comouted依赖的Observer也会马上收到通知,触发notify方法
  • Dep的notify最终触发的是Watcher的update方法里面判断了是否懒更新,即lazy值,如果是懒更新,把其dirty标志置为true,意即,这个watcher已经被更新了,需要重新计算,但此时并不会马上计算,也不会用queueWatcher把他保存在待执行队列中;而如果设置了sync同步标志,则马上执行run方法,run方法实际就是调用了一遍get方法,重新获取新值,并在最后调用cb方法;而除此之外的情况都会采用queueWatcher将变化保存在队列中,待后续进行,这也是vue批量更新的原因
  • queueWatcher后续再提及,先看evaluate方法,之前有提到,evaluate发生在computed的get方法上,结合update方法来看,也就是computed的依赖发生变化时,调用了update方法,但computed本身并不会马上被更新,而是通过一个dirty标签表明computed的值需要被更新,然后最终的更新是发生在读取computed值的时候,通过判断dirty值,调用了evaluate方法,更新了值,并将dirty重新置为false
  • teardown用于解除当前watcher实例的所有依赖关系,首先将自己从vue实例上面删除,然后依次调用其上保存的dep队列中的removeSub,即从其观察的Observer上面dep队列中删除自己
  • 回到queueWatcher,由于更新需要消耗性能,且工程中更新通常是大规模且高频地发生,所以需要有批量更新的策略,queueWatche就是用来实现这个目的的,queueWatcher代码如下:
let waiting = false
function queueWatcher (watcher) {
    const id = watcher.id
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
            queue.push(watcher)
        } else {
            let i = queue.length - 1
            while (i > index && queue[i].id > watcher.id) {
                i--
            }
            queue.splice(i + 1, 0, watcher)
        }
        if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue)
        }
    }
}
  • queueWatcher先判断这一轮更新是否已经记录过当前的watcher,只有未记录过的才需要处理,处理过程首先记录标记当前watcher已经被添加到队列中,然后判断是否正在执行更新,flushing代表目前正在执行更新,如果不是正在执行更新,则将watcher推入queue队列;而如果已经正在执行更新,将watcher插入队列中;index是当前queue执行到的位置,从队列末尾开始向前取出队列中的watcher做对比,如果队列中的watcher ID比当前watcher ID大,则继续往前寻找,知道找到比队列ID大的位置,将当前watcher插入其后,而如果直到遍历到当前队列执行位置都没有找到这个值,则直接把当前watcher插入下一个位置,即处理完队列正在处理的watcher后,就会处理当前watcher
  • 这里可能会有疑问,为什么不直接把当前watcher插入下一个要处理的queue位置就好,而要遍历判断watcher.id来判断插入位置,这是因为watcher的生成是有先后顺序的,而被依赖的对象有可能是data中的一个值,也可能是一个视图,如果是后者,其本身也有watcher,也依赖其他可观察对象,而如果这个视图里有一个组件,并接收一些props,则必须保证视图的更新要早于组件,才能保证数据的准确,而watcher的生成顺序是被依赖的先于依赖的生成,所以要通过id的大小判断插入的位置
  • 最后执行flushSchedulerQueue前会通过waiting标志来确保一次排队只会触发一次flushSchedulerQueue
  • flushSchedulerQueue代码如下:
function flushSchedulerQueue () {
    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.
    queue.sort((a, b) => a.id - b.id)
    // do not cache length because more watchers might be pushed
    // as we run existing watchers
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        has[id] = null
        watcher.run()
    }
}

function resetSchedulerState () {
    index = queue.length = activatedChildren.length = 0
    has = {}
    waiting = flushing = false
}
  • 这里可以看到flushSchedulerQueue中对队列进行了排序,让watcher按id从小到大排列,所以queueWatcher中针对正在处理flushing的队列插入watcher的逻辑才会成立
  • 执行更新前,会调用before方法,对于render watcher,定义的时候,before方法里写的是调用勾子函数beforeupdate
  • flush方法会执行执行队列里每一个watcher的run方法,即重新执行了取值操作,render watcher的get是其render方法。从而更新了值或视图
  • 最后执行resetSchedulerState,将各种标志复位
  • flushSchedulerQueue是作为一个参数传给nexttick的,nexttick的作用就是把回调放到事件循环的末尾去做,从而实现batch update
let pending = false
const callbacks = []
function nextTick (cb, ctx) {
    let _resolve
    callbacks.push(() => {
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    if (!pending) {
        pending = true
        timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}
  • 可以看到nexttick主要是通过将cb推入callback队列,并调用timerFunc来处理的,timerFunc代码如下
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () => {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
    let counter = 1
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
    }
    isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
    timerFunc = () => {
        setTimeout(flushCallbacks, 0)
    }
}

function flushCallbacks () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
        copies[i]()
    }
}
  • 这一段主要是各种兼容性代码,简单地讲就是优先将callback队列的执行放到微任务队列中等待,不支持微任务时放到宏任务队列中等待,从而保证你的更新是发生在同步任务执行完成之后才执行

数组的依赖收集

  • 数组的依赖收集,主要是Observer实例化时做的处理
  • 实例化数组Observer的时候,调用了copyAugment(value, arrayMethods, arrayKeys)/protoAugment(value, arrayMethods)方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]

methodsToPatch.forEach(function (method) {
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
            break
            case 'splice':
                inserted = args.slice(2)
            break
        }
        if (inserted) ob.observeArray(inserted)
        ob.dep.notify()
        return result
    })
})

function protoAugment (target, src) {
    target.__proto__ = src
}

function copyAugment (target, src, keys) {
    for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}
  • 因为value是数组,这两个方法其实就是改写了数组的原型,原本数组实例的原型指向Array.prototype,现在指向了arrayMethods,且arrayMethods的原型指向Array.prototype
  • 同时在arrayMethods上,针对会对数组造成变化的方法,定义了拦截器,当使用这些方法的时候,获取当前实例上的__ob__属性,即Observer实例,调用他们的通知方法,触发更新;
  • 注意这里的inserted变量,是针对会对数组进行插值的方法,获取他们插入的变量,调用observeArray,将他们转化为可观察对象
  • observeArray不止在插值时调用,拦截了数组原型方法后也有调用,代码如下,只是简单地观察其中的每个元素
observeArray (items: Array) {
    for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
    }
}
  • 至此,array的Observer就定义好了,其具备了一项功能,那就是当使用事先拦截的那几个会改变数组的方法时,如splice,会触发其对应的Observer上的通知方法,通知依赖更新
  • 触发功能有了,触发的对象是怎么收集的呢,因为无法通过defineProperty来拦截get方法,从而触发Observer的dep.depend,需要另外的机制来处理依赖的收集,这个方法就是defineReactive上面的dependArray方法,因为可监听的数组是定义在data上的,而data是一个对象,会在init阶段被observe处理,处理过程中就会调用到defineReactive方法,在其拦截get方法时,get方法中判断该属性是数组的话,就会触发dependArray方法,里面递归遍历每个元素,如果是响应式的,就调用dep.depend方法,这里就触发了依赖的收集
  • this.array或this.array[i]方法都会触发array对应的get方法,然后触发dependArray,最后触发依赖收集

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