Vue 的 computed、watch 的實現

前言

本文章純爲學習中的經驗總結,並不是教程,如有疑問歡迎討論。javascript


準備代碼

以前有總結 Vue 雙向綁定的實現,建議先去看看再看這篇文章,這篇文章是在其基礎之上進行拓展的,因此先去瞅瞅吧,若是已經看過的話,把代碼拷過來,後面要用。html

文章地址:Vue 雙向綁定的剖析vue

對於雙向綁定的回顧

以前說過,雙向綁定是對某個屬性進行了劫持,在其get的時候進行訂閱,而後在set的時候通知更新。java

二者的概念

想一想 computed 和 watch 的實現過程,我我的的理解爲:node

  • computed 是一個訂閱者訂閱多個屬性
  • watch 是一個訂閱者訂閱一個屬性,在其更新到時執行特定的方法

因此代碼也今後入手。app

實現

computed

首先改造一下 html 文件:函數

<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="app">
      <input v-model="name" />
      <h1>{{ name }}</h1>
      <h1>{{ age }}</h1>
      <h1>{{ hello }}</h1>
      <button v-on:click="addAge">過年了,大一歲</button>
      <button v-on:click="changeName">我叫李四</button>
    </div>

    <script src="./myvue.js"></script>
    <script> var vm = new MyVue({ el: '#app', data: { name: '張三', age: 20 }, computed: { hello() { return `你好我是${this.name}, 今年${this.age}歲。`; } }, methods: { addAge() { this.age++; }, changeName() { this.name = '李四'; } } }); </script>
  </body>
</html>
複製代碼

和以前對比,就是加了一個 computed 的屬性,而後頁面上渲染出來,沒什麼特殊的含義,來解決一下JS部分吧。post

首先在 MyVue 類的構造函數中,把 computed 掛載上去:學習

class MyVue {
  constructor({ el, data, computed, methods }) {
    let obs = new Observer(data);

    this.$el = el;
    this.$data = obs.data;
    this.$computed = computed; // add
    this.$methods = methods;

    Object.keys(this.$data).forEach(i => {
      this.proxyKeys(i);
    });

    new Compile(this);
  },
  
  // code ... 
}
複製代碼

而後在 Compile 中,渲染節點的地方,應該是 node.nodeType === 3 的部分,進行一下判斷,修改以下:ui

if (node.nodeType === 3) {
  let reg = /\{\{(.*)\}\}/;
  if (reg.test(node.textContent)) {
    // 這裏文本里也許會有多個 {{}} ,{{}} 內或許會有表達式,這裏簡單處理,就取一個值
    let exp = reg.exec(node.textContent)[1].trim();

    // old code
    // let value = this.vm.$data[exp];

    // node.textContent = typeof value === 'undefined' ? '' : value;

    // new Watcher(this.vm.$data, exp, newVal => {
    // node.textContent = typeof newVal === 'undefined' ? '' : newVal;
    // });

    // new code
    if (this.vm.$data[exp]) {
      let value = this.vm.$data[exp];
      node.textContent = typeof value === 'undefined' ? '' : value;

      new Watcher(this.vm.$data, exp, newVal => {
        node.textContent = typeof newVal === 'undefined' ? '' : newVal;
      });
    } else if (this.vm.$computed[exp]) {
      let computed = new ComputedWatcher(this.vm, exp, newVal => {
          node.textContent = typeof newVal === 'undefined' ? '' : newVal;
        }
      );
      node.textContent = computed.value;
    }
  }
}
複製代碼

其實就是在取值的時候,看有 data 沒有,沒有的話就看 computed 中有沒有,有的話就用計算屬性訂閱者生成器來生成計算屬性的訂閱者。

其結構和 Watcher 差很少,傳入 vm 、計算屬性的鍵和回調函數,在更新時去更新節點的內容。

因此如今寫一下 ComputedWatcher 類吧,它和 Watcher 特別像:

class ComputedWatcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    Dep.target = this;
    this.value = this.vm.$computed[key].call(this.vm);
    Dep.target = null;
  }

  update() {
    let newVal = this.vm.$computed[this.key].call(this.vm);
    let oldVal = this.value;
    if (newVal !== oldVal) {
      this.value = newVal;
      this.cb(newVal, oldVal);
    }
  }
}
複製代碼

