实现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
-----------
 */

你可能感兴趣的:(实现computed (knockout.js, vue.js))