Vue2是如何利用Object.defineProperty实现数据的双向绑定?

我们之前说道过Object.defineProperty方法有一关键特性,就是数据劫持,通过get/set 拦截属性的读取和修改操作。Vue主要是通过数据劫持结合发布-订阅模式来实现的,利用Object.defineProperty来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

1. Vue2 实现过程

我们来具体看一下Vue的实现过程吧

  1. 在Vue类中,接收一个options参数,里面包含data等选项,在构造函数里,我需要将data对象进行响应式处理。这一步应该是通过遍历data的所有属性,并用Object.defineProperty来重新定义它们,使得当属性被访问时收集依赖,被修改时触发更新。

  2. 对于每个属性,需要有一个Dep(依赖收集器)来管理所有的Watcher实例。每当属性被读取时,就将当前的Watcher添加到Dep中;当属性被修改时,Dep就会通知所有Watcher执行更新操作。

  3. 既然有Watcher实例,那么就有一个Watcher类,它负责订阅属性的变化,并在变化时执行回调函数,比如更新视图。在Watcher的构造函数中,需要将自己设置为全局的一个目标实例,然后触发属性的getter,从而将Watcher添加到Dep中。

  4. 还有,可能需要一个编译器(Compiler)来解析模板中的指令,比如v-model,然后初始化相应的数据绑定和事件监听。例如,对于v-model,需要将input元素的值与数据属性同步,并在输入时更新数据属性,从而触发数据的setter,进一步通知更新。

现在,我需要一步步构建这些类。首先是Vue类,它初始化数据,进行响应式处理。然后,每个数据属性都有一个对应的Dep实例。当数据被访问时,收集当前的Watcher;数据变化时,通知Dep里的所有Watcher更新。

然后,Watcher类的作用是连接数据属性和具体的更新函数,比如更新DOM。当数据变化时,Watcher会被触发,执行更新函数。

2. Vue2 组成结构

总体来说应有以下结构:

  1. Vue类:

作用:

  • 接收options,处理data。

  • 将data的属性转为getter/setter。

  • 调用observe函数进行响应式处理。

  1. Observer类:

作用:

  • 遍历对象的所有属性,转为响应式。

  • 每个属性有一个Dep实例。

  1. Dep类:

作用:

  • 收集Watcher实例(subs数组)。

  • 提供addSub方法添加Watcher。

  • 提供notify方法触发所有Watcher的update。

  1. Watcher类:

作用:

  • 在实例化时,将自身赋值给Dep.target。

  • 访问对应的数据属性,触发getter,从而将自身添加到Dep中。

  • 定义update方法,执行回调函数(比如更新DOM)。

接下来是具体的代码实现。可能还需要处理多个Watcher的情况,以及如何将它们正确地关联到对应的Dep。

然后,需要处理模板编译。比如,对于v-model,需要找到元素,添加事件监听,当输入时更新数据属性。同时,初始化input的值。对于{{}}文本,需要替换为对应的数据,并建立Watcher,当数据变化时更新文本内容。

不过,我们现在只是简单展示一下实现原理,直接操作DOM元素,而不是完整的编译器。例如,手动绑定一个input和显示元素,展示双向绑定的效果。

现在,我需要将上述思路转化为代码。可能先从Vue类开始:

javascript 复制代码
class Vue{
    constructor(options){
        this.$options = options;
        this._data = typeof options.data === 'function' ? options.data() : options.data;

        //调用Observer函数进行响应式处理
        new Observer(this._data);
        this.compile();

    }

    compile() {

        // 处理v-model绑定

        const inputs = document.querySelectorAll('[v-model]');

        inputs.forEach(input => {

            const key = input.getAttribute('v-model').trim();

            input.value = this._data[key];

            input.addEventListener('input', (e) => {

                this._data[key] = e.target.value;

            });

        });

        // 处理{{ }} 绑定

        const elements = document.querySelectorAll('p,span,div'); // 简化处理,实际应根据文本内容匹配

        elements.forEach(element => {

            const matches = element.innerHTML.match(/\{\{\s*(.*?)\s*\}\}/);

            if (matches) {

                const key = matches[1].trim();

                new Watcher(this._data, key, (newVal) => {

                    element.innerHTML = newVal;

                });

                // 初始化显示

                element.innerHTML = this._data[key];

            }

        });

    }
}
javascript 复制代码
 // Observer类:
 // 遍历对象的所有属性,转为响应式
 // 每个属性有一个Dep实例     

class Observer{

   constructor(data){
       this.walk(data);
   }

   walk(data){
       Object.keys(data).forEach(key =>{
           this.defineReactive(data,key,data[key]);
       })
       console.log(Object.keys(data))
   }

   defineReactive(obj,key,val){
       const dep = new Dep();
       Object.defineProperty(obj,key,{
           enumberable:true,
           configurable:true,
           get(){
               if(Dep.target){
                   dep.addSub(Dep.target)
               }
               return val;
           },
           set(newVal){
               if(newVal === val) return;
               val = newVal;
               dep.notify();
           }
       })
   }

}
javascript 复制代码
// 收集Watcher实例(subs数组)
// 提供addSub方法添加wathcer
 // 提供notify方法触发所有的Watcher 和 update。
 class Dep{

     constructor(){
         this.subs = [];
     }
     addSub(sub){
         this.subs.push(sub);
     }
     notify(){
         this.subs.forEach(sub => sub.update())
     }
 }

 Dep.target = null;
