Vue.js响应式原理,数据双向绑定

响应式原理

数据驱动

  1. 数据响应式
    • 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
  2. 双向绑定
    • 数据改变,视图改变;视图改变,数据也随之改变
    • 我们可以使用v-model在表单元素上创建双向绑定数据
  3. 数据驱动
    • 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图

Vue2 数据响应式核心原理:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="app">
            hello
        </div>
    </body>
    <script>
        // 模拟Vue中的data选项
        let data = {
            msg: "hello",
            count: 10,
        };
        // 模拟Vue中的实例
        let vm = {};

        // 调用封装响应式函数
        proxyData(data);

        function proxyData(data) {
            // 遍历data对象的所有属性
            Object.keys(data).forEach((key) => {
                // 数据劫持:当访问或者设置vm中的成员的时候,做一些干预操作
                Object.defineProperty(vm, key, {
                    // 可枚举
                    enumerable: true,
                    // 可配置(可以使用delete删除,可以通过defineProperty重新定义)
                    configurable: true,
                    // 当前获取值时候执行
                    get() {
                        console.log("get:", data[key]);
                        return data[key];
                    },
                    // 当设置值时候执行
                    set(newValue) {
                        console.log("set", newValue);
                        if (newValue === data[key]) {
                            return;
                        }
                        data[key] = newValue;
                        // 数据更改,更新DOM的值
                        document.querySelector("#app").textContent = data[key];
                    },
                });
            });
        }

        // 测试
        vm.msg = "Hello World";
        console.log(vm.msg);
    </script>
</html>

Vue3 数据响应式原理:

  • Proxy介绍
  • 直接监听对象,而非属性
  • ES6中新增,IE不支持,性能由浏览器优化
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="app">hello</div>
    </body>
    <script>
        // 模拟Vue中的data选项
        let data = {
            msg: "hello",
            count: 22,
        };
        // 模拟Vue实例
        let vm = new Proxy(data, {
            // 当访问vm的成员时会执行
            get(target, key) {
                console.log("get:", key, target[key]);
                return target[key];
            },
            // 当设置 vm 的成员时会执行
            set(target, key, newValue) {
                console.log(console.log("set:", key, newValue));
                if (target[key] === newValue) {
                    return;
                }
                target[key] = newValue;
                document.querySelector("#app").textContent = target[key];
            },
        });
    </script>
</html>
  • 使用Proxy代理的是整个对象,对象中所有属性在访问或设置时都会触发get,set
  • 使用defineProperty处理多个属性的时候需要循环

发布-订阅模式

  • 发布者
  • 订阅者
  • 信号中心(事件中心)

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"一个信号,其他任务可以向信号中心"订阅"这个信号,从而知道什么时候自己可以开始自行. 这就叫做"发布-订阅模式"

  • 兄弟组件通信过程
    • 通过事件中心来进行通信
// eventBus.js
// 事件中心
const eventHub = new Vue()

// ComponentA.vue
// 发布者
addTodo: function() {
    // 发布消息(事件)
    eventHub.$emit('add-todo',{ text: this.newTodoText })
    this.newTodoText = ''
}

// ComponentB.vue
// 订阅者
create: function() {
    // 订阅消息(事件)
    eventHub.$on('add-todo', this.addTodo)
}
  • 模拟vue自定义事件(事件中心)
    • 可以注册多个事件名称,也可以为一个事件注册多个处理函数
    • 内部以对象的形式存储注册的事件
    • 以下是不考虑传参情况的事件处理中心
    • 有了事件中心之后在兄弟组件之间通信就可以很清晰的了解发布-订阅模式
