vue-双向数据绑定

67 阅读2分钟

时间:2022-05-04 17:49:19

参考B站up蛋老师是的视频学习了一下vue的双向数据绑定以及模板解析的操作,实在是太巧妙了!视频地址

  直接放代码,前端部分,参考vue的写法,这里的data是一个普通的对象,vue官网的data是一个函数,官方的写法应该是为了数据绑定做出的优化,但是没有了解过相关原理,所有暂时还不太理解二者的区别。

<body>
    <div id="app">
        <label>用户名:
            <input v-model="name" type="text" name="username" id="username">
        </label><br>
        <label>&emsp;龄:
            <input v-model="info.age" type="text" name="age" id="age">
        </label><br>
        <label>&emsp;高:
            <input v-model="info.height" type="text" name="height" id="height">
        </label>
        <p> 用户名: {{ name }} </p>
        <p>年龄: {{info.age}}</p>
        <p>身高: {{info.height}}cm</p>
    </div>
</body>
<script src="./vue.js"></script>
<script>
    const vm = new Vue({
        el: "#app",
        data: {
            name: "啦啦啦种太阳",
            info: {
                age: 18,
                height: 170
            }
        }
    })
</script>

HTML部分参考官方的写法,本次的demo主要实现文本渲染、输入框的v-model监听,以及数据绑定

JS部分

// vue.js
class Vue {
    constructor({el, data}) {
        this.$el = el
        this.$data = data
        Observer(this.$data)
        Compile(el, this)
    }
}

// 为数据绑定监听者
function Observer(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]
        Observer(value)
        Object.defineProperty(data_instance, key, {
            enumerable: true,
            configurable: true,
            get() {
                console.log(`访问了属性:${key} -> 值 ${value}`)
                Dependency.temp && dependency.addSub(Dependency.temp)
                if(Dependency.temp) console.log(Dependency.temp)
                return value;
            },
            set(newValue) {
                console.log(`属性${key}的值"${value}"修改为 -> "${newValue}"`)
                value = newValue
                Observer(newValue)
                dependency.notify()
            }
        })
    })
}

// 模板解析
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 = pattern.exec( node.nodeValue)
            if(result) {
                const arr = result[1].split(".")
                const value = arr.reduce((total, current) => total[current], vm.$data)
                node.nodeValue = xxx.replace(pattern, value)
                new Watcher(vm, result[1], newValue => {
                    node.nodeValue = xxx.replace(pattern, newValue)
                })
            }
            return 
        } 
        // 实现input框的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.nodeValue, newValue => {
                        node.value = newValue
                    })
                    node.addEventListener('input', e => {
                        let name = i.nodeValue.split(".");
                        const final = name.slice(0, name.length - 1).reduce(
                            (total, current) => total[current], vm.$data
                        )
                        final[name[name.length - 1]] = e.target.value
                    })
                }
            })
        }
        node.childNodes.forEach(child => fragment_compile(child))
    }
    vm.$el.append(fragment)
}

/**发布订阅模式 */
class Dependency {
    temp = null
    constructor() {
        this.subscriber = []
    }
    addSub(sub) {
        this.subscriber.push(sub)
    }
    notify() {
        this.subscriber.forEach(sub => sub.update())
    }
}

// 观察者
class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm
        this.key = key
        this.callback = callback
        // 临时属性 触发getter
        Dependency.temp = this
        console.log(`用属性${key}创建了订阅者`)
        key.split(".").reduce((total, current) => total[current], vm.$data) // 触发属性的getter
        Dependency.temp = null
    }
    update() {
        const value = this.key.split(".").reduce((total, current) => total[current], this.vm.$data)
        this.callback(value)
    }
}

  视频看了两遍,全程跟着敲,第一遍还不太理解Watcher类是如何绑定对于数据的监听的,而且不太理解getter和setter的触发的巧妙之处。

  Watcher其实就是在虚拟dom添加至页面的时候,将每一个在页面中要被渲染的值进行监听。监听的本质就是将data中的一个值于Watcher的一个实例对象所提供的callback函数绑定在一起,每当setter函数被触发,及data中的值发生了改变,则调用callback函数。

  Dependency类中存储了所有的Watcher实例,此处demo中对于通知Watcher进行更新的方法是,一旦setter被触发就通知Dependency.subscriber中的所有watcher进行更新,Watcher实例自带的update函方法可以找到自己正在监听的数据,并从根实例vm中获取最新的数据值并调用自身的callback函数。callback是在解析数据的同时对Watcher实例化的时候传入的,所以callback可以直接访问到数据要解析的目标dom,当Watcher接收到了新的值,就可以直接对dom进行重新渲染,也就实现了所谓的“双向绑定”。