响应式系统(五)

前言

上文还漏了一些重要的诸如异步更新、computed等细节,本文补齐

正文

异步更新

上文讲到Watcher里的this.sync是来控制同步与异步触发依赖更新的。同步的话缺点很明显,试想一下如下例子

new Vue({
    data: {
        a: 1,
        b: 2
    },
    template: `
{{a}}{{b}}
`, methods: { change() { this.a = 2 this.b = 3 } } }).$mount('#app')

同时改动了this.a、this.b,因为这俩属性都收集了renderWatcher,若是同步的话那么就会执行俩遍渲染函数,这是不明智的,所以若是异步的话可以将其更新回调放入异步更新队列,就可以一次遍历触发,现在我们看看异步的逻辑,即

update() {
    if (this.computed) {
       // ...
    } else if (this.sync) {
        // ...
    } else {
        queueWatcher(this)
    }
}

queueWatcher方法在scheduler .js里,具体看下文

computed

首先看看initComputed,从此我们可得知initComputed就是劫持computed,将其转化为响应式对象

计算属性其实就是惰性求值watcher,它观测get里的响应式属性(若是如Date.now()之类非响应式是不会触发变化的),一旦其变化这个get就会触发(get作为WatcherexpOrFn),如此一来该计算属性的值也就重新求值了

和普通watcher区别就是它不会立即求值只有在被引用触发其响应式属性get才会求值,而普通watcher一旦创建就会求值

new Vue({
    data: {
        a: 1
    },
    computed: {
        b: {
            get() {
                return this.a + 1
            },
            set(val) {
                this.a = val - 1
            }
        }
    },
    template: `
{{a}}{{b}}
`, methods: { change() { this.a = 2 } } }).$mount('#app')

以此为例,转化结果如下

Object.defineProperty(target, key, {
    get: function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            watcher.depend()
            return watcher.evaluate()
        }
    },
    set: function set(val) {
        this.a = val - 1
    }
})

从这可见只有get被触发才会开始其依赖收集,这和普通watcher创建就求值从而开始依赖收集是不一样的

依赖收集

如此例所示,b是依赖a的,所以a的dep得收集到bwatcher(这样子a变化可以通知b重新求值),模板渲染依赖b,所以b的dep得收集到renderWatcher

也就是当计算属性b被读取(在此是模板引用{{ b }}),该get会被执行
首先其定义了watcher变量来存储this._computedWatchers[key],通过前文我们知道在initComputed里遍历vm.$options.computed给每个都new Watcher,所以该watcher就是计算属性b的观察者对象

watcher <==> new Watcher(vm, b.get, noop, { computed: true })

若该对象存在的话就会执行该对象的depend、evalute
首先我们看Watcher.depend

depend() {
    if (this.dep && Dep.target) {
        this.dep.depend()
    }
}

这个很简单,就是判断下this.dep && Dep.target,存在的话就调用this.dep.depend()。这个this.dep就是计算属性的dep,它初始化在Watcher constructor

constructor() {
    if (this.computed) {
        this.value = undefined
        this.dep = new Dep()
    } else {
        this.value = this.get()
    }
}

我们可见非计算属性watcher是直接执行this.get来触发响应式属性的get从而收集依赖,计算属性watcher就是初始化了this.dep也就是该响应式计算属性对应的dep

前者直接开始求值,后者只有在访问到的时候才求值

回到this.dep.depend()方法,我们看上诉提到的a的depb的dep如何收集依赖

  • 我们知道这个就是收集依赖,那么我们得知道Dep.target是什么,这个其实是renderWatcher,因为计算属性brenderWatcher依赖,也就是这b.getrender触发访问的,这就完成了b的dep收集

watcher.depend()完了之后还有return watcher.evaluate()

evaluate() {
    if (this.dirty) {
        this.value = this.get()
        this.dirty = false
    }
    return this.value
}

首先判断dirty,我们之前就有说过true为未求值、false为已求值,这个就是computed缓存来源
未求值的话就是执行this.get()求值,其实这相当于执行b.get。注意这里Dep.target已经变成了计算属性bwatcher

