setter
当我们修改数据之后,会触发数据的setter
方法,然后更新视图,从setter
下手:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// ...
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
}
// 如果shallow是false,将newVal设置为响应式
childOb = !shallow && observe(newVal)
// 通知订阅者,分析notify
dep.notify()
}
})
可见派发更新离不开Dep
上的notify
函数。在这里,它会遍历所有的 subs,也就是 Watcher 的实例数组,把在依赖过程中订阅的的所有观察者,也就是 watcher
,都触发它们的 update
过程
notify
class Dep {
// ...
notify () {
// 遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 分析update
subs[i].update()
}
}
}
watcher有很多中类型,有computed watcher
、sync watcher
、用户自定义watcher(即watch)、deep watcher 和_render时的render watcher。在update
时,需要考虑computed watcher
、sync watcher
。我们这次是以render watcher
为例子,所以直接分析queueWatcher
:
class Watcher {
// ...
update () {
// 先不讨论computed和sync的情况
if (this.computed) {
// ...
} else if (this.sync) {
// ...
} else {
// 分析queueWatcher
queueWatcher(this)
}
}
}
const queue: Array<Watcher> = []
let has: {
[key: number]: ?true } = {
}
let waiting = false
let flushing = false
/**
* 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
// 首先用 has 对象保证同一个 Watcher 只添加一次
if (has[id] == null) {
has[id] = true
// if else中都是新增watcher,判断条件是flushing
// render watcher一般下会走queue.push(watcher)
// 而在后期更新执行flushSchedulerQueue中会执行到watcher.run函数,
// 会重新新增watcher,而触发queueWatcher函数,此时flushing是ture.
// 这时候就会执行else语句,
// 找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。把 watcher 按照 id的插入到队列中
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)
}
// 最后通过 waiting 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次
if (!waiting) {
waiting = true
// 分析flushSchedulerQueue
nextTick(flushSchedulerQueue)
}
}
}
flushSchedulerQueue
在flushSchedulerQueue
中主要执行了三个步骤:
- 队列排序;
- 队列遍历;
- 状态恢复。
let flushing = false
let index = 0
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
flushing = true
let watcher, id
// 对队列做了从小到大的排序
// 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子
// 用户的自定义 watcher 要优先于渲染 watcher (_render时执行的new Watcher)执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
// 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行
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
// 在这里的时候由于执行了run函数,可能会再次添加新的watcher
// 所以会再次调用queueWatcher函数,虽然has[id] === null;
// 但是此时flushing是true,所以会执行else语句
// 从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。
// 把 watcher 按照 id的插入到队列中,因此 queue 的长度发生了变化。
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()
// 就是把这些控制流程状态的一些变量恢复到初始值,如waiting = flushing
// 把 watcher 队列清空
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
我们啃一下run
函数:
class Watcher {
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
getAndInvoke (cb: Function) {
// 先通过 this.get() 得到它当前的值
const value = this.get()
if (
// 新旧值不等
value !== this.value ||
// 新值是对象类型
isObject(value) ||
// 或者deep是true
this.deep
) {
// 获得旧的值
const oldValue = this.value
// 新的值
this.value = value
this.dirty = false
// 如果时自定义添加的watch
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)
}
}
}
}
这个过程又利用了队列做了进一步优化,在 nextTick
后执行所有 watcher
的 run
,最后执行它们的回调函数
nextTick
在学习nextTick
之前要学习一下JS的运行机制。
JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。遍历每个macro task时,执行macro task并遍历执行macro task中的 micro task。
在浏览器环境中,常见的 macro task 有
- setTimeout、
- MessageChannel、
- postMessage、
- setImmediate;
常见的 micro task 有
- MutationObsever
- Promise.then。
import {
noop } from 'shared/util'
import {
handleError } from './error'
import {
isIOS, isNative } from './env'
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let microTimerFunc // 微观函数
let macroTimerFunc // 宏观函数
let useMacroTask = false // 是否是宏观函数
// 优先检测是否支持原生 setImmediate
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
// 检测是否支持原生的 MessageChannel
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
// 如果也不支持的话就会降级为 setTimeout 0
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 检测浏览器是否原生支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
} else {
// 不支持的话直接指向 macro task 的实现
microTimerFunc = macroTimerFunc
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a (macro) task instead of a microtask.
* next-tick.js 还对外暴露了 withMacroTask 函数,
* 它是对函数做一层包装,确保函数执行过程中对数据任意的修改,
* 触发变化执行 nextTick 的时候强制走 macroTimerFunc。
* 比如对于一些 DOM 交互事件,如 v-on 绑定的事件回调函数的处理,会强制走 macro task。
*/
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// cb 压入 callbacks 数组
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,即执行flushCallbacks
// flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
next-tick.js
申明了 microTimerFunc
和 macroTimerFunc
2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生 setImmediate
,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel
,如果也不支持的话就会降级为 setTimeout 0
;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。
由此可见,我们在setter
的最后是用了nextTick
方法,说明视图的更新是一个异步的过程。
检测变化的注意事项
不是每次更改数据,都能触发setter
方法,引起视图变化的。
修改或者新增对象的属性
var vm = new Vue({
data:{
obj: {
a:1,
},
test1:1
}
})
// vm.test2是非响应的
vm.test2 = 1
// vm.obj.b 是非响应的
vm.obj.b = 2
// vm.obj.b 是响应式的
Vue.$set(this.obj,'b', 2);
上面的test2
是根级别的响应式属性,obj.b
是嵌套对象。
Vue无法检测到对象属性的添加和删除。对于已经创建的实例,Vue 不能动态添加根级别的响应式属性,可以使用 Vue.set(object, key, value)
方法向嵌套对象添加响应式属性。
分析Vue中的set
方法:
**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
* 在对象上设置属性。添加新属性并在属性不存在时触发更改通知
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${
(target: any)}`)
}
// 如果 target 是数组且 key 是一个合法的下标,则之前通过 splice 去添加进数组然后返回
// 这里的 splice 其实已经不仅仅是原生数组的 splice 了
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 接着又判断 key 已经存在于 target 中,则直接赋值返回,
// 则直接赋值返回,因为这样的变化是可以观测到了
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 接着再获取到 target.__ob__ 并赋值给 ob
// 它是在 Observer 的构造函数执行的时候初始化的,表示 Observer 的一个实例
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 如果它不存在,则说明 target 不是一个响应式的对象,则直接赋值并返回
if (!ob) {
target[key] = val
return val
}
// 把新添加的属性变成响应式对象
defineReactive(ob.value, key, val)
// 手动的触发依赖通知,更新视图
ob.dep.notify()
return val
}
对数组的操作
// 非响应的
vm.items[indexOfItem] = newValue;
// 响应的
Vue.set(vm.items, indexOfItem, newValue);
// 非响应的
vm.items.length = newLength
// 响应的
vm.items.splice(newLength)
这里的spilce
不是原生数组的splice
方法,在我们将数据变为响应式数据(Observe)时,会改变数据为数组类型的数组API:
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// protoAugment的作用是将当前数组的_proto__指向Vue封装好的array api;
// 这些封装好的array api 都会继承Array,其中push、unshift和splice方法获取到插入的值后
// 会把新增的值变为响应式,并调用obj.dep.notify通知视图更新
// copyAugment是用户自己对array的api的封装
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// ...
}
}
}
在vue中封装的数组API有:
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
总结
在vue中不是所有改变数据的操作都可以改变视图,主要要注意对对象和数组的修改:
- 对于对象而言,对于已经创建的实例,Vue 不能动态添加根级别的响应式属性,可以使用
Vue.set(object, key, value)
方法向嵌套对象添加响应式属性。 - 对于数组而言,Vue封装了
push、pop、shift、unshift、splice、sort、reverse
方法,其中push、unshift、splice
会对新增的数据转化为响应式数据,还有就是这些封装好的api会触发notify
更新视图。
介绍多种watcher
- render watcher
- computer watcher
- user watcher
- deep watcher
- sync watcher
render watcher
渲染watcher
在之前已经介绍过了,就略过。
computer watcher
在_initState
时对computed
的处理:
const computedWatcherOptions = {
computed: true }
function initComputed (vm: Component, computed: Object) {
// 函数首先创建 vm._computedWatchers 为一个空对象
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${
key}".`,
vm
)
}
if (!isSSR) {
// 为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,
// 它是一个 computed watcher,所以new Watcher的update是,iscomputed是true
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef)
if (!(key in vm)) {
// defineComputed中利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,
// setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${
key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${
key}" is already defined as a prop.`, vm)
}
}
}
}
这时候就有computed watcher
,当我们改变数据时,依旧换执行setter
方法,通知Dep
执行notify
方法,让Wacther
去update
,在_update
中有这么一段代码:
if (this.computed) {
this.value = undefined
this.dep = new Dep()
}
可以发现computed watcher
并不会立刻求值。我们来走一遍流程,假设你使用了computed
:
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
首先在initState
时,会调用initComputed
,创建computed watcher
,和绑定setter
和getter
事件;当_render
时,会访问到fullName
,就触发到了getter
,会调用createComputedGetter
会调用watcher.evaluate()
和watcher.depend()
函数。在watcher.evaluate()
中执行computed
中的方法(就是执行了 return this.firstName + ' ' + this.lastName
);而watcher.depend()
j就会调用Dep
的depend
将computed watcher
添加在render watcher
中,即render watcher
订阅了computed watcher
。
还需要注意的是,由于 this.firstName
和 this.lastName
都是响应式对象,这里会触发它们的 getter,它们会把自身持有的 dep
添加到当前正在计算的 watcher
中,这个时候 Dep.target
就是这个 computed watcher
。
当computed
中依赖的值(firstName
和lastName
)发生改变时,会触发 watcher.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.
// computed watcher有两种模式: lazy和activated
// 默认是lazy模式
// 只有在以下情况下才会变为activated
// 只有当至少有一个订阅者依赖于它时才会被激活,这通常是另一个计算属性或组件的render函数。
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.
// 在lazy模式下,直到必要时我们才执行计算,因此我们将观察者标记为dirty。
// 当访问computed属性时,实际计算在this.evaluate()中即时执行。
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) {
// ...
} else {
// ...
}
由于我们的computed watcher
被render watcher
订阅了,所以我们走this.getAndInvoke
, 在这个函数中,函数会重新计算,然后对比新旧值,如果变化了就执行回调函数,即执行this.dep.notify
,触发了render watcher
重新渲染,由此可见当computed
依赖的值发生改变时,会先判断计算后的值是否发生改变才会触发render watcher
重新渲染,而不是依赖的值改变就触发。
user watcher
在initState
对user watcher
的初始化发生在computed watcher
的初始化之后。在initWatch
中,会对handler
调用 createWatcher
方法;在 createWatcher
方法中又会调用vm.$watch
;在vm.$watch
中去实例化Watcher
,由于是vm.$watch
调用的,所以是user watcher
。通过实例化 watcher
的方式,一旦我们 watch
的数据发送变化,它最终会执行 watcher
的 run
方法。在vm.$watch
中还会判断immediate
的值,看看是否要立即调用一次handler
。
deep watcher
在user watcher
中将deep
设置为true,就是deep watcher
。
deep watcher
在get
时会调用traverse
对一个对象做深层递归遍历;因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher
,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id
记录到 seenObjects
,避免以后重复访问。那么在执行了 traverse
后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher
的回调函数了。
sync watcher
在user watcher
中将sync
设置为true,就是deep watcher
。
<c-input :value.sync="value" />
原先的watcher
在setter
时,即时响应式数据发生了变化,在 nextTick
后才会真正执行 watcher
的回调函数,所以更新视图是一个异步的过程。但sync watcher
不同的地方在于setter
过程执行update
时,直接调用run
,将异步更新转化为同步更新。
update () {
if (this.computed) {
// ...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
总结
- data和props会调用
getter
中的Dep.depend
将Dep.target
指向的Wacher
依赖收集到Dep
中;发生改变时会调用setter
方法,调用deep.notify
通知Dep.target
指向的Wacher
进行update
,同理,其他watcher
也是; computed watcher
的更新取决于依赖的值改变时计算后得到的结果是否改变,所以这也是为什么computed
比watche
更适合监听计算属性的原因;user watcher
中有deep
和immediate
;- 由于响应式是异步的,主要依靠了
nextTick
,所以可以使用sync
变为同步更新。