Description
前言
vue中有一个非常好用的功能:计算属性(computed)
在模板中绑定表达式是非常便利的,但是它们实际上只用于简单的操作。模板是为了描述视图的结构。在模板中放入太多的逻辑会让模板过重且难以维护。这就是为什么 Vue.js 将绑定表达式限制为一个表达式。如果需要多于一个表达式的逻辑,应当使用计算属性。
你可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 vm.b 依赖于 vm.a,因此当 vm.a 发生改变时,依赖于 vm.b 的绑定也会更新。
来源:https://vuejs.org.cn/guide/computed.html
问题
我们先来具象化一下问题。
// html
<div id="app">
<p>姓名:{{user.name}}</p>
<p>年龄: {{user.age}}</p>
<p>{{info}}</p>
</div>
// js
const app = new Bue({
el: '#app',
data: {
user: {
name: 'youngwind',
age: 24
}
},
computed: {
info: function () {
return `计算出来的属性-> 姓名: ${this.user.name}, 年龄: ${this.user.age}`;
}
}
});
问题是:如何让info跟着name和age动态改变呢?
我们把这个问题拆解成两个更小的问题,然后逐个击破。
- 如何根据name和age计算出info?
- 当name或者age改变的时候,重新进行第一步的计算。
静态计算属性
ok,我们先来解决第一个问题。(先把第二个问题放一边)
/**
* 初始化所有计算属性
* 主要完成一个功能:将计算属性定义的function当成是该属性的getter函数
* @private
*/
exports._initComputed = function () {
// 注意,这里的this指的是bue实例
let computed = this.$options.computed;
if (!computed) return;
for (let key in computed) {
let def = computed[key];
if (typeof def === 'function') {
def = {
get: def
};
def.enumerable = true;
def.configurable = true;
Object.defineProperty(this.$data, key, def);
}
}
};
关键点说明:
- info后面跟的是一个有return值的函数,我们直接把这个函数当成info的getter函数。
- 虽然info是计算出来的属性,但是到时候对DOM模板进行遍历的时候也是需要用到它的,所以需要将info定义到$data里面去。
从图中我们可以看到,$data里面的info已经有值,并且DOM模板里面的{{info}}也已经正确解析了。
第一个问题解决了,但是,我们同时也看到,当我们改变name和age的时候,info并不会跟着改变!!
下面我们来看看怎么解决这第二个问题。
动态计算属性
动态计算难在什么地方?
难在:当name或者age改变的时候,程序如何知道要改变info?
你可能会说,这不明摆着吗,一眼就看出来。
然而,程序不知道啊!我们拆解一下问题。
- 如何让程序知道info依赖于name和age。-> 如何收集计算属性的依赖属性
- 当name和age改变的时候,更新info对应的DOM元素
第2个问题好办,因为我们在《如何实现动态数据绑定》 #87 的时候就已经建立起一套完整的Binding、Watcher和Directive的体系。我们只需要把info指令分别push到wathcer name和wathcer age的_subs里面不就可以了,在这儿就不细说了。
我们重点看第一个问题。
解决思路还是从getter入手:定义info的function被当成了getter,那么当我们访问this.$data.info的时候,就会调用这个function。这个function又会去访问this.user.name和this.user.age,这意味着什么呢?
这意味着会去执行name和age的getter函数啊!所以我们可以自定义name和age的getter函数,让它做一些特殊的事情。
那要做什么事情呢?我们触发(notify)一个get事件,然后这个get事件会传播到$data顶层。我们在$data顶层注册一个colletDep(收集依赖)函数,这样我们不就能知道info依赖于user.name和user.age了吗?
嗯,没错,大概思路就是这样。下面展示部分关键代码,完整的代码可以参考这里
function Watcher(vm, expression, cb, ctx) {
this.id = ++uid;
this.vm = vm;
this.expression = expression;
this.cb = cb;
this.ctx = ctx || vm;
this.deps = Object.create(null);
// 这里的getter可以不去细究,其实就是根据expression(比如user.name)
// 拼接出它对应的函数,当成getter
// 你完全可以理解为调用this.getter()方法其实就是为了得到user.name的值
this.getter = expParser.compileGetter(expression);
this.initDeps(expression);
}
/**
* 要注意,这里的getter.call是完成计算属性的核心,
* 因为正是这里的getter.call, 执行了该计算属性的getter方法,
* 从而执行该计算属性所依赖的其他属性的get方法
* 从而发出get事件,冒泡到底层, 触发collectDep事件
* @param path {String} 指令表达式对应的路径, 例如: "user.name"
*/
Watcher.prototype.initDeps = function (path) {
this.addDep(path);
Observer.emitGet = true;
this.vm._activeWatcher = this;
// 就是在这儿调用info的getter,进而调用name和age的getter
// 进入触发和传播get事件
this.value = this.getter.call(this.vm, this.vm.$data);
Observer.emitGet = false;
this.vm._activeWatcher = null;
};
/**
* 收集依赖。
* 为什么需要这个东西呢?
* 因为在实现computed计算属性功能的过程中,
* 发现程序需要知晓计算出来的属性到底依赖于哪些原先就有的属性
* 这样才能做到在对应原有的属性的_subs数组中添加新属性指令的watcher事件
* @param path {String} get事件传播到顶层时的路径,比如"user.name"
* @private
*/
exports._collectDep = function (event, path) {
let watcher = this._activeWatcher;
if (watcher) {
watcher.addDep(path);
}
};
// 看,就是在这儿给$data顶层注册收集依赖的事件的
this.observer.on('set', this._updateBindingAt.bind(this))
.on('get', this._collectDep.bind(this));
有两个小细节务必要注意:
- Observer构造函数有一个emitGet属性,默认为false。当emitGet为true时,代表调用属性的getter会触发并且传播get事件,当emitGet为false时,则不会触发。只有在初始化构造Binding才将它开启,因为不能任意时候调用属性getter都触发这个get事件,这个get事件只是我们在收集依赖才需要的。
- Watcher实例有一个dep字段,它的作用是为了避免重复收集依赖。如果没有他,那么在当前例子的情况下,user的bingding._subs里面会出现两个user的watcher,name亦然。原因是我们在计算属性的时候再次读取了user和name。为了避免这种情况,需要引入dep,记录下:“哦,原来程序刚刚已经分析过这里了,忽略就好。”
具体的实现效果如下图所示,完整的代码参考这里
---EOF---