vue.js数据双向绑定原理及实现

82 阅读1分钟
<!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>Vue双向绑定实现</title>
</head>
<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>
class Vue {
    constructor(obj_instance) {
        this._data = obj_instance.data
        Observe(this._data)
        Compile(obj_instance.el, this)
    }
}

// 为数据绑定 监听属性
function Observe(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]
        Observe(value) // 递归 用于劫持对象里面的子属性
        Object.defineProperty(data_instance, key, {
            enumerable: true,
            configurable: true,
            get() {
                // console.log(`访问了属性: ${key} --> 值: ${value}`);
                // 订阅者加入  依赖实例的数组
                Dependency.temp && dependency.addSub(Dependency.temp)
                // console.log(dependency.subscribers);
                // console.log(Dependency.tem);
                return value
            },
            set(newValue) {
                // console.log(`修改了属性: ${key} 的值 "${value} "为 --> ${newValue}`);
                value = newValue
                Observe(newValue)
                // console.log(data_instance);
                dependency.notify(key)
            }
        })
    })
}

// HTML 模板解析 - 替换 DOM 内的内容
function Compile(element, vm) {
    vm.$el = document.querySelector(element)
    // console.log(vm.$el);
    // console.log(vm.$el.firstChild);
    // console.log(vm.$el.childNodes);
    const fragment = document.createDocumentFragment()
    let child;
    // 创建循环 把创建一个child 把这些节点 都放到 fragment 里面
    while (child = vm.$el.firstChild) {
        fragment.append(child)
    }
    // console.log(fragment); // #document-fragment
    // console.log(fragment.childNodes); // [text, span, text, input, text, span, text, input, text]

    // 页面的dom 都被存起来了 页面也就空了 没内容了
    fragment_compile(fragment)
    //替换文档碎片的内容
    function fragment_compile(node) {
        // console.log(node);
        const pattern = /\{\{\s*(\S+)\s*\}\}/
        if (node.nodeType === 3) {
            const originNode = node.nodeValue
            const result_regex = pattern.exec(node.nodeValue) // 去掉那些换行符的空字符
            // console.log(result_regex);
            if (result_regex) {
                const arr = result_regex[1].split('.')
                // console.log(arr);
                // 根据属性拿到的对应的数据
                const value = arr.reduce((total, current) =>
                    total[current],
                    vm._data)
                // console.log(value);
                node.nodeValue = originNode.replace(pattern, value)
                // 绑定一个 订阅者
                new Watcher(vm, result_regex[1], newValue => {
                    node.nodeValue = originNode .replace(pattern, newValue)
                })
            }
            return
        }
        // 数据改变视图
        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 => {
                        const arr1 = i.nodeValue.split('.')
                        const arr2 = arr1.slice(0, arr1.length - 1)
                        const final = arr2.reduce(
                            (total, current) => total[current], vm._data
                        )
                        // console.log(arr1);
                        // console.log(arr2);
                        // console.log(final);
                        final[arr1[arr1.length - 1]] = e.target.value
                    })
                }
            })
        }
        node.childNodes.forEach(child => fragment_compile(child))
    }
    vm.$el.appendChild(fragment)
}

// 依赖 - 收集和通知订阅者
class Dependency {
    constructor() {
        this.subscribers = []
    }
    addSub(sub) {
        // console.log(sub);
        const index = this.subscribers.findIndex((item)=>{
            return item.key === sub.key
        })
        index === -1 && this.subscribers.push(sub)
        // this.subscribers.push(sub)
    }
    notify(key) {
        // console.log(key);
        // console.log(this.subscribers);
        this.subscribers.forEach(sub => {
            // console.log(sub);
            // 这里还需要优化 
            // if(sub.key === key){
                sub.update()
            // }
        })
    }
}

// 订阅者
class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm
        this.key = key
        this.callback = callback;
        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)
    }
}