實際上,它就是 Watcher 複製過來,在第一次取值的時候,使用 computed 定義的方法,這樣的話,在方法中使用的每個屬性都會把這個計算屬性的訂閱者加入其訂閱器中,因此只要其中的一個更改了,就會通知這個計算屬性。

而 update 方法則執行方法比較舊值判斷是否更新節點,這樣就完成了 computed 的實現。

watch

watch 就很簡單了,它是監聽某個屬性,更新和執行相應的方法,其實和解析DOM節點同樣,更新數據後,節點的回調方法的操做是更新節點文本內容,而 watch 的回調方法就是本身寫的一些業務。

因此先改造一下 html

<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="app">
      <input v-model="name" />
      <h1>{{ name }}</h1>
      <h1>{{ age }}</h1>
      <h1>{{ hello }}</h1>
      <button v-on:click="addAge">過年了,大一歲</button>
      <button v-on:click="changeName">我叫李四</button>
    </div>

    <script src="./myvue.js"></script>
    <script> var vm = new MyVue({ el: '#app', data: { name: '張三', age: 20 }, computed: { hello() { return `你好我是${this.name}, 今年${this.age}歲。`; } }, watch: { name() { alert(`你好我是${this.name}!`); } }, methods: { addAge() { this.age++; }, changeName() { this.name = '李四'; } } }); </script>
  </body>
</html>
複製代碼

更改時就 alert 一下。

由於 watch 和頁面無關,因此僅須要更改一下 MyVue,代碼以下:

class MyVue {
    constructor({ el, data, computed, watch, methods }) {
    let obs = new Observer(data);

    this.$el = el;
    this.$data = obs.data;
    this.$computed = computed;
    this.$watch = watch; // add1
    this.$methods = methods;

    Object.keys(this.$data).forEach(i => {
      this.proxyKeys(i);
    });

    new Compile(this);
    
    // add2
    Object.keys(this.$watch).forEach(key => {
      new Watcher(this.$data, key, () => {
        this.$watch[key].call(this);
      });
    });
  },
  
  // code ... 
}
複製代碼
  • add1: 掛載一下 watch
  • add2:將每個 watch 生成爲訂閱者,回調函數裏執行相應的方法。

如此,就ok了。


全部代碼:

  • index.html
<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="app">
      <input v-model="name" />
      <h1>{{ name }}</h1>
      <h1>{{ age }}</h1>
      <h1>{{ hello }}</h1>
      <button v-on:click="addAge">過年了,大一歲</button>
      <button v-on:click="changeName">我叫李四</button>
    </div>

    <script src="./myvue.js"></script>
    <script> var vm = new MyVue({ el: '#app', data: { name: '張三', age: 20 }, computed: { hello() { return `你好我是${this.name}, 今年${this.age}歲。`; } }, watch: { name() { alert(`你好我是${this.name}!`); } }, methods: { addAge() { this.age++; }, changeName() { this.name = '李四'; } } }); </script>
  </body>
</html>
複製代碼
  • myvue.js
// 訂閱者生成器
class Observer {
  constructor(data) {
    this.data = data;
    Object.keys(data).forEach(key => {
      let value = data[key];
      let dep = new Dep();

      Object.defineProperty(data, key, {
        get() {
          Dep.target && dep.add(Dep.target);
          return value;
        },
        set(newVal) {
          if (newVal !== value) {
            value = newVal;
            dep.notify(newVal);
          }
        }
      });
    });
  }
}

// 訂閱者生成器
class Watcher {
  constructor(data, key, cb) {
    this.data = data;
    this.cb = cb;
    this.key = key;

    Dep.target = this;
    this.value = data[key];
    Dep.target = null;
  }

  update(newVal) {
    let oldVal = this.value;
    if (newVal !== oldVal) {
      this.value = newVal;
      this.cb(newVal, oldVal);
    }
  }
}

// 計算屬性訂閱者生成器
class ComputedWatcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    Dep.target = this;
    this.value = this.vm.$computed[key].call(this.vm);
    Dep.target = null;
  }

  update() {
    let newVal = this.vm.$computed[this.key].call(this.vm);
    let oldVal = this.value;
    if (newVal !== oldVal) {
      this.value = newVal;
      this.cb(newVal, oldVal);
    }
  }
}

// 訂閱庫生成器
class Dep {
  constructor() {
    this.subs = [];
  }

