Vue数据双向绑定实现

50 阅读1分钟

1.实现数据劫持

function observeData(data) {
    if (!data || typeof data !== 'object') return
    let dependecy = new Dependency()
    Object.keys(data).forEach(key => {
        let value = data[key];
        observeData(value)
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            // getter
            get() {
                if (Dependency.watcher) {
                    dependecy.addSub(Dependency.watcher)  // 添加一个订阅者
                }
                return value;
            },
            // setter
            set(newValue) {
                value = newValue
                observeData(newValue)
                dependecy.notify()
            }
        })
    })
}

2.实现Compile函数

// 将模版语法解析成dom 
function Compile(el, vm) {
    vm.$el = document.querySelector(el)
    // 创建文档碎片 并添加节点
    const fragment = document.createDocumentFragment()
    let child;
    while (child = vm.$el.firstChild) {
        fragment.append(child)
    }
    CompileElement(fragment)
    // 为文档中文本节点添加内容
    function CompileElement(node) {
        let childNodes = node.childNodes
        let reg = /\{\{(.*)\}\}/  // 匹配模版变量
        if (node.nodeType === 3) {
            // 提前保存nodeValue 否则watcher 回调函数nodeValue为 不带大括号的 匹配不成功
            let temp = node.nodeValue;
            let nodeContent = reg.exec(node.nodeValue)  // 双大括号内容键名
            if (nodeContent) {
                // 若为对象  链式访问属性得到数据
                const value = nodeContent[1].split('.').reduce((total, current) => total[current], vm.$data)
                node.nodeValue = temp.replace(reg, value)

                // 当前值为展示的最新值 所以在此处创建订阅者
                new Watcher(vm, nodeContent[1], newValue => {
                    // node.nodeValue = node.nodeValue.replace(reg, newValue)
                    node.nodeValue = temp.replace(reg, newValue)
                })
            }
        }
        // 递归调用 所有的子节点
        childNodes.forEach(child => CompileElement(child))
    }
    vm.$el.appendChild(fragment)
}

3.实现Dep

// 依赖对象 收集和通知订阅者
class Dependency {
    constructor() {
        this.subscribe = []
    }
    // 添加订阅者
    addSub(sub) {
        this.subscribe.push(sub)
        console.log('this.subscribe', this.subscribe);
    }
    // 通知Watcher更新
    notify() {
        this.subscribe.forEach(sub => {
            console.log('sub', sub)
            sub.update()
        }
        )
    }
}

4.实现Watcher

// Watcher 订阅者
class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm
        this.key = key
        this.callback = callback
        Dependency.watcher = this
        key.split('.').reduce((total, current) => total[current], vm.$data)
        // console.log(`用属性${key}创建了订阅者`);
        Dependency.watcher = null
    }
    update() {
        const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data)
        this.callback(value)
    }
}

5.初始化Vue

// Vue 数据双向绑定原理
class Vue {
    constructor(obj_instance) {
        this.$el = obj_instance.el;
        this.$data = obj_instance.data;
        observeData(this.$data)
        Compile(this.$el, this)
    }
}

6.测试

<!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">
        <h1>姓名: {{name}}</h1>
        <h2>更多: {{more.like}}</h2>
    </div>
</body>
<script src="./minivue.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data: {
            name: 'siyuyu',
            more: {
                like: 'song'
            }
        }
    })
</script>

</html>