(手写vue/vue3)使用发布订阅模式实现vue双向绑定[Object.defineProperty、new Proxy]

演示效果

custom-vue.gif

一、问题:在 new Vue() 的时候发生了什么?vue双向绑定是如何实现的?

回顾在vue中的用法:

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

二、分析

  • 在vue内部其实是使用的发布订阅模式,其中observe方法设置需要观察(监听)的数据,compile方法遍历dom节点(解析指令),拿到指令绑定的key,再根据key设置需要观察的数据和订阅管理器
  • 在执行new操作的时候传入了el-需要挂载到的dom id,data-绑定的数据。
    今天我们来实现一个v-model、v-text(包括{{ xxx }})
  • dom结构
<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实例
new Vue({
    el: '#app',
    data: {
        nickname: '双流儿',
        age: 18
    }
});

三、定义一个Vue类

class Vue {
  constructor({ el, data }) {
      // 获取dom
      this.$el = document.querySelector(el);
      // 监听(观察)的数据
      this.$data = data || {};
      // 订阅每个key(订阅管理器)
      this.$directives = {};
      this.observe(this.$data);
      this.compile(this.$el);
  }
}

四、监听器observe

// 设置监听数据
observe(data) {
    const _this = this;
    for (const key in data) {
        // 当前每项的value
        let value = data[key];
        if (typeof value === 'object') this.observe(value);
        Object.defineProperty(data, key, {
            enumerable: true, // 设置属性可枚举
            configurable: true, // 设置属性可删除
            get() {
                return value;
            },
            set(newValue) {
                // 新的值与原来的值相等就不用执行以下(更新)操作
                if (newValue === value) return;
                value = newValue;
                // 监听到值改变后更新对应指令的数据
                _this.$directives[key].forEach(fn => {
                    fn.update();
                });
            }
        });
    }
}

五、解析器compile

// 设置指令(设置每个订阅者)
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上的指令(这里其实是把指令当做自定义属性来处理)
compile(dom) {
    const _this = this;
    const reg = /\{\{(.*)\}\}/; // 来匹配{{ xxx }}中的xxx
    const ndoes = dom.childNodes; // 节点集
    // ndoes是类数组对象不能使用es迭代器,需要转成数据
    Array.from(ndoes).forEach(node => {
        // 如果node还有子项,执行递归
        if (node.childNodes.length) _this.compile(node);
        // 在本例中使用nodeType来判断是什么类型,如nodeType为3时表示node的子节点有且仅有一个文本类型,也就是{{ xxx }}
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                const key = RegExp.$1.trim(); // $1获取reg匹配到的第一个值
                // 声明 {{ 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就是实例化Vue是传入的nickname/age
                node.removeAttribute('v-text'); // 移除node上的自定义属性
                _this.setDirective(node, key, 'textContent');
            }
            // v-model 且node必须是input标签
            if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
                const key = node.getAttribute('v-model'); // key就是实例化Vue是传入的nickname/age
                node.removeAttribute('v-model'); // 移除node上的自定义属性
                _this.setDirective(node, key, 'value');
                // 设置input事件监听
                node.addEventListener('input', e => {
                    _this.$data[key] = e.target.value;
                });
            }
        }
    });
}

六、观察者Watcher

class Watcher {
    constructor({ node, key, attr, data }) {
        this.node = node; // 指令对应的DOM节点
        this.key = key; // data的key
        this.attr = attr; // 绑定的html原生属性,本例v-text对应textContent
        this.data = data; // 监听的数据
        this.update(); // 初始化更新数据
    }
    // 更新
    update() {
        this.node[this.attr] = this.data[this.key];
    }
}

以上使用 Object.defineProperty 来实现数据劫持,那么怎么使用ES6的Proxy代理数据呢?
我们只需要修改 observe 方法

七、使用Proxy实现监听器

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) {
               // 当status为true时,表示数据已经改变
               _this.$directives[key].forEach(fn => {
                   fn.update();
               });
           }
           return status;
       }
   });
}

八、Object.defineProperty vs Proxy

从上可以看出,在使用Object.defineProperty时,需要递归遍历data中的每个属性,Proxy不需要,所以Proxy性能会优于Object.defineProperty,这就是说vue3初始化比vue2性能更好的原因之一。

九、在vue3中实现数据双向绑定

思路同上,这里是把Vue作为一个对象

class Watcher {
    constructor({ node, key, attr, data }) {
        this.node = node; // 指令对应的DOM节点
        this.key = key; // data的key
        this.attr = attr; // 绑定的html原生属性,本例v-text对应textContent
        this.data = data; // 监听的数据
        this.update(); // 初始化更新数据
    }
    // 更新
    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) {
                   // 当status
                   _this.$directives[key].forEach(fn => {
                       fn.update();
                   });
               }
               return status;
           }
        });
        return this;
    },
    mount(el) {
        this.$el = document.querySelector(el);
        this.compile(this.$el);
    },
    // 设置指令(设置每个订阅者)
    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; // 节点集
        // ndoes是类数组对象不能使用es迭代器,需要转成数据
        Array.from(ndoes).forEach(node => {
            // 如果node还有子项,执行递归
            if (node.childNodes.length) _this.compile(node);
            // 在本例中使用nodeType来判断是什么类型,如nodeType为3时表示node的子节点有且仅有一个文本类型,也就是{{ xxx }}
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    const key = RegExp.$1.trim(); // $1获取reg匹配到的第一个值
                    // 声明 {{ 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就是实例化Vue是传入的nickname/age
                    node.removeAttribute('v-text'); // 移除node上的自定义属性
                    _this.setDirective(node, key, 'textContent');
                }
                // v-model 且node必须是input标签
                if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
                    const key = node.getAttribute('v-model'); // key就是实例化Vue是传入的nickname/age
                    node.removeAttribute('v-model'); // 移除node上的自定义属性
                    _this.setDirective(node, key, 'value');
                    // 设置input事件监听
                    node.addEventListener('input', e => {
                        _this.$data[key] = e.target.value;
                    });
                }
            }
        });
    }
};
const obj = {
    data() {
        return {
            nickname: '双流儿',
            age: 18
        }
    }
}
Vue.createApp(obj).mount('#app');
  • vue3实例
const obj = {
    data() {
        return {
            nickname: '双流儿',
            age: 18
        }
    }
}
Vue.createApp(obj).mount('#app');

总结

不管哪种思路都需要:

  • 观察者observe
  • 解析器compile
  • 监听器Watcher
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容