前言
通过这篇文章可以了解如下内容
- 计算属性响应原理
- 侦听器响应原理
- 计算属性和侦听器的区别
Computed
初始化过程
Vue 的计算属性在两个地方都有初始化过程,一个是在initState
中,一个是在Vue.extend
中
在initState
中
export function initState (vm: Component) {
// ...
const opts = vm.$options
// ...
if (opts.computed) initComputed(vm, opts.computed)
// ...
}
如果vm.$options
中有computed
,就会调用initComputed
// computed Watcher 的 lazy 为 true
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// 创建 vm._computedWatchers 对象
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
// 遍历 computed 中的 属性
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) {
// 为 computed 属性创建 Watcher
// 创建 computedWatcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 通过 extend 方法创建组件函数(Sub)的时候,已经将 computed 属性挂载到了 Sub 的 prototype 上
// 在这里仅仅是定义实例化时定义的计算属性。比如 根组件的计算属性
// 并保证 computed 的属性名和 data、props 的属性名没有重复
if (!(key in vm)) {
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)
}
}
}
}
initComputed
会为每个计算属性创建一个 Computed Watcher
,这里要注意的点是Computed Watcher
的options.lazy
为true
,并将计算属性赋值给Watcher
实例的getter
属性。而创建的Computed Watcher
会添加到vm._computedWatchers
里面;然后执行defineComputed
添加响应
看下 Computed Watcher
和 Render Watcher
的区别
// Watcher类内部代码
this.lazy = !!options.lazy
// ...
this.dirty = this.lazy
// ...
this.value = this.lazy ? undefined : this.get()
相对于 Render Watcher
,Computed Watcher
的lazy
为true
,并且dirty
也为true
;因为 lazy
为true
,所以在创建Computed Watcher
过程中并不会执行this.get()
方法;也就不会获取计算属性的返回值。
而 在创建Render Watcher
过程中会执行this.get()
,从而执行组件的render
函数。也就是说计算属性的返回值不是在创建Computed Watcher
时获取的。
在Vue.extend
中
创建子组件的Vue实例的构造函数时,会提前处理props
和computed
,👉 Vue 源码(一)如何创建VNode👈
if (Sub.options.computed) {
initComputed(Sub)
}
Vue.extend
会执行initComputed
函数
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
// 将组件的计算属性挂载到 组件构造函数函数的原型上
// 作用:当实例化时,就可以通过 this.key 的方式访问了
defineComputed(Comp.prototype, key, computed[key])
}
}
initComputed
函数会对所有计算属性执行defineComputed
方法。
defineComputed
定义在src/core/instance/state.js
中
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 如果不是 ssr 则为 true
const shouldCache = !isServerRendering()
// 设置 取描述符
// userDef 可能是一个函数,也可能是一个有 getter 和 setter 属性的对象
if (typeof userDef === 'function') {
// 设置取描述符
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
// 设置取描述符
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
// 设置存描述符
sharedPropertyDefinition.set = userDef.set || noop
}
// 如果 计算属性没有设置 setter 方法,则对计算属性赋值时,报错(计算属性 key 没有分配 setter 方法)
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 添加拦截,让开发者可以通过 this.key(vm.key) 的方式访问
Object.defineProperty(target, key, sharedPropertyDefinition)
}
defineComputed
方法通过Object.defineProperty
将所有计算属性代理到vm
/ Sub.prototype
上,并将createComputedGetter
函数的返回值设置成 取描述符;
将计算属性的set
方法设置成 存描述符
接着来看 createComputedGetter
function createComputedGetter (key) {
return function computedGetter () {}
}
createComputedGetter
函数的内部逻辑一会再看,现在就知道它返回一个函数就行,并且这个函数的执行时机是 获取该计算属性时触发
小结
组件computed
的初始化
对于组件computed
的初始化,就是在创建组件构造函数时,通过Object.defineProperty
方法将组件中所有计算属性添加到组件构造函数的原型对象上,并设置存取描述符。
当创建组件实例时,为每个计算属性创建一个Computed Watcher
,并将计算属性复制给Watcher
实例的getter
属性;并且开发环境下会判断computed
中的key
和data
、props
中的key
是否重复。
根实例computed
的初始化
对于根实例computed
的初始化,就比较简单了,就是获取计算属性,并给computed
的每个key
创建一个Computed Watcher
,通过Object.defineProperty
方法将所有计算属性挂载到组件实例上,并设置存取描述符。
响应原理
依赖收集
组件执行render
函数时,如果使用了某个计算属性,会触发该计算属性的getter
,这个方法就是上面createComputedGetter
中的返回值
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 只做一次依赖收集
if (watcher.dirty) {
// 执行 定义的计算属性函数
watcher.evaluate()
}
if (Dep.target) {
// 将render watcher 添加到 依赖属性的 dep 中,当依赖属性修改后,通过 render watcher 的get方法去触发组件更新
watcher.depend()
}
return watcher.value
}
}
}
首先会根据key
获取对应计算属性的Computed Watcher
,因为在初始化过程中,watcher.dirty
是true
,所以会执行 watcher.evaluate()
方法
evaluate () {
this.value = this.get()
this.dirty = false
}
evaluate
方法会执行this.get()
方法,获取计算属性的返回值,并将当前Watcher
的 dirty
置为 false
,从而防止多次执行this.get()
方法。
get
方法在响应式原理
一节中看过
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
// ...
popTarget()
this.cleanupDeps()
}
return value
}
首先将Computed Watcher
入栈,执行this.getter
也就是计算属性的属性值并获取结果value
,然后出栈,将依赖属性的dep
添加到depIds
和deps
中,并将结果返回。
在执行this.getter
过程中,会获取计算属性中依赖属性的变量值,从而触发响应式变量的getter
,将Computed Watcher
添加到响应式变量的dep.subs
中。
回到createComputedGetter
,此时Dep.target
指向的是组件的Render Watcher
,因为在执行组件render
函数时,会将组件的Render Watcher
入栈,当获取计算属性的属性值时会将Computed Watcher
入栈,执行结束后Computed Watcher
出栈,所以此时的Dep.target
指向的是组件的Render Watcher
;接下来执行Computed Watcher
的depend
方法
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
depend
方法内,遍历deps
,deps
是一个存放当前计算属性Dep
实例的数组,执行每个Dep
实例的depend
方法,将组件的Render Watcher
添加到该计算属性所有依赖属性的dep.subs
里面
watcher.depend
执行完成之后,会返回计算属性的返回值,到此依赖收集结束
依赖收集小结
计算属性的依赖收集过程其实是对使用到的响应式属性进行依赖收集
当组件的render
函数中使用了某个计算属性时,会执行计算属性,在这期间会将Computed Watcher
、Render Watcher
添加到计算属性依赖属性的dep.subs
中
更新
当计算属性依赖的响应式属性修改时,会触发依赖属性的setter
方法
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
setter
方法中,会通知所有Watcher
更新,其中就包括Computed Watcher
和Render Watcher
;调用Watcher
的update
方法
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
对于 Computed Watcher
就是将dirty
设为true
。而Render Watcher
会执行Watcher
实例的run
方法,从而重新执行组件的render
函数,更新计算属性的返回值
dirty
的作用其实就是只在相关响应式属性发生改变时才会重新求值。如果重复获取计算属性的返回值,只要响应式属性没有发生变化,就不会重新求值。
也就是说当响应式属性改变时,触发响应式属性的setter
,通知Computed Watcher
将dirty
置为false
;等再次获取时,会获取到最新值,并重新给响应式属性的dep.subs
添加Watcher
Watch
初始化过程
watch
的初始化过程也是发生在initState
中
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
//...
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
首先为vm
添加一个_watchers
数组,用来存放当前组件的watch
;然后调用initWatch
方法初始化watch
所有属性
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
initWatch
对所有watch
调用createWatcher
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
// 处理参数
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
// handler 可以是方法名
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
createWatcher
就是获取回调函数,并调用vm.$watch
方法
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 如果通过 this.$watch 设置的监听,则会执行 createWatcher 获取回调函数
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// 此时 user 为 true,说明创建的 Watcher 是一个 User Watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
// 如果 options.immediate 为 true,则立刻执行一次回调函数
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回一个函数,作用是取消监听
return function unwatchFn () {
watcher.teardown()
}
}
Vue.prototype.$watch
就是创建一个User Watcher
,并判断options.immediate
是否为true
,如果为true
则立即执行一次回调函数。最后会返回一个取消监听的函数。
自此watch
的初始化过程结束
小结
watch
的初始化过程最终目的就是给每个watch
创建一个 User Watcher
,在创建过程中会对被监听的属性做依赖收集(下一小节介绍)
依赖收集
在初始化过程中会为每个watch
创建一个User Watcher
,而创建过程中会对被监听属性做依赖收集
const watcher = new Watcher(vm, expOrFn, cb, options)
先看下参数
vm 组件实例
expOrFn 被监听的属性名(xxx、'xxx.yyy')
cb 回调函数
options { user: true, deep: [自定义配置项], async: [自定义配置项] }
User Watcher
的创建过程
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
// 如果不是渲染 Watcher 则不会将 _watcher 挂载到 vm 上
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
/**
* computedWatcher 的 lazy 为 true
* userWarcher 的 user 为 true
* deep、sync 是 watch 的配置项
*/
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()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// User Watcher 的 expOrFn 是一个字符串,代表被监听属性的属性名
this.getter = parsePath(expOrFn)
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
)
}
}
// computed Watcher 的 lazy 属性为 true,即不会立刻执行 get 方法
// render Watcher 的 lazy 属性为 false,会立刻执行 get 方法,返回值为 undefined
// user Watcher 的 lazy 属性为 false,会立刻执行 get 方法,返回值为 被监听属性的属性值
this.value = this.lazy
? undefined
: this.get()
}
User Watcher
除了user
属性为true
外,还有deep
、async
两个属性,这两个属性都是watch
的配置项。
实例化一个Watcher
时会判断expOrFn
参数的数据类型,对于User Watcher
而言,expOrFn
就是被监听的属性名,是一个字符串,所以会执行parsePath
方法。
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
parsePath
方法根据.
将字符串切割成字符串数组,并返回一个函数,这个函数会赋值给User Watcher
的getter
属性;函数内部会依次获取数组中所有元素对应的属性值并返回该属性值
假设被监听的属性名是a.b.c
,则此函数会依次获取this.a
、this.a.b
、this.a.b.c
的属性值
回到User Watcher
的创建过程,此时this.getter
已经赋好值,接下来会去执行this.get
方法;也就是说只有Computed Watcher
在创建过程中不会执行this.get
方法
再看一遍get
方法
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {} finally {
if (this.deep) {
// 如果 deep 为 true,并且被监听属性是一个对象,则对象内的所有属性都做一次依赖收集
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
其实不管是计算属性还是data
、props
、watch
他们的get
方法整体逻辑都是一样的,
- 当前
Watcher
入栈 - 执行
this.getter
(每类Watcher
的getter
属性不同) - 执行
traverse
方法 (只有deep
为true
的User Watcher
才会执行) - 当前
Watcher
出栈 - 处理
Watcher
的deps
属性 - 返回
value
对于 User Watcher
,他的getter
是parsePath
函数的返回值,在执行getter
过程中,会获取被监听属性的属性值,从而触发被监听属性的getter
方法,将User Watcher
添加到此属性的dep.subs
中。
上述执行完成后,会判断deep
是否为true
,如果为true
,执行traverse
方法
const seenObjects = new Set()
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
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
方法整体思路其实很简单,如果被监听的属性是一个对象,则把对象的所有属性都访问一遍,从而触发所有属性的依赖收集;将User Watcher
添加到每个属性的dep.subs
中,这样当某个属性修改时,会触发属性的setter
,从而触发watch
回调
触发回调
当修改被监听属性的属性值时,触发属性的setter
,通知 dep.subs
中所有 Watcher
更新,执行watcher.update
方法
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
如果 User Watcher
的sync
属性为true
,立刻执行run
方法;如果sync
属性为false
,通过queueWatcher(this)
在下一次任务队列中执行User Watcher
的 run
方法
先看下run
方法
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 当添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因
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)
}
}
}
}
对于User Watcher
的run
方法,首先会调用this.get()
重新让被监听属性做依赖收集,并获取最新值;如果最新值和老值不想等,调用回调函数,并将新老值传入
上面的判断逻辑中除了判断新老值是否想等还会判断isObject(value) || this.deep
,这是因为如果被监听的属性是一个对象/数组的话,修改对象/数组的属性后,新老值是相同的,所以为了防止出现这种情况导致回调不执行,从而增加这段逻辑
接下来看下User Watcher
在queueWatcher
中是怎么被调用的;正常情况下,和data
相同就是将User Watcher
添加到队列中,并保证同一队列中每个User Watcher
都是唯一的
不同情况
在watch
回调中修改另一个被监听属性的值
他的执行逻辑如下:
export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 执行组件的 beforeUpdate 钩子, 先父后子
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
}
}
}
在下一个队列中执行flushSchedulerQueue
方法
flushing
置为true
,说明正在更新队列中的Watcher
;- 队列排序,保证
Watcher
更新顺序; - 遍历队列,更新队列中的所有
Watcher
has[id] = null
,将正在更新的Watcher
从has
中去掉;- 执行
User Watcher
的run
方法; - 执行第一个
watch
的回调,回调内修改被监听属性的值,触发属性的setter
,将监听这个属性的User Wather
通过queueWatcher
添加到队列中,此时和之前就有差别了
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// 此时 flushing 是 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)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
因为在上面已经将flushing
置为true
了,所以会走 else
逻辑;else
逻辑就是遍历队列,并将Watcher
添加到对应位置。位置逻辑如下
1. 组件更新是从父到子。(因为父组件总是在子组件之前创建)
2. User Watcher 在 Render Watcher 之前执行
3. 如果一个组件在父组件的 Watcher 执行期间被销毁,那么它对应 Watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行
添加到对应位置后,因为waiting
已经是true
了,所以不会再次执行nextTick(flushSchedulerQueue)
,而是回到flushSchedulerQueue
方法继续循环
在watch
回调内修改当前被监听属性的值
其实整体逻辑和上面说的一样,但是会多一步,就是在flushSchedulerQueue
方法的循环里面
因为新添加的User Watcher
和刚执行完的User Watcher
是同一个Watcher
,所以接下来触发的if
条件在开发环境下是成立的
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
}
}
此时circular[id]
数量会加一,并按照上面的逻辑一直重复执行,直到总数量大于MAX_UPDATE_COUNT
时会报错
总结
Computed 和 watch 的区别
computed
- 本质是一个具备缓存的 Watcher,只有依赖属性发生变化时才会更新视图,而且结果是在下次使用时获取
- 不支持异步,当
computed
内有异步操作时无效,无法监听数据的变化 - 当需要进行数值计算,并且依赖于其它数据时,应该使用
computed
,因为可以利用computed
的缓存特性,避免每次获取值时都要重新计算
watch
- 没有缓存性,更多的是观察的作用,某些数据变化时会执行回调
- watch支持异步;可以设置异步返回前的中间状态
- 可以在初始化时执行回调
- 可以深度监听对象属性
- 可以设置回调的执行时机,通过设置
sync
属性可以在当前队列执行,默认是下一队列 - 通过
vm.$watch
注册的监听,会返回一个unWatch
函数,调用该函数可以取消监听
computed 的响应原理
在初始化阶段,会为每个计算属性创建一个Computed Watcher
,通过Object.defineProperty
将所有计算属性添加到组件实例 / 组件构造函数的原型对象
上,并为所有计算属性添加存取描述符。
当获取计算属性时,触发计算属性的getter
,计算computed
的值,并将dirty
置为false
,这样做的目的是再次获取计算属性时直接返回缓存值;在计算computed
值的过程中会将Computed Watcher
、Render Watcher
添加到依赖属性的Dep
中
当依赖属性发生变化会触发Computed Watcher
的更新,将dirty
置为true
,在下次获取计算属性时,会重新计算computed
的值。也会触发当前Render Watcher
的更新,从而获取最新的计算属性的值
watch 的响应原理
在初始化阶段,会为每个watch
创建一个User Watcher
,如果watch
的immediate
为true
,会马上执行一次回调;创建User Watcher
过程中会获取一次被监听属性的值,从而触发被监听属性的getter
方法,将User Watcher
添加到被监听属性的Dep
实例中。
当被监听属性发生改变时,通知User Watcher
更新,如果watch
的sync
为true
,会马上执行watch
的回调;否则会将User Watcher
的update
方法通过nextTick
放到缓存队列中,在下一个的事件循环中,会重新获取被监听属性的属性值,并判断新旧值是否想等、是否设置了deep
为true
、被监听属性是否是对象类型,如果成立就执行回调。