Vue实现原理——实现双向绑定mvvm 学习笔记

实现数据绑定的几种方法

  • 发布者-订阅者模式
  • 脏值检查:如angular.js,在指定事件触发时,通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。这些事件有:DOM事件(输入文本、点击按钮)、XHR响应事件、浏览器Location变更事件、Timer事件($timeout、$interval)、执行$digest()或apply()。
  • 数据劫持:如vue.js,采用数据劫持结合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

实现vue的双向绑定

主要通过ObjectdefineProperty属性,重写data的setget函数来实现的。
双向绑定
下面是一个实现的例子,主要实现v-modelv-bindv-click三个命令。

最后实现结果

1
2
3
4
5
<div id="app">
<input type="text" v-model="number" />
<button type="button" v-click="increment">增加</button>
<h3 v-bind="number"></h3>
</div>
1
2
3
4
5
6
7
8
9
10
11
var app = new myVue({
el: '#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number++
}
}
})

mvvm实例

myVue构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function myVue(options) {
this._init(options)
}
myVue.prototype._init = function (options) {
this.$options = options
this.$el = document.querySelector(options.el) // 根节点dom
this.$data = options.data
this.$methods = options.methods

this._binding = {} // 绑定的订阅者
this._observe(this.$data) // 监听属性
this._compile(this.$el)
}

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
30
31
myVue.prototype._observe = function(obj) {
var value
for (key in obj) {
if (obj.hasOwnProperty(key)) {
value = obj[key]
if (typeof value === 'object') {
this._observe(value)
}
this._binding[key] = {
_watchers: []
}
var binding = this._binding[key]
Object.defineProperty(this.$data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log('获取属性' + key + ': ' + value)
return value
},
set: function(newVal) {
console.log('设置属性' + key + ': ' + newVal)
value = newVal
// 通知该属性的所有订阅者更新数据
binding._watchers.forEach(function(item) {
item.update()
})
}
})
}
}
}

订阅者

用来绑定更新函数,实现对DOM元素的更新

1
2
3
4
5
6
7
8
9
10
11
12
function Watcher(el, vm, exp, attr) {
this.el = el  // 指令对应的DOM元素
this.vm = vm // 指令所属的myVue实例
this.exp = exp // myVue实例更新的属性
this.attr = attr // DOM元素绑定的属性值

this.update()
}
// 更新视图
Watcher.prototype.update = function() {
this.el[this.attr] = this.vm.$data[this.exp]
}

Compile解析

解析v-bindv-modelv-click等指令,并对view与model进行绑定

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
myVue.prototype._compile = function(app) {
var nodes = app.children
var _this = this
for (let i = 0, len = nodes.length; i < len; i++) {
var node = nodes[i]
if (node.children.length) {
this._compile(node)
}
// v-click
if (node.hasAttribute('v-click')) {
var methodName = node.getAttribute('v-click')
node.onclick = _this.$methods[methodName].bind(_this.$data)
}
// v-model
if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
var dataName = node.getAttribute('v-model')
// 添加订阅者
_this._binding[dataName]._watchers.push(new Watcher(node, _this, dataName, 'value'))
node.addEventListener('input', function() {
_this.$data[dataName] = this.value // 使data与dom的值保持一致
})
}
// v-bind
if (node.hasAttribute('v-bind')) {
var dataName = node.getAttribute('v-bind')
// 添加订阅者
_this._binding[dataName]._watchers.push(new Watcher(node, _this, dataName, 'innerHTML'))
}
}
}

源码

mvvm-demo

参考