Vue计算属性
夏日 人气:0前言
官网对计算属性的介绍在这里:传送门
计算属性是Vue
中很常用的一个配置项,我们先用一个简单的例子来讲解它的功能:
<div id="app"> {{fullName}} </div> <script> const vm = new Vue({ data () { return { firstName: 'Foo', lastName: 'Bar' }; }, computed: { fullName () { return this.firstName + this.lastName; } } }); </script>
在例子中,计算属性中定义的fullName
函数,会最终处理为vm.fullName
的getter
函数。所以vm.fullName = this.firstName + this.lastName = 'FooBar'
。
计算属性有以下特点:
- 计算属性可以简化模板中的表达式,用户可以书写更加简洁易读的
template
Vue
为计算属性提供了缓存功能,只有当它依赖的属性(例子中的this.firstName
和this.lastName
)发生变化时,才会重新执行属性对应的getter
函数,否则会将之前计算好的值返回。
正是由于computed
的缓存功能,使得用户在使用时会优先考虑它,而不是使用watch
、methods
属性。
在了解了计算属性的用法后,我们通过代码来一步步实现computed
,并让它完成上边的例子。
初始化计算属性
初始化computed
的逻辑会书写在scr/state.js
中:
function initState (vm) { const options = vm.$options; // some code ... if (options.computed) { initComputed(vm); } }
在initComputed
中,可以通过vm.$options.computed
拿到所有定义的计算属性。对于每个计算属性,需要对其做如下处理:
- 实例化计算属性对应的
Watcher
- 取到计算属性的
key
,通过Object.defineProperty
为vm
实例添加key
属性,并设置它的get/set
方法
function initComputed (vm) { const { computed } = vm.$options; // 将计算属性watcher存储到vm._computedWatchers属性中,之后方法直接通过实例vm来获取 const watchers = vm._computedWatchers = {}; for (const key in computed) { if (computed.hasOwnProperty(key)) { const userDef = computed[key]; // 计算属性key的值有可能是对象,在对象中会设置它的get set 方法 const getter = typeof userDef === 'function' ? userDef : userDef.get; // 为每一个计算属性创建一个watcher watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }); // 将计算属性的key添加到实例vm上 defineComputed(vm, key, userDef); } } }
计算属性也可以传入set
方法,用于设置值时处理的逻辑,此时计算属性的value
是一个对象:
new Vue({ // ... computed: { fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } } //... )
在defineComputed
函数中,我们会根据计算属性的类型来确定是否为其定义set
方法:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function defineComputed (target, key, userDef) { if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key); } else { sharedPropertyDefinition.get = createComputedGetter(key); // 如果是对象,用户会传入set方法 sharedPropertyDefinition.set = userDef.set; } Object.defineProperty(target, key, sharedPropertyDefinition); } // 创建Object.defineProperty的get函数 function createComputedGetter (key) { return function () { // 通过之前保存的_computedWatchers来取到对应的计算属性watcher const watcher = this._computedWatchers[key]; if (watcher.dirty) { // 只有在dirty为true的时候才会重新执行计算属性 watcher.evaluate(); if (Dep.target) { // 此时,如果栈中有渲染watcher,会为当前计算属性watcher中收集的所有dep再收集渲染watcher // 在watcher收集的dep对应的属性(this.firstName,this.lastName)更新后,通知视图更新,从而更新页面中的计算属性 watcher.depend(); } } return watcher.value; }; }
在对计算属性取值时,首先会调用它在vm.fullName
上定义的get
方法,也就是上边的createComputedGetter
执行后返回的函数。在函数内部,只有当watcher.dirty
为true
时,才会执行watcher.evaluate
。
下面我们先看下Watcher
中关于计算属性的代码:
import { popTarget, pushTarget } from './dep'; import { nextTick } from '../shared/next-tick'; import { traverse } from './traverse'; let id = 0; class Watcher { constructor (vm, exprOrFn, cb, options = {}) { // some code ... // 设置dirty的初始值为false this.lazy = options.lazy; this.dirty = this.lazy; if (typeof exprOrFn === 'function') { this.getter = this.exprOrFn; } // some code ... // 初始化时计算属性的getter不会执行,用到的时候才会执行 this.value = this.lazy ? undefined : this.get(); } // 执行传入的getter函数进行求值,将其赋值给this.value // 求值完毕后,将dirty置为false,下次将不会再重新执行求值函数 evaluate () { this.value = this.get(); this.dirty = false; } // 为watcher中的dep,再收集渲染watcher depend () { this.deps.forEach(dep => dep.depend()); } get () { pushTarget(this); const value = this.getter.call(this.vm); if (this.deep) { traverse(value); } popTarget(); return value; } update () { if (this.lazy) { // 依赖的值更新后,只需要将this.dirty设置为true // 之后获取计算属性的值时会再次执行evaluate来执行this.get()方法 this.dirty = true; } else { queueWatcher(this); } } // some code ... }
watcher.evaluate
中的逻辑便是执行我们在定义计算属性时传入的回调函数(getter
),将其返回值赋值给watcher.value
,并在取值完毕后,将watcher.dirty
置为false
。这样再次取值时便直接将watcher.value
返回即可,而不用再执行回调函数进行重复计算。
当计算属性的依赖属性(this.firstName
和this.lastName
)发生变化后,我们要更新视图,让计算属性重新执行getter
函数获取到最新值。所以代码中判断Dep.target
(此时为渲染watcher
) 是否存在,如果存在会为依赖属性收集对应的渲染watcher
。这样在依赖属性更新时,便会通过渲染watcher
来通知视图更新,获取到最新的计算属性。
依赖属性更新
以文章开始时的demo
为例,首次执行时的逻辑如下图:
用文字来描述:
- 初始化计算属性,为
vm
添加fullName
属性,并设置其get
方法 - 首次渲染页面,
stack
中存储了渲染watcher
。由于页面中用到了fullName
属性,所以在渲染时会触发fullName
的get
方法 fullName
执行get
会通过依赖属性firstName
和lastName
来求值,computed watcher
会进入stack
中- 此时又会触发
firstName
和lastName
的get
方法,收集computed watcher
fullName
求值方法执行完成,computed watcher
出栈,Dep.target
为渲染watcher
- 此时为
fullName
对应的computed watcher
中的dep
(也就是firstName
和lastName
对应的dep
)收集渲染watcher
- 完成
fullName
的取值过程,此时firstName
和lastName
的dep
中分别收集的watcher
为[computed watcher, render watcher]
假设我们更新了依赖,会通知收集的watcher
进行更新:
vm.firstName = 'F'
在firstName
属性更新后,会触发其对应的set
方法,执行dep
中收集的computed watcher
和render watcher
:
computed watcher
: 将this.dirty
设置为true
,fullName
之后取值时需要重新执行用户传入的getter
函数render watcher
: 通知视图更新,获取fullName
的最新值
到这里我们实现的computed
属性便能正常工作了!
总结
本文从一个简单的计算属性例子开始,一步步实现了计算属性。并且针对这个例子,详细分析了页面渲染时的整个代码执行逻辑。希望小伙伴们在读完本文后,能够从源码的角度,分析自己代码中对应计算属性相关代码的执行流程,体会一下Vue
的computed
属性到底帮我们做了些什么。
加载全部内容