// 事件触发器
class EventEmitter {
    constructor() {
        // 不设置原型的对象方法,可以提高性能
        // 对象的形式{ 'click': [fn1, fn2], 'change': [fn]}
        this.subs = Object.create(null);
    }
    // 注册事件(订阅)
    $on(eventType, handler) {
        // 确保事件类型的值一定是一个数组
        this.subs[eventType] = this.subs[eventType] || [];
        // 赋值操作,将事件的处理函数赋值给事件名
        this.subs[eventType].push(handler);
    }
    // 触发事件(发布)
    $emit(eventType) {
        if (this.subs[eventType]) {
            this.subs[eventType].forEach((handler) => {
                handler();
            });
        }
    }
}
let em = new EventEmitter();
// 注册事件(订阅)
em.$on("dataChange", () => {
    console.log("dataChange1");
});
em.$on("dataChange", () => {
    console.log("dataChange2");
});
// 触发事件(发布)       
em.$emit("dataChange");

观察者模式

  • 观察者(订阅者) --Watcher

    • update(): 当事件发生时,具体要做的事情
  • 目标(发布者) --Dep

    • subs数组: 存储所有的观察者
    • addSub(): 添加观察者
    • notify(): 当事件发生,调用所有观察者的update()方法
  • 没有事件中心

 // 发布者(目标)
class Dep {
    constructor() {
        // 记录所有的订阅者
        this.subs = [];
    }
    // 添加订阅者
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub);
        }
    }
    // 调用订阅者update方法
    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        });
    }
}
// 订阅者(观察者)
class Watcher {
    update() {
        console.log("update");
    }
}
// 创建发布者
let dep = new Dep();
// 创建订阅者
let watcher = new Watcher();

// 添加订阅者
dep.addSub(watcher);
// 通知所有订阅者,调用订阅者update方法
dep.notify();
  • 总结:
    • 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者方法,所以观察者模式的订阅者与发布者之间是存在依赖的
    • 发布-订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在.

发布-订阅模式和观察者模式

Vue 响应式原理

一个简易版Vue的核心功能

  1. 负责接收初始化参数
  2. 负责把data中属性注入到Vue实例,转换成getter/setter
  3. 负责调用observer监听data中所有属性的变化
  4. 负责调用compiler解析指令/插值表达式
  5. 通过dep收集依赖(收集观察者),watcher更新视图

Vue响应式流程

  • 下面用代码来具体体现一下

首先创建一个Vue类

结构

  • $options
  • $el
  • $data

  • _proxyData()
// 创建Vue类
class Vue {
    constructor(options) {
        // 1.通过属性保存选项数据
        this.$options = options || {};
        this.$data = options.data || {};
        this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el;
        // 2.把data中成员转换成getter和setter,注入到vue实例中
        this._proxyData(this.$data);
        // 3.调用observer对象,监听数据的变化
        new Observer(this.$data);
        // 4.调用compiler对象,解析指令和插值表达式
        new Compiler(this);
    }
    // 代理数据,让vue实例代理options中data的属性
    _proxyData(data) {
        // 遍历data中所有的属性
        Object.keys(data).forEach(key => {
            // 把data的属性注入vue的实例
            Object.defineProperty(this, key, {
                // 可枚举
                enumerable: true,
                // 可遍历
                configurable: true,
                // 获取值
                get() {
                    return data[key];
                },
                // 设置值
                set(newValue) {
                    if (data[key] === newValue) {
                        return;
                    }
                    data[key] = newValue;
                },
            });
        });
    }
}

Observer类(数据劫持)

功能:

  1. 负责把data选项中的属性转换成响应式数据
  2. data中某个属性也是对象,把该属性转换成响应式数据
  3. 数据变化发送通知

结构

  • walk(data)
  • defineReactive(data, key, value)
