手寫 Vue3 數據雙向綁定 理解Proxy


手寫 Vue3 數據雙向綁定 理解Proxy

前言

vue3的 Proxy 最近貌似各大網紅公眾號都有發,我也來蹭蹭熱度寫一篇吧!我們也可以結合vue2來看看vue3到底發生了些什么變化。

目錄結構

  • Proxy是什么?
    • 簡單用法
  • 嘗試案例
    • proxy - target 參數
  • Proxy - handler 參數
    • handler
      • get()
      • set()
  • 什么叫做數據雙向綁定?
  • 簡單實現數據渲染
  • Proxy實現雙向綁定
  • 回顧 Vue2 雙向綁定實現
  • Proxy 解決了Vue2的哪些痛點
  • Proxy 的缺陷
  • 延伸閱讀

Proxy是什么?

Proxy 翻譯過來就是代理的意思,何為代理呢?就是 用 new 創建一個目標對象(traget)的虛擬化對象,然后代理之后就可以攔截JavaScript引擎內部的底層對象操作;這些底層操作被攔截后會觸發響應特定操作的陷阱函數。

簡單用法

const p = new Proxy(target, handler)

target

​ 要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。

handler

​ 一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。

嘗試案例

講再多,看再多,不如寫寫再說

Proxy - target 參數

// 定義一個空對象
let data = {};
// 創建一個 Proxy , 將 data 作為目標對象
let proxy = new Proxy(data, {});
// 修改Proxy 代理對象的name屬性
proxy.name = '嚴老濕';
console.log(proxy); // { name: '嚴老濕' }
console.log(data); // { name: '嚴老濕' }

看了上面的案例,現在的你應該已經大概知道這個 Proxy 的目標對象(target)是怎么使用的了

Proxy - handler 參數

handler 單獨抽離出來作為一個大標題是因為里面的內容有點多

handler

handler 對象是一個容納一批特定屬性的占位符對象。它包含有 Proxy 的各個捕獲器(trap)。它里面的參數有太多了,我們就拿會用到幾個講講吧!有像深究的同學可以去看看文檔 Proxy handler [1]

參數 作用
handler.get() 屬性讀取操作的捕捉器。
handler.set() 屬性設置操作的捕捉器。

handler.set

handler.set() 方法用於攔截設置屬性值的操作。

文檔上面呢基本上就是這樣寫的

// 定義一個對象
let data = {
  name: "嚴老濕",
  age: '24'
};
// handler 抽離出來
let handler = {
  set(target, prop, value) {
    console.log(target, prop, value)
  }
}
let p = new Proxy(data, handler);
p.age = 18;

個人習慣直接這樣寫

// 定義一個對象
let data = {
  name: "嚴老濕",
  age: '24'
};
// 創建一個 Proxy , 將 data 作為目標對象
let p = new Proxy(data, {
  set(target, prop, value) {
    // target = 目標對象
    // prop = 設置的屬性
    // value = 修改后的值
    console.log(target, prop, value)
    // { name: '嚴老濕', age: '24' } 'age' 18
  }
});
// 直接修改p就可以了
p.age = 18;
console.log(data)
// { name: '嚴老濕', age: '24' }

在我們設置值的時候會觸發里面的 set 方法;

我們已經捕捉到修改后的 屬性 以及 他的值

但是打印data 並沒有發生任何變化,那這還有啥用呢?

請看官方示例handler.set()[2]

在示例中出現了一個 Reflect.set()[3]

return Reflect.set(...arguments);

Reflect

Reflect對象與Proxy對象一樣,也是 ES6 為了操作對象而提供的新 API [4]

我們需要在 handler.set()return 一個 Reflect.set(...arguments) 來進行賦值給目標對象。

Reflect.set

Reflect.set方法設置target對象的name屬性等於value。如果name屬性設置了賦值函數,則賦值函數的this綁定receiver

Reflect.get

