(手寫vue/vue3)使用發(fā)布訂閱模式實(shí)現(xiàn)vue雙向綁定[Object.defineProperty、new Proxy]

演示效果

custom-vue.gif

一、問題:在 new Vue() 的時(shí)候發(fā)生了什么?vue雙向綁定是如何實(shí)現(xiàn)的?

回顧在vue中的用法:

new Vue({
  el: '#app',
  data: {
    nickname: '雙流兒',
    age: 18
  }
})

二、分析

  • 在vue內(nèi)部其實(shí)是使用的發(fā)布訂閱模式,其中observe方法設(shè)置需要觀察(監(jiān)聽)的數(shù)據(jù),compile方法遍歷dom節(jié)點(diǎn)(解析指令),拿到指令綁定的key,再根據(jù)key設(shè)置需要觀察的數(shù)據(jù)和訂閱管理器
  • 在執(zhí)行new操作的時(shí)候傳入了el-需要掛載到的dom id,data-綁定的數(shù)據(jù)。
    今天我們來實(shí)現(xiàn)一個(gè)v-model、v-text(包括{{ xxx }})
  • dom結(jié)構(gòu)
<div id="app">
    <h1>昵稱</h1>
    <div v-text="nickname"></div>
    <input type="text" v-model="nickname">
    <br>
    <h1>年齡</h1>
    <div>{{ age }}</div>
    <input type="text" v-model="age">
</div>
  • vue2實(shí)例
new Vue({
    el: '#app',
    data: {
        nickname: '雙流兒',
        age: 18
    }
});

三、定義一個(gè)Vue類

class Vue {
  constructor({ el, data }) {
      // 獲取dom
      this.$el = document.querySelector(el);
      // 監(jiān)聽(觀察)的數(shù)據(jù)
      this.$data = data || {};
      // 訂閱每個(gè)key(訂閱管理器)
      this.$directives = {};
      this.observe(this.$data);
      this.compile(this.$el);
  }
}

四、監(jiān)聽器observe

// 設(shè)置監(jiān)聽數(shù)據(jù)
observe(data) {
    const _this = this;
    for (const key in data) {
        // 當(dāng)前每項(xiàng)的value
        let value = data[key];
        if (typeof value === 'object') this.observe(value);
        Object.defineProperty(data, key, {
            enumerable: true, // 設(shè)置屬性可枚舉
            configurable: true, // 設(shè)置屬性可刪除
            get() {
                return value;
            },
            set(newValue) {
                // 新的值與原來的值相等就不用執(zhí)行以下(更新)操作
                if (newValue === value) return;
                value = newValue;
                // 監(jiān)聽到值改變后更新對應(yīng)指令的數(shù)據(jù)
                _this.$directives[key].forEach(fn => {
                    fn.update();
                });
            }
        });
    }
}

五、解析器compile

// 設(shè)置指令(設(shè)置每個(gè)訂閱者)
setDirective(node, key, attr) {
    const watcher = new Watcher({ node, key, attr, data: this.$data });
    if (this.$directives[key]) this.$directives[key].push(watcher);
    else this.$directives[key] = [watcher];
}
// 解析器-遍歷拿到dom上的指令(這里其實(shí)是把指令當(dāng)做自定義屬性來處理)
compile(dom) {
    const _this = this;
    const reg = /\{\{(.*)\}\}/; // 來匹配{{ xxx }}中的xxx
    const ndoes = dom.childNodes; // 節(jié)點(diǎn)集
    // ndoes是類數(shù)組對象不能使用es迭代器,需要轉(zhuǎn)成數(shù)據(jù)
    Array.from(ndoes).forEach(node => {
        // 如果node還有子項(xiàng),執(zhí)行遞歸
        if (node.childNodes.length) _this.compile(node);
        // 在本例中使用nodeType來判斷是什么類型,如nodeType為3時(shí)表示node的子節(jié)點(diǎn)有且僅有一個(gè)文本類型,也就是{{ xxx }}
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                const key = RegExp.$1.trim(); // $1獲取reg匹配到的第一個(gè)值
                // 聲明 {{ xxx }} 為text類型
                _this.setDirective(node, key, 'nodeValue');
            }
        }
        if (node.nodeType === 1) {
            // v-text
            if (node.hasAttribute('v-text')) {
                const key = node.getAttribute('v-text'); // key就是實(shí)例化Vue是傳入的nickname/age
                node.removeAttribute('v-text'); // 移除node上的自定義屬性
                _this.setDirective(node, key, 'textContent');
            }
            // v-model 且node必須是input標(biāo)簽
            if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
                const key = node.getAttribute('v-model'); // key就是實(shí)例化Vue是傳入的nickname/age
                node.removeAttribute('v-model'); // 移除node上的自定義屬性
                _this.setDirective(node, key, 'value');
                // 設(shè)置input事件監(jiān)聽
                node.addEventListener('input', e => {
                    _this.$data[key] = e.target.value;
                });
            }
        }
    });
}

六、觀察者Watcher

