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

 

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