Reflect.get方法查找並返回target對象的name屬性,如果沒有該屬性,則返回undefined

let data = {
  name: "嚴老濕",
  age: '24'
};

let p = new Proxy(data, {
  set(target, prop, newV) {
    // target = 目標對象
    // prop = 設置的屬性
    // newV = 修改后的值
    return Reflect.set(...arguments)
  }
});
p.age = 18;
console.log(data)
// { name: '嚴老濕', age: 18 }

就像這樣,已經打印成功了

handler.get

剛剛我們已經將 set 理解的已經差不多了,get還會難么?我們來看看

let data = {
  name: "嚴老濕",
  age: '24'
};

let p = new Proxy(data, {
  get(target, prop) {
    // target = 目標對象
    // prop = 獲取的屬性
    console.log(target, prop)
    // { name: '嚴老濕', age: '24' } 'age'
    return Reflect.get(...arguments)
    // 這里的 Reflect.get 我們在上面已經講到了
  }
});
// 獲取
console.log(p.age)
// 24

什么叫數據雙向綁定?

當數據發生變化的時候,視圖也就發生變化,當視圖發生變化的時候,數據也會跟着同步變化。

上栗子:

html

<h2 class="app"></h2>

js

// 獲取元素
let app = document.querySelector('.app');
// 定義 data
let data = {
    name: "嚴老濕",
    age: 24
};
// 替換成data.age 此時我們頁面上應該是有個24
app.innerHTML = data.age;
// 我們在這里修改 age 
data.age = 21;
console.log(data);
// {name: "嚴老濕", age: 21}

這樣看確實沒啥毛病

但是呢在 vue 中,我們在下面異步修改data中的值,頁面上的值不應該是跟着一起變化的么?雖然data 對象已經發生變化,但是它並不能觸發一些其他操作;

<div id="Foo">
    <h2>hello {{msg}}</h2>
    <input type="text" v-model="msg">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script> let vm = new Vue({ el: '#Foo', data: { msg: "嚴家輝" } }); </script>

我們現在對雙向綁定有了一個基本的認知。

簡單實現數據渲染

等會兒我們實現雙向綁定,在此之前我們做一個數據渲染過程,也簡單的了解一下其原理

