双向绑定主要就是包括两个方面:数据变化更新视图,视图变化更新数据。

所以需要去进行双向的监听,页面上的变化可以通过事件来监听,例如:input标签可以通过监听 ‘input’ 事件获取页面上输入的变化,进而进行相应的操作。

所以,关键点在于如何监听data以及如何更新view,vue2是通过Object.defineProperty()进行数据的监听。数据的更新则是通过发布订阅模式实现的。

这样就实现了:数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变。

基本实现

下面就是通过Object.defineProperty()实现最简单的双向绑定。

随着input输入文字的变化,p标签中会同步显示相同的文字内容;点击button模拟了value的变化,随着value的变化p标签中会同步显示相同的文字内容。这样就实现了 model => view 以及 view => model 的双向绑定

1
2
3
4
5
6
<!-- html -->
<div class="main">
<p class="value"></p>
<input type="text">
<button>修改值</button>
</div>
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
//js
let out = document.getElementsByClassName('value')[0]
let input = document.getElementsByTagName('input')[0]
let btn = document.getElementsByTagName('button')[0]

let obj = {}
let value = null

input.addEventListener('input', function (e) {
obj.value = e.target.value
})


btn.onclick = function () {
obj.value = '修改了'
}

Object.defineProperty(obj, 'value', {
get() {
return value
},
set(newVal) {
value = newVal
input.value = value
out.innerHTML = value
}
})

vue的双向绑定分析

首先我们用vue实现一下刚才的功能,然后再对代码进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 代码中html -->
<div id="vue">
<p class="value">{{text}}</p>
<input type="text" v-model="text">
<button>修改值</button>
</div>

<!-- 页面中解析出来的html -->
<div id="vue">
<p class="value"></p>
<input type="text">
<button>修改值</button>
</div>
1
2
3
4
5
6
7
//js
let vm = new Vue({
el: '#vue',
data: {
text: '',
}
})

通过HTML的代码可以看出来,除了通过数据劫持实现了双向绑定外,还需要对网页模板进行解析。主要是两个部分,v-model{{ text}}。解析的时候,先通过el获取根节点,然后对其中的所有节点一一进行解析,在有{{ text}}的地方使用text中的内容替换了{{ text}},在有v-model的地方将input的输入值绑定到了text上,当所有节点都解析完成后再将解析后的内容替换到原来的节点上。

然后是创建vue实例时传入的基本参数,el是需要解析的模板的根节点,data是需要通过Object.defineProperty()进行数据劫持的数据集合。

这样的话,我们最基础的版本需要实现的就是两个函数:

  • defineReactive()—-通过Object.defineProperty()进行数据劫持
  • compile()—-根据一定的规则对模板进行解析

简易vue实现

首先是defineReactive(),根据最上面最基本的实现方案可知,目前是需要三个参数objkey以及用来存值的val

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key , {
get() {
return val
},
set(newVal) {
//为了减少性能损耗,如果更新后的值和之前的值是一样的,就不变
if(newVal === val) {
return
}
val = newVal
}
})
}

然后是compile(),在这里我们知道,我们需要进行三部分操作:

  • 获取所有的节点
  • 根据规则进行解析
  • 将解析后的节点替换到原来的节点的位置

所以在这个函数中,我们需要两个参数nodevmnode是需要解析的节点,vm中包含着解析时需要的data

首先是 获取所有的节点 ,在这里我们引入一个东西—-documentFragment

以下是mdn中的关于documentFragment的介绍。

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

最常用的方法是使用文档片段作为参数(例如,任何 Node 接口类似 Node.appendChildNode.insertBefore 的方法),这种情况下被添加(append)或被插入(inserted)的是片段的所有子节点,而非片段本身。因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作,而不是每个节点分别被插入到文档中,因为后者会发生多次重渲染的操作。

总的来说就是,documentFragment是一个保存多个element的容器对象(保存在内存)当更新其中的一个或者多个element时,页面不会更新。只有当documentFragment容器中保存的所有element更新后再将其插入到页面中才能更新页面。非常适合用来批量更新。

1
func