class Observer {
    constructor(data) {
        this.walk(data);
    }
    walk(data) {
        // 判断data是否有值且对象, 如果不是对象则直接返回
        if (!data || typeof data !== "object") {
            return;
        }
        // 遍历data对象的所有属性,(如果接收的data是对象则将对象内部的值设置为响应式的)
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]);
        });
    }
    // 1. 传入的data 2. 属性 3. 属性对应的值
    defineReactive(obj, key, val) {
        const self = this;
        // 负责收集依赖, 并发送通知;
        const dep = new Dep();
        // 接收到的值如果是对象则调用walk方法,将val内部属性转换成响应式数据
        this.walk(val);
        // 注意: 这里拦截obj的属性key,如果下面get的时候是获取的obj[key]会陷入一个死循环
        Object.defineProperty(obj, key, {
            // 可枚举
            enumerable: true,
            // 可配置
            configurable: true,
            get() {
                Dep.target && dep.addSub(Dep.target);
                // 这里必须是返回val,如果返回自身的obj[key]会无限触发get,陷入死循环
                return val;
            },
            set(newValue) {
                if (newValue === val) {
                    return;
                }
                val = newValue;
                // 把重新赋值的如果是个新对象内部的值也设置为响应式的
                self.walk(val);
                // 发送通知
                dep.notify();
            },
        });
    }
}

Compiler类(解析指令文本等)

功能:

  1. 负责编译模板,解析指令/插值表达式
  2. 负责页面的首次渲染
  3. 当数据变化后重新渲染视图

结构:

  • el
  • vm

  • compile(el)
  • compileElement(node)
  • compileText()
// 1. 负责编译模板,解析指令/插值表达式
// 2. 负责页面的首次渲染
// 3. 当数据变化后重新渲染视图
class Compiler {
    constructor(vm) {
        // 模板
        this.el = vm.$el;
        // vue实例
        this.vm = vm;
        this.compile(this.el);
    }
    // 编译模板,处理元素节点和文本节点
    compile(el) {
        // 获取所有的子节点。这是一个伪数组
        let childNodes = el.childNodes;
        Array.from(childNodes).forEach((node) => {
            if (this.isTextNode(node)) {
                // 处理文本节点
                this.compileText(node);
            } else if (this.isElementNode(node)) {
                // 处理元素节点
                this.compileElement(node);
            }
            // 判断node节点是否存在子节点,如果有子节点要递归调用compile
            if (node.childNodes && node.childNodes.length) {
                this.compile(node);
            }
        });
    }
    // 编译元素节点,处理指令
    compileElement(node) {
        // 获取dom的属性节点 遍历
        Array.from(node.attributes).forEach((attr) => {
            let attrName = attr.name;
            // 判断节点是否是指令
            if (this.isDirective(attrName)) {
                // 如果用if去判断,指令非常的多,判断起来代码会非常的多
                // 以下进行的判断不使用if语句
                // v-text -> text  截取前面的v-
                attrName = attrName.substr(2);
                // 属性的值就是变量名
                let key = attr.value;
                // 根据不同的指令名称调用对应的处理函数
                this.update(node, key, attrName);
            }
        });
    }
    // 集中判断是什么指令然后调用相对应的指令函数
    update(node, key, attrName) {
        // 通过截取v-的指令名称匹配相对应的指令处理函数
        let updateFn = this[attrName + "Updater"];
        // 如果匹配到了这个函数,才执行该函数的调用需要指定调用者
        updateFn && updateFn.call(this, node, this.vm[key], key);
    }
    // 关联指令的方法
    // 处理text指令,传入dom和最后输出的值
    textUpdater(node, value, key) {
        node.textContent = value;
        // 创建watcher对象,当数据改变更新视图
        new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue;
        });
    }
    // v-html处理
    htmlUpdater(node, key, value) {
        const decoder = document.createElement('div');
        decoder.innerHTML = value;
        node.innerHTML = decoder;

        new Watcher(this.vm, key, (newValue) => {
          node.innerHTML = newValue;
     });
    }

    // v-on处理 只考虑了单一事件
    onUpdater(node) {
        // 获取属性
        const list = node.attributes;
        // 遍历
      for (let i = 0; i < list.length; i++) {
      // 取得属性名
      let name = list[i];
      // 取得属性值
      const value = node.getAttribute(name);
      // 判断以v-on或者@开头
      if (/^@|^v-on:/.test(name)) {
        // 替换@或v-on为空
        name = name.replace(/^@|^v-on:/, '');
        // 向node节点添加事件
        node.addEventListener(name, value);
      }
    }
  }
    // 更新表单元素
    modelUpdater(node, value, key) {
        node.value = value;
        // 创建watcher对象,当数据改变更新视图
        new Watcher(this.vm, key, (newValue) => {
            node.value = newValue;
        });
        // 监听input事件,视图值变化时改变数据
        node.addEventListener("input", () => {
            this.vm[key] = node.value;
        });
    }

    // 编译文本节点,处理插值表达式
    compileText(node) {
        //插值表达式例子: {{ msg}}  用正常匹配插值表达式中的变量名
        let reg = /\{\{(.+?)\}\}/;
        // 获取到文本节点的内容
        let value = node.textContent;
        if (reg.test(value)) {
            // 获取到匹配的第一个分组内容(其实就是插值表达式中的变量)
            let key = RegExp.$1.trim();
            // 重新赋值
            node.textContent = value.replace(reg, this.vm[key]);

            // 创建watcher对象,当数据改变更新视图
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = value.replace(reg, newValue);
            });
        }
    }
    // 判断元素属性是否是指令
    isDirective(attrName) {
        // 指令是v-开头的
        return attrName.startsWith("v-");
    }
    // 判断节点是否是文本节点
    isTextNode(node) {
        return node.nodeType === 3;
    }
    // 判断节点是否是元素节点
    isElementNode(node) {
        return node.nodeType === 1;
    }
}

