vue中的双向数据绑定原理

首先依赖与发布订阅者模式,然后关键技术就是Object.defineProperty()方法来进行数据拦截,然后对于其中属性的属性要用到reduce()循环计算方法,直接上代码。附我自己的注释

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变更的双向绑定效果。
以上描述非常正确,来自这里
在这里插入图片描述

<!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>年龄:{{age}}</span>
        <input type="text" v-model="age">
        <span>更多:{{info.like}}</span>
        <input type="text" v-model="info.like">
    </div>
    <script src="./vue.js"></script>

    <script>
        const vm = new Vue({
            el: '#app', //这个app就是vue控制的范围,将data中的值全部填充到el的值中去
            data: {
                name: '小琪',
                age: 24,
                info: {
                    like: '吃喝玩乐'
                }
            }
        })
        console.log(vm)
    </script>
</body>

</html>

下面就是实现的vue.js中的代码

class Vue {
    constructor(obj_instance) {
        this.$data = obj_instance.data
            //数据劫持
        Observer(this.$data)

        //属性代理,主要作用就是当访问vm中的属性时,不用vm.$data.XX,可以直接vm.XX
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, { //为vm定义了$data中的属性
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newVal) {
                    this.$data[key] = newVal
                }
            })
        })

        //调用模板编译的函数。实现将vm中data中的数据渲染到相应控制的dom内部
        Compile(obj_instance.el, this)
    }
}

//数据劫持-监听实例中的数据
function Observer(data_instance) {
    //递归出口:已经没有子属性或者没有检测到对象
    if (!data_instance || typeof data_instance !== 'object') return; //递归终止条件

    const dep = new Dep()

    //这里keys循环数据中的所有属性
    Object.keys(data_instance).forEach(key => {
        //为当前得到的属性,也就是key值添加getter和setter
        let value = data_instance[key]; //先将key对应的值存起来
        Observer(value) //递归-  子属性数据劫持
        Object.defineProperty(data_instance, key, {
            enumerable: true,
            configurable: true,
            get() {

                //只要执行了下面这一行代码,那么刚刚new的Watcher实例就会被放入dep.subs数组中
                Dep.target && dep.addSub(Dep.target)

                // console.log(`有人获取了${key}的值`)
                return value
            },
            set(newValue) {
                // console.log(`属性${key}的值${value}修改为->${newValue}`)
                value = newValue
                Observer(value) //监听新传入的对象 

                //通知每一个订阅者更新自己的文本
                dep.notify() //实现了单向数据绑定,改变vm中data的值会同步到dom中
            }
        })
    })

}

//对HTML结构进行解析,实现就是将vm中data中的数据渲染到相应控制的dom内部
//设置两个参数,第一个是元素->vue实例中挂载的元素,也就是需要控制的哪片区域,第二个参数是vue元素
function Compile(el, vm) {
    //获取el对应的DOM元素。首先要做的就是将el转换为真实的dom对象,当前传入的el是一个选择器而已
    vm.$el = document.querySelector(el)

    //文档碎片,存储每个DOM节点。内存中操作文档碎片,不是在页面上操作DOM元素,然后再渲染到页面上。可以避免重绘和重排
    //创建文档碎片
    let childNode
    const fragment = document.createDocumentFragment()
    while ((childNode = vm.$el.firstChild)) {
        fragment.appendChild(childNode)
    } //while循环之后,页面中所有的dom节点都存在了文档碎片中,此时dom节点为空

    //进行模板编译
    replace(fragment)

    vm.$el.appendChild(fragment) //将dom子节点全部重新渲染到页面中

    //定义对dom节点进行编译的方法
    function replace(node) {
        //定义匹配插值表达式的正则
        const regMustache = /\{\{\s*(\S+)\s*\}\}/ //其中\s表示提取空白字符,\S表示提取非空白字符,+代表至少一个或者多个

        //当前的node节点是一个文本子节点,需要进行正则替换
        if (node.nodeType === 3) {
            //文本子节点也是一个DOM对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
            // console.log(node.textContent)//输出所有文本的内容
            const text = node.textContent

            //进行字符串的正则匹配与提取
            const execResult = regMustache.exec(text)
                // console.log(execResult) //0{{name}} 1name ...
            if (execResult) {
                const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm) //如果存在属性中的属性就可以直接XX.XX.XX
                    // console.log(value)
                node.textContent = text.replace(regMustache, value) //替换操作噢!!!在这里就实现了dom元素替换,重要的一步

                //在这里创建Watcher类的实例
                new watcher(vm, execResult[1], (newValue) => { //进入watcher的构造函数中
                    node.textContent = text.replace(regMustache, newValue)
                })

            }

            //终止递归的条件
            return
        }

        //下面这一段实现视图到vm中数据的绑定
        //判断当前的node节点是否为input输入框
        if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
            //得到当前元素的所有属性节点
            const attrs = Array.from(node.attributes) //node.attributes是一个伪数组,用Array.from改为真数组
                // console.log(attrs)
            const findResult = attrs.find(x => x.name === 'v-model')
                // console.log(findResult)
            if (findResult) {
                //获取到当前v-model属性的值 v-model = "name"...
                const expStr = findResult.value
                const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
                    // console.log(value) //拿到的值
                node.value = value

                //创建Watcher的实例,为了当vm里面元素改变时候让input框里面的元素也改变
                new watcher(vm, expStr, (newValue) => { //文本框实现了单向数据绑定
                    node.value = newValue
                })

                //实现文本框的双向数据绑定
                //监听文本框的input输入事件,拿到文本框最新的值,把最新的值更新到vm上即可。
                node.addEventListener('input', (e) => {
                    // console.log(e.target.value)
                    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))

    }
}

//发布订阅者模式

//依赖收集的类/收集watcher订阅者的类
class Dep {
    constructor() {
        //定义一个存储water的数组
        this.subs = []
    }

    //向subs数组中添加watch的方法
    addSub(watcher) {
        this.subs.push(watcher)
    }

    //通知订阅的方法
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

//创建订阅者的类
class watcher {
    //cb是一个回调函数,记录当前watch是如何更新自己的内容
    //但是,只知道如何更新自己还不行,还必须拿到最新的数据
    //因此,还需要在new Watcher期间,传入vm,因为vm中保存着最新的数据
    //另外,还需要知道在vm身上众多的数据中,自己所需要的数据是哪个。所以,在new Watcher期间,需要指定watcher对应的数据属性名字key
    constructor(vm, key, cb) {
        this.vm = vm //给当前的watcher挂载上vm属性
        this.key = key
        this.cb = cb


        //下面代码负责把创建的watcher实例存在Dep实例的subs数组中
        Dep.target = this //this就是当前new的Watcher实例,给这个实例添加了一个target属性
        key.split('.').reduce((newObj, k) => newObj[k], vm) //这里做了一个取值的操作,会触发get(),然后接下来的操作见上面Object.definedProperty。其真实目的不是为了取值,而是执行get(),然后将当前的watcher加入到Dep定义的数组中
        Dep.target = null
    }

    //watcher实例还需要有一个update函数,让发布者能够通知到我们从而进行更新。
    update() {
        const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
        this.cb(value)
    }
}

参考视频链接

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值