基於 Proxy 實現簡易版 Vue

分解剖析

  1. 實現 new Vue() 實例化
  2. 實現 {{ prop }} 綁定值
  3. 實現 v-model 雙向綁定值
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue</title>
  </head>
  <body>
    <div id="app">
      <input v-model="text" />
      {{text}}
      <span>{{text}}</span>
    </div>
    <script src="./vue.js"></script>
    <script> var app = new Vue({ el: "#app", data: { text: "hello world", }, }); </script>
  </body>
</html>
複製代碼

實現 class Vue

初始化

// 這裏繼承 EventTarget 提供 Vue 能夠接收事件、而且能夠建立偵聽器的功能
class Vue extends EventTarget {
  constructor(options) {
    this.options = options;
    this.$el = document.querySelector(options.$el);
    // 數據雙向綁定
    this.data = this.observerData(options.data);
    // 數據模板渲染
    this.compileTemplate(this.$el);
  }
}
複製代碼

渲染模板

  • 遍歷子元素,拆解文本節點,拆解文本中符合 {{**}} 特徵數據值,綁定data中的值;
  • 元素節點中涵蓋 v-model 屬性的對該屬性值進行數據data綁定;
compileTemplate(node) {
  // 子節點
  const children = node.childNodes;
  children.forEach((it) => {
    if (it.nodeType === 3) {
      // text 文本節點
      // 正則匹配 {{}} 特徵的綁定值
      const regexp = /\{\{\s*([^\s\{\}]+)\s*\}\}/gi;
      const textContent = it.textContent;
      if (textContent.match(regexp)) {
        const prop = RegExp.$1;
        it.textContent = textContent.replace(regexp, this.data[prop]);
        // 節點事件響應監聽
        // 用於接收屬性 set 後的事件響應
        this.addEventListener(
          prop,
          function (event) {
            it.textContent = textContent.replace(regexp, event.detail);
          },
          false
        );
      }
    } else if (it.nodeType === 1) {
      // node 元素節點
      this.compileTemplate(it);
      // check v-model
      const attrs = it.attributes;

      if (attrs.hasOwnProperty("v-model")) {
        const _this = this;
        const prop = attrs["v-model"].nodeValue;
        it.value = this.data[prop];
        // 監聽輸入 change
        it.addEventListener(
          "input",
          function (event) {
            // TODO 入口須要作XSS校驗
            _this.data[prop] = event.target.value;
          },
          false
        );
      }
    }
  });
}
複製代碼

數據雙向綁定

// 雙向綁定
  observerData(data) {
    const _this = this;
    return new Proxy(data, {
      set: function (target, prop, newValue) {
        // 建立 set 屬性事件
        const event = new CustomEvent(prop, { detail: newValue });
        // 廣播該 set 屬性事件
        _this.dispatchEvent(event);

        return Reflect.set(...arguments);
      },
    });
  }
複製代碼

相關對象

EventTarget

EventTarget 是一個 DOM 接口,由能夠接收事件、而且能夠建立偵聽器的對象實現。javascript

Reflect

Reflect 是一個內置的對象,它提供攔截 JavaScript 操做的方法。這些方法與 proxy handlers 的方法相同。Reflect 不是一個函數對象,所以它是不可構造的。
與大多數全局對象不一樣,Reflect 不是一個構造函數。你不能將其與一個 new 運算符一塊兒使用,或者將 Reflect 對象做爲一個函數來調用。Reflect 的全部屬性和方法都是靜態的(就像 Math 對象)。html

  • Reflect.get(): 獲取對象身上某個屬性的值,相似於 target[name]。
  • Reflect.set(): 將值分配給屬性的函數。返回一個 Boolean,若是更新成功,則返回 true。

Proxy

Proxy 對象用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。vue

  • target:要使用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。
  • handler:一個一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理 p 的行爲。
    • handler.get():屬性讀取操做的捕捉器。

      該方法會攔截目標對象的如下操做:java

      1. 訪問屬性: proxy[foo]proxy.bar
      2. 訪問原型鏈上的屬性: Object.create(proxy)[foo]
      3. Reflect.get():Reflect.get()方法與從 對象 (target[propertyKey]) 中讀取屬性相似,但它是經過一個函數執行來操做的。
    • handler.set():屬性設置操做的捕捉器。

      該方法會攔截目標對象的如下操做:node

      1. 指定屬性值:proxy[foo] = barproxy.foo = bar
      2. 指定繼承者的屬性值:Object.create(proxy)[foo] = bar
      3. Reflect.set():靜態方法 Reflect.set() 工做方式就像在一個對象上設置一個屬性。
const handler = {
  get: function(target, prop, receiver){
    // 攔截讀取
    return Reflect.get(...arguments);
  },
  set: function(target, prop, newValue, receiver){
    // 攔截設置
    return Reflect.set(...arguments);
  }
};
const p = new Proxy(target, handler);
複製代碼

CustomEvent

CustomEvent 事件是由程序建立的,能夠有任意自定義功能的事件。數組

  • CustomEvent.detail: 只讀,任什麼時候間初始化時傳入的數據

完整代碼

class Vue extends EventTarget {
  constructor(options) {
    super();

    this.options = options;
    this.$el = document.querySelector(options.el);
    this.data = this.observerData(options.data);
    this.compileTemplate(this.$el);
  }

  // 雙向綁定
  observerData(data) {
    const _this = this;
    return new Proxy(data, {
      set: function (target, prop, newValue) {
        // 事件發佈
        const event = new CustomEvent(prop, { detail: newValue });
        _this.dispatchEvent(event);

        return Reflect.set(...arguments);
      },
    });
  }

  // 模板編譯
  compileTemplate(node) {
    const children = node.childNodes;
    children.forEach((it) => {
      if (it.nodeType === 3) {
        // text 文本節點
        const regexp = /\{\{\s*([^\s\{\}]+)\s*\}\}/gi;
        const textContent = it.textContent;
        if (textContent.match(regexp)) {
          const prop = RegExp.$1;
          it.textContent = textContent.replace(regexp, this.data[prop]);
          // 事件接收
          this.addEventListener(
            prop,
            function (event) {
              it.textContent = textContent.replace(regexp, event.detail);
            },
            false
          );
        }
      } else if (it.nodeType === 1) {
        // node 元素節點
        this.compileTemplate(it);
        // check v-model
        const attrs = it.attributes;

        if (attrs.hasOwnProperty("v-model")) {
          const _this = this;
          const prop = attrs["v-model"].nodeValue;
          it.value = this.data[prop];
          it.addEventListener(
            "input",
            function (event) {
              // TODO 入口須要作XSS校驗
              _this.data[prop] = event.target.value;
            },
            false
          );
        }
      }
    });
  }
}
複製代碼

相關文章
相關標籤/搜索