编者按:咱们会不时邀请工程师谈谈有意思的技术细节,但愿知其因此然能让你们在面试有更出色表现。
也给面试官提供更多思路。前端
虽然目前的技术栈已由 Vue 转到了 React,但从以前使用 Vue 开发的多个项目实际经从来看仍是很是愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至我的认为在不少场景使用 Vue 比 React 开发效率更高,以前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,因此在此作一些技术概括同时也加深本身对 Vue 的理解,那么今天要写的即是 Vue 中最经常使用到的 API 之一 computed
的实现原理。vue
话很少说,一个最基本的例子以下:面试
<div id="app">
<p>{{fullName}}</p>
</div>
复制代码
new Vue({
data: {
firstName: 'Xiao',
lastName: 'Ming'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
复制代码
Vue 中咱们不须要在 template 里面直接计算 {{this.firstName + ' ' + this.lastName}}
,由于在模版中放入太多声明式的逻辑会让模板自己太重,尤为当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性形成很大的影响,而 computed
的设计初衷也正是用于解决此类问题。api
watch
固然不少时候咱们使用 computed
时每每会与 Vue 中另外一个 API 也就是侦听器 watch
相比较,由于在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,全部依赖这个数据的相关数据或函数都会自动发生变化或调用。数组
虽然计算属性在大多数状况下更合适,但有时也须要一个自定义的侦听器。这就是为何 Vue 经过
watch
选项提供了一个更通用的方法来响应数据的变化。当须要在数据变化时执行异步或开销较大的操做时,这个方式是最有用的。缓存
从 Vue 官方文档对 watch
的解释咱们能够了解到,使用 watch
选项容许咱们执行异步操做(访问一个 API)或高消耗性能的操做,限制咱们执行该操做的频率,并在咱们获得最终结果前,设置中间状态,而这些都是计算属性没法作到的。微信
下面还另外总结了几点关于 computed
和 watch
的差别:app
computed
是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch
是监听已经存在且已挂载到 vm
上的数据,因此用 watch
一样能够监听 computed
计算属性的变化(其它还有 data
、props
)computed
本质是一个惰性求值的观察者,具备缓存性,只有当依赖变化后,第一次访问 computed
属性,才会计算新的值,而 watch
则是当数据发生变化便会调用执行函数computed
适用一个数据被多个数据影响,而 watch
适用一个数据影响多个数据;以上咱们了解了 computed
和 watch
之间的一些差别和使用场景的区别,固然某些时候二者并无那么明确严格的限制,最后仍是要具体到不一样的业务进行分析。异步
言归正传,回到文章的主题 computed
身上,为了更深层次地了解计算属性的内在机制,接下来就让咱们一步步探索 Vue 源码中关于它的实现原理吧。函数
在分析 computed
源码以前咱们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。
当你把一个普通的 JavaScript 对象传给 Vue 实例的
data
选项时,Vue 将遍历此对象全部的属性,并使用Object.defineProperty
把这些属性所有转为getter/setter
,这些getter/setter
对用户来讲是不可见的,可是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每一个组件实例都有相应的watcher
实例对象,它会在组件渲染的过程当中把属性记录为依赖,以后当依赖项的setter
被调用时,会通知watcher
从新计算,从而导致它关联的组件得以更新。
Vue 响应系统,其核心有三点:observe
、watcher
、dep
:
observe
:遍历 data
中的属性,使用 Object.defineProperty 的 get/set
方法对其进行数据劫持;dep
:每一个属性拥有本身的消息订阅器 dep
,用于存放全部订阅了该属性的观察者对象;watcher
:观察者(对象),经过 dep
实现对响应属性的监听,监听到结果后,主动触发本身的回调进行响应。对响应式系统有一个初步了解后,咱们再来分析计算属性。 首先咱们找到计算属性的初始化是在 src/core/instance/state.js
文件中的 initState
函数中完成的
export function initState (vm: Component) {
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
函数(其先后也分别初始化了 initData
和 initWatch
)并传入两个参数 vm
实例和 opt.computed
开发者定义的 computed
选项,转到 initComputed
函数:
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)
}
}
}
}
复制代码
从这段代码开始咱们观察这几部分:
获取计算属性的定义 userDef
和 getter
求值函数
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
复制代码
定义一个计算属性有两种写法,一种是直接跟一个函数,另外一种是添加 set
和 get
方法的对象形式,因此这里首先获取计算属性的定义 userDef
,再根据 userDef
的类型获取相应的 getter
求值函数。
计算属性的观察者 watcher
和消息订阅器 dep
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
复制代码
这里的 watchers
也就是 vm._computedWatchers
对象的引用,存放了每一个计算属性的观察者 watcher
实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher
均指代同一个意思但注意和 Watcher
构造函数区分),Watcher
构造函数在实例化时传入了 4 个参数:vm
实例、getter
求值函数、noop
空函数、computedWatcherOptions
常量对象(在这里提供给 Watcher
一个标识 {computed:true}
项,代表这是一个计算属性而不是非计算属性的观察者,咱们来到 Watcher
构造函数的定义:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
if (options) {
this.computed = !!options.computed
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
popTarget()
}
return value
}
update () {
if (this.computed) {
if (this.dep.subs.length === 0) {
this.dirty = true
} else {
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
}
复制代码
为了简洁突出重点,这里我手动去掉了咱们暂时不须要关心的代码片断。 观察 Watcher
的 constructor
,结合刚才讲到的 new Watcher
传入的第四个参数 {computed:true}
知道,对于计算属性而言 watcher
会执行 if
条件成立的代码 this.dep = new Dep()
,而 dep
也就是建立了该属性的消息订阅器。
export default class Dep {
static target: ?Watcher;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
复制代码
Dep
一样精简了部分代码,咱们观察 Watcher
和 Dep
的关系,用一句话总结
watcher
中实例化了dep
并向dep.subs
中添加了订阅者,dep
经过notify
遍历了dep.subs
通知每一个watcher
更新。
defineComputed
定义计算属性
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)
}
}
复制代码
由于 computed
属性是直接挂载到实例对象中的,因此在定义以前须要判断对象中是否已经存在重名的属性,defineComputed
传入了三个参数:vm
实例、计算属性的 key
以及 userDef
计算属性的定义(对象或函数)。 而后继续找到 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
方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition
,初始化为:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
复制代码
随后根据 Object.defineProperty
前面的代码能够看到 sharedPropertyDefinition
的 get/set
方法在通过 userDef
和 shouldCache
等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition
的 get
函数也就是 createComputedGetter(key)
的结果,咱们找到 createComputedGetter
函数调用结果并最终改写 sharedPropertyDefinition
大体呈现以下:
sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
},
set: userDef.set || noop
}
复制代码
当计算属性被调用时便会执行 get
访问函数,从而关联上观察者对象 watcher
而后执行 wather.depend()
收集依赖和 watcher.evaluate()
计算求值。
computed
和 data
会分别创建各自的响应系统,Observer
遍历 data
中每一个属性设置 get/set
数据拦截computed
会调用 initComputed
函数
watcher
实例,并在内实例化一个 Dep
消息订阅器用做后续收集依赖(好比渲染函数的 watcher
或者其余观察该计算属性变化的 watcher
)Object.defineProperty
的get
访问器函数watcher.depend()
方法向自身的消息订阅器 dep
的 subs
中添加其余属性的 watcher
watcher
的 evaluate
方法(进而调用 watcher
的 get
方法)让自身成为其余 watcher
的消息订阅器的订阅者,首先将 watcher
赋给 Dep.target
,而后执行 getter
求值函数,当访问求值函数里面的属性(好比来自 data
、props
或其余 computed
)时,会一样触发它们的 get
访问器函数从而将该计算属性的 watcher
添加到求值函数中属性的 watcher
的消息订阅器 dep
中,当这些操做完成,最后关闭 Dep.target
赋为 null
并返回求值函数结果。set
拦截函数,而后调用自身消息订阅器 dep
的 notify
方法,遍历当前 dep
中保存着全部订阅者 wathcer
的 subs
数组,并逐个调用 watcher
的 update
方法,完成响应更新。文 / 亦然
一枚向往诗与远方的 coder
编 / 荧声
本文已由做者受权发布,版权属于创宇前端。欢迎注明出处转载本文。本文连接:knownsec-fed.com/2018-09-12-…
想要订阅更多来自知道创宇开发一线的分享,请搜索关注咱们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,咱们会尽量回复。
感谢您的阅读。