vue双向绑定原理

vue双向绑定原理

首先看一下我们正常导入vue.js之后书写的代码

    <div class="app">
        <input type="text" v-model="name">
        <span>姓名:{{ name }}</span>
        <input type="text" v-model="body.age">
        <span>年龄:{{ body.age }}</span>
    </div>
    <script>
        const vm = new Vue({
            el: '#app', // 绑定的根节点
            data: {     // 数据
                name:'小明',
                body: {
                    age: 20
                }
            }
        })
    </script>

然后我们就来自己大概写一下 vue如何实现双向绑定的
1、首先新建一个Vue实例

    <script>
        // 首先我们先新建一个Vue实例
        class Vue {
            constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
                // vue的源码也是$data,这里我们也使用$data
                this.$data = obj_instance.data;
            }
        }
    </script>
    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                name:'小明',
                body: {
                    age: 20
                }
            }
        })
        console.log(vm);
    </script>

这个时候打印的结果如图
2、双向绑定,我们当然需要监听data中的数据

        // 首先我们先新建一个Vue实例
        class Vue {
            constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
                // vue的源码也是$data,这里我们也使用$data
                this.$data = obj_instance.data;
                // 在这里,我们初始化实例的时候,需要监听data数据,我们用Observer来监听数据
                Observer(this.$data)
            }
        }
        // 监听数据变化,我们需要用到js的原生 api 
        function Observer(data_instance){
            // 遍历对象  Object.keys 可以遍历对象的key  返回key数组
            console.log(Object.keys(data_instance)); // => [name,body]
            
        }

3、数据劫持

        // 监听数据变化,我们需要用到js的原生 api  Object.defineProperty 进行数据劫持
        function Observer(data_instance){
            // 遍历对象  Object.keys 可以遍历对象的key  返回key数组
            Object.keys(data_instance).forEach(key=>{
                // Object.defineProperty(操作对象, 操作属性, {
                //     enumerable: true, // 数据是否可以被枚举
                //     configurable: true, // 属性描述符是否可以被改变
                //     get(){ }, // 属性被访问的时候触发
                //     set(){ } // 属性改变的时候触发
                // })
                let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
                Object.defineProperty(data_instance, key, {
                    enumerable: true,
                    configurable: true,
                    get(){
                        console.log(`访问了属性${key} => 值 ${value}`);
                        return value
                    },
                    set(newValue){
                        console.log(`属性${key}的值被修改为:${newValue}`);
                        value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
                    }
                })
            })
            
        }

此时就有个问题了,这样我们只能访问到第一层,此时用个递归就可解决
注意代码

        // 监听数据变化,我们需要用到js的原生 api  Object.defineProperty 进行数据劫持
        function Observer(data_instance){
            if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在  是否为object 主要作用于递归的时候
                 return 
            }

            // 遍历对象  Object.keys 可以遍历对象的key  返回key数组
            Object.keys(data_instance).forEach(key=>{
                // Object.defineProperty(操作对象, 操作属性, {
                //     enumerable: true, // 数据是否可以被枚举
                //     configurable: true, // 属性描述符是否可以被改变
                //     get(){ }, // 属性被访问的时候触发
                //     set(){ } // 属性改变的时候触发
                // })
                let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
                Observer(value) // 递归
                Object.defineProperty(data_instance, key, {
                    enumerable: true,
                    configurable: true,
                    get(){
                        console.log(`访问了属性${key} => 值 ${value}`);
                        return value
                    },
                    set(newValue){
                        console.log(`属性${key}的值被修改为:${newValue}`);
                        value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
                    }
                })
            })
            
        }

很多人听到递归就头皮发麻,但是这里看上去应该比较简单
不过这样递归又会出现一个问题,就是比如name属性,原本的value是string , 现在改为object就会出现问题,这个细节很多人都会忽略,如果此时你重新赋值为ojbect, 从控制台可以看出此object没有get set
在这里插入图片描述
此时只要在set 中添加 Observer 数据监听

        function Observer(data_instance){
            if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在  是否为object 主要作用于递归的时候
                 return 
            }

            // 遍历对象  Object.keys 可以遍历对象的key  返回key数组
            Object.keys(data_instance).forEach(key=>{
                // Object.defineProperty(操作对象, 操作属性, {
                //     enumerable: true, // 数据是否可以被枚举
                //     configurable: true, // 属性描述符是否可以被改变
                //     get(){ }, // 属性被访问的时候触发
                //     set(){ } // 属性改变的时候触发
                // })
                let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
                Observer(value) // 递归
                Object.defineProperty(data_instance, key, {
                    enumerable: true,
                    configurable: true,
                    get(){
                        console.log(`访问了属性${key} => 值 ${value}`);
                        return value
                    },
                    set(newValue){
                        console.log(`属性${key}的值被修改为:${newValue}`);
                        value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
                        Observer(newValue)
                    }
                })
            })
            
        }

