老席老席杂货铺
Picture of github

Vue 2的异步更新

上一节 VUE2.0 - 数据双向绑定 读了vue基本的数据双向绑定原理代码,这节看看数据变化后,异步更新dom的逻辑

先看看数据变化时,更新数据的代码入口

vue/src/observer/index.js

/**
 * Define a reactive property on an Object.
 *
 * @param {Object} obj
 * @param {String} key
 * @param {*} val
 */

export function defineReactive(obj, key, val) {
    var dep = new Dep()

    var property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get
    var setter = property && property.set

    var childOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            var value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                }
                if (isArray(value)) {
                    for (var e, i = 0, l = value.length; i < l; i++) {
                        e = value[i]
                        e && e.__ob__ && e.__ob__.dep.depend()
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            var value = getter ? getter.call(obj) : val
            // value值没有变化时,则return
            if (newVal === value) {
                return
            }
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            // 递归的对当前的newVal做监听拦截,以做到value为object对象的情况下,
            // 直接替换对象仍然可以监听到数据变化
            childOb = observe(newVal)
            // dep通知所有的watcher更新(这就是今天要读的逻辑的代码入口)
            dep.notify()
        }
    })
}

接下来看看**dep.notify()**的逻辑

vue/src/observer/dep.js

Dep.prototype.notify = function() {
    // stablize the subscriber list first
    var subs = this.subs.slice()
    for (var i = 0, l = subs.length; i < l; i++) {
        // 执行所有的watcher.update函数,完成监听者的更新
        subs[i].update()
    }
}

看看**watcher.update()**的逻辑

vue/src/observer/watcher.js

/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 *
 * @param {Boolean} shallow
 */

Watcher.prototype.update = function(shallow) {
    if (this.lazy) {
        // 如果是lazy的话,只标记为dirty,不做任何操作
        this.dirty = true
    } else if (this.sync || !config.async) {
        // 如果是sync的话,直接run,也就是直接执行watcher的callback进行dom更新
        this.run()
    } else {
        // if queued, only overwrite shallow with non-shallow,
        // but not the other way around.
        this.shallow = this.queued ?
            shallow ?
            this.shallow :
            false :
            !!shallow
        this.queued = true
        // record before-push error stack in debug mode
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.debug) {
            this.prevError = new Error('[vue] async stack trace')
        }
        pushWatcher(this)
    }
}

从上面代码中可以看到,默认行为是pushWatcher(this),看看它里面做了什么事情

vue/src/observer/batcher.js

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 *
 * @param {Watcher} watcher
 *   properties:
 *   - {Number} id
 *   - {Function} run
 */

export function pushWatcher(watcher) {
    var id = watcher.id
    if (has[id] == null) {
        if (internalQueueDepleted && !watcher.user) {
            // an internal watcher triggered by a user watcher...
            // let's run it immediately after current user watcher is done.
            userQueue.splice(queueIndex + 1, 0, watcher)
        } else {
            // push watcher into appropriate queue
            var q = watcher.user ?
                userQueue :
                queue
            has[id] = q.length
            q.push(watcher)
            // queue the flush
            if (!waiting) {
                waiting = true
                nextTick(flushBatcherQueue)
            }
        }
    }
}

上面代码的注释已经解释很清楚了,看看nextTick的逻辑

vue/src/util/env.js

/**
 * Defer a task to execute it asynchronously. Ideally this
 * should be executed as a microtask, so we leverage
 * MutationObserver if it's available, and fallback to
 * setTimeout(0).
 *
 * @param {Function} cb
 * @param {Object} ctx
 */

export const nextTick = (function() {
    var callbacks = []
    var pending = false
    var timerFunc

    function nextTickHandler() {
        pending = false
        var copies = callbacks.slice(0)
        callbacks = []
        for (var i = 0; i < copies.length; i++) {
            copies[i]()
        }
    }

    // 如果当前环境支持Mutatuion Observer,则通过Mutation Oberver进行异步更新,
    // 方式是创建一个text节点,通过更新text节点的内容,出发Mutation Oberver的回掉函数,
    // 否则调用setImmediate/setTimeout做到异步更新
    if (typeof MutationObserver !== 'undefined') {
        var counter = 1
        var observer = new MutationObserver(nextTickHandler)
        var textNode = document.createTextNode(counter)
        observer.observe(textNode, {
            characterData: true
        })
        timerFunc = function() {
            counter = (counter + 1) % 2
            textNode.data = counter
        }
    } else {
        // webpack attempts to inject a shim for setImmediate
        // if it is used as a global, so we have to work around that to
        // avoid bundling unnecessary code.
        const context = inBrowser ?
            window :
            typeof global !== 'undefined' ? global : {}
        timerFunc = context.setImmediate || setTimeout
    }
    return function(cb, ctx) {
        var func = ctx ?
            function() { cb.call(ctx) } :
            cb
        callbacks.push(func)
        if (pending) return
        pending = true
        timerFunc(nextTickHandler, 0)
    }
})()

以上就是异步更新的主要逻辑。可以看出nextTick通过闭包的方式,收集当前这个frame中所有的watcher的callback,在下一个frame中执行所有的callback,达到统一最少DOM更新次数的目的