用过Vue的都知道Vue是数据驱动视图,Input可以实现双向绑定,这篇我将带大家从源码的角度解析
开始之前我们先看一个案例,思考一下打印的顺序和打印结果!
<div id="app">
<button ref="button">{{ message.name }}</button>
</div>
<script>
const app = new Vue({
data: {
message: { name: '测试' }
},
updated() {
console.log(this.$refs.button.innerText, 333)
},
mounted() {
this.message.name = '挂载'
this.$nextTick(() => {
console.log(this.$refs.button.innerText, 111)
})
console.log(this.$refs.button.innerText, 222)
}
}).$mount("#app")
</script>
在上一篇我们已经讲过Vue创建一个实例的过程及其代码执行顺序,也说过Vue源码中除了nextTick不存在任何异步代码,从下面这张图我们很清楚的知道Vue的钩子函数执行顺序是
beforeCreate:在回调beforeCreate之前,会初始化vue会用到的事件和属性等,但是这里通过this还拿不到data的值,因为data的值还没有绑定到实例vm上,全部在实例的$options中。
created::在回调created之前,就是beforeCreate之后,会初始化inject、provid、props、methods、watch、computed,也是在这一步之前data的值被绑定到了实例中,同时添加了监听,所以在created中都可以通过this在实例中访问到data的值和methods等等,注意:这里修改实例data的值是在生成虚拟dom之前,所以不会触发更新,因为还没有建立起发布订阅之间的联系。
beforeMount::在回调beforeMount之前,就是created之后,Vue实例中的初始化已经全部完成,在这之间就开始回去判断传入的options是否有有el属性,这里不做细讲,可以看上一篇文章,最后根据传入属性是否有template来生产对应的render函数,注意:这里修改实例data的值是在生成虚拟dom之前,所以不会触发更新,因为还没有建立起发布订阅之间的联系
mounted::在回调mounted之前,就是beforeMount之后,这个阶段就是根据render函数根元把生成的虚拟dom挂载到根元素上,注意:如果在这里修改data的值,就有一个很重要的过程,就是在建立虚拟Dom的过程中,会创建一个Watcher,这个Watcher是啥呢,就是保存我HTML元素使用一次这个{{}}调用,就是订阅一次data的数据,就会存一个Dep的实例,Dep是啥,Dep实例有两个属性,一个是唯一的id,一个是subs就是订阅的的组件Watcher。
所以只有在mounted钩子函数修改data的值才会触发更新。
beforeUpdate::在mounted之后,回调就是updated之前,这一步做了啥,就是把所有触发了更新的Watcher存在一个队列里。注意:记住别人问你为什么beforeUpdate和updated始终在mounted之后,你就问答因为更新函数使用的是Vue的nextTick,nextTick是一个异步的玩意儿,其它的钩子函数回调都是同步的。
updated::在回调updated之前,就是beforeUpdate之后,这一步就是更新值,重新生成虚拟dom挂载。注意:在这个钩子函数里面不能写触发更新的操作,不然会陷入死循环。
记住以下几点:一个组件一个Watcher,一个data中的数据一个Observer,对象数组这些可分解成多个Observer,Watcher的作用就是用来管理data的订阅和发布,经典发布订阅不用多说了吧!
现在我们来看一下源码是如何数据驱动视图的,上一篇已经讲了初始化会做什么,所以这里就直接进入初始化data,这个函数里面直接会去调用observe
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function' // 这里就是判断data是对象还是方法,是方法就用getData取出data
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) { // 这里判断如果data是一个方法,返回的不是对象
data = {};
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
observe(data, true /* asRootData */); // 调用observe
}
我们看一下observe方法内部
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 这里是干啥的呢,就是已经监听了的数据不用在监听了
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value); // 这里就是构造出一个Observer对数据进行监听
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
我们看一下构造函数Observer
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep(); // 这里创建一个Dep,下面还有个地方也创建了Dep,这里代表data的每个数据都有唯一的触发者
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value); // 如果是数组就逐个调用observer创建Observer
} else {
this.walk(value); // 不是就直接走这里
}
};
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]); // 这里就是大家众所周知的vue的数据驱动视图原理了
}
};
/**
* Observe a list of Array items.
*/
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
数据驱动视图的核心就是在defineReactive$$1里面
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep(); // 这里也创建一个Dep,这是因为对象可能存在多层级的情况,所以每个层级都会分配一个触发者
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
} // 这里就是判断你定义的data是否可以枚举
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val); // 这里是要去检查对象是否存在多层级,比如 message: { name: "测试" }
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend(); // 这里会在创建虚拟dom的时候来执行,上面已经讲了,就是Watcher会去保存谁订阅这个数据,这个就把触发者Dep存在Watcher里面去
if (childOb) {
childOb.dep.depend(); // 这里同理
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
// 这里就是你改变了data的值就会去通知Watcher,凡是订阅了这条数据的去执行更新,这里利用了闭包去保存dep
dep.notify();
}
});
}
最后总结一下
Vue数据驱动,就是利用Object.defineProperty或Proxy,重写set和get方法,在利用发布订阅的方式修改实例的值,最后重新生成虚拟dom,重新渲染视图,上面讲了一个Watcher对应一个组件,一个数据会保存一份Dep,哪个组件订阅这份数据,就把这份Dep存到Watcher中,Dep中的订阅者也会存下这个Watcher,这样我们既可以知道哪份数据被订阅了,也能知道谁订阅这份数据,最后通过触发Dep的Watcher原型上的update方法实现视图的更新。