class Watcher {
    constructor({ node, key, attr, data }) {
        this.node = node; // 指令對應(yīng)的DOM節(jié)點(diǎn)
        this.key = key; // data的key
        this.attr = attr; // 綁定的html原生屬性,本例v-text對應(yīng)textContent
        this.data = data; // 監(jiān)聽的數(shù)據(jù)
        this.update(); // 初始化更新數(shù)據(jù)
    }
    // 更新
    update() {
        this.node[this.attr] = this.data[this.key];
    }
}

以上使用 Object.defineProperty 來實(shí)現(xiàn)數(shù)據(jù)劫持,那么怎么使用ES6的Proxy代理數(shù)據(jù)呢?
我們只需要修改 observe 方法

七、使用Proxy實(shí)現(xiàn)監(jiān)聽器

observe(data) {
    const _this = this;
   this.$data = new Proxy(data, {
       get(target, key) {
           return target[key];
       },
       set(target, key, value) {
           const status = Reflect.set(target, key, value);
           if (status) {
               // 當(dāng)status為true時(shí),表示數(shù)據(jù)已經(jīng)改變
               _this.$directives[key].forEach(fn => {
                   fn.update();
               });
           }
           return status;
       }
   });
}

八、Object.defineProperty vs Proxy

從上可以看出,在使用Object.defineProperty時(shí),需要遞歸遍歷data中的每個(gè)屬性,Proxy不需要,所以Proxy性能會(huì)優(yōu)于Object.defineProperty,這就是說vue3初始化比vue2性能更好的原因之一。

九、在vue3中實(shí)現(xiàn)數(shù)據(jù)雙向綁定

思路同上,這里是把Vue作為一個(gè)對象

class Watcher {
    constructor({ node, key, attr, data }) {
        this.node = node; // 指令對應(yīng)的DOM節(jié)點(diǎn)
        this.key = key; // data的key
        this.attr = attr; // 綁定的html原生屬性,本例v-text對應(yīng)textContent
        this.data = data; // 監(jiān)聽的數(shù)據(jù)
        this.update(); // 初始化更新數(shù)據(jù)
    }
    // 更新
    update() {
        this.node[this.attr] = this.data[this.key];
    }
}

const Vue = {
    $data: {},
    $directives: {},
    createApp({ data }) {
        const _this = this;
        this.$data = new Proxy(typeof data === 'function' ? data(): data, {
           get(target, key) {
               return target[key];
           },
           set(target, key, value) {
               const status = Reflect.set(target, key, value);
               if (status) {
                   // 當(dāng)status
                   _this.$directives[key].forEach(fn => {
                       fn.update();
                   });
               }
               return status;
           }
        });
        return this;
    },
    mount(el) {
        this.$el = document.querySelector(el);
        this.compile(this.$el);
    },
    // 設(shè)置指令(設(shè)置每個(gè)訂閱者)
    setDirective(node, key, attr) {
        const watcher = new Watcher({ node, key, attr, data: this.$data });
        if (this.$directives[key]) this.$directives[key].push(watcher);
        else this.$directives[key] = [watcher];
    },
    compile(dom) {
        const _this = this;
        const reg = /\{\{(.*)\}\}/; // 來匹配{{ xxx }}中的xxx
        const ndoes = dom.childNodes; // 節(jié)點(diǎn)集
        // ndoes是類數(shù)組對象不能使用es迭代器,需要轉(zhuǎn)成數(shù)據(jù)
        Array.from(ndoes).forEach(node => {
            // 如果node還有子項(xiàng),執(zhí)行遞歸
            if (node.childNodes.length) _this.compile(node);
            // 在本例中使用nodeType來判斷是什么類型,如nodeType為3時(shí)表示node的子節(jié)點(diǎn)有且僅有一個(gè)文本類型,也就是{{ xxx }}
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    const key = RegExp.$1.trim(); // $1獲取reg匹配到的第一個(gè)值
                    // 聲明 {{ xxx }} 為text類型
                    _this.setDirective(node, key, 'nodeValue');
                }
            }
            if (node.nodeType === 1) {
                // v-text
                if (node.hasAttribute('v-text')) {
                    const key = node.getAttribute('v-text'); // key就是實(shí)例化Vue是傳入的nickname/age
                    node.removeAttribute('v-text'); // 移除node上的自定義屬性
                    _this.setDirective(node, key, 'textContent');
                }
                // v-model 且node必須是input標(biāo)簽
                if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
                    const key = node.getAttribute('v-model'); // key就是實(shí)例化Vue是傳入的nickname/age
                    node.removeAttribute('v-model'); // 移除node上的自定義屬性
                    _this.setDirective(node, key, 'value');
                    // 設(shè)置input事件監(jiān)聽
                    node.addEventListener('input', e => {
                        _this.$data[key] = e.target.value;
                    });
                }
            }
        });
    }
};
const obj = {
    data() {
        return {
            nickname: '雙流兒',
            age: 18
        }
    }
}
Vue.createApp(obj).mount('#app');
  • vue3實(shí)例
const obj = {
    data() {
        return {
            nickname: '雙流兒',
            age: 18
        }
    }
}
Vue.createApp(obj).mount('#app');

總結(jié)

不管哪種思路都需要:

  • 觀察者observe
  • 解析器compile
  • 監(jiān)聽器Watcher
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容