VUE的實現原理(數據劫持、觀察者模式)

記一次vue原理學習記錄,涉及瞭如何進行數據劫持、數據代理、觀察者模式(發佈訂閱)、數據雙向綁定功能等知識點,也簡單實現了vue中一些經常使用功能,代碼中作了比較詳細的註釋,能夠直接複製執行查看結果javascript

index.htmlhtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MVVM原理</title>
</head>
<body>
    <div id="app">
        {{school.name}}{{age}}
        <div>{{age}}</div>
        <input type="text" v-model="school.name">
        <div>{{school.name}}</div>
        <div>compute: {{getNewName}}</div>
        <ul>
            <li>li_1</li>
            <li>li_2</li>
        </ul>
        <button v-on:click="changeName">methods</button>
        <div v-html="htmlName"></div>
    </div>
    <!-- <script src="./node_modules/vue/dist/vue.js"></script> -->
    <script src="mvvm.js"></script>
    <script> let vm = new Vue({ // el: document.querySelector('#app'), el: '#app', data: { school: { name: '學校名' }, age: 20, htmlName: '<h1>inner html</h1>' }, methods: { changeName () { this.school.name = '我是methods方法修改的' } }, computed: { getNewName () { return this.school.name + 'new' } } }) </script>
</body>
</html>
複製代碼

mvvm.jsvue

// 基礎類 負責調度
class Vue {
    constructor (options) {
        this.$el = options.el
        this.$data = options.data
        let computed = options.computed
        let methods = options.methods
        // 編譯模版
        if (this.$el) {
            // 數據劫持 把數據所有轉化爲Object.defineProperty來定義
            new Observer(this.$data)
            // computed存在依賴關係,執行computed方法會取值觸發get
            for (let key in computed) {
                Object.defineProperty(this.$data, key, {
                    get: () => {
                        return computed[key].call(this)
                    }
                })
            }
            for (let key in methods) {
                Object.defineProperty(this, key, {
                    get () {
                        return methods[key]
                    }
                })
            }
            // 數據代理 vm上的數據操做代理到vm.$data上
            this.proxyVm(this.$data)
            // 編譯模版
            new Compiler(this.$el, this)
        }
    }
    // 將vm操做代理到vm.$data上取值
    proxyVm (data) {
        for (let key in data) {
            Object.defineProperty(this, key, {
                get () {
                    return data[key] // 進行轉化操做
                }, 
                set (newVal) {
                    data[key] = newVal
                }
            })
        }
    }
}

// 對數據進行劫持
class Observer {
    constructor (data) {
        this.observer(data)
    }
    observer (data) {
        // 若是是對象就進行觀察
        if (data && typeof data === 'object') {
            for (let key in data) {
                this.defineReactive(data, key, data[key])
            }
        }
    }
    defineReactive (obj, key, value) {
        // 給每一個數據都添加一個具備發佈訂閱的功能
        let dep = new Dep()
        Object.defineProperty(obj, key, {
            get () {
                // 當建立watcher時候會取到值,會觸發get,而且將watcher放到全局
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set: (newVal) => {
                if (value !== newVal) {
                    this.observer(newVal) // 對新賦值的進行劫持
                    value = newVal
                    dep.notify() // 觸發發佈方法,通知watcher修改了數據
                }
            }
        })
        this.observer(value) // 進行遞歸劫持數據
    }
}

// 觀察者模式 (發佈訂閱模式)觀察者 被觀察者
class Watcher { // 觀察者
    constructor (vm, expr, cb) {
        this.vm = vm
        this.expr = expr
        this.cb = cb
        this.oldVal = this.getValue() // 先存放一個老值
    }
    getValue () {
        Dep.target = this // 先將本身放到Dep.target
        let value = CompilerUtils.getValue(this.vm, this.expr)
        Dep.target = null // 觸發get後清除掉
        return value
    }
    update () { // 更新操做據發生變化了會執行觀察者的update方法
        let newVal = CompilerUtils.getValue(this.vm, this.expr)
        if (newVal !== this.oldVal) {
            this.cb(newVal)
        }
    }
}
// 發佈訂閱
class Dep {
    constructor () {
        this.subs = [] // 存放全部的watcher
    }
    // 訂閱
    addSub (watcher) {
        this.subs.push(watcher)
    }
    // 發佈
    notify () { // 執行全部watcher的update方法
        this.subs.forEach(watcher => watcher.update())
    }
}

// 編譯類
class Compiler {
    constructor (el, vm) { // 傳入 '#app' || document.querySelector('#app')
        this.vm = vm
        // 判斷el是否爲元素節點
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        // console.log(this.el)
        // 將元素使用文檔碎片存到內存中
        this.fragment = this.el2fragment(this.el)
        // console.log('fragment:', this.fragment)
        // 編譯模版 將fragment中的變量進行替換
        this.compile(this.fragment)
        // 從新將fragment塞回el
        this.el.appendChild(this.fragment)
    }
    isElementNode (node) { // 判斷是否元素節點
        return node.nodeType === 1
    }
    // 把節點存到內存中
    el2fragment (node) {
        // 建立一個文檔碎片
        let fragment = document.createDocumentFragment()
        let firstChild
        while (firstChild = node.firstChild) {
            // console.log(firstChild)
            // fragment.appendChild 具備移動性,會刪除相應的節點
            fragment.appendChild(firstChild)
        }
        return fragment
    }
    compile (fragment) { // 用來編譯內存中的dom節點
        let childNodes = [...fragment.childNodes]// 類數組
        childNodes.forEach(child => {
            if (this.isElementNode(child)) {
                this.compileElement(child)
                // 遞歸編譯子節點
                this.compile(child)
            } else {
                // console.log('text: ', child)
                this.complieText(child)
            }
        })
    }
    // 編譯節點
    compileElement (node) {
        let attrs = [...node.attributes]
        // console.log('attrs:', [...attrs])
        attrs.forEach(attr => {
            let { name, value:expr } = attr
            // 判斷是否節點
            if (this.isDirecttive(name)) {
                let [, directive] = name.split('-')
                let arr
                if (directive.split(':').length > 0) {
                    arr = directive.split(':')
                } else {
                    arr = [directive]
                }
                let [directiveName, eventName] = arr
                CompilerUtils[directiveName](node, expr, this.vm, eventName)
            }
        })
    }
    // 編譯文本節點
    complieText (node) {
        let content = node.textContent
        if (/\{\{(.+?)\}\}/.test(content)) {
            // console.log(content)
            CompilerUtils['text'](node, content, this.vm)
        }
    }
    // 判斷是否指令
    isDirecttive (attrName) {
        return attrName.startsWith('v-')
    }
}

// 編譯工具方法
CompilerUtils = {
    // 根據表達式取值
    getValue (vm, expr) {
        return expr.split('.').reduce((data, curKey) => {
            return data[curKey]
        }, vm.$data)
    },
    setValue (vm, expr, value) {
        expr.split('.').reduce((data, curKey, index, arr) => {
            if (index === arr.length -1) { // 給表達式最後一個賦值
                return data[curKey] = value
            }
            return data[curKey]
        }, vm.$data)
    },
    updater: { // 更新值的方法
        modelUpdater (node, value) {
            node.value = value
        },
        htmlUpdater (node, value) {
            node.innerHTML = value
        },
        textUpdater (node, value) { // 更新文本節點
            node.textContent = value
        }
    },
    model (node, expr, vm) { // node 節點 expr 表達式 vm 當前實例
        // 給輸入框賦值value
        let value = this.getValue(vm, expr)
        let fn = this.updater['modelUpdater']
        // 放入觀察者
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal) // 將新值給輸入框賦值
        })
        fn(node, value)
        // 視圖驅動數據 給v-model綁定input事件
        node.addEventListener('input', evt => {
            let val = evt.target.value
            this.setValue(vm, expr, val)
        })
    },
    on (node, expr, vm, eventName) {
        node.addEventListener(eventName, (evt) => {
            // console.log(vm, expr)
            vm[expr].call(vm, evt) // 執行相應方法methods
        })
    },
    getContentVal (vm, expr) {
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(vm, args[1])
        })
        return content
    },
    text (node, expr, vm) { // 處理文本節點
        let fn = this.updater.textUpdater
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // 爲文本插入觀察者 給{{}}的都加上觀察者
            new Watcher(vm, args[1], () => {
                fn(node, this.getContentVal(vm, expr)) // 返回一個全字符串
            })
            return this.getValue(vm, args[1])
        })
        fn(node, content) // 更新值
    },
    html (node, expr, vm) {
        let fn = this.updater['htmlUpdater']
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal) // 將新值給v-html賦值
        })
        fn(node, this.getValue(vm, expr))
    }
}



複製代碼
相關文章
相關標籤/搜索