javascript 复制代码
// 在实例化时,将自身赋值给Dep.target
// 访问对应的数据属性,触发getter,从而将自身添加到Dep中。
 // 定义update方法,执行回调函数(比如更新Dom)
 class Watcher{
     constructor(data,key,cb){
         this.data = data;
         this.key = key;
         this.cb= cb;
         this.value = this.get();
     }

     get(){
         Dep.target = this; // 将当前Watcher实例赋值给Dep.target
         const value = this.data[this.key]; // 触发getter,从而将Watcher添加到Dep中
         Dep.target = null;//收集完清空
         return value;
     }

     update(){
         const newVal = this.data[this.key];
         if (newVal !== this.value) {

             this.value = newVal;

             this.cb(newVal);

         }
     }
 }

以下是完成的可运行代码:

javascript 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue2数据的双向绑定</title>
</head>
<body>
    <input type="text" v-model="message">
    <div id="app">
        <p>{{ message }}</p>
    </div>
    <script>

        // 收集Watcher实例(subs数组)
        // 提供addSub方法添加wathcer
        // 提供notify方法触发所有的Watcher 和 update。
        class Dep{

            constructor(){
                this.subs = [];
            }
            addSub(sub){
                this.subs.push(sub);
            }
            notify(){
                this.subs.forEach(sub => sub.update())
            }
        }

        Dep.target = null;


        // 在实例化时,将自身赋值给Dep.target
        // 访问对应的数据属性,触发getter,从而将自身添加到Dep中。
        // 定义update方法,执行回调函数(比如更新Dom)
        class Watcher{
            constructor(data,key,cb){
                this.data = data;
                this.key = key;
                this.cb= cb;
                this.value = this.get();
            }

            get(){
                Dep.target = this; // 将当前Watcher实例赋值给Dep.target
                const value = this.data[this.key]; // 触发getter,从而将Watcher添加到Dep中
                Dep.target = null;//收集完清空
                return value;
            }

            update(){
                const newVal = this.data[this.key];
                if (newVal !== this.value) {

                    this.value = newVal;

                    this.cb(newVal);

                }
            }

        }

        class Vue{
            constructor(options){
                this.$options = options;
                this._data = typeof options.data === 'function' ? options.data() : options.data;

                //调用Observer函数进行响应式处理
                new Observer(this._data);
                this.compile();

            }

            compile() {

                // 处理v-model绑定

                const inputs = document.querySelectorAll('[v-model]');

                inputs.forEach(input => {

                    const key = input.getAttribute('v-model').trim();

                    input.value = this._data[key];

                    input.addEventListener('input', (e) => {

                        this._data[key] = e.target.value;

                    });

                });

                // 处理{{ }} 绑定

                const elements = document.querySelectorAll('p,span,div'); // 简化处理,实际应根据文本内容匹配

                elements.forEach(element => {

                    const matches = element.innerHTML.match(/\{\{\s*(.*?)\s*\}\}/);

                    if (matches) {

                        const key = matches[1].trim();

                        new Watcher(this._data, key, (newVal) => {

                            element.innerHTML = newVal;

                        });

                        // 初始化显示

                        element.innerHTML = this._data[key];

                    }

                });

            }
        }

            // Observer类:
            // 遍历对象的所有属性,转为响应式
            // 每个属性有一个Dep实例     

        class Observer{

            constructor(data){
                this.walk(data);
            }

            walk(data){
                Object.keys(data).forEach(key =>{
                    this.defineReactive(data,key,data[key]);
                })
                console.log(Object.keys(data))
            }

            defineReactive(obj,key,val){
                const dep = new Dep();
                Object.defineProperty(obj,key,{
                    enumberable:true,
                    configurable:true,
                    get(){
                        if(Dep.target){
                            dep.addSub(Dep.target)
                        }
                        return val;
                    },
                    set(newVal){
                        if(newVal === val) return;
                        val = newVal;
                        dep.notify();
                    }
                })
            }

        }

      
        new Vue({
            data: () => ({
                message: "hello world"
            })
        });
    </script>
</body>
</html>

看完了此篇文章,您是不是对Vue2实现原理有了更深刻的理解呢,如果觉得对您有帮助,还请一键三连哦!非常感谢!

相关推荐
伍哥的传说15 分钟前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang45322 分钟前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself24337 分钟前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
苹果醋338 分钟前
iview中实现点击表格单元格完成编辑和查看(span和input切换)
运维·vue.js·spring boot·nginx·课程设计
武昌库里写JAVA40 分钟前
iView Table组件二次封装
vue.js·spring boot·毕业设计·layui·课程设计
三口吃掉你42 分钟前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat
Trust yourself24344 分钟前
在easyui中如何设置自带的弹窗,有输入框
前端·javascript·easyui
烛阴1 小时前
Tile Pattern
前端·webgl
前端工作日常1 小时前
前端基建的幸存者偏差
前端·vue.js·前端框架
Electrolux1 小时前
你敢信,不会点算法没准你赛尔号都玩不明白
前端·后端·算法