目前数据监听到此为止了,下面我们肯定需要把更改的数据让页面同时也需要跟着更改,但是需要考虑的是 我们不能数据变一次,页面就跟着改一次,这样太消耗性能了。继续往下走吧
4、我们下面创建一个数据解析的函数 compile

        // html 模板解析   element:dom里挂载的元素  vm:vue实例
        function Compile(element, vm){
            vm.$el = document.querySelector(element) // 获取挂载元素
            const fragment = document.createDocumentFragment() // 创建文档碎片
            let child;
            while (child = vm.$el.firstChild) { // 遍历所有节点  将节点放入文档碎片中 (相当于一个临时空间)
                fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
            }
            
            // 此时我们需要解释 fragment 
            fragment_compile(fragment)
            function fragment_compile(node){
                const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }}  模版语法
                // 首先做个判断,看一下node类型
                if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
                    // console.log(node); 
                    // console.log(node.nodeValue);
                    const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
                    if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
                        // console.log(reg_regex);
                        const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
                        const value = arr.reduce((total, current) => total[current], vm.$data)
                        console.log(value);
                        node.nodeValue = node.nodeValue.replace(pattern, value) // 将data中的数据 替换到 元素中
                        console.log(node.nodeValue);
                    }
                    return 
                }
                // 如果不是文本类型,我们需要再去遍历
                node.childNodes.forEach(child => {
                    fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
                })
            }
            vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
        }

此时已经可以在页面中显示了(当然是需要调用 ),大家可以看到页面已经没有{{}}了,这里提醒下大家好好学习下正则,递归,es6

// 首先我们先新建一个Vue实例
        class Vue {
            constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
                // vue的源码也是$data,这里我们也使用$data
                this.$data = obj_instance.data;
                // 在这里,我们初始化实例的时候,需要监听data数据,我们用Observer来监听数据
                Observer(this.$data)
                // 调用html数据解析
                Compile(obj_instance.el, this)
            }
        }

下面,我们就需要添加大家常听到的发布者,订阅者了
5、首先先创建个Dependency 收集与通知订阅者

        // 收集与通知订阅者 类
        class Dependency{
            constructor(){
            this.subscribes = []; // 存入订阅者的信息

            }
            addSub(sub){ // 添加 订阅者
                this.subscribes.push(sub)
            }
            notify(){ // 通知订阅者的方法
                this.subscribes.forEach(sub => sub.update())
            }
        }

然后创建订阅者

        // 订阅者 类
        class Watcher{
            constructor(vm, key, callback){
                this.vm = vm // vue实例
                this.key = key // vue实例对应的属性 
                this.callback = callback // 记录如何更新文本内容
                // 临时属性,触发getter
                Dependency.temp = this
                key.split('.').reduce((total, current) => total[current], vm.$data)
                Dependency.temp = null // 防止订阅者多次加入到subscribes中
            }
            update(){
                const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data)
                this.callback(value)
            }
        }

订阅者在哪调用创建呢呢

        // html 模板解析   element:dom里挂载的元素  vm:vue实例
        function Compile(element, vm){
            vm.$el = document.querySelector(element) // 获取挂载元素
            const fragment = document.createDocumentFragment() // 创建文档碎片
            let child;
            while (child = vm.$el.firstChild) { // 遍历所有节点  将节点放入文档碎片中 (相当于一个临时空间)
                fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
            }
            
            // 此时我们需要解释 fragment 
            fragment_compile(fragment)
            function fragment_compile(node){
                const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }}  模版语法
                // 首先做个判断,看一下node类型
                if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
                    // console.log(node); 
                    // console.log(node.nodeValue);
                    const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
                    if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
                        const node_value = node.nodeValue
                        const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
                        const value = arr.reduce((total, current) => total[current], vm.$data)
                        node.nodeValue = node_value.replace(pattern, value) // 将data中的数据 替换到 元素中
                        
                        // 创建订阅者
                        new Watcher(vm, reg_regex[1], newValue=>{
                            node.nodeValue = node_value.replace(pattern, newValue) // 将data中的数据 替换到 元素中

                        })
                    }
                    return 
                }
                // 如果不是文本类型,我们需要再去遍历
                node.childNodes.forEach(child => {
                    fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
                })
            }
            vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
        }

