• Vue双向绑定原理


    Vue双向绑定原理

    在这里插入图片描述
    上图来源于Vue官网:https://v2.cn.vuejs.org/v2/guide/reactivity.html
    在这里插入图片描述
    我们希望 页面变化数据变化 数据变化页面变化
    需要实现 永远根据数据的变化去渲染页面 在页面改变数据也能收到通知去修改数据
    在这里插入图片描述
    1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者
    2.实现一个订阅者Watcher,每个Watcher都绑定一个更新函数,Watcher可以收到属性的变化通知并执行相应的函数,从而更新视图
    3.实现一个消息订阅器 Dep ,主要收集订阅者,当 Observe监听到发生变化,就通知Dep 再去通知Watcher去触发更新。
    4.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,若节点存在指令,则Compile初始化这类节点的模板数据(使其显示在视图上),以及初始化相应的订阅者。

    //html中部分代码
      <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="./vue.js"></script>
        <script>
            const vm = new Vue({
              el:'#app',
              data: {
                name:'小胡子',
                more: {
                  like: '羽毛球'
                }
              }
            })
            console.log(vm)
        </script>
      </body>
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在vue.js文件中模拟vue双向数据绑定

    1.创建Vue类(或创建构造函数)

    创建一个Vue实例的时候需要传入一个对象,所以constructor 传入 obj_instance 形参
    这个对象告知挂载到哪个元素上,并且设置数据。
    需要把对象里面的data属性赋值给实例里面的$data属性

    class Vue {
        constructor(obj_instance) {
            this.$data = obj_instance.data
            // 创建实例时监听
            Oberver(this.$data)
            Compile(obj_instance.el, this)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.实现数据监听 Oberver(this.$data)

    初始化实例的时候就需要调用Oberver(this.$data)
    对象属性发生变化了需要 通知 更改后的属性更新到DOM节点里去

    使用Object.defineProperty进行数据劫持-监听实例里面的数据
    //Object.defineProperty 使用方式
    /* Object.defineProperty(操作对象,操作属性,{
        enumerable:true, //属性可以枚举
        configurable:true, //属性描述符可以被改变
        get(){},
        set(){}
    })*/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    data里面每一个属性都需要监听,使用Object.keys(data_instance) 获取对象里面的每一个属性,返回的是一个数组

    //需要对每个key进行监听
     Object.keys(data_instance).forEach(key =>{	
     	let value = data_instance[key]
     	 Object.defineProperty(data_instance, key, {
              enumerable: true,
              configurable: true,
              //访问值的时候会调用get函数
              //不设置getter函数对象就没有值
              //不能直接在函数中返回属性对应的值,没有使用Object.defineProperty之前属性里面的值是没有被修改的,使用后就会被修改,使用我们在Object.defineProperty之前应该把属性存起来,再在getter函数中返回
              get() {
                  return value
              },
              //设置setter函数 不设置修改不会成功
              set(newValue) {
              //newValue 新传入的值
    			// 赋新值
    			value= newValue
              }
          })
     })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    属性值是可能一个对象,对象下面可能还有对象,我们就需要重复调用 Oberver(value) 方法 递归方式

    //递归出口  没有子属性或没检测到对象都需要终止
    if (!data_instance || typeof data_instance !== 'object') return
    
    • 1
    • 2

    把一个字符串修改为对象,我们发现它没有get 和 set 所以需要 在set中调用 Oberver(newValue) 不是对象就结束,是就进行数据劫持的操作

    //数据监听完整代码
    function Oberver(data_instance) {
        if (!data_instance || typeof data_instance !== 'object') return
        const dependency = new Dependency()
        Object.keys(data_instance).forEach(key => {
            let value = data_instance[key]
            Oberver(value) //递归 - 子属性数据劫持
            Object.defineProperty(data_instance, key, {
                enumerable: true,
                configurable: true,
                get() {
                    Dependency.temp && dependency.addSub(Dependency.temp)
                    return value
                },
                set(newValue) {
                    value = newValue
                    Oberver(newValue)
                    dependency.notity()
                }
            })
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3.HTML模板解析 - 替换内存

    获取页面元素——应用Vue数据——渲染页面
    上面的流程 获取一次元素渲染一次页面会频繁的操作DOM节点
    所以我们需要新增一个步骤
    获取页面元素——放入临时内存区域——应用Vue数据——渲染页面

    //模板解析函数
    //需要两个参数  element:Vue实例挂载的元素  vm:Vue实例(this)
    function Compile(element, vm){
    	//获取元素保存到实例的$el
        vm.$el = document.querySelector(element)
        // 放入临时内存
        // fragment临时接受dom元素
        //创建文档碎片
        const fragment = document.createDocumentFragment()
        //使用while 循环添加到fragment
        let child;
        //循环时先把页面的子节点赋值给child变量
        while (child = vm.$el.firstChild) {
            fragment.append(child)
        }
    	//所有节点都添加到fragment后 修改节点
    	//创建一个修改fragment节点的函数
    	fragment_compile(fragment)
    	function fragment_compile(node){
    		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    修改 fragment 的 fragment_compile(fragment) 函数实现
    // 替换文档碎片内容
        function fragment_compile(node) {
            // 转义花括号     \{\{\}\}
            // 匹配前后空格     \s*     \s*
             // 核心字符     \S+
            const pattern = /\{\{\s*(\S+)\s*\}\}/
            //只需要修改文本节点 文本节点为3  我们就进行数据替换 并且返回
            // 如果不是就继续循环一下文档碎片的所有子节点
            if(node.nodeType === 3){
                const xxx = node.nodeValue
                // 过滤掉其他空节点
                const result_regex = pattern.exec(node.nodeValue)
                 //如果是文本且是我们要的节点
                 //可以用vue实例访问对应的值
                if(result_regex){
                    const arr = result_regex[1].split('.')
                     //需要链式获取子属性
                    const value = arr.reduce(
                        (total, current) => total[current],vm.$data
                    )
                    node.nodeValue = xxx.replace(pattern, value)
                    // 创建订阅者
                    new Watcher(vm, result_regex[1], newValue => {
                        node.nodeValue = xxx.replace(pattern, newValue)
                    })
                } 
                //递归出口
                return
            }
    		// v-model 实现
            if(node.nodeType === 1 && node.nodeName === 'INPUT'){
                const attr = Array.from(node.attributes);
                attr.forEach(i => {
                    if(i.nodeName === 'v-model'){
                        const value = i.nodeValue.split('.').reduce(
                            (total, current) => total[current], vm.$data
                        )
                        node.value = value
                        new Watcher(vm, i.nodeName, newValue => {
                            node.value = newValues
                        })
                        node.addEventListener('input', e => {
                            console.log(i)
                            // const name = i.nodeValue
                            // vm.$data[name] = e.target.value
                            const arr1 = i.nodeValue.split('.')
                            const arr2 = arr1.slice(0, arr1.length - 1)
                            console.log(arr1)
                            console.log(arr2)
                            const final = arr2.reduce(
                                (total, current) => total[current],vm.$data
                            )
                            console.log(final)
                            final[arr1[arr1.length - 1]] = e.target.value
                        })
                    }
                })
            }
            // 节点里面可能还有节点文本节点就遍历解析 
            node.childNodes.forEach(child => fragment_compile(child))
        }
        //将文本碎片添加到vm里的$el属性里面
        vm.$el.appendChild(fragment)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    function Compile(element, vm){
        vm.$el = document.querySelector(element) 
        const fragment = document.createDocumentFragment()
        
        let child;
        while (child = vm.$el.firstChild) {
            fragment.append(child)
        }
        
        fragment_compile(fragment)
        function fragment_compile(node) {
            const pattern = /\{\{\s*(\S+)\s*\}\}/
            if(node.nodeType === 3){
                const xxx = node.nodeValue
                const result_regex = pattern.exec(node.nodeValue)
                
                if(result_regex){
                    const arr = result_regex[1].split('.')
                    const value = arr.reduce(
                        (total, current) => total[current],vm.$data
                    )
                    node.nodeValue = xxx.replace(pattern, value)
                    
                    new Watcher(vm, result_regex[1], newValue => {
                        node.nodeValue = xxx.replace(pattern, newValue)
                    })
                } 
                return
            }
    		// 元素节点 节点类型为1
            if(node.nodeType === 1 && node.nodeName === 'INPUT'){
                const attr = Array.from(node.attributes);
                attr.forEach(i => {
                    if(i.nodeName === 'v-model'){
                        const value = i.nodeValue.split('.').reduce(
                            (total, current) => total[current], vm.$data
                        )
                        node.value = value
                        
                        new Watcher(vm, i.nodeName, newValue => {
                            node.value = newValues
                        })
                        
                        node.addEventListener('input', e => {
                            const arr1 = i.nodeValue.split('.')
                            const arr2 = arr1.slice(0, arr1.length - 1)
                            const final = arr2.reduce(
                                (total, current) => total[current],vm.$data
                            )
                            final[arr1[arr1.length - 1]] = e.target.value
                        })
                    }
                })
    
            }
            node.childNodes.forEach(child => fragment_compile(child))
        }
        vm.$el.appendChild(fragment)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    4.实现发布者订阅模式

    // 依赖- 收集和通知订阅者
    class Dependency {
        constructor(){
            //保存订阅者信息
            this.subscribers = [];
        }
        // 添加订阅者
        addSub(sub) {
            this.subscribers.push(sub)
        }
        notity() {
            // update 调用自身方法更新
          this.subscribers.forEach(sub => sub.update())
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    订阅者实现
    
    //订阅者 
    class Watcher {
        constructor(vm, key, callback){
            this.vm = vm
            this.key = key
            this.callback = callback
            // 临时属性 - 触发getter
            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)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    以上内容代码是根据视频总结:https://www.bilibili.com/video/BV1934y1a7MN?spm_id_from=333.788.top_right_bar_window_history.content.click&vd_source=d69c710a9302f968dc33b3bb707e83bc
    参考文章:https://blog.csdn.net/weixin_52092151/article/details/119810514

  • 相关阅读:
    手把手教你如何编写一个Makefile文件
    本博客IDL 学习目录
    JAVA+SpringBoot心灵心理健康平台(心灵治愈交流平台)(含论文)源码
    7.2 Verilog 文件操作
    【Java盲点攻克】「时间与时区系列」让我们一起完全吃透对于时区和日期相关的功能开发原理
    从哪些方面分析Linux内核源码
    C++ 并发编程实战 第六章 设计基于锁的并发数据结构
    Zabbix与乐维监控对比分析(二)——Agent管理、自动发现、权限管理
    【微信小程序】实现投票功能(附源码)
    Spring Boot Admin 参考指南
  • 原文地址:https://blog.csdn.net/qq_38367703/article/details/126800184