  add(sub) {
    this.subs.push(sub);
  }

  notify(newVal) {
    this.subs.forEach(sub => {
      sub.update(newVal);
    });
  }
}
Dep.target = null;

// 片斷解析器
class Compile {
  constructor(vm) {
    this.vm = vm;

    let el = document.querySelector(this.vm.$el);
    let fragment = document.createDocumentFragment();

    if (el) {
      while (el.firstChild) {
        fragment.appendChild(el.firstChild);
      }

      // 編譯片斷
      this.compileElement(fragment);

      el.appendChild(fragment);
    } else {
      console.log('掛載元素不存在!');
    }
  }

  compileElement(el) {
    for (let node of el.childNodes) {
      /* node.nodeType 1:元素節點 3:文本節點 */
      if (node.nodeType === 1) {
        for (let attr of node.attributes) {
          let { name: attrName, value: exp } = attr;

          // v- 表明存在指令
          if (attrName.indexOf('v-') === 0) {
            /* <div v-xxx=""> 元素上,能夠用不少指令,這裏僅作學習,因此不判斷太多了 on 事件綁定 model 表單綁定 */
            let [dir, value] = attrName.substring(2).split(':');
            if (dir === 'on') {
              // 取 vm.methods 相應的含稅,進行綁定
              let fn = this.vm.$methods[exp];
              fn && node.addEventListener(value, fn.bind(this.vm), false);
            } else if (dir === 'model') {
              // 取 vm.data 進行 input 的賦值,而且在 input 的時候更新 vm.data 上的值
              let value = this.vm.$data[exp];
              node.value = typeof value === 'undefined' ? '' : value;

              node.addEventListener('input', e => {
                if (e.target.value !== value) {
                  this.vm.$data[exp] = e.target.value;
                }
              });

              new Watcher(this.vm.$data, exp, newVal => {
                node.value = typeof newVal === 'undefined' ? '' : newVal;
              });
            }
          }
        }
      } else if (node.nodeType === 3) {
        let reg = /\{\{(.*)\}\}/;
        if (reg.test(node.textContent)) {
          // 這裏文本里也許會有多個 {{}} ,{{}} 內或許會有表達式,這裏簡單處理,就取一個值
          let exp = reg.exec(node.textContent)[1].trim();

          // old code
          // let value = this.vm.$data[exp];

          // node.textContent = typeof value === 'undefined' ? '' : value;

          // new Watcher(this.vm.$data, exp, newVal => {
          // node.textContent = typeof newVal === 'undefined' ? '' : newVal;
          // });

          // new code
          if (this.vm.$data[exp]) {
            let value = this.vm.$data[exp];
            node.textContent = typeof value === 'undefined' ? '' : value;

            new Watcher(this.vm.$data, exp, newVal => {
              node.textContent = typeof newVal === 'undefined' ? '' : newVal;
            });
          } else if (this.vm.$computed[exp]) {
            let computed = new ComputedWatcher(this.vm, exp, newVal => {
              node.textContent = typeof newVal === 'undefined' ? '' : newVal;
            });
            node.textContent = computed.value;

            // 將 computed 作一個代理,以便 this.xxx
            Object.defineProperty(this.vm, exp, {
              enumerable: false,
              configurable: true,
              get() {
                return computed.value;
              }
            });
          }
        }
      }

      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node);
      }
    }
  }
}

class MyVue {
  constructor({ el, data, computed, watch, methods }) {
    let obs = new Observer(data);

    this.$el = el;
    this.$data = obs.data;
    this.$computed = computed;
    this.$watch = watch;
    this.$methods = methods;

    Object.keys(this.$data).forEach(i => {
      this.proxyKeys(i);
    });

    new Compile(this);

    Object.keys(this.$watch).forEach(key => {
      new Watcher(this.$data, key, () => {
        this.$watch[key].call(this);
      });
    });
  }

  proxyKeys(key) {
    let _this = this;
    Object.defineProperty(_this, key, {
      enumerable: false,
      configurable: true,
      get() {
        return _this.$data[key];
      },
      set(newVal) {
        _this.$data[key] = newVal;
      }
    });
  }
}
複製代碼

PS:在 new ComputedWatcher 後,加了一個代理的方法,將計算屬性代理到 this 上面,以便以 this.xxx 調用。

相關文章
相關標籤/搜索