Dep类(收集观察者)

功能:

  1. 收集依赖,添加观察者(watcher)
  2. 通知所有观察者

结构:

  • subs

  • addSub(sub)
  • notify()
    
    // 发布者
    class Dep {
    constructor() {
    // 存储所有的观察者
    this.subs = [];
    }
// 添加观察者
addSub(sub) {
    if (sub && sub.update) {
        this.subs.push(sub);
    }
}
// 发送通知
notify() {
    this.subs.forEach(sub => {
        sub.update();
    });
}

}


### Watcher类(更新视图)
>功能
  1. 当数据变化触发依赖,dep通知所有的Watcher 实例更新视图
  2. 自身实例化的时候往dep对象中添加自己
>
>结构:
  - vm
  - key
  - cb
  - oldValue
  ---
  - update()
```javascript
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm;
        // data中的属性名称
        this.key = key;
        // 回调函数, 负责更新视图
        this.cb = cb;
        // 把watcher对象记录到Dep类的静态属性target
        Dep.target = this;
        // 触发get方法,在get方法中会调用addSub
        this.oldValue = this.vm[this.key];
        Dep.target = null;
    }
    // 当数据发生变化的时候更新视图
    update() {
        // 在Observer类中监听了所有数据的变化,所以一旦值改变这里获取到的就是新值
        let newValue = this.vm[this.key];
        if (this.oldValue === newValue) {
            return;
        }
        // 更新视图
        this.cb(newValue);
    }
}

最终引用使用

  • 接下来我们引入这些文件,创建一个vue实例
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>
            简易版Vue响应式流程
        </title>
    </head>
    <body>
        <div id="app">
            <h1>插值表达式</h1>
            <h3>你好{{msg}}</h3>
            <h3>{{count}}</h3>
            <h1>v-text</h1>
            <div v-text="msg"></div>
            <h1>v-model</h1>
            <input type="text" v-model="msg" />
            <input type="text" v-model="count" />
        </div>
    </body>
    <script src="js/dep.js"></script>
    <script src="js/watcher.js"></script>
    <script src="js/compiler.js"></script>
    <script src="js/observer.js"></script>
    <script src="js/miniVue.js"></script>
    <script>
        // 使用vue
        const vm = new Vue({
            el: "#app",
            data: {
                msg: "Hello Vue",
                count: 123,
                person: { name: "zs" },
            },
        });
    </script>
</html>
  • 最终的使用结果如下图

使用后结果

所有定义的属性对象均具备了get,set方法,并且修改了msg也同步更新到了视图上了!!!

讨论数量: 1

此贴备受大伙儿青睐呀~!

4年前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!