双向数据绑定的原理

102 阅读3分钟

原理

  • Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的 setter、getter【Vue3中使用Proxy】,在数据变动时发布消息给订阅者,触发相应的监听回调

实现步骤

  • 将需要observe的数据对象都加上 setter 和 getter, 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
  • compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  • Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
    • ① 在自身实例化时往属性订阅器(dep)里面添加自己
    • ② 自身必须有一个 update()方法
    • ③ 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退
  • MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input)-> 数据 model 变更的双向绑定效果

1716349397703.png

  • 从图可看出,双向数据绑定可总结为三个部分
    • Observer 用来监听拦截data的属性为监察者
    • Dep用来添加订阅者,为订阅器
    • Watcher 就是订阅者
    • 最后:监察者通过 Dep 向 Watcher发布更新消息

模拟实现

        function observer(obj,vm) {
            //obj app.data
            //vm app
            Object.entries(obj).map(([key,val]) => {
                //key num val 1
                //以app.data里的属性作为原始数据 给app对象本身挂载上这些属性(修饰后的)
                //app.data.num => num(set,get)修饰 => app.num
                defineReactive(vm,key,val)
            })
        }

        //监听数据改变 defineProperty proxy
        function defineReactive(obj, key, val) {
            let dep = new Dep()
            Object.defineProperty(obj,key,{
                get() {
                    //obj的key属性的值被获取的时候,触发
                    console.log(`读取数据:${val}`)
                    if(Dep.target) {
                        dep.addSub(Dep.target)
                    }
                    return val
                },
                set(newVal) {
                    //obj的key属性的值被修改的时候,触发
                    if(val === newVal) {
                        return
                    }
                    val = newVal
                    console.log(`更新数据:${val}`)
                    //发布通知 通知watcher update 
                    dep.notify()
                }
            })
        }


        //创建一个新的空白的文档片段,并将处理后的dom加入该文档片段
        function nodeToFragment(node,vm) {
            let flag = document.createDocumentFragment()
            let child
            // 循环取出node中的第一个子节点,放入compile处理,后移动到fragement
            while (child = node.firstChild) {
                compile(child,vm)
                flag.append(child)
            }
            return flag
        }

        /**
         * 1. 解析原始dom,获取内容的差值标识 {{model}}
         * 2. 提取的差值标识 数据与model层进行绑定
         */
        // 解析原始view,提取其中的model差值
        function compile(node,vm) {
            let reg = /\{\{(.*)\}\}/
            if(node.nodeType === 1) {
                // node 为Element的节点
                let attr = node.attributes
                for(let i = 0,len = attr.length; i<len; i++) {
                    if(attr[i].nodeName === 'v-model') {
                        let name = attr[i].nodeValue
                        node.addEventListener('input',(e) => {
                            vm[name] = e.target.value
                        })
                        node.value = vm[name] //model层数据对象
                        node.removeAttribute('v-model')
                        new Watcher(vm,node,name)
                    }
                }
            }
            if(node.nodeType === 3) {
                // node为text类型节点
                if(reg.test(node.nodeValue)) {
                    let name = reg.exec(node.nodeValue)
                    node.nodeValue = vm[name[1]] //model层数据对象
                    new Watcher(vm,node,name[1])
                }
            }
        }

        // 发布订阅设计模式
        class Dep {
            constructor() {
                //观察者/订阅者列表
                this.subs = []
            }
            addSub(sub) {
                this.subs.push(sub)
            }
            notify() {
                this.subs.map(sub => {
                    //通知所有订阅者
                    sub.update()
                })
            }
        }

        //订阅者
        class Watcher {
            constructor(vm,node,name) {
                this.name = name
                Dep.target = this
                this.node = node
                this.vm = vm
                this.init()
            }
            init() {
                this.update()
                Dep.target = null
            }
            update() {
                //更新view展示 获取数据,修改dom
                this.get()
                this.node.value = this.value
                this.node.nodeValue = this.value
            }
            get() {
                this.value = this.vm[this.name]
            }
        }

        class Vue {
            constructor({el,data}) {
                this.data = data
                // this.num = this.data.num
                // defineReactive(this,'num')
                observer(this.data,this)
                let ele = document.querySelector(el)
                let dom = nodeToFragment(ele,this)
                ele.appendChild(dom)
            }
        }

        const app = new Vue({
            el: '#app',
            data: {
                num: 1
            }
        })

-------------------------------------------------------------------------------2024.5.22每日一题