模仿vue自己動手寫響應式框架(四) - Vue物件構建

wls1036發表於2020-07-22

概述

之前的章節中,我們建立了一個沒有任何邏輯的vue物件,僅僅只是保證了var app = new Vue({...})不報錯而已,這一篇我們將構建一個真正的vue物件,實現真正的值繫結。

build(構建)

這是html中建立vue的程式碼

var app = new Vue({
            el: '#app',
            data: {
                newTodo: '',
                todos: []
            },
            methods: {
                addTodo: function () {
                    this.todos.push({ text: this.newTodo });
                    this.newTodo = '';
                },
                deleteTodo: function (index) {
                    this.todos.splice(index, 1);
                }
            }
        })

思路是

  • 建立vue物件
  • 將data資料直接掛載到物件上,這樣可以實現vue.newTodo的訪問效果
  • 將method直接掛載到桂香上,可以實現vue.addTodo的效果

當然,實際上vue並不是這麼實現的,vue透過proxy方式實現直接訪問的效果,我們的目的是能用就行(大家真正實現一個框架不要報這種想法,本系列是為了讓所有人都能理解,都能入門才用該方式實現)。

var vue = {};
function build() {
        for (let k in options.data) {
            let v = options.data[k];
            defineProperty(k, v);
        }
        for (let key in options.methods) {
            vue[key] = options.methods[key];
        }
    }

defineProperty

這個是實現繫結的核心步驟,程式碼如下:

function defineProperty(name, value) {
        Object.defineProperty(vue, name, {
            get: function() {
                return value;
            },
            set: function(newValue) {
                value = newValue;
                let items = subscriber[name];
                if (items) {
                    for (let i = 0; i < items.length; ++i) {
                        items[i].change(name, newValue);
                    }
                }
            }
        })
    }

當我們給一個物件新增一個屬性或者修改屬性值的時候可以直接透過user.name='張三'實現,也可以透過函式Object.defineProperty進行定義,明顯後者更為麻煩,但是Object.defineProperty可以監聽值的變化,獲取值和設定值都可以監聽到,這就是為實現值變化更新dom的功能打下了基礎。我們在set中監聽值變化,從訂閱者裡面根據變數名稱取出訂閱者的指令,依次呼叫指令change方法。defineProperty其他用法參考這裡

我們的data除了newTodo這個字串變數外還有todos這個變數,其中新增代辦的程式碼如下:

addTodo: function () {
            this.todos.push({ text: this.newTodo });
            this.newTodo = '';
        }

todos是透過呼叫push方法進行插入,那這樣會觸發set事件嗎,答案是不會的,只有當this.todos=[]這種todos賦予新值才會觸發值改變,這個肯定是不行的,這個要求開發者必須構建一個push後的陣列再賦值給todos,可以用原型解決這個問題,原型最大的好處就是可以重新定義js的標準方法。

function defineArrayProperty() {
        var method=['push','splice'];
        for(let i=0;i<method.length;++i) {
            var origin = Array.prototype[method[i]];
            var fn = function () {
                origin.apply(this, arguments);
                let names = Object.getOwnPropertyNames(vue);
                for (let i = 0; i < names.length; ++i) {
                    if (vue[names[i]] === this) {
                        let items = subscriber[names[i]];
                        if (items) {
                            for (let j = 0; j < items.length; ++j) {
                                items[j].change(names[i], this);
                            }
                        }
                    }
                }
            }
            Object.defineProperty(Array.prototype, method[i], {
                value: fn
            });
        }
    }

透過apply方法呼叫原來的方法,再找到訂閱者通知訂閱者,對於變數可以一個個定義屬性,這樣可以獲取到名稱,但是對於修改js基本物件的屬性,這裡無法獲取變數名,只能透過遍歷vue屬性找到變數名。

valueTrigger

完成了以上工作,我們上一篇中的valueTrigger邏輯就可以實現

function valueTrigger(name, value) {
    if (vue[name] != undefined) {
        vue[name] = value;
    }
}

當值發生變化,由於變數都直接繫結到vue物件本身,因此可以直接透過屬性名找到並賦值。

事件

我們將配置中methods的方法也掛載到了vue物件上,那麼事件響應就可以做了,我們稍微修改下上一篇中compile函式,增加事件響應

function compile(node) {
        let element = node.cloneNode(false);
        for (let i = 0; i < node.childNodes.length; ++i) {
            element.appendChild(compile(node.childNodes[i]));
        }
        if (element.nodeType == 3) {
            //文字型別解析
            let vars = parseVariable(element.textContent);
            for (let i = 0; i < vars.length; ++i) {
                let directive = Directive(element, vars[i], element.textContent);
                addSubscriber(vars[i], directive);
            }
        } else if (element.nodeType == 1 && element.attributes) {
            //元素型別解析
            let attrs = element.attributes;
            for (let i = 0; i < attrs.length; ++i) {
                let name = attrs[i].name;
                if (name.startsWith("v-bind") || name.startsWith(":") || name.startsWith("v-model")) {
                    let vars = parseVariable(attrs[i].value);
                    if (vars.length == 0) {
                        let directive = Directive(element, name, attrs[i].value);
                        addSubscriber(attrs[i].value, directive);
                    } else {
                        for (let i = 0; i < vars.length; ++i) {
                            let directive = Directive(element, name, attrs[i].value);
                            addSubscriber(vars[i], directive);
                        }
                    }
                }
                //事件響應
                if (name.startsWith("v-on:")) {
                    let event = name.substr(5);
                    addEvent(element, event, parseMethod(attrs[i].value));
                }
            }
        }
        return element;
    }
  • parseMethod透過正規表示式進行解析
function parseMethod(exp) {
        var method = {};
        let m;
        let methodRegExp = /([^\(]+)\(([^\)]*)\)/g;
        if (m = methodRegExp.exec(exp)) {
            method.name = m[1];
            let params = m[2];
            params = params.replace(/\s+/g, '');
            if (params && params.length > 0) {
                method.params = params.split(",");
            } else {
                method.params = [];
            }
        }
        return method;
    }
  • addEvent程式碼:
function addEvent(element, event, method) {
        element.addEventListener(event, function(e) {
            let params = [];
            let paramNames = method.params;
            if (paramNames) {
                for (let i = 0; i < paramNames.length; ++i) {
                    params.push(vue[paramNames[i]]);
                }
            }
            vue[method.name].apply(vue, params);
        })
    }

透過呼叫addEventListener給節點增加事件,比如v-on:click就會增加click事件,透過apply方法動態呼叫定義在vue中的方法,完成事件的響應,

效果

可以點選這裡檢視效果,為了用上style屬性,修改了下html,增加了style屬性,效果如下:

完整的js程式碼可以點選這裡檢視

參考

點選餘下連結,檢視該系列其他文章

相關文章