系列文章:
computed处理
处理computed
相关的逻辑,发生在initState
中,接下来详细分析与computed
相关的逻辑。
export function initState (vm: Component) {
// 省略代码
const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed)
}
知道computed
计算属性是依赖于其它响应式变量的,因此分析computed
的时候会分为两个步骤:computed初始化和computed更新。
computed初始化
在initState()
方法中如果传递了computed
,那么会调用initComputed()
方法。initComputed()
方法定义在src/core/instance/state.js
文件中,其代码如下:
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
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) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
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()
方法中,首先定义了一个_computedWatchers
的变量,这个变量的作用是缓存当前实例所有计算属性的watcher
。 - 接下来遍历所有的
computed
,然后对每一个computed
进行类型判断,如果是function
类型,那么直接使用,如果是对象则代表是get/set
形式,则直接取get
。如果最后获取到的computed
的getter
为null
,则在开发环境下提示错误。
// 两种类型的计算属性
export default {
props: ['index'],
data () {
return {
firstName: 'first',
lastName: 'last'
}
},
computed: {
fullName () {
return this.firstName + this.lastName
},
active: {
get: function () {
return this.index
},
set: function (newVal) {
this.$emit('update:index', newVal)
}
}
}
}
以上面代码为例,两种类型的computed
获取到的getter
分别如下:
// function类型
const getter = function () {
this.firstName + this.lastName
}
// get/set类型
const getter = function () {
return this.index
}
- 然后在非
SSR
服务端渲染的情况下,会在_computedWatchers
上新建一个Watcher
的实例。以上面代码为例,_computedWatchers
在遍历完毕后,可以用如下代码表示:
// 当前vm实例
{
_computedWatchers: {
fullName: new Watcher(),
active: new Watcher()
}
}
- 最后、首先判断了当前遍历的
computed
是否已经在vm
实例上,如果不在则调用defineComputed()
方法,如果在还需要判断当前遍历的computed
是否和props
、data
命名冲突,如果冲突则提示错误。 注意:对应子组件而言,这个时候当前遍历的computed
已经在vm
实例上了,所以并不会调用defineComputed()
方法,从上面代码注释也能看的出来。对于子组件而言,真正initComputed
的过程是发生在Vue.extend
方法中:
Vue.extend = function (extendOptions) {
// 省略代码
const Super = this
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 初始化子组件的computed
if (Sub.options.computed) {
initComputed(Sub)
}
}
// extend.js中的initComputed定义
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
然后initComputed
调用的defineComputed()
方法,就和现在的defineComputed()
方法是同一个方法,它和此时的initComputed()
方法定义在同一个位置(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
) {
const shouldCache = !isServerRendering()
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
}
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
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
defineComputed()
方法的逻辑很简单,根据不同的类型的computed
,对sharedPropertyDefinition
的get
和set
进行赋值。sharedPropertyDefinition
在之前的proxy
中,已经介绍过, 它就是Object.defineProperty()
方法descriptor
参数的一个共享配置。
在非SSR
服务端渲染的情况,sharedPropertyDefinition.get
的值是调用了createComputedGetter()
方法,而在SSR
服务端渲染的情况下是调用了createGetterInvoker()
方法。在分析Vue
源码的过程中,因为侧重于Web
浏览器端的表现,因此接下来会分析createComputedGetter()
方法的实现。createComputedGetter()
方法和defineComputed()
方法定义在同一个位置,代码如下:
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
的时候被调用,例如组件渲染的时候:
<template>
<div>{{fullName}}</div>
</template>
根据以上代码再来看computedGetter()
方法:组件渲染的时候会获取fullName
计算属性,然后调用computedGetter()
方法,在这个方法执行的时候,首先判断watcher.dirty
属性,这个属性在new Watcher()
的时候与传入的const computedWatcherOptions = { lazy: true }
有关。在Watcher
类的构造函数中,有这样一段代码:
class Watcher {
// 省略代码
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
if (options) {
this.lazy = !!options.lazy
} else {
this.lazy = false
}
this.dirty = this.lazy
}
}
因为传入的lazy
值为true
,因此watcher.dirty
条件判断为真,进行watcher.evaluate()
计算。随后判断了Dep.target
为真,则进行依赖收集watcher.depend()
,关于依赖收集会在之后的章节详细介绍。只要知道,当在组件渲染的时候触发的computed
依赖收集,收集的是render watcher
。最后,看一下watcher.evaluate()
方法的实现:
class Watcher {
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
}
evaluate()
方法它的实现非常简单,就是触发computed
的getter
进行求值,然后把dirty
设置为false
。
computed更新
在介绍完了computed
的初始化后,再来看computed
的更新过程,以下面为例:
export default {
template: `
<div>{{fullName}}</div>
<button @click="change">change</button>
`
data () {
return {
total: 0,
firstName: 'first',
lastName: 'last'
}
},
computed: {
fullName () {
if (this.total > 0) {
return this.firstName + this.lastName
} else {
return 'pleace click'
}
}
},
methods: {
change () {
this.total++
}
}
}
因为total
、firstName
和lastName
全部为响应式变量,所以fullName
这个计算属性初始化的时候,此时total
值为0
,fullName
计算属性有两个Watcher
,其中一个是计算属性watcher
,另外一个是渲染watcher
。当点击按钮触发事件后,会触发total
属性的setter
方法,进而调用一个叫做notify
的方法。
set: function reactiveSetter (newVal) {
// 省略
dep.notify()
}
其中notify()
是定义在Dep
类中的一个方法:
export default class Dep {
constructor () {
this.subs = []
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
代码分析:
subs
就是收集起来的watcher
,它是一个数组,对应上面案例的话它是一个长度为2的数组并且其中一个为render watcher
。- 在
notify()
方法调用时,会遍历subs
数组,然后依次调用当前watcher
的update
方法。其中update
方法是定义在Watcher
类中的一个实例方法,代码如下:
class Watcher {
// 省略其它
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
当第一次遍历时,此时的watcher
为计算属性watcher
,已经在前面介绍过计算属性watcher
它的this.lazy
值为true
,因此会进行this.dirty = true
。
当第二次遍历时,此时的watcher
为渲染watcher
,对于渲染watcher
而言,它的lazy
值为false
,this.sync
为false
,因此会调用queueWatcher()
方法。目前不需要知道queueWatcher
是怎么实现的,只需要知道queueWatcher()
方法在调用时,会触发updateComponent()
方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
可以看到updateComponent()
方法调用了vm._update
方法,而这个方法的作用就是重新进行组件渲染,在组件渲染的过程中,会再次读取fullName
的值,也就是说会调用下面这段代码:
fullName () {
if (this.total > 0) {
return this.firstName + this.lastName
} else {
return 'pleace click'
}
}
因为此时的total
值为1
,所以会返回this.firstName + this.lastName
的值,而firstName
和lastName
又是定义在data
中的响应式变量,会依次触发firstName
和lastName
的getter
,然后进行依赖收集。在组件渲染完毕后,fullName
的依赖数组subs
此时会有四个watcher
,分别是三个计算属性watcher
和一个渲染watcher
。无论这三个计算属性watcher
哪一个值更新了,都会再出重复以上的流程,这就是computed
更新的过程。
在分析完computed
的相关流程后,可以得到如下流程图
watch处理
在介绍完处理computed
相关的逻辑后,我们接下来看watch
是如何处理的。
watch初始化
export function initState (vm: Component) {
// 省略代码
const opts = vm.$options
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
我们可以看到,处理watch
的逻辑发生在initWatch()
方法中,在这个方法调用之前,首先对watch
做了判断,其中nativeWatch
是定义在src/core/util/env.js
中的一个常量:
// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
然后,让我们来看一下initWatch
的实现,它定义在src/core/instance/state.js
文件中:
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()
方法,如果不是则直接调用createWatcher()
。按照watch
的使用规则,我们有如下几种形式的写法:
export default {
data () {
return {
age: 23,
name: 'AAA',
nested: {
a: {
b: 'b'
}
}
}
},
watch: {
name (newVal, oldVal) {
console.log(newVal, oldVal)
},
nested: {
handler (newVal, oldVal) {
console.log(newVal, oldVal),
},
deep: true
}
}
}
接着,我们需要来看一下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 = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
代码分析:
createWatcher()
方法的主要作用就是进行watch
参数规范化,然后将规范化后的参数传递给vm.$watch()
。- 在
createWatcher()
中首先判断了handler
参数是否为普通对象,如果是普通对象则代表是如下形式定义的watch
:
{
watch: {
nested: {
handler (newVal, oldVal) {
console.log(newVal, oldVal),
},
deep: true
}
}
}
此时,应该把handler
赋值给可选的options
参数,然后handler
赋值为真正的回调函数。
- 接着,对
handler
进行了类型判断,如果是string
类型则把此时vm[handler]
赋值给它。根据这段代码的逻辑,意味着我们可以选择把watch
回调函数定义在methods中
:
export default {
data () {
return {
name: 'AAA'
}
},
watch: {
name: 'nameWatchCallback'
},
methods: {
nameWatchCallback (newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
- 最后,把规范化后的参数传递给
vm.$watch()
。关于$watch()
何时挂载到Vue.prototype
上,我们已经在之前介绍过了,它发生在stateMixin
中。
在分析完createWatcher()
方法实现逻辑后,我们接着来看$watch()
方法的具体实现逻辑:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
我们可以发现,$watch
方法主要做两件事情:创建Watcher实例和返回unwatchFn函数,接下来我们分别对这两部分的逻辑进行详细的解释。
创建Watcher实例
我们先来看一下Watcher
构造函数的代码:
// 精简代码
class Watcher {
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
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
}
}
}
我们从构造函数中可以看到,当实例化一个watch
的时候,会根据传递的options
来处理deep
、user
、lazy
、sync
以及before
属性。watcher
根据不同的用法,有几种不同的分类:
render watcher
:渲染watcher
,例如当在template
模板中使用{{}}
语法读取一个变量的时候,此时这个变量收集的依赖就是render watcher
,当这个变量值更新的时候会触发render watcher
进行组件的重新渲染。是否为渲染warcher
,使用构造函数参数isRenderWatcher
为true
进行区分。computed watcher
:计算属性watcher
,当我们在定义计算属性的时候,计算属性收集的依赖就是另外一个或者多个变量,当其中一个变量的值发生变量,就会触发计算属性重新进行求值。是否为计算属性watcher
,使用options.lazy
为true
进行区分。user watcher
:用户自定义watcher
,多发生在this.$watch
或者组件watch
选择配置中,此时收集的依赖就是变量自身,当变量的值发生变化的时候,就会调用watch
提供的回调函数。是否为用户自定义watcher
,使用options.user
为true
进行区分。
返回unwatchFn函数
在构造函数中可以发现,它定义了一个_watchers
变量,然后在每次实例化的时候,把自身添加到这个数组中,这样做的目的是为了方便清除依赖。在之前的介绍中,我们知道$watch
返回了一个unwatchFn
函数,它用来取消监听。接下来,看一下teardown()
方法的具体实现。
// Watcher类精简代码
class Watcher {
constructor () {
this.active = true
this.deps = []
}
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
}
}
}
// Dep类精简代码
class Dep {
constructor () {
this.subs = []
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
}
teardown()
方法的实现很简单,就是从deps
数组中移除当前的watcher
,其中deps
存储的是Dep
实例。