剖析手寫Vue,你也可以手寫一個MVVM框架


剖析手寫Vue,你也可以手寫一個MVVM框架#

郵箱:563995050@qq.com
github: https://github.com/xiaoqiuxiong
作者:肖秋雄(eddy)
溫馨提示:感謝閱讀,筆者創作辛苦,如需轉載請自覺注明出處哦

Vue MVVM響應式原理剖釋

Vue是采用數據劫持配合發布者和訂閱者模式,通過Object.definerProperty()來劫持各個屬性的setter和setter,在數據變動時,發布消息給依賴收集器Dep,去通知觀察者Watcher,觸發對應的解釋模板回調函數去更新視圖。
詳細點說就是MVVM作為綁定的入口,整合了Observer,Compile和Watcher三者,通過Observer來劫持且監聽數據,通過Compile來解釋編譯模板指令,然后利用Watcher搭建Observer,Compile之間的聯系,達到數據變化=>視圖更新,視圖交互變化=>數據變化=>視圖更新雙向綁定的效果。
示列:

手寫Vue源碼


inex.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>手寫Vue</title>
</head>

<body>
    <div id="app">
        <h2>{{ person.name }} -- {{ person.age }}</h2>
        <h3>{{ person.favorite }}</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{ msg }}</h3>
        <div v-text='person.name'></div>
        <div v-text='msg'></div>
        <div v-html='htmlStr'></div>
        <input type="text" v-model='msg'>
        <button v-on:click="handlerClick">點擊按鈕</button>
        <br>
        <br>
        <div>
            數量: 
            <button v-on:click="sub">-</button>
            {{num}}
            <button v-on:click="add">+</button>
        </div>
    </div>
    <script src="./Observer.js"></script>
    <script src="./MVue.js"></script>
    <script>
        let vm = new MVue({
            el: '#app',
            data: {
                num: 1,
                person: {
                    name: '小馬哥',
                    age: 18,
                    favorite: '喜歡大長腿妹妹!'
                },
                msg: '學習手寫Vue框架',
                htmlStr: '<h3>我愛學習vue</h3>'
            },
            methods: {
                handlerClick() {
                    this.msg = '66'
                    this.person = {
                        name: '大馬哥',
                        age: 99,
                        favorite: '喜歡大哥哥!'
                    }
                },
                add() {
                    this.num++
                },
                sub() {
                    if(this.num === 1) return
                    this.num = this.num -1
                }
            }
        })
    </script>

</html>

MVue.js


// 入口方法
class MVue {
    constructor(options) {
        this.$options = options
        this.$el = options.el
        this.$data = options.data

        if (this.$el) {
            // 1.實現一個數據觀察者
            new Observer(this.$data)
            // 2.實現一個指令解釋器
            new Compile(this.$el, this)
            // 代理this.$data => this
            this.proxyData(this.$data)
        }
    }

    // 代理
    proxyData(data) {
        for (const key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set: newVal => {
                    data[key] = newVal
                }
            })
        }
    }
}

// 指令解釋器 
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        this.vm = vm

        // 1.獲取文檔碎片對象,放入內存中可以減少頁面回流和重繪
        const fragment = this.node2Fragment(this.el)
        // console.log(fragment);

        // 2.編譯模板
        this.compile(fragment)

        // 3.追加子元素到根元素
        this.el.appendChild(fragment)
    }

    compile(fragment) {
        // 1.獲取每一個子節點
        let childNodes = fragment.childNodes
        childNodes = this.convertToArray(childNodes)
        childNodes.forEach(child => {
            if (this.isElementNode(child)) {
                // console.log('元素節點', child);
                this.compileElement(child)
            } else {
                // console.log('文檔節點', child);
                this.compileText(child)
            }
            if (child.childNodes && child.childNodes.length) {
                this.compile(child)
            }
        });
    }

    isDirective(name) {
        return name.startsWith('v-')
    }

    isElementNode(node) {
        // 判斷是否是元素節點
        return node.nodeType === 1
    }

    convertToArray(nodes) {
        // 將childNodes返回的數據轉化為數組的方法
        var array = null;
        try {
            array = Array.prototype.slice.call(nodes, 0);
        } catch (ex) {
            array = new Array();
            for (var i = 0, len = nodes.length; i < len; i++) {
                array.push(nodes[i]);
            }
        }
        return array;
    }

    compileElement(node) {
        // console.log(node);
        // <div v-text="msg"></div>
        const attributes = node.attributes
        // console.log(attributes);
        this.convertToArray(attributes).forEach(attr => {
            const { name, value } = attr
            // console.log(value);
            if (this.isDirective(name)) {
                // 是一個指令
                const [, dirctive] = name.split('-')
                const [dirName, eventName] = dirctive.split(':')
                //  console.log(dirName, eventName);
                // 更新數據 數據驅動視圖
                compileUtil[dirName](node, value, this.vm, eventName)

                // 刪除標簽上的指令
                node.removeAttribute('v-' + dirctive)
            }
        })
    }

    compileText(node) {
        // 匹配雙大括號 {{}}
        const content = node.textContent
        if (/\{\{(.+?)\}\}/.test(content)) {
            // console.log(content);
            compileUtil['text'](node, content, this.vm)
        }
    }

    node2Fragment(el) {
        // 創建文檔碎片
        let f = document.createDocumentFragment()
        while (el.firstChild) {
            f.appendChild(el.firstChild)
        }
        return f
    }
}

