Vue响应式原理与虚拟DOM实现步骤详细讲解
1. Vue响应式原理
Vue的响应式原理核心是利用Object.defineProperty
方法对数据进行拦截,当数据发生变化时,通知对应的界面进行更新。
1.1 监听对象
在Vue中对数据的监听由Observer对象负责,在Observer对象中使用Object.defineProperty
方法对数据进行监听。在Observer对象的实现中,需要考虑如下几个问题:
- 如何递归观察对象或数组中的每一个属性。
- 如何判断一个变量是对象还是数组。
- 如何在数组中添加或删除元素时实时更新View。
示例1
我们在Vue的实例中定义一个data对象,在其中定义了两个属性,num和obj。
var vm = new Vue({
data: {
num: 1,
obj: {
name: 'John',
age: 23
}
}
})
在Observer对象中,对数据进行递归监听,使得Vue能够监听到data对象的变化,并自动更新View。
function Observer(data) {
this.data = data;
this.observe(data);
}
Observer.prototype = {
observe: function(data) {
var self = this;
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, /* 可枚举 */
configurable: true, /* 可删除 */
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
childObj = observe(newVal);
dep.notify();
}
});
}
};
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
function Dep() {
this.subs = []; /* subscribers */
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
removeSub: function(sub) {
var index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
},
depend: function() {
Dep.target.addDep(this);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
1.2 发布订阅模式
在Vue中,数据变化时会触发对应组件的重新渲染。而这个过程是通过订阅发布模式实现的,其中过程如下:
- 初始化时,Vue会为每个观察的对象创建一个Dep对象
- 当组件初始化完成后,Vue会对需要监听的属性(computed/watch data)强制求值,此时计算property.get
- Dep.target属性会被设置为当前运行中的Watcher对象
- 当获取到对象的值后,把Watcher对象添加到对应Dep对象的subs数组中
- 后续调用set方法时,observer会向dep.subs数组中的Watcher对象通知数据的更新。
- Watcher对象通知组件重新渲染
示例2
我们在Vue的实例中定义一个computed属性,以及一个Watch属性,其中computed属性的值将通过data.num和data.obj.age的计算得出,Watch属性会监听data.num的变化,并在num发生变化后调用回调函数。
var vm = new Vue({
data: {
num: 1,
obj: {
name: 'John',
age: 23
}
},
computed: {
sum: function() {
return this.num + this.obj.age;
}
},
watch: {
num: function(newVal, oldVal) {
console.log('num changed:', newVal);
}
}
})
在Watcher对象中,对数据进行监听。
function Watcher(vm, expOrFn, cb) {
this.vm = vm;
expOrFn = expOrFn.trim();
this.expOrFn = expOrFn;
this.cb = cb;
Dep.target = this;
this.value = this.get();
Dep.target = null;
}
Watcher.prototype = {
update: function() {
var value = this.get();
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
var value;
try {
value = this.vm.$data[this.expOrFn];
} catch (e) {
value = undefined;
}
return value;
},
addDep: function(dep) {
dep.addSub(this);
}
};
2. Vue虚拟DOM实现
Vue的虚拟DOM实现非常简单,其核心思想是:在操作真实DOM时,避免直接修改DOM而导致大量重排,而是采用虚拟DOM来计算出最少的需要修改的节点,并进行批量修改。
2.1 节点的类型
在Vue的虚拟DOM实现中,将真实DOM分为以下三种类型的节点:
- 元素节点(
Element
): 代表一个HTML元素,比如div
,p
,ul
等,包含标签名,属性,子节点等信息。 - 文本节点(
Text
): 代表一段文本内容。 - 占位符节点(
Comment
): 代表HTML注释,用于模版,介绍等。
示例3
我们假设需要把以下HTML元素渲染到页面中,其中标题部分为动态内容,其余部分为静态内容。
<div class="container">
<h1>{{title}}</h1>
<p>这是一个演示文本。</p>
<ul>
<li>测试1</li>
<li>测试2</li>
<li>测试3</li>
</ul>
</div>
在render
函数中,我们使用虚拟DOM的方式对HTML进行解析。
function createElement(tagName, attrs, children) {
return new VNode(tagName, attrs, children)
}
function createTextNode(text) {
return new VText(text)
}
function createComment(text) {
return new VComment(text)
}
function VNode(tagName, attrs, children) {
this.type = 'Element';
this.tagName = tagName;
this.attrs = attrs || {};
this.children = children || [];
}
function VText(text) {
this.type = 'Text';
this.text = text;
}
function VComment(text) {
this.type = 'Comment';
this.text = text;
}
function render(data) {
var container = document.createElement('div');
var titleNode = createTextNode(data.title);
var p1 = createTextNode('这是一个演示文本。');
var ul1 = createElement('ul', null, [
createElement('li', null, [createTextNode('测试1')]),
createElement('li', null, [createTextNode('测试2')]),
createElement('li', null, [createTextNode('测试3')])
]);
container.appendChild(createElement('h1', null, [titleNode]));
container.appendChild(createElement('p', null, [p1]));
container.appendChild(ul1);
return container;
}
2.2 DOM的更新
在Vue的虚拟DOM实现中,更新DOM的过程发生在如下两个步骤中:
- 使用虚拟DOM计算出需要修改的节点
- 对需要修改的节点进行优化后,一次性修改DOM。
示例4
我们假设我们需要根据用户的操作在已渲染的HTML中动态更新title
属性。
function update(data) {
// 对比旧虚拟节点和新虚拟节点,得出需要更新的节点
var patch = diff(previousVNode, newVNode);
// 对需要更新的节点进行优化后,一次性修改DOM
patch(rootDomNode);
}
结论
通过对响应式原理和虚拟DOM的分析,我们了解了Vue是如何实现数据的自动更新和性能的优化的。Vue的响应式原理核心是使用Object.defineProperty
方法对数据进行监听,而虚拟DOM的核心思想是在操作真实DOM时,在避免直接修改DOM的前提下,采用虚拟DOM来计算出最少的需要修改的节点,并进行批量修改DOM的方式来实现性能优化。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Vue响应式原理与虚拟DOM实现步骤详细讲解 - Python技术站