Vue双向绑定原理

详述Vue的数据双向绑定原理

前端知识

Vue实现双向数据绑定的原理就是利用了 Object.defineProperty() 这个方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。

另一种说法:vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的。(这里不懂没关系,接着往下看…)
首先,我们我们需要一个监听器Observer来给所有的属性设置set函数。如果属性发生了变化,就要通知所有的订阅者Watcher。而这些Watcher统一存放在消息订阅器Dep中,这样比较方便统一管理。Watcher接受到来自Dep的通知后就执行相应的操作去更新视图。
 

Observer

监听器的核心代码如下:

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
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) { // 遍历属性,递归设置set函数
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val) {
observe(val)
var dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) {
dep.addSub(Dep.target) // 添加watcher
}
return val
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
dep.notify() // 通知dep
}
})
}

通过调用observe()函数来递归地给data对象设置set和get函数,在data的属性被get时添加watcher,被set时通知dep,dep的notify会接着通知所有的watcher去执行更新操作。
 
这里需要对defineProperty做一个补充,上述的observe递归过程,在value值为对象时会继续递归,只有当value值是非对象时才return,然后调用definePropery。所以对于data里面的数组arr,vue实际监听的是arr[0]、arr[1]…arr[n],而不是arr本身。所以对于改变arr的操作,arr[0] = 9这样是可以被监听到的,而arr.push(‘123’)这样是不行的,因为push方法本质上只是改变了arr[n+1]的值,而这个值本身是没有被监听的,即没有设置set函数。
vue为了方便我们对数组的操作,对数组的一些常用方法进行额外的封装,即对vue的data的属性的原型赋值为封装层,当我们使用this.arr.push时,根据原型链向上找会先找到封装层的push,而不会使用原生的push。封装层的push做的事情是先触发原生push方法,然后再监听新push的项,再触发消息订阅器dep的notify方法,从而提醒watcher去更新视图。
 

Dep

消息订阅器的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dep() {
this.subs = [] // 订阅者数组
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
}
Dep.target = null

消息订阅器比较简单,就是维护一个subs数组。当监听新属性时把它push进subs数组中,然后dep被通知时触发notify函数,从而触发subs数组中每个watcher的update操作。
 

Watcher

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
function Watcher(vm, exp, cb) {
this.cb = cb
this.vm = vm
this.exp = exp
this.value = this.get()
}

Watcher.prototype = {
update: function() {
this.run()
},
run: function() {
var value = this.vm.data[this.exp]
var oldVal = this.value
if (value !== oldVal) {
this.value = value
this.cb.call(this.vm, value, oldVal) // 执行更新时的回调函数
}
},
get: function() {
Dep.target = this
var value = this.vm.data[this.exp] // 读取data的属性,从而执行属性的get函数
Dep.target = null
return value
}
}

Watcher的主要功能是去触发属性的get函数,从而添加watcher到Dep的subs数组中。另外就是在update()中更新属性的值并触发更新回调函数。
使用Watcher的方法如下:

1
2
3
4
5
var el = document.getElementById('XXX')
observe(data)
new Watcher(vm, exp, function(value) { // vm表示某个实例,exp表示属性名
el.innerHTML = value
})

为了使用时的整洁,我们需要把代码稍微包装下。

SimpleVue

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
function SimpleVue (data, el, exp) {
var self = this
this.data = data
Object.keys(data).forEach(function(key) {
self.proxyKeys(key)
})
observe(data)
el.innerHTML = this.data[exp]
new Watcher(this, exp, function(value) {
el.innerHTML = value
})
return this
}

SimpleVue.prototype = {
proxyKeys: function(key) {
var self = this
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function() {
return self.data[key]
},
set: function(newVal) {
self.data[key] = newVal
}
})
}
}

SimpleVue做的事情就是使用observe递归地给data的每个属性都加上get和set,然后对于要监听的属性exp新建一个Watcher对象去监听。(Watcher对象触发属性exp的get函数从而添加订阅事件到Dep,而且会在属性的update方法里面触发监听回调函数)
使用如下:

1
2
3
4
5
6
7
8
9
// html
<h1 id="name">{{name}}</h1> //这个{{name}}暂时没用

// js
var el = document.querySelector('#name')
var selfVue = new SimpleVue({ name: 'hello'}, el, 'name')
setTimeout(function() {
selfVue.name = '123'
}, 2000)

需要注意的是SimpleVue原型的proxyKeys是为了将selfVue.data.name这种操作代理为selfVue.name。这下我们就可以直接通过selfVue.name = “XXX”来改变数据了,并且视图也会相应变化。

Compile

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
nodeToFragement: function(el) {
var fragment = document.createDocumentFragment()
var child = el.firstChild
// 将dom节点移到fragment
while(child) {
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileElement: function(el) {
var childNodes = el.childNodes
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/
var text = node.textContent
if (self.isTextNode(node) && reg.test(text)) {
self.compileText(node, reg.exec(text)[1])
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node) // 递归遍历子节点
}
});
},
compileText: function(node, exp) {
var self = this
var initText = this.vm[exp]
this.updateText(node, initText)
new Watcher(this.vm, exp, function(value) {
self.updateText(node, value)
})
},

compile将dom节点移入DocumentFragment中去,并递归调用compileElement函数来遍历所有子节点,compileText函数创建新的watcher。

1
2
3
4
5
6
7
8
9
10
11
function SimpleVue (options) {
var self = this
this.vm = this
this.data = options.data
Object.keys(this.data).forEach(function(key) {
self.proxyKeys(key)
})
observe(this.data)
new Compile(options.el, this.vm)
return this
}

 


好了!以上就是全部内容啦~~希望可以帮到你!!!

文章目录
  1. 1. 详述Vue的数据双向绑定原理
    1. 1.1. 前端知识
      1. 1.1.1. Observer
      2. 1.1.2. Dep
      3. 1.1.3. Watcher
      4. 1.1.4. SimpleVue
      5. 1.1.5. Compile
    2. 1.2. 好了!以上就是全部内容啦~~希望可以帮到你!!!
,