Vue中MVVM模式的雙向綁定原理 和 代碼的實現

 
今天帶大家簡單的實現MVVM模式,Object.defineProperty代理(proxy)數據
 
MVVM的實現方式:
  • 模板編譯(Compile)
  • 數據劫持(Observer) Object.defineProperty
  • 發佈的訂閱(Dep)
  • 觀察者(Watcher)
 
 
MVVM:
 
  • 數據就是簡單的javascript對象,需要將數據綁定到模板上
  • 監聽視圖的變化,視圖變化後通知數據更新,數據更新會再次導致視圖的變化!
 
下面是實現方法:
---------------------------------------demo-start--
這是我打的demo:
 
 {{message}}
  {{message}}
{{info}}
 ---------------------------------------demo-end--
 
 
demo圖例:
 
簡單的mock Vue MVVM:
 
html內容:
<body>
    <div id="app">
        <!-- 測試data數據:實現雙向綁定 -->
        <input type="text" id="input" />
        <div>
            &nbsp;{{message}}
            <div>
                &nbsp;&nbsp;{{message}}
            </div>
        </div>
        {{info}}
    </div>
    <!-- 簡單實現 Vue MVVM模式 -->
    <script src="ziChin_mock_vue.js"></script>
    <script>
        let message = '子卿的初始message'
        // 實例MVVM:
        var vm = new Vue({
            el: '#app',
            data: {
                message,
                info:'初始info'
            }
        })
        // 利用oninput輸入框測試雙向綁定:
        let input = document.querySelector('#input')
        input.value = message
        input.oninput = function (e) {
            vm.$data.message = e.target.value
        }
    </script>
</body>

 
ziChin_mock_vue.js文件:
// 構建一個MVVM實例(ES6實現)
class Vue {
    constructor(options) {
        // 初始化變量
        this.$options = options
        this.$el = options.el
        this.$data = options.data
        // 1.監聽數據
        this.observer(this.$data)
        // 2.編譯模版
        this.compile(this.$el)
    }
    compile(el) {
        // ...
    }
    observer(data) {
        // ...
    }
}
// 觀察者模式
class Dep {
    // ...
}
class Watcher {
    // 訂閱信息
    // ...
}

 

 
 
observer 監聽數據以便更新視圖(數據劫持):
observer(data) {
        Object.keys(data).forEach(key => {
            let dep = new Dep()
            let value = data[key]
            // 數據劫持的核心方法:
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get() {
                    if (Dep.target) {
                        dep.addSub(Dep.target) // 把訂閱信息緩存起來
                    }
                    return value
                },
                set(newValue) {
                    dep.notify(newValue, value)
                    value = newValue
                }
            })
        })
    }

 

 
 
compile 編譯模版(這裏我沒有用虛擬Node):
compile(el) {
        let element = document.querySelector(el)
        let childNodes = element.childNodes
        const compileNodes = childNodes => { // 遞歸
            Array.from(childNodes).forEach(node => {
                if (node.nodeType === 3) { // 文本節點
                    let reg = /\{\{\s*(\S*)\s*\}\}/
                    let dataKey = null
                    node.textContent = node.textContent.replace(reg, ($0, $1) => {
                        dataKey = $1
                        return this.$data[dataKey]
                    })
                    if (dataKey !== null) { // 監聽(視圖與數據一一對應)
                        new Watcher(this, dataKey, (newValue, value) => {
                            node.textContent = node.textContent.replace(value, newValue)
                        })
                    }
                } else if (node.nodeType === 1) { // 標籤節點
                    compileNodes(node.childNodes)
                }
            })
        }
        compileNodes(childNodes)
    }

 

 
Dep、Watcher 觀察者模式:
// 觀察者模式
class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) { // 緩存訂閱內容
        this.subs.push(sub)
    }
    notify(newValue, value) { // 發佈信息
        this.subs.forEach(item => item.update(newValue, value))
    }
}
class Watcher {
    constructor(vm, dataKey, cb) {
        Dep.target = this
        vm.$data[dataKey]  // 觸發Object中get函數的 --> addSub,緩存訂閱內容
        Dep.target = null
        this.cb = cb
    }
    update(newValue, value) {
        this.cb(newValue, value)  // 由notify觸發
    }
}

 

所有代碼:

html:

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>ziChin_mock_vue</title>
</head>
<body>
    <div id="app">
        <!-- 測試data數據:實現雙向綁定 -->
        <input type="text" id="input" />
        <div>
            &nbsp;{{message}}
            <div>
                &nbsp;&nbsp;{{message}}
            </div>
        </div>
        {{info}}
    </div>
    <!-- 簡單實現 Vue MVVM模式 -->
    <script src="ziChin_mock_vue.js"></script>
    <script>
        let message = '子卿的初始message'
        // 實例MVVM:
        var vm = new Vue({
            el: '#app',
            data: {
                message,
                info:'初始info'
            }
        })
        // 利用oninput輸入框測試雙向綁定:
        let input = document.querySelector('#input')
        input.value = message
        input.oninput = function (e) {
            vm.$data.message = e.target.value
        }
    </script>
</body>
</html>
View Code

js:

ziChin_mock_vue.js
// 構建一個MVVM實例(ES6實現)
class Vue {
    constructor(options) {
        // 初始化變量
        this.$options = options
        this.$el = options.el
        this.$data = options.data
        // 1.監聽數據以便更新視圖(數據劫持)
        this.observer(this.$data)
        // 2.編譯模版(這裏我沒有用虛擬Node)
        this.compile(this.$el)
    }
    compile(el) {
        let element = document.querySelector(el)
        let childNodes = element.childNodes
        const compileNodes = childNodes => {
            Array.from(childNodes).forEach(node => {
                if (node.nodeType === 3) { // 文本節點
                    let reg = /\{\{\s*(\S*)\s*\}\}/
                    let dataKey = null
                    node.textContent = node.textContent.replace(reg, ($0, $1) => {
                        dataKey = $1
                        return this.$data[dataKey]
                    })
                    if (dataKey !== null) { // 監聽(視圖與數據一一對應)
                        new Watcher(this, dataKey, (newValue, value) => {
                            node.textContent = node.textContent.replace(value, newValue)
                        })
                    }
                } else if (node.nodeType === 1) { // 標籤節點
                    compileNodes(node.childNodes)
                }
            })
        }
        compileNodes(childNodes)
    }
    observer(data) {
        Object.keys(data).forEach(key => {
            let dep = new Dep()
            let value = data[key]
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get() {
                    if (Dep.target) {
                        dep.addSub(Dep.target)
                    }
                    return value
                },
                set(newValue) {
                    dep.notify(newValue, value)
                    value = newValue
                }
            })
        })
    }
}
// 觀察者模式
class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    notify(newValue, value) {
        this.subs.forEach(item => item.update(newValue, value))
    }
}
class Watcher {
    constructor(vm, dataKey, cb) {
        Dep.target = this
        vm.$data[dataKey]
        Dep.target = null
        this.cb = cb
    }
    update(newValue, value) {
        this.cb(newValue, value)
    }
}
View Code

 

 
 
 
 
 
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章