vue双向绑定

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <span>{{name}}</span>
        <input type="text" v-model="name">
        <span>{{more.like}}</span>
        <input type="text" v-model="more.like">
    </div>
    <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> -->
    <script>
        /**
         * Observer 将data对象进行递归便利 都加上setter和getter监听数据变化
         * Complie 解析模板指令,将目标中的变量替换为数据 
         * Watcher 节点值替换的时候更新自己
         * 
         */

        class Vue {
            // 参数为对象实例 这个对象用于告知vue需要挂载到哪个元素并挂载数据
            constructor(obj_instance) {
                // 给实例赋值对象的data属性
                this.$data = obj_instance.data;
                // 进行数据劫持 监听对象里属性的变化
                Observer(this.$data);
                Complie(obj_instance.el, this);
            }
        }

        //数据监听函数
        function Observer(data_instance) {
            // 递归出口
            if (!data_instance || typeof data_instance !== "object") return;
            // 每次数据劫持一个对象时都创建Dependency实例 用于区分哪个对象对应哪个依赖实例和收集依赖
            const dependency = new Dependency();
            //
            Object.keys(data_instance).forEach((key) => {
                // 使用defineProperty后属性里的值会被修改 需要提前保存属性的值
                let value = data_instance[key];
                // 递归劫持data里的子属性
                Observer(value);
                Object.defineProperty(data_instance, key, {
                    enumerable: true,//设置属性是否可以枚举,即是否允许遍历
                    configurable: true, // 设置属性是否可以删除或编辑
                    // 收集数据依赖
                    get() {
                        // console.log(`获取了属性值 ${value}`);
                        Dependency.temp && dependency.addSub(Dependency.temp);
                        return value;
                    },
                    // 触发视图更新
                    set(newVal) {
                        // console.log(`修改了属性值`, `${value}修改为->${newVal}`);
                        value = newVal;
                        // 设置为对象的情况
                        Observer(newVal);
                        dependency.notify();
                    },
                });
            });
        }

        //模板解析 —— 替换DOM内容 把vue实例上的数据解析到页面上
        // 接收两个参数 1.vue实例挂载的元素<div id="app"> 2.vue实例
        function Complie(element, vm) {
            vm.$el = document.querySelector(element);
            // 使用文档碎片来临时存放DOM元素 减少DOM更新
            const fragment = document.createDocumentFragment();
            let child;
            // 将页面里的子节点循环放入文档碎片
            while ((child = vm.$el.firstChild)) {
                fragment.appendChild(child);
            }
            fragment_compile(fragment);
            // 替换fragment里文本节点的内容
            function fragment_compile(node) {
                // 使用正则表达式去匹配并替换节点里的{{}}
                const pattern = /\{\{\s*(\S+)\s*\}\}/;
                if (node.nodeType === 3) {
                    // 提前保存文本内容 否则文本在被替换一次后 后续的操作都会不生效
                    // 打工人: {{name}}  => 打工人:西维 如果不保存后续修改name会匹配不到{{name}} 因为已经被替换
                    const texts = node.nodeValue;
                    // 获取正则表达式匹配文本字符串获得的所有结果
                    const result_regex = pattern.exec(node.nodeValue);
                    if (result_regex) {
                        const arr = result_regex[1].split("."); // more.salary => ['more', 'salary']
                        // 使用reduce归并获取属性对应的值 = vm.$data['more'] => vm.$data['more']['salary']
                        const value = arr.reduce((total, current) => total[current], vm.$data);
                        node.nodeValue = texts.replace(pattern, value);
                        // 在节点值替换内容时 即模板解析的时候 添加订阅者
                        // 在替换文档碎片内容时告诉订阅者如何更新 即告诉Watcher如何更新自己
                        new Watcher(vm, result_regex[1], (newVal) => {
                            node.nodeValue = texts.replace(pattern, newVal);
                        });
                    }
                }
                // 替换绑定了v-model属性的input节点的内容
                if (node.nodeType === 1 && node.nodeName === "INPUT") {
                    const attr = Array.from(node.attributes);
                    attr.forEach((item) => {
                        if (item.nodeName === "v-model") {
                            const value = item.nodeValue
                                .split(".")
                                .reduce((total, current) => total[current], vm.$data);
                            node.value = value;
                            new Watcher(vm, item.nodeValue, (newVal) => {
                                node.value = newVal;
                            });
                            node.addEventListener("input", (e) => {
                                // ['more', 'salary']
                                const arr1 = item.nodeValue.split(".");
                                // ['more']
                                const arr2 = arr1.slice(0, arr1.length - 1);
                                // vm.$data.more
                                const final = arr2.reduce(
                                    (total, current) => total[current],
                                    vm.$data
                                );
                                // vm.$data.more['salary'] = e.target.value
                                final[arr1[arr1.length - 1]] = e.target.value;
                            });
                        }
                    });
                }
                // 对子节点的所有子节点也进行替换内容操作
                node.childNodes.forEach((child) => fragment_compile(child));
            }
            // 操作完成后将文档碎片添加到页面
            // 此时已经能将vm的数据渲染到页面上 但还未实现数据变动的及时更新
            vm.$el.appendChild(fragment);
        }

        //依赖 —— 实现发布-订阅模式 用于存放订阅者和通知订阅者更新
        class Dependency {
            constructor() {
                this.subscribers = []; // 用于收集依赖data的订阅者信息
            }
            addSub(sub) {
                this.subscribers.push(sub);
            }
            notify() {
                this.subscribers.forEach((sub) => sub.update());
            }
        }

        // 订阅者
        class Watcher {
            // 需要vue实例上的属性 以获取更新什么数据
            constructor(vm, key, callback) {
                this.vm = vm;
                this.key = key;
                this.callback = callback;
                //临时属性 —— 触发getter 把订阅者实例存储到Dependency实例的subscribers里面
                Dependency.temp = this;
                key.split(".").reduce((total, current) => total[current], vm.$data);
                Dependency.temp = null; // 防止订阅者多次加入到依赖实例数组里
            }
            update() {
                const value = this.key
                    .split(".")
                    .reduce((total, current) => total[current], this.vm.$data);
                this.callback(value);
            }
        }
    </script>

    <script>
        const vm = new Vue({
            el: "#app",
            data: {
                name: 'ddd',
                more: {
                    like: '666'
                }
            }
        })
    </script>
