Vue双向绑定实现

321 阅读2分钟

参考文章

  1. www.cnblogs.com/kidney/p/60…
  2. segmentfault.com/a/119000000…

效果

整体步骤

  1. 首先是在Vue的构造函数中对data中的数据响应化(使用的是defineProperty)
  2. 然后对html中相关指令和{{变量}}形式代码编译(compile), 因为这些变量与Vue.data里的数据有关,需要绑定在一起。
  3. 绑定过程是每一个与Vue.data中数据相关的节点,都会创建Watcher对象,然后这个代表节点的Watcher会被收集到数据自己的dep中,当这个数据变化时,会遍历dep中的Watcher。
  4. 数据变化的通知过程,使用defineProperty实现,通过劫持set操作,在其中通知Watcher,通过劫持get操作,来收集依赖。

代码地址

gist.github.com/ssdemajia/5…

主要代码

复制保存为vmodel.js

// vm == view model

class Dep{
    constructor(){
        this.subscribers = []; 
    }
    addSubscriber(sub) {
        this.subscribers.push(sub);
    }
    notify() {
        this.subscribers.forEach((sub) => {
            sub.update();
        });
    }
}

class Watcher {
    constructor(vm, node, name, nodeType) {
        Dep.target = this;
        this.vm = vm;
        this.node = node;
        this.name = name;
        this.nodeType = nodeType;
        this.update();
        Dep.target = null;
    }

    update() {
        this.get();
        if (this.nodeType == 'text') {
            this.node.data = this.value;
        }
        if (this.nodeType == 'input') {
            this.node.value = this.value;
        }
    }

    get() {
        this.value = this.vm.data[this.name];
    }
}

class Vue{
    constructor(options) {
        this.data = options.data;
        toObserve(this.data);
        let root = document.querySelector(options.el);
        var newChild = nodeToFragment(root, this);
        root.appendChild(newChild);
    }
}
function toReactive(key, value, obj) {
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get() {
            if (Dep.target) {
                dep.addSubscriber(Dep.target);
            }
            return value;
        },
        set(newValue) {
            if (newValue == value) {
                return;
            }
            value = newValue;
            dep.notify();
        }
    });
}

function toObserve(obj) {
    Object.keys(obj).forEach((key) => {
        toReactive(key, obj[key], obj);
    });
}
function nodeToFragment(node, vm) {
    let flag = document.createDocumentFragment();
    let child = node.firstChild;
    while (child) { // 遍历child
        compile(child, vm);
        flag.appendChild(child);
        child = node.firstChild;
    }
    return flag;
}

function compile(node, vm) {
    let reg = /\{\{(.*)\}\}/;
    if (node.nodeType == Node.ELEMENT_NODE) {
        let attrNode = node.attributes;
        for (let i = 0; i < attrNode.length; i++) {
            let attr = attrNode[i];
            if (attr.nodeName == 'v-model') {
                let name = attr.nodeValue;
                node.addEventListener('input', (e) => {
                    console.log(e);
                    vm.data[name] = e.target.value;     // 这里会通知name的subscribers进行update
                    console.log(vm);
                });                
                node.value = vm.data[name]; // 通知
                node.removeAttribute('v-model');
                new Watcher(vm, node, name, 'input');
            }
        }
    }
    else if (node.nodeType == Node.TEXT_NODE) {
        if (reg.test(node.nodeValue)) {
            let name = RegExp.$1;
            name = name.trim();
            node.nodeValue = vm.data[name];
            new Watcher(vm, node, name, 'text');
        }
    }
}

使用

复制保存为index.html,和vmodel.js放在同一文件夹。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="vueData">
        {{ vueData }}
        <button type="button" onclick="ssClick()">change</button>
    </div>
    <script src="vmodel.js"></script>
    <script>
        
        let vm = new Vue({
            el: '#app',
            data: {
                vueData: 'ssss'
            }
        })
        function ssClick() {
            vm.data['vueData'] = 'shaoshuai';
        }
        
    </script>


</body>

</html>