1. 程式人生 > 前端設計 >基於 Proxy 實現簡易版 Vue

基於 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 介面,由可以接收事件、並且可以建立偵聽器的物件實現。

Reflect

Reflect 是一個內建的物件,它提供攔截 JavaScript 操作的方法。這些方法與 proxy handlers 的方法相同。Reflect 不是一個函式物件,因此它是不可構造的。
與大多數全域性物件不同,Reflect 不是一個建構函式。你不能將其與一個 new 運算子一起使用,或者將 Reflect 物件作為一個函式來呼叫。Reflect 的所有屬性和方法都是靜態的(就像 Math 物件)。

  • Reflect.get(): 獲取物件身上某個屬性的值,類似於 target[name]。
  • Reflect.set(): 將值分配給屬性的函式。返回一個 Boolean,如果更新成功,則返回 true。

Proxy

Proxy 物件用於定義基本操作的自定義行為(如屬性查詢、賦值、列舉、函式呼叫等)。

  • target:要使用 Proxy 包裝的目標物件(可以是任何型別的物件,包括原生陣列,函式,甚至另一個代理)。
  • handler:一個通常以函式作為屬性的物件,各屬性中的函式分別定義了在執行各種操作時代理 p 的行為。
    • handler.get():屬性讀取操作的捕捉器。

      該方法會攔截目標物件的以下操作:

      1. 訪問屬性: proxy[foo]proxy.bar
      2. 訪問原型鏈上的屬性: Object.create(proxy)[foo]
      3. Reflect.get():Reflect.get()方法與從 物件 (target[propertyKey]) 中讀取屬性類似,但它是通過一個函式執行來操作的。
    • handler.set():屬性設定操作的捕捉器。

      該方法會攔截目標物件的以下操作:

      1. 指定屬性值:proxy[foo] = barproxy.foo = bar
      2. 指定繼承者的屬性值:Object.create(proxy)[foo] = bar
      3. Reflect.set():靜態方法 Reflect.set() 工作方式就像在一個物件上設定一個屬性。
const handler = {
  get: function(target,receiver){
    // 攔截讀取
    return Reflect.get(...arguments);
  },set: function(target,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,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
          );
        }
      }
    });
  }
}
複製程式碼