get() {
    return this.a + 1
}
  • 关键到了,这里访问了this.a就触发了a.get,这样子就会导致a的dep收集到计算属性bwatcher

如此我们就完成了依赖收集

依赖触发

我们现在触发了例子里的change函数,也就是修改this.a的值(其实修改this.b也一样内在还是修改this.a
我们知道

a.dep.subs <===> [renderWatcher, bWatcher]

那么修改a就会触发这俩个watcher.update

update() {
    /* istanbul ignore else */
    if (this.computed) {
        // 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.
        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)
    }
}

这里除了computed我们都有讲过,所以我们这里讲computed

this.dep <==> b.dep
this.dep.subs.length <==> b.dep.subs.length <==> [renderWatcher].length === 1

首先判断下这个this.dep.subs.length === 0,我们知道dirty === true是未求值,所以在该属性未被依赖的时候(未被引用)将dirty置为true(其实就是重置默认值),这样子当被依赖的时候(被引用)evaluate就会重新求值(dirty === false的话evaluate不会重新求值)
此例来看this.dep.subs.length === 1,所以走else分支,调用getAndInvoke方法重新求值设置新值之后执行this.dep.notify(),通知订阅了b变化的更新(也就是通知renderWatcher更新)

scheduler.js

这个文件里存储的都是异步执行更新的相关方法

queueWatcher

就是个watcher入队的方法

const queue: Array = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher(watcher: Watcher) {
    const id = watcher.id
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
            queue.push(watcher)
        } else {
            // 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)
        }
        if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue)
        }
    }
}

就像这个注释所言该方法就是push一个watcher到观察者队列,总体来看就是入队列,然后调用nextTick在下一个Tick执行flushSchedulerQueue,也就是在下一个Tick之前会入队完毕,接下来我我们看如何入队的
首先就是获取这个入队的watcher.id,我们定义了has这个对象用于纪录入队的watcher。先判断下这个watcher是否已经入队,已入队的话就啥也不干,未入队的话就给此watcher标记在has
然后就是判断下这个flushing。它是用于判断是否执行更新中,也就是更新队列是否正在被执行,默认是false

  • 若是未执行更新中自然就是简单入队即可
  • 若是在执行更新中却有观察者要入队那么就得考虑好这个要入队的watcher插在哪,也就是得插入到正在执行的watcher后面,假设已经有俩[{id: 1}, {id: 2}],假设已经循环到{id: 1}这个watcher,那么这时候index还是0,我们要插入的位置也是{id: 1}后面
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
    i--
}

很明显这个寻找插入点是倒序查找,这里判断queue[i].id > watcher.id是因为flushSchedulerQueue里对queue做了针对id的排序
最后就是判断下waiting,这个就是个防止多次触发nextTick(flushSchedulerQueue)的一个标志,算是个小技巧

这个方法包含俩部分:

  • 观察者入队
  • 下一个tick执行更新队列
flushSchedulerQueue
export const MAX_UPDATE_COUNT = 100
const activatedChildren: Array = []
let circular: { [key: number]: number } = {}
function flushSchedulerQueue() {
    flushing = true
    let watcher, id

    queue.sort((a, b) => a.id - b.id)

    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        has[id] = null
        watcher.run()
        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
            }
        }
    }

    const activatedQueue = activatedChildren.slice()
    const updatedQueue = queue.slice()
    resetSchedulerState()

    callActivatedHooks(activatedQueue)
    callUpdatedHooks(updatedQueue)

    if (devtools && config.devtools) {
        devtools.emit('flush')
    }
}

这个方法就是具体的执行更新队列的所在,首先就是将flushing置为true,然后就是将queue队列按照watcher.id从小到大排列这是有门道的主要是以下三点:

  • 组件更新是父到子的,先创建父然后是子,所以需要父在前
  • userWatchrenderWatch之前,因为userWatch定义的更早
  • 若是一个组件在父组件的watcher执行期间被销毁,那么子组件的watcher自然也得跳过,所以父组件的先执行
for (index = 0; index < queue.length; index++) {
    // ...
}