因為內容有點多,所以講解呢全部在注釋里面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="./src/index.js"></script>
</head>
<body>
    <div id="app">{{name}} 
        <h2>{{age}}</h2>
    </div>
    <script> let vm = new Reactive({ // 掛載元素 el:"#app", data:{ name:"嚴老濕", age:24 } }); </script>
</body>
</html>

index.js

class Reactive{
    // 接收參數
    constructor(options){
        this.options = options;
        // data 賦值
        this.$data = this.options.data;
        // 掛載元素
        this.el = document.querySelector(this.options.el)
        // 調用 compile 函數
        this.compile(this.el)
    }
    // 渲染數據
    compile(el){
        // 獲取el的子元素
        let child = el.childNodes;
        // 遍歷判斷是否存在文本
        [...child].forEach(node=>{
            // 如果node的類型是TEXT_NODE
            if(node.nodeType === 3){
                // 拿到文本內容
                let txt = node.textContent;
                // 正則匹配{{}} 空格
                let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
                if(reg.test(txt)){
                    let $1 = RegExp.$1;
                    this.$data[$1] && (node.textContent=txt.replace(reg,this.$data[$1]))
                }
            // 如果node的類型是ELEMENT_NODE
            }else if(node.nodeType === 1){
                // 遞歸執行
                this.compile(node)
            }
        })
    }
}

圖片來源自 公眾號律神仙ScarSu圖片來源自 公眾號律神仙ScarSu

一個簡單並且潦草一點的的渲染數據功能已經完成了

Proxy實現雙向綁定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="./src/index.js"></script>
</head>
<body>
    <div id="app">{{name}} 
        <h2>{{age}}</h2>
        <input type="text" v-model="name">
        {{name}}
    </div>
    <script> let vm = new Reactive({ // 掛載元素 el: "#app", data: { name: "嚴老濕", age: 24, } }); </script>
</body>
</html>

index.js

// EventTarget [6]
class Reactive extends EventTarget {
  // 接收參數
  constructor(options) {
    super();
    this.options = options;
    // data 賦值
    this.$data = this.options.data;
    // 掛載元素
    this.el = document.querySelector(this.options.el);
    // 調用 compile 函數
    this.compile(this.el);
    // 調用雙向綁定
    this.observe(this.$data);
  }
  // 雙向綁定
  observe(data) {
    // 備份this
    let _this = this;
    // 接收目標對象進行代理
    this.$data = new Proxy(data, {
      set(target, prop, value) {
        // 創建一個自定義事件 CustomEvent [5]
        // 事件名稱使用的是 prop 
        let event = new CustomEvent(prop, {
          // 傳入新的值
          detail: value
        })
        // 派發 event 事件
        _this.dispatchEvent(event);
        return Reflect.set(...arguments);
      }
    })
  }
  // 渲染數據
  compile(el) {
    // 獲取el的子元素
    let child = el.childNodes;
    // 遍歷判斷是否存在文本
    [...child].forEach(node => {
      // 如果node的類型是TEXT_NODE
      if (node.nodeType === 3) {
        // 拿到文本內容
        let txt = node.textContent;
        // 正則匹配
        let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
        if (reg.test(txt)) {
          let $1 = RegExp.$1;
          this.$data[$1] && (node.textContent = txt.replace(reg, this.$data[$1]))
          // 綁定自定義事件
          this.addEventListener($1, e => {
            // 替換成傳進來的 detail
            node.textContent = txt.replace(reg, e.detail)
          })
        }
        // 如果node的類型是ELEMENT_NODE
      } else if (node.nodeType === 1) {
        // 獲取attr 
        let attr = node.attributes;
        // 判斷是否存在v-model屬性
        if (attr.hasOwnProperty('v-model')) {
          // 獲取v-model中綁定的值
          let keyName = attr['v-model'].nodeValue;
          // 賦值給元素的value
          node.value = this.$data[keyName]
          // 綁定事件
          node.addEventListener('input', e => {
            // 當事件觸發的時候我們進行賦值
            this.$data[keyName] = node.value
          })
        }
        // 遞歸執行
        this.compile(node)
      }
    })
  }
}

這樣我們就實現了一個雙向綁定的小 demo ,當然代碼還不夠嚴謹,比如v-model的元素篩選都還不夠完善,只是帶大家簡單的了解一下實現邏輯

回顧 Vue2 雙向綁定實現

vue2 大部分同學刷題也經常會碰到 ,我們接下來看看vue2如何實現的呢

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <h2 id="txt"></h2>
    <input type="text" id="el">
    <script> let obj = {}; // 獲取節點 let el = document.querySelector('#el'); let txt = document.querySelector('#txt'); Object.defineProperty(obj, 'foo', { set: function (newValue) { // 修改后的值 進行賦值 txt.innerHTML = newValue; } }); // 綁定事件 el.addEventListener('input', e => { // 賦值給obj數據 obj.foo = e.target.value; }); </script>
</body>
</html>

Proxy解決了vue2的哪些痛點

  • Object.defineProperty只能劫持對象的屬性,而Proxy是直接代理對象;
  • Object.defineProperty對新增屬性需要手動進行Observe
  • vue2.x無法監控到數組下標的變化,因為vue2放棄了這個特性;
  • Proxy支持13種攔截操作,這是defineProperty所不具有的;

Proxy的缺陷

其他的不想多說,就一個 兼容真的挺難受的,硬傷了

延伸閱讀

[1] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler

[2] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set

[3] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set

[4] https://www.cnblogs.com/zczhangcui/p/6486582.html

[5] https://developer.mozilla.org/zh-CN/docs/Web/API/CustomEvent

[6] https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM