前言
本人在工作中一开始只负责前端,到后来用nodejs写服务以及负责一些团队基础设施等运维工作。由于做的事情太杂,最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。
描述
VUE中看似有个很多可以替代计算属性的实现手段,但是实际上计算属性是有自己不可被轻易取缔的优势的.本文就从业务场景讲解一下computed的优势,以及从源码看了解它的实现.
为什么要用计算属性?
我们先用一个具体的场景,引入这个问题。我们页面上有2个输入框,分别输入数字1和数字2。下方的结果部分,实时显示2个数字的相加结果。
<div id="app">
<div>数字1<input v-model.number="number1" /></div>
<div>数字2<input v-model.number="number2" /></div>
<div>结果:</div>
</div>
<script>
...
data(){
return {
number1:0,
number2:0
}
}
...
</script>
当然我们可以直接在template中写这段逻辑。以下的代码是能生效的。
<div>结果:{{number1+number2}}</div>
可是我们当然不希望逻辑跟代码在一块,因为如果逻辑复杂起来,不仅会增加代码阅读的难度,还会增加维护的成本。
另一个方式就是用methods,这种方式同样可以实现上述的结果。
<div>结果:{{sum()}}</div>
methods: {
sum(){
return this.number1 + this.number2;
}
},
目前我们提到了2种不同的实现方式,可是他们都有一个问题。就是会触发没必要的函数执行。我们在页面上加入第三个输入框,他与这个业务需求无关,属于其他的需求。
<div id="app">
<div>数字1<input v-model.number="number1" /></div>
<div>数字2<input v-model.number="number2" /></div>
<div>无关字符<input v-model="str" /></div>
<div>结果1:{{console.log('触发template渲染')}}</div>
<div>结果2:{{sum()}}</div>
</div>
...
methods: {
sum(){
console.log('触发sum')
return this.number1 + this.number2;
}
},
});
这时当我们在字符输入框,输入的时候,2个console.log()都会触发。
也就是说,这2种方式实际上带来了一些,我们期待以外的运行成本。因此我们需要一个新的解决方案。那就是计算属性computed。
<div>结果:{{sum}}</div>
computed:{
sum(){
console.log('触发computed')
return this.number1 + this.number2;
}
},
我们发现用了computed之后,修改字符输入框,并不会触发console.log();
计算属性的优势
从上述我们可以总结,计算属性的最大优势其实是,他会找到函数内部用到的data,只在这些data被修改了之后才会触发函数。假如是函数依赖到的data之外的内容被修改,函数是不会被执行的。
从源码看计算属性的实现
接下来我们一起看看在vue2中computed是怎么实现的.
// computed的入口在 initState函数中
export function initState(vm: Component) {
//初始化实例的_watchers数组
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 如果当前组件中有computed,就触发处理.
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initComputed
computed初始化的入口
const computedWatcherOptions = { lazy: true }
function initComputed(vm, computed) {
// 初始化一个watcher对象
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
// 由于是浏览器执行这里isSSR应是false
if (!isSSR) {
// 在初始化的watcher对象中,每一个用户定义的computed都用一个watcher实例来管理
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 把computed挂在组件实例上,(可以用this.xxx访问)
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)
}
}
}
}
Watcher
Watcher类,管理每个computed
constructor
export default class Watcher {
constructor(
vm: Component, // 传入组件实例
expOrFn: string | Function, // computed 函数
cb: Function, // 无
options?: ?Object, //{ lazy: true }
isRenderWatcher?: boolean // undefined
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
// 在组件实例的_watchers中加入当前watcher实例。
vm._watchers.push(this)
// 处理options
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
}
this.cb = cb
this.id = ++uid
this.active = true
// 当前watcher的dirty为true
this.dirty = this.lazy
// 记录当前watcher的依赖
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// 把computed的内容赋予给getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
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
)
}
}
// 初始化的value是undefined
this.value = this.lazy
? undefined
: this.get()
}
}
evaluate
当触发computed的get,且当前watcher的dirty是true时,会触发这个方法.
export default class Watcher {
...
evaluate() {
// 调用真实用户定义的函数内容,修改watcher的value.
this.value = this.get()
// 把dirty设为false.
this.dirty = false
}
...
}
update
当计算属性的依赖被修改后,Dep的notify最终会触发watcher的udate方法.把drity设为true.
update() {
/* istanbul ignore else */
if (this.lazy) {
// 触发update方式时,会把dirty设为true
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
defineComputed
定义computed
export function defineComputed(
target: any, // 组件实例
key: string, // computed的命名
userDef: Object | Function // computed内容
) {
const shouldCache = !isServerRendering()
// 根据computed2种不同写法,处理内容。
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
}
// 如果用户没有指定computed的set,就默认创建一个,内容是警告提示。
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数据劫持computed
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter
创建computed的getter,如果没有set.默认创建一个
function createComputedGetter(key) {
// 返回一个函数作为computed的get
return function computedGetter() {
// 获取对应的watcher实例
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果dirty是true,执行evaluate
if (watcher.dirty) {
watcher.evaluate()
}
// 关联watcher与计算属性依赖
if (Dep.target) {
watcher.depend()
}
// 返回watcher的value(如果dirty是false,就直接返回之前的value)
return watcher.value
}
}
}
总结
通过上述内容,我们可以大概总结出VUE2中computed的实现