还需要通知订阅者

        // 监听数据变化,我们需要用到js的原生 api  Object.defineProperty 进行数据劫持
        function Observer(data_instance){
            if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在  是否为object 主要作用于递归的时候
                 return 
            }
            const dependency = new Dependency()
            // 遍历对象  Object.keys 可以遍历对象的key  返回key数组
            Object.keys(data_instance).forEach(key=>{
                // Object.defineProperty(操作对象, 操作属性, {
                //     enumerable: true, // 数据是否可以被枚举
                //     configurable: true, // 属性描述符是否可以被改变
                //     get(){ }, // 属性被访问的时候触发
                //     set(){ } // 属性改变的时候触发
                // })
                let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
                Observer(value) // 递归
                Object.defineProperty(data_instance, key, {
                    enumerable: true,
                    configurable: true,
                    get(){
                        console.log(`访问了属性${key} => 值 ${value}`);
                        // 订阅者加入subscribes
                        Dependency.temp && dependency.addSub(Dependency.temp)
                        return value
                    },
                    set(newValue){
                        console.log(`属性${key}的值被修改为:${newValue}`);
                        value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
                        Observer(newValue) // 这里不用担心 啊, 不是对象就会直接被return
                        dependency.notify() // 通知 遍历自己的数组
                    }
                })
            })
            
        }

上面核心的内容已经实现了,下面只需要将有v-model 属性的input标签进行处理

        function Compile(element, vm){
            vm.$el = document.querySelector(element) // 获取挂载元素
            const fragment = document.createDocumentFragment() // 创建文档碎片
            let child;
            while (child = vm.$el.firstChild) { // 遍历所有节点  将节点放入文档碎片中 (相当于一个临时空间)
                fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
            }
            
            // 此时我们需要解释 fragment 
            fragment_compile(fragment)
            function fragment_compile(node){
                const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }}  模版语法
                // 首先做个判断,看一下node类型
                if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
                    // console.log(node); 
                    // console.log(node.nodeValue);
                    const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
                    if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
                        const node_value = node.nodeValue
                        const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
                        const value = arr.reduce((total, current) => total[current], vm.$data)
                        node.nodeValue = node_value.replace(pattern, value) // 将data中的数据 替换到 元素中
                        
                        // 创建订阅者
                        new Watcher(vm, reg_regex[1], newValue=>{
                            node.nodeValue = node_value.replace(pattern, newValue) // 将data中的数据 替换到 元素中
                        })
                    }
                    return 
                }

                if(node.nodeType === 1 && node.nodeName === 'INPUT'){ // 找到input标签中有v-modle属性的
                    // const attr = node.attributes
                    // console.log(attr);
                    const attr = Array.from(node.attributes)
                    console.log(attr);
                    attr.forEach(val => {
                        if(val.nodeName === 'v-model'){
                            console.log(val.nodeValue);
                            const value = val.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
                            console.log(value);
                            node.value = value
                            new Watcher(vm, val.nodeValue, newValue => {
                                node.value = newValue
                            })
                            node.addEventListener('input', e => {
                                const arr1 = val.nodeValue.split('.')
                                const arr2 = arr1.slice(0, arr1.length - 1)
                                const final = arr2.reduce((total, current) => {
                                    console.log(total, current);
                                    return total[current]
                                }, vm.$data)
                                console.log(arr1, arr2, final);
                                final[arr1[arr1.length - 1]] = e.target.value
                            })
                        }
                    })
                }
                // 如果不是文本类型,我们需要再去遍历
                node.childNodes.forEach(child => {
                    fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
                })
            }
            vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
        }

ps:最后附上全部代码

<!--
 * @Author: Qi
 * @version: v1.2
 * @Date: 2022-10-30 14:39:45
 * @LastEditors: Qi
 * @LastEditTime: 2022-10-30 17:27:08
 * @Descripttion: 
