天天看點

vue資料響應式的實作(附圖)

根據對vue源碼的了解,對vue的資料響應式做一個簡單的實作。

定義myvue,使用方式仿造vue,簡單實作插值表達式、資料雙向綁定、事件及指令。

直接上代碼

建立index.html,代碼如下:

<!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></title>
</head>

<body>
    <div id="app">

        <!-- 插值 -->
        <p>{{title}}</p>

        <!-- 雙向綁定 -->
        <input type="text" my-model="name" />

        <!-- 指令 -->
        <p my-text="name"></p>

        <!-- 事件 -->
        <button @click="clear">清空</button>

        <!-- html -->
        <div my-html="html"></div>
    </div>

    <script src='./myvue.js'></script>
    <script src='./compile.js'></script>
    <script>
        const myvue = new MyVue({
            el: '#app',
            data: {
                title: "vue響應式",
                name: "_BuzzLy",
                html: "<button>按鈕</button>"
            },
            methods: {
                clear() {
                    this.name = "";
                }
            },
        })
    </script>
</body>

</html>
           

建立myvue.js,主要作用是資料的響應化,代碼如下:

class MyVue {
    constructor(options) {
        this.$options = options;
        this.$data = options.data;

        this.observe(this.$data);
		
		// 在compile.js中實作
        new Compile(options.el, this);
    }

    observe(data) {
        if (!data || typeof data !== 'object')
            return;

        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]);
            this.proxyData(key);
        })
    }

	// 資料響應化
    defineReactive(data, key, value) {
        this.observe(value);

        var dep = new Dep();
        Object.defineProperty(data, key, {
            get() {
                Dep.target && dep.addDep(Dep.target);
                return value;
            },
            set(newValue) {
                if (value === newValue) return;
                value = newValue;
                dep.notify();
            }
        })
    }

	// 代理data中的屬性到vue執行個體上
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key];
            },
            set(newValue) {
                this.$data[key] = newValue;
            }
        })
    }

}

// 依賴收集器,管理Watcher
class Dep {
    constructor() {
    	// 用來存放watcher
        this.deps = [];
    }

    addDep(dep) {
        this.deps.push(dep);
    }

    notify() {
        this.deps.forEach(dep => dep.update())
    }
}

// Watcher 訂閱者
class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm;
        this.key = key;
        this.callback = callback;

		// 将目前執行個體指向Dep類的靜态屬性target
        Dep.target = this;
        this.vm[this.key];
        Dep.target = null;
    }

    update() {
        this.callback.call(this.vm, this.vm[this.key]);
    }
}
           

建立compile.js,主要作用是編譯,代碼如下:

class Compile {
    constructor(el, vm) {
        this.$el = document.querySelector(el);
        this.$vm = vm;

        if (this.$el) {
            this.$fragment = this.node2Fragment(this.$el);
            // 核心——編譯(處理html模闆,解析指令事件等,以及收集相關依賴)
            this.compile(this.$fragment);
            this.$el.appendChild(this.$fragment);
        }
    }

	// 将dom結構轉換為fragment片段進行操作,提升性能
    node2Fragment(el) {
        const frag = document.createDocumentFragment();

        let child;
        while (child = el.firstChild) {
            frag.appendChild(child);
        }
        return frag;
    }

    compile(el) {
        const childNodes = el.childNodes;
        // 周遊所有節點
        Array.from(childNodes).forEach(node => {
        	// 元素節點
            if (this.isElement(node)) {
                const nodeAttrs = node.attributes;
                // 周遊所有屬性
                Array.from(nodeAttrs).forEach(attr => {
                    const attrName = attr.name;
                    const attrVal = attr.value;
                    // 指令
                    if (this.isDirective(attrName)) {
                        const dirName = attrName.substring(3);
                        this[dirName] && this[dirName](node, this.$vm, attrVal);
                    } else if (this.isEvent(attrName)) { // 事件
                        const eventName = attrName.substring(1);
                        this.eventHandler(node, this.$vm, attrVal, eventName);
                    }
                })
            } else if (this.isText(node)) { // 文本節點
                this.compileText(node);
            }

            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node);
            }
        })
    }

	// 調用指令方式初始化,并收集依賴
    update(node, vm, val, dir) {
        const updateFn = this[dir + 'Updater'];
        updateFn && updateFn(node, vm[val]);

        new Watcher(vm, val, function (value) {
            updateFn && updateFn(node, value);
        });
    }

	// 處理插值表達式
    compileText(node) {
        this.update(node, this.$vm, RegExp.$1, "text");
    }

	// text指令方法
    text(node, vm, attrVal) {
        this.update(node, vm, attrVal, "text");
    }

	// text指令更新函數
    textUpdater(node, value) {
        console.log('set:' + value)
        node.textContent = value;
    }
	
	// model指令方法
    model(node, vm, attrVal) {
    	// 指定input的value屬性,模型對視圖的響應
        this.update(node, vm, attrVal, "model");

		// 視圖對模型響應
        node.addEventListener('input', function (e) {
            vm[attrVal] = e.target.value;
        })
    }

	// 雙向綁定更新函數
    modelUpdater(node, value) {
        node.value = value;
    }
	
	// html指令方法
    html(node, vm, attrVal) {
        this.update(node, vm, attrVal, "html");
    }

	// html更新函數
    htmlUpdater(node, value) {
        node.innerHTML = value;
    }

	// 事件處理函數
    eventHandler(node, vm, val, event) {
    	// 在vue執行個體的methods中找到對應的方法
        let fn = vm.$options.methods && vm.$options.methods[val];
        if (event && fn)
            node.addEventListener(event, fn.bind(vm))
    }

	// 判斷屬性是否為指令
    isDirective(attr) {
        return attr.indexOf('my-') === 0;
    }

	// 判斷屬性是否為自定義事件
    isEvent(attr) {
        return attr.indexOf('@') === 0;
    }

	// 判斷節點為元素節點
    isElement(node) {
        return node.nodeType === 1;
    }
	
	// 判斷節點是文本節點并且為插值表達式
    isText(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
}
           

代碼到這就結束了,代碼都有簡單的注釋,就不再用文字說明了,看一張圖就好。(本來想着用文字描述的,但是文字不太容易了解,索性就畫了個圖,覺得比文字容易了解)

vue資料響應式的實作(附圖)

内容有點多,字有點小,可以儲存下來看。