const compileUtil = {
    text(node, expr, vm) {
        let value
        if (expr.indexOf('{{') !== -1) {
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                // 綁定觀察者,將來數據發生變化,觸發這里的回調函數去更是對應的視圖
                new Watcher(vm, args[1], (newVal) => {
                    this.updater.textUpdater(node, this.getContentVal(expr, vm))
                })
                return this.getValue(args[1], vm)
            })
        } else {
            value = this.getValue(expr, vm)
            new Watcher(vm, expr, (newVal) => {
                this.updater.textUpdater(node, newVal)
            })
        }

        this.updater.textUpdater(node, value)
    },
    html(node, expr, vm) {
        let value = this.getValue(expr, vm)
        new Watcher(vm, expr, (newVal) => {
            this.updater.htmlUpdater(node, newVal)
        })
        this.updater.htmlUpdater(node, value)
    },
    model(node, expr, vm) {
        const value = this.getValue(expr, vm)
        // 綁定更新函數 數據=>視圖
        new Watcher(vm, expr, (newVal) => {
            this.updater.modelUpdater(node, newVal)
        })
        // 視圖=>數據=>視圖
        node.addEventListener('input', e => {
            // 設置值
            this.setValue(expr, vm, e.target.value)
        })
        this.updater.modelUpdater(node, value)
    },
    on(node, expr, vm, eventName) {
        let fn = vm.$options.methods && vm.$options.methods[expr]
        node.addEventListener(eventName, fn.bind(vm), false)
    },
    getValue(expr, vm) {
        expr = expr.replace(/\s+/g, "")
        return expr.split('.').reduce((data, currentVal) => {
            // console.log(currentVal);
            return data[currentVal]
        }, vm.$data)
    },
    setValue(expr, vm, newVal) {
        expr = expr.replace(/\s+/g, "")
        return expr.split('.').reduce((data, currentVal) => {
            data[currentVal] = newVal
        }, vm.$data)
    },
    updater: {
        textUpdater(node, value) {
            node.textContent = value
        },
        htmlUpdater(node, value) {
            node.innerHTML = value
        },
        modelUpdater(node, value) {
            node.value = value
        }
    },
    getContentVal(expr, vm) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(args[1], vm)
        })
    }
}

Observer.js


class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm
        this.expr = expr
        this.cb = cb
        // 先把舊值保存起來
        this.oldVal = this.getOldVal()
    }

    getOldVal() {
        Dep.target = this
        const oldVal = compileUtil.getValue(this.expr, this.vm)
        Dep.target = null
        return oldVal
    }

    update() {
        const newVal = compileUtil.getValue(this.expr, this.vm)
        this.cb(newVal)
    }
}


class Dep {
    constructor() {
        // 定義觀察者數組
        this.subs = []
    }

    // 收集觀察者
    addSub(watcher) {
        this.subs.push(watcher)
    }

    // 通知觀察者去更新視圖
    notify() {
        // console.log('通知了觀察者');
        this.subs.forEach(w => w.update())
    }
}


// 數據劫持監聽
class Observer {
    constructor(data) {
        this.observer(data)
    }

    observer(data) {
        if (data && typeof data === 'object') {
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key])
            })
        }
    }

    defineReactive(obj, key, value) {
        // 遞歸遍歷,直到最后一個值不是對象
        this.observer(value)
        const dep = new Dep()
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            get() {
                // 訂閱數據變化時,往Dep中添加觀察者
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set: (newVal) => {
                this.observer(newVal)
                // 重新更新值之前先對新值劫持監聽
                value = newVal
                // 告訴Dep通知變化
                dep.notify()
            }
        })
    }
}


感謝閱讀,筆者創作辛苦,如需轉載請自覺注明出處哦


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM