Vue雙向繫結實現

愛吃叉燒發表於2018-09-25

參考文章

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

效果

Vue雙向繫結實現

整體步驟

  1. 首先是在Vue的建構函式中對data中的資料響應化(使用的是defineProperty)
  2. 然後對html中相關指令和{{變數}}形式程式碼編譯(compile), 因為這些變數與Vue.data裡的資料有關,需要繫結在一起。
  3. 繫結過程是每一個與Vue.data中資料相關的節點,都會建立Watcher物件,然後這個代表節點的Watcher會被收集到資料自己的dep中,當這個資料變化時,會遍歷dep中的Watcher。
  4. 資料變化的通知過程,使用defineProperty實現,通過劫持set操作,在其中通知Watcher,通過劫持get操作,來收集依賴。

Vue雙向繫結實現

程式碼地址

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>
複製程式碼

相關文章