vue的双向数据绑定原理


vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。

具体步骤:

第一步: 需要observer(观察者)对数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

第二步: compile(模板解析器)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

第三步: Watcher(订阅者)是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

第四步: MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

回答以上内容即可,下方内容,可以帮助大家理解

代码实现vue2双向数据绑定


 vue.js

class Vue{
    constructor(options){
        this.$data = options.data
        // 调用数据劫持的方法
        Observe(this.$data)
        // 属性代理
        Object.keys(this.$data).forEach(key=>{
            Object.defineProperty(this,key,{
                enumerable:true,
                configurable:true,
                get(){
                    return this.$data[key]
                },
                set(newValue){
                    this.$data[key] = newValue
                }
            })
        })
        // 调用模板编译的函数
        Compile(options.el,this)
    }

}
// 定义一个数据劫持的方法
function Observe(obj){
    // 递归的终止条件
    if(!obj || typeof obj !== 'object') return
    const dep = new Dep()
    // 通过Object.keys(obj) 获取到当前obj上的每个属性
    Object.keys(obj).forEach(key=>{
        // 当前被循环的key所对应的属性值
        let value = obj[key]
        // 把value这个子节点,进行递归
        Observe(value)
        // 需要为当前的key所对应的属性,添加getter和setter
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
                Dep.target && dep.addSub(Dep.target)
                console.log(`有人读取了${key}的值`);
                return value
            },
            set(newVal){
                value = newVal
                Observe(value)
                dep.notify()
            },
           
        })
    })
}
// 对HTML结构进行模板编译的方法
function Compile(el,vm){
    // 获取el对应的DOM元素
    vm.$el = document.querySelector(el)
    const fragment = document.createDocumentFragment()
    while((childNode = vm.$el.firstChild)){
        fragment.appendChild(childNode)
    }
    // 进行模板编译
    replace(fragment)

    vm.$el.appendChild(fragment)

    function replace(node){
        // 定义匹配插值表达式的正则
        const regMustache = /\{\{\s*(\S+)\s*\}\}/
        // 证明当前的node节点是一个文本子节点,需要进行正则的替换
        if(node.nodeType===3){
            // 注意:文本子节点,也是一个DOM对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
            const text = node.textContent
            const execResult = regMustache.exec(text)
            if(execResult){
                const value = execResult[1].split('.').reduce((newObj,k)=>newObj[k],vm)
                node.textContent = text.replace(regMustache,value)

                new Watcher(vm,execResult[1],(newValue)=>{
                    node.textContent = text.replace(regMustache,newValue)
                })
            }
            // 终止递归的条件
            return
        }

        // 判断当前的node节点是否为input输入框
        if(node.nodeType===1 && node.tagName.toUpperCase() === 'INPUT'){
            const attrs = Array.from(node.attributes)
            const findResult = attrs.find((x)=>x.name==='v-model')
            if(findResult){
                const expStr = findResult.value
                const value = expStr.split('.').reduce((newObj,k)=>newObj[k],vm)
                node.value = value
                new Watcher(vm,expStr,(newValue)=>{
                    node.value = newValue
                })
                // 监听文本框的input输入事件,拿到文本框最新的值,把最新的值,更新到vm上即可
                node.addEventListener('input',(e)=>{
                    const keyArr = expStr.split('.')
                    const obj = keyArr.slice(0,keyArr.length-1).reduce((newObj,k)=>newObj[k],vm)
                    obj[keyArr[keyArr.length-1]] = e.target.value
                })

            }
        }
        // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
        node.childNodes.forEach(child=>replace(child))
    }
}
// 收集依赖/收集订阅者
class Dep{
    constructor(){
        // 今后,所有的watcher都要存到这个数组中
        this.subs = []
    }
    // 向 subs 数组中,添加watcher的方法
    addSub(watcher){
        this.subs.push(watcher)
    }
    // 负责通知每个watcher的方法
    notify(){
        this.subs.forEach(watcher=>watcher.update())
    }
}

class Watcher{
    constructor(vm,key,cb){
        this.vm = vm
        this.key = key
        this.cb = cb
        Dep.target = this
        key.split('.').reduce((newObj,k)=>newObj[k],vm )
        Dep.target = null
    }
    update(){
        const value = this.key.split('.').reduce((newObj,k)=>newObj[k],this.vm)
        this.cb(value)
    }
}

html文件

<!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">
        <h3>姓名:{{name}}</h3>
        <h3>年龄:{{age}}</h3>
        <h3>info.a的值是:{{info.a}}</h3>
        <div>name的值:<input type="text" v-model="name"></div>
        <div>info.a的值:<input type="text" v-model="info.a"></div>
    </div>
    <script src="./vue.js"></script>
    <script>
        const vm = new Vue({
            el:"#app",
            data:{
                name:'zs',
                age:20,
                info:{
                    a:'a1',
                    c:'c1'
                }
            }
        })
    </script>
</body>
</html>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值