这里就是存储执行更新时当前watcher的索引index的地方,这里有个点需要注意的是不存储queue.length,因为在执行更新中queue可能会变化

watcher = queue[index]
if (watcher.before) {
    watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()

这里就是before钩子所在

new Watcher(vm, updateComponent, noop, {
    before() {
        if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */ )

就像这个renderWatcher就有传入before

这就是beforeUpdate所在

然后就是给当前watcher移出has这个记录表,表示这个idwatcher已经处理了,可以继续入队,因为执行更新中也可能有watcher入队,然后执行watcher.run()

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

这段很重要,就是在开发环境下对无限循环的一个提示。因为watcher.runwatcher回调)可能会导致has[id]有值,如下所示:

new Vue({
    data: {
        a: 1
    },
    watch: {
        'a': function aCallback(nVal, oVal) {
            this.a = Math.random()
        }
    },
    template: `
{{a}}
`, methods: { change() { this.a = 2 } } }).$mount('#app')

在执行到flushSchedulerQueue时,queue会有俩个watchera:userWatcherrenderWatcher
首先是userWatcher,在watcher.run()(也就是aCallback这个回调函数)之前has[id] = null,然后执行了这个aCallback又给this.a赋值,这样子就是在执行更新中watcher入队(set() -> dep.notify() -> watcher.update() -> queueWatcher(this)
如此一来执行到queue下一个项其实还是当前这个userWatcher,就没完没了了
所以这里定了个规矩,就是这同一个watcher执行了MAX_UPDATE_COUNT也就是100次那说明这个有问题,可能就是无限循环了。circular就是这么个标识变量

const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()

这里我们先看updatedQueue,它是queue的浅拷贝对象。这是因为紧随其后调用了resetSchedulerState,若不浅拷贝的话queue就被置空了,这也杜绝了queue被影响

// 重置scheduler状态
function resetSchedulerState() {
    index = queue.length = activatedChildren.length = 0
    has = {}
    // 若是开发环境,那么就每轮更新执行之后置空这个无限循环检测标志
    // 这是因为下面检测也是开发环境下检测的
    // 也就是默认生存环境下不会出现这种糟糕的代码
    if (process.env.NODE_ENV !== 'production') {
        circular = {}
    }
    waiting = flushing = false
}

他就是重置scheduler里这么些方法所用到的标识变量

这里只在非生产环境重置了circular,这就代表生存环境下不会出现这种糟糕的代码

callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)

然后就是callUpdatedHooks

// 执行updated钩子
function callUpdatedHooks(queue) {
    let i = queue.length
    while (i--) {
        const watcher = queue[i]
        const vm = watcher.vm
        // 要是当前这个watcher是渲染watcher,而且已经挂载了,那么触发updated钩子
        if (vm._watcher === watcher && vm._isMounted) {
            callHook(vm, 'updated')
        }
    }
}

这里就是统一调用update钩子,这个和before非统一调用不一样,这里通过watcher获取到vm也就是当前的实例对象,这样子就可以取到vm. _watcher也就是renderWatcher,也可以取到vm._isMounted。有了这俩个条件就可以调用生命周期update

这就是updated所在

if (devtools && config.devtools) {
    devtools.emit('flush')
}

这里是开发者工具的事件传递flush

queueActivatedComponent

这个其实是入队激活的组件,类似queueWatcher入队watcher

export function queueActivatedComponent(vm: Component) {
    // setting _inactive to false here so that a render function can
    // rely on checking whether it's in an inactive tree (e.g. router-view)
    vm._inactive = false
    activatedChildren.push(vm)
}

这里就是将入队的实例的激活状态(_inactive)置为激活,然后入队到activatedChildren以待后用

function callActivatedHooks(queue) {
    for (let i = 0; i < queue.length; i++) {
        queue[i]._inactive = true
        activateChildComponent(queue[i], true /* true */)
    }
}

这个其实就是统一遍历调用actibated钩子,给每一项实例的激活状态(_inactive)置为未激活(从未激活到激活)
注意这里activateChildComponent传入的第二参数是true,在讲到keep-alive详解

你可能感兴趣的:(响应式系统(五))