computed 是什么
knockout.js和vue.js都采用observable来实现Template中的数据绑定,然而很多情况下,我们需要用到或绑定的是通过一个或多个observables所计算出来的值 -- 一个实现以下功能的computed对象:
- 当它依赖的observables改变时,它也能更新为正确的值
- computed本身也能被监听和绑定。
computed API
以下是knockout.js和vue.js中创建computed的API:
import ko from "knockout";
let x = ko.observable(1);
let plus = ko.computed(()=> x()+100);
console.log(x()); // 1
console.log(plus()); // 101
x(2); // set x
console.log(plus()); // 102
import Vue from "vue";
let vm = new Vue({
data: { x: 1 },
computed: {
plus: function () {
return this.x + 100
}
}
});
console.log(vm.x); // 1
console.log(vm.plus); // 101
vm.x = 2;
console.log(vm.plus); // 102
基本的用法都是一样的,只需传一个函数定义它的值是如何计算的。
微小的区别只是knockout中observable/computed都是函数(ob()读,ob(v)写),而vue的observable/computed为Object的property(直接读写,但重写了Object.defineProperty get/set)。
妙处在于:这里我们都只需要传求值函数,至于它怎么监听osbservable和缓存状态都不用操心,这些都在ko.computed()或new Vue()的时候隐式地做好了。
实现computed API
怎么能通过求值函数去subscribe所有依赖到的observable/computed对象呢?
-
技巧在于:我们不需要让computed主动找到所有依赖,而是让observables来主动添加当前computed:
- 设置全局变量CURRENT_COMPUTED
- 然后执行一遍求值函数
- 执行求值函数时每个依赖到的observable的get()都会被执行,所以我们只要在每个observable的get()中把当前设置全局变量CURRENT_COMPUTED添加到它的subscribers中。
- 清空CURRENT_COMPUTED=null, 这样constructor之外读observable时不会改变它们的subscribers。
实现knockout.js:
// knockout.js like:
let CURRENT_COMPUTED = null;
const Observable = val => {
const subs = [];
return new_val => {
if(new_val === undefined){ //getter
if(CURRENT_COMPUTED)
subs.push(CURRENT_COMPUTED);
} else{ //setter
if(val !== new_val){
console.log(`updated: ${val} -> ${new_val}`);
let old = val;
val = new_val;
subs.forEach(sub => sub && sub(val, old));
}
}
return val;
}
};
const Computed = valueFunc => {
let val;
const subs = [];
CURRENT_COMPUTED = () => {
let new_val = valueFunc();
if(new_val !== val){
let old = val;
val = new_val;
subs.forEach(sub => sub && sub(val, old));
}
return val;
};
val = valueFunc();
CURRENT_COMPUTED = null;
let computed = () => { // only getter
if(CURRENT_COMPUTED)
subs.push(CURRENT_COMPUTED);
return val;
};
computed.subscribe = onUpdate => {
subs.push(onUpdate);
let i = subs.length-1;
return () => subs[i] = null;
};
return computed;
};
// test:
let a = Observable(1);
let b = Observable(2);
let c = Observable(3);
let a_plus_b = Computed(() => a()+b());
a_plus_b.subscribe((new_val, old_val) =>
console.log(`a_plus_b updated: ${old_val} => ${new_val}`)
);
let a_plus_b_plus_c = Computed(() => a_plus_b()+c());
a_plus_b_plus_c.subscribe((new_val, old_val) =>
console.log(`a_plus_b_plus_c updated: ${old_val} => ${new_val}`)
);
let print = () => {
console.log('-----------');
console.log(`a() = ${a()}`);
console.log(`b() = ${b()}`);
console.log(`c() = ${c()}`);
console.log(`a_plus_b() = ${a_plus_b()}`);
console.log(`a_plus_b_plus_c() = ${a_plus_b_plus_c()}`);
console.log('-----------\n');
};
console.log("init:");
print();
console.log("a(10)");
a(10);
print();
console.log("c(3) -- no change");
c(3);
print();
console.log("c(30)");
c(30);
print();
/* output:
init:
-----------
a() = 1
b() = 2
c() = 3
a_plus_b() = 3
a_plus_b_plus_c() = 6
-----------
a(10)
updated: 1 -> 10
a_plus_b updated: 3 => 12
a_plus_b_plus_c updated: 6 => 15
-----------
a() = 10
b() = 2
c() = 3
a_plus_b() = 12
a_plus_b_plus_c() = 15
-----------
c(3) -- no change
-----------
a() = 10
b() = 2
c() = 3
a_plus_b() = 12
a_plus_b_plus_c() = 15
-----------
c(30)
updated: 3 -> 30
a_plus_b_plus_c updated: 15 => 42
-----------
a() = 10
b() = 2
c() = 30
a_plus_b() = 12
a_plus_b_plus_c() = 42
-----------
*/
- 实现vue.js:
// vue.js like
let CURRENT_COMPUTED = null;
class ObservableContainer{
define(name, val){
let subs = [];
Object.defineProperty(this, name, {
get(){
if(CURRENT_COMPUTED)
subs.push(CURRENT_COMPUTED);
return val;
},
set(x){
if(x !== val) {
console.log(`${name} updated: ${val} -> ${x}`);
val = x;
subs.forEach(sub => sub && sub())
}
return val
}
})
}
}
class ComputedContainer{
define(name, valueFunc){
let dirty = false, val;
const subs = [];
CURRENT_COMPUTED = () => {
dirty = true; // lazy evaluate
console.log(`${name} updated`);
subs.forEach(sub => sub && sub());
};
val = valueFunc();
CURRENT_COMPUTED = null;
Object.defineProperty(this, name, {
get(){
if(CURRENT_COMPUTED)
subs.push(CURRENT_COMPUTED);
if(dirty){
dirty = false;
val = valueFunc();
}
return val;
},
// subscribe(cb){
// subs.push(cb);
// let i = subs.length-1;
// return () => subs[i] = null;
// }
});
}
}
// test:
let observable = new ObservableContainer();
observable.define('a', 1);
observable.define('b', 2);
observable.define('c', 3);
let computed = new ComputedContainer();
computed.define('a_plus_b', () => observable.a + observable.b);
computed.define('a_plus_b_plus_c', () => observable.c + computed.a_plus_b);
let print = () => {
console.log('-----------');
console.log(`a = ${observable.a}`);
console.log(`b = ${observable.b}`);
console.log(`c = ${observable.c}`);
console.log(`a_plus_b = ${computed.a_plus_b}`);
console.log(`a_plus_b_plus_c = ${computed.a_plus_b_plus_c}`);
console.log('-----------\n');
};
console.log("init:");
print();
observable.a = 10;
print();
observable.c = 3; // no change
print();
observable.c = 30;
print();
/* output
init:
-----------
a = 1
b = 2
c = 3
a_plus_b = 3
a_plus_b_plus_c = 6
-----------
a updated: 1 -> 10
a_plus_b updated
a_plus_b_plus_c updated
-----------
a = 10
b = 2
c = 3
a_plus_b = 12
a_plus_b_plus_c = 15
-----------
-----------
a = 10
b = 2
c = 3
a_plus_b = 12
a_plus_b_plus_c = 15
-----------
c updated: 3 -> 30
a_plus_b_plus_c updated
-----------
a = 10
b = 2
c = 30
a_plus_b = 12
a_plus_b_plus_c = 42
-----------
*/