vue--實現MVVM原理

遨翔在知識的海洋裡發表於2019-03-09

1. 說明

  1. compile 編譯,即模板解析器,能夠對模板中的指令和插值表示式進行解析
  2. observer 資料劫持,即資料監聽器,能夠對資料物件(data)的所有屬性進行監聽
  3. watcer 監聽者,將compile的解析結果,與observer所觀察的物件連線起來,建立關係,在observer觀察到資料物件變化時,接收通知,並更新DOM

gitbub

2. 實現MVVM原理

2.1 目錄結構

vue--實現MVVM原理

2.2 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>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="message.a">
        <div>{{b}}</div>
        <ul>
            <li>{{message.a}}</li>
        </ul>
        {{b}}
    </div>
    <!-- <script src="https://cdn.jsdelivr.net/npm/vue"></script> -->
    <script src="./watcher.js"></script>
    <script src="./observer.js"></script>
    <script src="./compile.js"></script>
    <script src="./mvvm.js"></script>
    <script>
        let vm = new MVVM({
            el: '#app',
            data: {
                message: {
                    a: 'aa'
                },
                b: 'bb'
            }
        })
    </script>
</body>

</html>
複製程式碼

3. mvvm

  1. 整合編譯和資料劫持
  2. 代理,使vm.$data.message => vm.message

3.1 完整mvvm.js

class MVVM {
    constructor(options) {
            // 例項上的dom元素,<div id="app"></div>
            this.$el = options.el;
            // 例項上的所有資料,data
            this.$data = options.data;
            // 如果有這個dom元素,才開始
            if (this.$el) {
                // 資料劫持,就是對資料的所有屬性,改成set和get的方法,以至可以在資料獲取前和改變後,觸發其它方法(做點事情)
                new Observer(this.$data);
                this.proxyData(this.$data)
                    // 編譯元素,例如<input type="text" v-model="message.a">,根據message.a,找到data中對應的message.a的資料,賦值給input的value
                new Compile(this.$el, this)
            }
        }
        // proxy代理: vm.$data.message => vm.message
    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(newValue) {
                    data[key] = newValue;
                }
            })
        })
    }
}
複製程式碼

4. compile

4.1 nodeType

nodeType 屬性返回節點型別。

  1. 如果節點是一個元素節點,nodeType 屬性返回 1。

  2. 如果節點是屬性節點, nodeType 屬性返回 2。

  3. 如果節點是一個文字節點,nodeType 屬性返回 3。

vue--實現MVVM原理

4.2 createDocumentFragment()

  1. DocumentFragments 是DOM節點。它們不是主DOM樹的一部分。通常的用例是建立文件片段,將元素附加到文件片段,然後將文件片段附加到DOM樹。在DOM樹中,文件片段被其所有的子元素所代替。
  2. 因為文件片段存在於記憶體中,並不在DOM樹中,所以將子元素插入到文件片段時不會引起頁面迴流(對元素位置和幾何上的計算)。因此,使用文件片段通常會帶來更好的效能。

4.3 reduce

4.3.1 說明

接收一個函式作為累加器,陣列中的每個值(從左到右)開始縮減,最終計算為一個值

4.3.2 有一個字串message.a.b,有一個物件{"message":{"a":{"b":"我是bb"}}},想要找到字條串中的b,在物件中key為b,對應的value

        //物件
        var dataObj = {
            message: {
                a: {
                    b: '我是bb'
                }
            },
        };
        //字串
        var dataStr = 'message.a.b';
        // 字串轉成陣列
        var dataArray = dataStr.split('.')
        console.log(dataArray)
            //找到字條串中的b,在物件中key為b,對應的value
        var result = dataArray.reduce((prev, next) => {
            return prev[next]
        }, dataObj)
        console.log(result)
複製程式碼

vue--實現MVVM原理

4.4 /\{\{([^}]+)\}\}/g;

將{{a}} => a

        let expr = "{{message.a.b}}"; // 取文字中的內容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}} 
        var result = expr.replace(reg, 'a');
        console.log(result)
複製程式碼

vue--實現MVVM原理

4.5 ...運算子

        function sub(...arg) {
            let sum = 0;
            arg.forEach(item => {
                sum += item;
            })
            return sum;
        }
        var a = sub(1, 2, 3)
        var b = sub(1, 2, 3, 4)
        console.log(a)   //6
        console.log(b)   //10
複製程式碼

4.6 setVal()

