前言
此文采用了设计模式中的观察者模式,模拟实现了 Vue 中数据双向绑定的效果。如果您对观察者模式不熟悉,可先阅读《JS 设计模式|观察者模式》,本文只模拟了数据双向绑定的效果,与 Vue 源码有很大出入
实现效果
DOM
使用 Vue 语法规则书写 DOM
Vue 2.0 使用
使用原生的 Vue 2.0 实现数据双向绑定
模拟实现
myVue
类使用
myVue
类实现
myVue 类中实现对 data 的数据监听,DOM 的简单解析
被观察者 data
_obverse
方法通过 Object.defineProperty()
方法来将 data
改造成一个可观察对象
同时将 data
对象属性名作为通知事件的 key
注册到 _directives
对象中,实现 data
中的 value
更新时,通知绑定的观察者更新
按照“myVue 类使用”中的方式调用 myVue 类,当 myVue
类中执行了 this._obverse()
方法后 this._directives
中的属性如下图所示
观察者 Watcher 类
声明观察者类 Watcher
当数据更新时执行 Watcher
实例中的 _update()
方法,更新绑定的 DOM 上的数据
DOM 解析方法
myVue 中的 _compile()
方法用来解析 DOM 节点
遍历 DOM 节点
解析 DOM 节点上的 v-text
属性,为相应的节点绑定 Watcher 监听,当监听的值改变时,通过 Watcher 中的 _update()
方法进行更新
解析 DOM 节点上的 v-model
属性,对此节点注册 input 输入监听,当 input 输入时,更新 data 中相应的值,同时在此节点上绑定 Watcher 监听,将值更新到 DOM 节点的 value 属性上进行值同步
按照“myVue 类使用”中的方式调用 myVue 类,当 myVue
类中执行了 this._obverse()
方法和 this._compile()
方法后 this._directives
中的属性如下图所示
实验源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div class="text" v-text="myText"></div>
<input class="input" type="text" v-model="myText" >
</div>
</body>
<script>
class myVue {
constructor (options) {
this.$el = document.querySelector(options.el)
this.$data = options.data
this._directives = {}
this._obverse(this.$data)
this._compile(this.$el)
}
_obverse (data) {
let _dir
let value
// 遍历 data 对象
Object.entries(data).forEach(dataArray => {
this._directives[dataArray[0]] = [];
value = dataArray[1]
// 值存在且为对象
if(value && value === 'object'){
this._obverse(value)
}
_dir = this._directives[dataArray[0]]
Object.defineProperty(this.$data, dataArray[0], {
enumerable: true,
configurable: true,
get: function() {
return value
},
set: function(newValue) {
// 此处值做简单的恒等比较,不考虑复杂对象
if(value !== newValue){
value = newValue
_dir.forEach(function(item){
item._update();
})
}
}
})
})
}
_compile (el) {
let nodes = el.children
for(let i = 0; i < nodes.length; i ++){
if(nodes[i].children.length){
this._compile(nodes[i])
}
// 处理 dom 上的 v-text 属性
if(nodes[i].hasAttribute('v-text')){
let attrValue = nodes[i].getAttribute('v-text')
this._directives[attrValue].push(new Watcher(nodes[i], this, attrValue, 'innerHTML'))
}
// 处理 input 和 textArea 上的 v-model 属性
if(nodes[i].hasAttribute('v-model') && (nodes[i].tagName === 'INPUT' || nodes[i].tagName === 'TEXTAREA')){
let attrValue = nodes[i].getAttribute('v-model')
this._directives[attrValue].push(new Watcher(nodes[i], this, attrValue, 'value'))
nodes[i].addEventListener('input', () => {
this.$data[attrValue] = nodes[i].value
})
}
}
}
}
class Watcher {
constructor (el, vm, dataKey, elAttr) {
// 需更新的 dom 节点
this.el = el
// myVue 实例
this.vm = vm
// data 的属性名
this.dataKey = dataKey
// dom 节点的属性值
this.elAttr = elAttr
}
_update() {
this.el[this.elAttr] = this.vm.$data[this.dataKey]
}
}
</script>
<script>
const app = new myVue({
el: '#app',
data: {
myText: ''
}
})
</script>
</html>
复制代码
总结
此文参考了大佬的博文进行改写,有兴趣读者可通过“参考资料”跳转阅读,给大老点个赞。这是观察者模式在 JS 项目中的实际应用,通过此例子可以更深层次的理解 Vue 中一些 api 设计的意义,比如 Vue.set()
方法。本人是 react 开发者,没有多少 vue 经验,如果文中存在着 bug,望读者指点