【框架底层简单实现(1)】简单实现双向数据绑定

作为MVVM的核心内容数据绑定,是非常要必要去探究和简单实现一下的。

Vue.js数据绑定功能

来看一下Vue.js的数据是如何使用的:

1
2
3
4
5
6
7
<div id="app">
<input v-model="message" />
</div>
<script type="text/javascript">
var message = 'Vue.js is rad';
new Vue({ el: '#app', data: { message } });
</script>

如果使用{ {} }的话就会涉及到模板解析方面的知识,这里为求把焦点更集中,所以使用了inputv-model的配合。

首先可以看到,我们的input标签是通过v-model来绑定数据对象的,当数据对象message内容发生改变的时候就会引起inputvalue的改变。

实现

HTML配置

先看看HTML配置

1
2
3
4
5
<div id="app">
<input s-model="message" />
<button id="click">设置message为Hello World</button>
<br/><span s-model="message"></span>
</div>

主要设置了一个绑定了message数据对象的input输入框,一个点击后会设置message对象值为Hello worldbutton,还有一个绑定了message数据对象的span用来看看它的值的改变。

Proxy Handler

这里的object劫持采用了Proxy的方式,下面是handler的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function BindingProxyHandler () {
return {
get: (target, key, receiver) => {
return Reflect.get(target, key, receiver);
},
set: (target, key, value, receiver) => {
// 搜索绑定key的model的DOM
let doms = document.querySelectorAll(`${this.elTag} [s-model="${key}"]`) || null
if (!doms || doms.length === 0)return
// update DOM
doms.forEach(dom => {
dom.value = dom.innerHTML = value
})
return Reflect.set(target, key, value, receiver);
}
}
}

该函数返回了一个闭包函数组,封装成函数的形式是为了方便后面注入this的作用域。
主要看set部分:当有值改变的时候,会通过querySelectorAll来获取有绑定该被改变模型对象的DOM,当然这部分可以加入Cache缓存绑定结果,就不需要每次都去query一次了,为了思路更清晰,这里先不加。

初始化工作

然后我们定义一个类似Vue配置形式的函数,叫它Sue吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Sue(options) {
if (!options.el) throw Error('`el` can not be null')

// 保存tag名称,方面query
this.elTag = options.el
// 保存tag dom
this.el = document.querySelectorAll(options.el)
if (this.el.length > 0){
this.el = this.el[0]
} else {
throw new Error('`el` element not exist')
}
// data binding
this.datas = new Proxy(options.data || {}, BindingProxyHandler.call(this)) // 绑定当前作用域

this.bindInput()
// set default
Object.keys(this.datas).forEach(key => this.set(key, this.datas[key]))
return this
}

Sue.prototype.set = function (key, value) {
if (!this.datas[key])return
this.datas[key] = value
}

Sue.prototype.bindInput = function (modelName) {
let el = document.querySelector(`${this.elTag}`)
el.addEventListener('keyup', (evt) => {
let node = evt.target
if (node.tagName === 'INPUT' && node.getAttribute('s-model') !== '') {
console.log(node.value)
this.datas[node.getAttribute('s-model')] = node.value
}
})
return el
}

这里面主要一步就是数据通过Proxy来进行劫持绑定,传入的data对象的数值在get/set操作的时候都会先经过我们上面设置的BindingProxyHandler里面的方法。

最后调用:

1
2
3
4
5
6
7
8
9
10
window.onload = function () {
let app = new Sue({
el: '#app',
data: {
message: '123'
}
})

document.querySelector('#click').addEventListener('click', () => app.set('message','Hello World'))
}

测试

DEMO

总结

到这一步,其实已经完成了,整个流程很清晰:

  1. 劫持数据get/set方法
  2. 监听输入类组件的输入事件,当触发的时候就把值传递给this.datas[model]
  3. 传递成功后,就会自动触发BindingProxyHandler中的set事件,然后再查询绑定了该数据模型的DOM,将其的’value/innerHTML’设为新的value

当然,代码中其实有很多地方可以优化的,例如缓存,防抖等,为了表达清晰脉络,所以简化一下。