(手写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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容