实现computed (knockout.js, vue.js)

computed 是什么

knockout.js和vue.js都采用observable来实现Template中的数据绑定,然而很多情况下,我们需要用到或绑定的是通过一个或多个observables所计算出来的值 -- 一个实现以下功能的computed对象:

  1. 当它依赖的observables改变时,它也能更新为正确的值
  2. 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:

    1. 设置全局变量CURRENT_COMPUTED
    2. 然后执行一遍求值函数
    3. 执行求值函数时每个依赖到的observable的get()都会被执行,所以我们只要在每个observable的get()中把当前设置全局变量CURRENT_COMPUTED添加到它的subscribers中。
    4. 清空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
-----------
 */

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260