Vue 有两个比较核心的特性,一个是非侵入式的响应式数据绑定系统,另一个是组件系统。由于这两个功能非常重要,所以在求职面试的时候,面试官也通常会围绕这两部分进行提问,如数据绑定的底层实现,组件之间的通讯等等。接下来,本篇博客将主要探讨一下Vue的数据绑定原理与实现。
Vue是如何监听数据变化的
当实例化一个Vue组件的时候,我们会把一个普通的JavaScript对象传递给Vue实例的data选项,然后Vue会对这个对象里面的每个属性进行遍历,然后使用ES5的Object.defineProperty给这些属性设置getter和setter。由于Object.defineProperty是ES5无法shim的一个特性,所以Vue也不支持IE8以及更低版本的浏览器。Vue通过getter和setter劫持传入对象的属性后,然后在内部会跟踪依赖,当属性被访问或者被修改时通知变化。
Vue数据绑定原理
先看一张摘自Vue官方文档的图片:Vue的数据绑定是通过Object.defineProperty劫持数据并结合发布者-订阅者的设计模式来实现的。前面也已经提到了,Vue劫持数据后会对数据进行跟踪依赖,也就是监听它们的变化,所以我们需要设置一个Obsver监听器,用来监听所有劫持到的属性,当属性发生变化时,会通知Watcher订阅者来重新计算判断是否需要更新。由于会有很多订阅者,所以需要一个消息订阅器Dependency,用来专门收集这些订阅者,然后Vue在监听器Observer和订阅者Watcher之间进行统一管理。由于要更新组件视图,所以还需要有一个指令解析器Compile,它将对每个节点元素进行解析,识别出绑定在这些元素上的相关指令,同时将这些指令分别初始化为一个订阅者Watcher,并替换掉模板的数据或者绑定相应的更新函数,此时,如果订阅者Watcher计算到到属性的变化,就会执行相应的更新函数,从而更新视图。从上面的分析,我们知道要实现数据绑定,可以通过以下三个步骤完成:
- 实现一个监听器Observer,借助Object.defindProperty劫持所有属性,如果有变化,就会通知订阅者
- 实现一个订阅者Watcher,每一个订阅者都绑定一个更新函数,订阅者计算属性变化并执行相应的更新函数,从而更新视图
- 实现一个解析器Compile,解析和识别每个元素上的指令,并初始化这些包含指令的元素的模板数据以更新视图,并初始化相应的订阅者Watcher
Vue数据绑定的实现
监听器Observer
核心功能是监听数据的变化,实现的核心方法是Object.defineProperty,劫持每个属性的setter和getter属性:
1 | // Dep用于订阅者的存储和收集,将在下面实现 |
消息订阅器Dependency
用于对订阅者进行收集和通知
1 | export default class Dep{ |
订阅者Watcher
每个被劫持的属性都对应一个订阅者,当属性被访问时,订阅者会对新旧数据进行比较,如果发生了变化,则会执行相应的更新函数,从而更新视图
1 | import Dep from 'Dep' |
解析器Compile
解析每个元素上的指令,并将它们对应的节点绑定相应的更新函数,初始化相应的订阅者,或者替换模板数据,初始化视图。
先创建一个fragment片段,并将要解析的dom节点存入fragment片段:
1
2
3
4
5
6
7
8
9
10function nodeToFragment (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
}遍历各个节点,对包含相关指令的节点进行处理:
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
27function compileElement (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); // 继续递归遍历子节点
}
});
},
function compileText (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);
});
},
function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}