vue 源码解析computed
计算属性 VS 侦听属性
Vue 的组件对象支持了计算属性 computed
和侦听属性 watch
2 个选项,很多同学不了解什么时候该用 computed
什么时候该用 watch
。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。
#computed
计算属性的初始化是发生在 Vue 实例初始化阶段的 initState
函数中,执行了 if (opts.computed) initComputed(vm, opts.computed)
,initComputed
的定义在 src/core/instance/state.js
中:
const computedWatcherOptions = { computed: 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)
}
}
}
}
函数首先创建 vm._computedWatchers
为一个空对象,接着对 computed
对象做遍历,拿到计算属性的每一个 userDef
,然后尝试获取这个 userDef
对应的 getter
函数,拿不到则在开发环境下报警告。接下来为每一个 getter
创建一个 watcher
,这个 watcher
和渲染 watcher
有一点很大的不同,它是一个 computed watcher
,因为 const computedWatcherOptions = { computed: true }
。computed watcher
和普通 watcher
的差别我稍后会介绍。最后对判断如果 key
不是 vm
的属性,则调用 defineComputed(vm, key, userDef)
,否则判断计算属性对于的 key
是否已经被 data
或者 prop
所占用,如果是的话则在开发环境报相应的警告。
那么接下来需要重点关注 defineComputed
的实现:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.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)
}
这段逻辑很简单,其实就是利用 Object.defineProperty
给计算属性对应的 key
值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set
方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key)
的返回值,来看一下它的定义:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
createComputedGetter
返回一个函数 computedGetter
,它就是计算属性对应的 getter。
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher
,它和普通的 watcher
有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher
的实现。
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
当初始化这个 computed watcher
实例的时候,构造函数部分逻辑稍有不同:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
可以发现 computed watcher
会并不会立刻求值,同时持有一个 dep
实例。
然后当我们的 render
函数执行访问到 this.fullName
的时候,就触发了计算属性的 getter
,它会拿到计算属性对应的 watcher
,然后执行 watcher.depend()
,来看一下它的定义:
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意,这时候的 Dep.target
是渲染 watcher
,所以 this.dep.depend()
相当于渲染 watcher
订阅了这个 computed watcher
的变化。
然后再执行 watcher.evaluate()
去求值,来看一下它的定义:
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
evaluate
的逻辑非常简单,判断 this.dirty
,如果为 true
则通过 this.get()
求值,然后把 this.dirty
设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm)
,这实际上就是执行了计算属性定义的 getter
函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName
。
这里需要特别注意的是,由于 this.firstName
和 this.lastName
都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep
添加到当前正在计算的 watcher
中,这个时候 Dep.target
就是这个 computed watcher
。
最后通过 return this.value
拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。
一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher
更新,执行 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.
if (this.dep.subs.length === ) {
// 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 watcher
,它实际上是有 2 种模式,lazy 和 active。如果this.dep.subs.length === 0
成立,则说明没有人去订阅这个computed watcher
的变化,仅仅把this.dirty = true
,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染watcher
订阅了这个computed watcher
的变化,那么它会执行:
this.getAndInvoke(() => {
this.dep.notify()
}) getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
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)
}
}
}
getAndInvoke
函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify()
,在我们这个场景下就是触发了渲染 watcher
重新渲染。
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher
,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher
重新渲染,本质上是一种优化。
接下来我们来分析一下侦听属性 watch
是怎么实现的。
#watch
侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState
函数中,在 computed
初始化之后,执行了:
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.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 = ; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
这里就是对 watch
对象做遍历,拿到每一个 handler
,因为 Vue 是支持 watch
的同一个 key
对应多个 handler
,所以如果 handler
是一个数组,则遍历这个数组,调用 createWatcher
方法,否则直接调用 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)
}
这里的逻辑也很简单,首先对 hanlder
的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options)
函数,$watch
是 Vue 原型上的方法,它是在执行 stateMixin
的时候定义的:
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) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
也就是说,侦听属性 watch
最终会调用 $watch
方法,这个方法首先判断 cb
如果是一个对象,则调用 createWatcher
方法,这是因为 $watch
方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options)
实例化了一个 watcher
,这里需要注意一点这是一个 user watcher
,因为 options.user = true
。通过实例化 watcher
的方式,一旦我们 watch
的数据发送变化,它最终会执行 watcher
的 run
方法,执行回调函数 cb
,并且如果我们设置了 immediate
为 true,则直接会执行回调函数 cb
。最后返回了一个 unwatchFn
方法,它会调用 teardown
方法去移除这个 watcher
。
所以本质上侦听属性也是基于 Watcher
实现的,它是一个 user watcher
。其实 Watcher
支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。
#Watcher options
Watcher
的构造函数对 options
做的了处理,代码如下:
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
// ...
} else {
this.deep = this.user = this.computed = this.sync = false
}
所以 watcher
总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。
#deep watcher
通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:
var vm = new Vue({
data() {
a: {
b:
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal)
}
}
}
})
vm.a.b =
这个时候是不会 log 任何数据的,因为我们是 watch 了 a
对象,只触发了 a
的 getter,并没有触发 a.b
的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2
赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
watch: {
a: {
deep: true,
handler(newVal) {
console.log(newVal)
}
}
}
这样就创建了一个 deep watcher
了,在 watcher
执行 get
求值的过程中有一段逻辑:
get() {
let value = this.getter.call(vm, vm)
// ...
if (this.deep) {
traverse(value)
}
}
在对 watch 的表达式或者函数求值后,会调用 traverse
函数,它的定义在 src/core/observer/traverse.js
中:
import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode' const seenObjects = new Set() /**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
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
的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher
,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id
记录到 seenObjects
,避免以后重复访问。
那么在执行了 traverse
后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher
的回调函数了。
对 deep watcher
的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep
为 true,但是因为设置了 deep
后会执行 traverse
函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。
#user watcher
前面我们分析过,通过 vm.$watch
创建的 watcher
是一个 user watcher
,其实它的功能很简单,在对 watcher
求值以及在执行回调函数的时候,会处理一下错误,如下:
get() {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
},
getAndInvoke() {
// ...
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)
}
}
handleError
在 Vue 中是一个错误捕获并且暴露给用户的一个利器。
#computed watcher
computed watcher
几乎就是为计算属性量身定制的,我们刚才已经对它做了详细的分析,这里不再赘述了。
#sync watcher
在我们之前对 setter
的分析过程知道,当响应式数据发送变化后,触发了 watcher.update()
,只是把这个 watcher
推送到一个队列中,在 nextTick
后才会真正执行 watcher
的回调函数。而一旦我们设置了 sync
,就可以在当前 Tick
中同步执行 watcher
的回调函数。
update () {
if (this.computed) {
// ...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
只有当我们需要 watch 的值的变化到执行 watcher
的回调函数是一个同步过程的时候才会去设置该属性为 true。
欢迎关注公众号,进一步技术交流:
vue 源码解析computed的更多相关文章
- 【VUE】Vue 源码解析
Vue 源码解析 Vue 的工作机制 在 new vue() 之后,Vue 会调用进行初始化,会初始化生命周期.事件.props.methods.data.computed和watch等.其中最重要的 ...
- 【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy.它内 ...
- 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...
- Vue源码解析---数据的双向绑定
本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...
- Vue源码解析之nextTick
Vue源码解析之nextTick 前言 nextTick是Vue的一个核心功能,在Vue内部实现中也经常用到nextTick.但是,很多新手不理解nextTick的原理,甚至不清楚nextTick的作 ...
- Vue源码解析(一):入口文件
在学习Vue源码之前,首先要做的一件事情,就是去GitHub上将Vue源码clone下来,目前我这里分析的Vue版本是V2.5.21,下面开始分析: 一.源码的目录结构: Vue的源码都在src目录下 ...
- Vue源码解析之数组变异
力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...
- Vue源码解析:AST语法树转render函数
开始 今天要说的代码全在codegen文件夹中,在说实现原理前,还是先看个简单的例子! <div class="container"> <span>{{ms ...
- VUE源码解析心得
解读vue源码比较好奇的几个点: VUE MVVM 原理 http://www.cnblogs.com/guwei4037/p/5591183.html https://cn.vuejs.org/v2 ...
随机推荐
- hdu 5446 lucas+crt+按位乘
http://acm.hdu.edu.cn/showproblem.php?pid=5446 题意:题目意思很简单,要你求C(n,m)mod p的值 p=p1*p2*...pn; 题解:对于C(n,m ...
- 【web安全】浅谈web安全之XSS
XSS定义 XSS, 即为(Cross Site Scripting), 中文名为跨站脚本, 是发生在目标用户的浏览器层面上的,当渲染DOM树的过程成发生了不在预期内执行的JS代码时,就发生了XSS攻 ...
- 函数——es6函数扩展(二)
一.声明 1. let(变量) 可以只声明不给值(默认为undefined),或者是先声明后给值,但是必需声明后再使用,可以重复赋值,可以防止变量泄露: 同一作用域里不能重复的声明,不同作用域里可以, ...
- 关于小程序去除view/navigator 点击后默认阴影效果
hover:class :定义容器在被触发时的样式 通常无用,但若不去除则影响用户体验: 为避免被覆盖,约定在wxss底部添加class,比如: <!-- wxml --> <na ...
- python小知识-sys.argv
sys.argv 就是一个从程序外部获取参数的桥梁 1.t1.py import sys a = sys.argv b = len(sys.argv) print(a) print(b) 在pytho ...
- PXE批量部署安装Linux系统
PXE介绍 1)Preboot Excution Environment 预启动执行环境 2)Intel公司研发 3)基于Client/Server的网络模式,支持远程主机通过网络从远端服务器下载映 ...
- vim文本编辑及文件查找应用2
vim编辑器: vim末行模式: 内建的命令行接口 (1)地址定界 :start_pos[,end_pos] #:特定的第#行,例如5即第5行 .:当前行 .,+#:当前行,加#行的行范围 $:最后一 ...
- Matlab---绘图及其位置摆放
Matlab---绘图及其位置摆放 [@WP@20180509] 一.绘图函数 (1)绘制二维图形 (1.1) plot( ) 函数的应用格式. 1,plot(x). 当x 为一向量时,以x 元素的值 ...
- 开启aix SFTP日志 是否和链接SFTP有关呢
1.修改SSH配置 vi /etc/ssh/sshd_config 在sftp配置处添加-l INFO -f AUTH Subsystem sftp /usr/lib64/ssh/sftp-serve ...
- shell脚本基础编写
shell脚本的格式 名称:Shell 脚本文件的名称可以任意,但为了避免被误以为是普通文件,建议将 .sh 后缀加上,以表示是一个脚本文件. shell 脚本中一般会出现三种不同的元素: 第一行的脚 ...