監聽input輸入框的值,根據<input type="text" v-model="message.a">(message.a),然後把值賦給vm.data裡對應的鍵(vm.data.message.a = 值),再更新檢視上的顯示modelUpdater

    model(node, vm, expr) { 
        let updateFn = this.updater['modelUpdater'];
        node.addEventListener('input', (e) => {
            let newValue = e.target.value;
            this.setVal(vm, expr, newValue)
        })
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
複製程式碼
    setVal(vm, expr, value) { // [message,a]
        expr = expr.split('.');
        return expr.reduce((prev, next, currentIndex) => {
            if (currentIndex === expr.length - 1) {
                return prev[next] = value;
            }
            return prev[next];
        }, vm.$data);
    },
複製程式碼
    updater: {
        modelUpdater(node, value) {
            node.value = value;
        }
    }
複製程式碼

4.6 完整compile.js程式碼

class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        if (this.el) {
            let fragment = this.node2fragment(this.el);
            this.compile(fragment);
            this.el.appendChild(fragment)
        }
    }
    isElementNode(node) {
        return node.nodeType === 1;
    }
    isDirective(name) {
        return name.includes('v-')
    }
    compileElement(node) {
        let attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
            let attrName = attr.name;
            if (this.isDirective(attrName)) {
                let expr = attr.value;
                let [, type] = attrName.split('-');
                CompileUtil[type](node, this.vm, expr)
            }
        })
    }
    compileText(node) {
        let expr = node.textContent;
        let reg = /\{\{([^}]+)\}\}/g;
        if (reg.test(expr)) {
            CompileUtil['text'](node, this.vm, expr)
        }
    }
    compile(fragment) {
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                this.compileElement(node)
                this.compile(node)
            } else {
                this.compileText(node)
            }
        })
    }
    node2fragment(el) {
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
}

CompileUtil = {
    getVal(vm, expr) {
        expr = expr.split('.');
        return expr.reduce((prev, next) => {
            return prev[next]
        }, vm.$data)
    },
    getTextVal(vm, expr) {
        return expr.replace(/\{\{([^}]+)\}\}/g, (...argument) => {
            return this.getVal(vm, argument[1])
        })
    },
    setVal(vm, expr, value) {
        expr = expr.split('.');
        return expr.reduce((prev, next, currentIndex) => {
            if (currentIndex === expr.length - 1) {
                return prev[next] = value
            }
            return prev[next]
        }, vm.$data)
    },
    model(node, vm, expr) {
        let updateFn = this.updater['modelUpdater'];
        let value = this.getVal(vm, expr);
        new Watcher(vm, expr, (newValue) => {
            updateFn && updateFn(node, this.getVal(vm, expr))
        })
        node.addEventListener('input', (e) => {
            let newValue = e.target.value;
            this.setVal(vm, expr, newValue)
        })
        updateFn && updateFn(node, value)
    },
    text(node, vm, expr) {
        let updateFn = this.updater['textUpdater'];
        let value = this.getTextVal(vm, expr);
        expr.replace(/\{\{([^}]+)\}\}/g, (...argument) => {
            new Watcher(vm, argument[1], (newValue) => {
                updateFn && updateFn(node, this.getTextVal(vm, expr))
            })
        })
        updateFn && updateFn(node, value)
    },
    updater: {
        modelUpdater(node, value) {
            node.value = value;
        },
        textUpdater(node, value) {
            node.textContent = value;
        }
    }
}
複製程式碼

5. observer

5.1 this.subs=[]

<div id="app"></div>下面的節點為準,{{b}}算1個,message.a算2個

3個watcher

    <div id="app">
        <input type="text" v-model="message.a">
        <div>{{b}}</div>
    </div>
複製程式碼
    addSub(watcher) {
        this.subs.push(watcher)
        console.log(this.subs)
    }
複製程式碼

vue--實現MVVM原理

5個watcher

    <div id="app">
        <input type="text" v-model="message.a">
        <div>{{b}}</div>
        {{message.a}}
    </div>
複製程式碼

vue--實現MVVM原理

5.2完整observer.js程式碼

class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        if (!data || typeof data !== 'object') {
            return;
        }
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]);
            this.observer(data[key])
        })
    }
    defineReactive(obj, key, value) {
        let that = this;
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue) {
                if (newValue != value) {
                    that.observer(newValue)
                    value = newValue;
                    dep.notify();
                }
            }
        })
    }
}


class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
複製程式碼

6. watcer

6.1 new Watcher()

<div id="app"></div>下面的節點(有expr),一個expr對應一個watcher,一個watcher後續變化都儲存到一個dep.subs[]裡

6.2 完整watcer.js程式碼

class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        this.value = this.get();
    }
    getVal(vm, expr) {
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    }
    get() {
        Dep.target = this;
        let value = this.getVal(this.vm, this.expr);
        Dep.target = null;
        return value;
    }
    update() {
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if (newValue != oldValue) {
            this.cb(newValue)
        }
    }
}
複製程式碼

7. 效果

GifCam錄製gif

7.1 修改資料,檢視變化

  1. 操作前,根據表示式v-model="message.a",得到vm.data裡的資料,compile,渲染到頁面
  2. vm.data裡的資料變化,觸發observer.set(),
  3. 因為新舊資料不一樣,觸發dep.notify()
  4. 觸發watcher裡的this.cb(newValue)
  5. 觸發compile裡的CompileUtil.updater()

vue--實現MVVM原理

7.2 修改檢視,資料變化

  1. node.addEventListener('input'),監聽輸入框,得到新值newValue
  2. setVal(),使用vm.data裡的資料等於新值
  3. 重複上面操作

vue--實現MVVM原理

相關文章