美文网首页
(手写vue/vue3)使用发布订阅模式实现vue双向绑定[Ob

(手写vue/vue3)使用发布订阅模式实现vue双向绑定[Ob

作者: eks | 来源:发表于2021-05-27 16:04 被阅读0次

    演示效果

    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

    相关文章

      网友评论

          本文标题:(手写vue/vue3)使用发布订阅模式实现vue双向绑定[Ob

          本文链接:https://www.haomeiwen.com/subject/alersltx.html