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>

在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)
    }
}

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

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

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

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
          }
      })
 })

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

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

把一个字符串修改为对象,我们发现它没有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()
            }
        })
    })
}

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){
		
	}
}
修改 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)
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)
}

4.实现发布者订阅模式

// 依赖- 收集和通知订阅者
class Dependency {
    constructor(){
        //保存订阅者信息
        this.subscribers = [];
    }
    // 添加订阅者
    addSub(sub) {
        this.subscribers.push(sub)
    }
    notity() {
        // update 调用自身方法更新
      this.subscribers.forEach(sub => sub.update())
    }
}
订阅者实现

//订阅者 
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)
    }
}

以上内容代码是根据视频总结: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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值