-->
<!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>vue 双向绑定</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="name">
        <span>姓名:{{ name }}</span>
        <input type="text" v-model="body.age">
        <span>年龄:{{ body.age }}</span>
    </div>
    <script>
        // 首先我们先新建一个Vue实例
        class Vue {
            constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
                // vue的源码也是$data,这里我们也使用$data
                this.$data = obj_instance.data;
                // 在这里,我们初始化实例的时候,需要监听data数据,我们用Observer来监听数据
                Observer(this.$data)
                Compile(obj_instance.el, this)
            }
        }
        // 监听数据变化,我们需要用到js的原生 api  Object.defineProperty 进行数据劫持
        function Observer(data_instance){
            if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在  是否为object 主要作用于递归的时候
                 return 
            }
            const dependency = new Dependency()
            // 遍历对象  Object.keys 可以遍历对象的key  返回key数组
            Object.keys(data_instance).forEach(key=>{
                // Object.defineProperty(操作对象, 操作属性, {
                //     enumerable: true, // 数据是否可以被枚举
                //     configurable: true, // 属性描述符是否可以被改变
                //     get(){ }, // 属性被访问的时候触发
                //     set(){ } // 属性改变的时候触发
                // })
                let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
                Observer(value) // 递归
                Object.defineProperty(data_instance, key, {
                    enumerable: true,
                    configurable: true,
                    get(){
                        console.log(`访问了属性${key} => 值 ${value}`);
                        // 订阅者加入subscribes
                        Dependency.temp && dependency.addSub(Dependency.temp)
                        return value
                    },
                    set(newValue){
                        console.log(`属性${key}的值被修改为:${newValue}`);
                        value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
                        Observer(newValue) // 这里不用担心 啊, 不是对象就会直接被return
                        dependency.notify() // 通知 遍历自己的数组
                    }
                })
            })
            
        }
        // html 模板解析   element:dom里挂载的元素  vm:vue实例
        function Compile(element, vm){
            vm.$el = document.querySelector(element) // 获取挂载元素
            const fragment = document.createDocumentFragment() // 创建文档碎片
            let child;
            while (child = vm.$el.firstChild) { // 遍历所有节点  将节点放入文档碎片中 (相当于一个临时空间)
                fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
            }
            
            // 此时我们需要解释 fragment 
            fragment_compile(fragment)
            function fragment_compile(node){
                const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }}  模版语法
                // 首先做个判断,看一下node类型
                if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
                    // console.log(node); 
                    // console.log(node.nodeValue);
                    const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
                    if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
                        const node_value = node.nodeValue
                        const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
                        const value = arr.reduce((total, current) => total[current], vm.$data)
                        node.nodeValue = node_value.replace(pattern, value) // 将data中的数据 替换到 元素中
                        
                        // 创建订阅者
                        new Watcher(vm, reg_regex[1], newValue=>{
                            node.nodeValue = node_value.replace(pattern, newValue) // 将data中的数据 替换到 元素中
                        })
                    }
                    return 
                }

                if(node.nodeType === 1 && node.nodeName === 'INPUT'){ // 找到input标签中有v-modle属性的
                    // const attr = node.attributes
                    // console.log(attr);
                    const attr = Array.from(node.attributes)
                    console.log(attr);
                    attr.forEach(val => {
                        if(val.nodeName === 'v-model'){
                            console.log(val.nodeValue);
                            const value = val.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
                            console.log(value);
                            node.value = value
                            new Watcher(vm, val.nodeValue, newValue => {
                                node.value = newValue
                            })
                            node.addEventListener('input', e => {
                                const arr1 = val.nodeValue.split('.')
                                const arr2 = arr1.slice(0, arr1.length - 1)
                                const final = arr2.reduce((total, current) => {
                                    console.log(total, current);
                                    return total[current]
                                }, vm.$data)
                                console.log(arr1, arr2, final);
                                final[arr1[arr1.length - 1]] = e.target.value
                            })
                        }
                    })
                }
                // 如果不是文本类型,我们需要再去遍历
                node.childNodes.forEach(child => {
                    fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
                })
            }
            vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
        }

        // 收集与通知订阅者 类
        class Dependency{
            constructor(){
            this.subscribes = []; // 存入订阅者的信息

            }
            addSub(sub){ // 添加 订阅者
                this.subscribes.push(sub)
            }
            notify(){ // 通知订阅者的方法
                this.subscribes.forEach(sub => sub.update())
            }
        }

        // 订阅者 类
        class Watcher{
            constructor(vm, key, callback){
                this.vm = vm // vue实例
                this.key = key // vue实例对应的属性 
                this.callback = callback // 记录如何更新文本内容
                // 临时属性,触发getter
                Dependency.temp = this
                key.split('.').reduce((total, current) => total[current], vm.$data)
                Dependency.temp = null // 防止订阅者多次加入到subscribes中
            }
            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:'小明',
                body: {
                    age: 20
                }
            }
        })
        console.log(vm);
    </script>
</body>
</html>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值