</body>

</html>
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue双向绑定原理是通过数据劫持和发布订阅模式相结合的方式来实现的。在Vue中,当用户操作View时,ViewModel会感知到变化并通知Model进行相应的改变;反之,当Model发生改变时,ViewModel也能感知到变化并使View作出相应的更新。双向绑定的核心是使用了Object.defineProperty()方法来实现。 在Vue的初始化过程中,会对data数据进行劫持监听,这个过程由监听器Observe来完成。监听器会监听所有属性,当属性发生变化时,会通知订阅者Watcher来判断是否需要更新。由于订阅者Watcher可能有多个,所以需要一个消息订阅器Dep来统一管理这些订阅者。同时,还需要一个指令解析器Compile,用来扫描和解析每个节点的相关指令,将其初始化为一个订阅者Watcher,并替换模板数据或绑定相应的函数。 当订阅者Watcher接收到属性的变化通知时,会执行对应的更新函数,从而更新视图。整个过程中,监听器Observer负责劫持并监听所有属性,订阅者Watcher负责接收属性的变化通知并执行相应的函数,消息订阅器Dep负责收集订阅者并通知Watcher触发更新,指令解析器Compile负责扫描和解析节点的指令并初始化相应的订阅者。 综上所述,Vue双向绑定原理是通过数据劫持+发布订阅模式相结合的方式来实现的,通过监听器、订阅者、消息订阅器和指令解析器等组件的协作,实现了数据和视图之间的双向绑定。 #### 引用[.reference_title] - *1* *2* [vue双向绑定原理](https://blog.csdn.net/qq_41645323/article/details/123324680)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Vue双向绑定原理](https://blog.csdn.net/weixin_52092151/article/details/119810514)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值