Vue源码学习

Vue源码学习

在今年的10月份尤大大在多伦多发布了在明年发布Vue3.0的消息,但是我自己对vue的源码不是很了解,虽然在去年看了一下vue的源码,了解到数据双向绑定的原理,但是其他的方面没了解过,后面有段时间想去继续了解,但是没有坚持,所以打算在vue3.0来之前弥补自己的过错。
将从这几个方面来学习vue的源码

  1. 数据响应之Observer
  2. 数据响应之Watcher
  3. 模板渲染之render
  4. 模板渲染之解析vnode
  5. 模板渲染之patch

时间期限:至今到2019-01-01

Vue的数据响应

Vue提供了基于依赖收集的数据绑定的机制,当你数据发生改变,就会立即通知该数据的依赖对象。想必大家都知道Vue2.0是依赖于Object.defineProperty重新定义属性,通过getter/setter函数实现数据响应。那现在我们来看看源码是怎样实现数据的响应。

==大家注意了,请系好安全带,Vue的公交车要发动了==

我们先来看一个Demo,通过小案例来看vue源码的构建过程

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
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>源码学习</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="app">
<span>hello world !!!{{name}}</span>
<p>{{message}}</p>
<button @click="changeName">change name</button>
</div>
<script>
var vm = new Vue({
el:'#app',
data:{
name:'Ming'
},
methods:{
changeName(){
this.name = 'UZI'
}
},
computed: {
message(){
return `RNG ${this.name}`
}
},
watch: {
name(){
console.log('change Name')
}
}
})
</script>
</body>
</html>
Vue的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将vue需要的配置项传递到Vue的构造函数,进行初始化
function Vue (options) {
if (!(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}

// 以下这几个方法都是在Vue.prototype上添加一些方法
initMixin(Vue);// 里面包含了_init()函数,对Vue的初始化
stateMixin(Vue);// 里面是对
eventsMixin(Vue);// 主要是添加事件监听的方法,有$on,$off,$emit,$once
lifecycleMixin(Vue);// 主要添加Vue的生命周期的方法,有 _update, $forceUpdate, $destroy
renderMixin(Vue); // 主要添加了 $nextTick 和 _render 两个方法

可以从上面的Vue的构造函数看出,Vue构造时调用了_init的函数进行配置项的初始化,忽略在开发模式下的警告外,你是不是觉得Vue的构造函数就一行,特别简单,接下来看看_init函数中到底做了什么?

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++;

var startTag, endTag;
/* istanbul ignore if */
if (config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}

// a flag to avoid this being observed
vm._isVue = true;
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');

/* istanbul ignore if */
if (config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}

if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}

当你看到源码中的_init函数,是不是感觉很复杂的,只要你仔细的阅读一下源码,你立马就能知道只有其中几行代码是核心的,现在我们去掉Vue警告和报错,还有一些判断条件,就可得到以下代码:

1
2
3
4
5
6
7
8
9
10
11
// expose real self
vm._self = vm;

initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm); // data computed watch prop等都是在这里初始化的
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // 到这 create 阶段完成

这一段代码承担了Vue的初始化大部分的工作,也是Vue中最核心代码的所在地。只要我们弄懂了这个几个函数,那么我们差不多知道Vue的运行的工作原理,我们这一章中就会从其中的一个函数开始,大概大家都已经知道了。我们继续往下看:

1
2
3
4
// 将VM实例挂载到dom元素上
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}

这里mount函数通过传递的options的el元素挂载,我们也可以通过Vue官网提供的api函数$mount将Vue实例挂载到dom元素上。

不知不觉就已经完成了对Vue初始化,让我们通过_init函数深度探究Vue中数据响应是咋会事

大家提起精神来,这一章我们主要关注数据响应和处理,数据的依赖关系是怎样建立的;所以我们从data开始,这时候initState这扇门就需要你推开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化创建Vue实例中配置项
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}

从上面代码可以看到主要是对props,methods,data,computed,watch这五个部分,为了探究Vue的数据响应,我们先对其data的初始化进行分析,看看initData函数主要的工作是啥?

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
33
34
35
36
37
38
39
40
41
42
43
44
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
// 判断data是否为object, 如果不是,打印一段警告,告诉你必须传递给我一个对象
if (!isPlainObject(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];
// methods 中有和 data 上定义重复的key,那么就打印一个警告。
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
// props 中有和data上定义重复的key,那么打印一个警告
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
observe(data, true /* asRootData */);
}

通过上面代码的注释,我们差不多了解到其他代码是起什么作用的,让我们来看最后一行代码:

1
2
// observe data
observe(data, true /* asRootData */);

其实只要我们弄懂最后一行代码,就大概了解了observe是如何工作的,这是Vue数据响应的核心代码。

Vue的数据响应—Observer

上一节了解到Vue是怎么构造函数的,接下来我们去探究observe是如何工作的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function observe (value, asRootData) {
// 判断data是否为对象和VNode实例
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);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}

其中有一些if条件判断,我们先不管if判断,代码精简一下,就是剩下两行:

1
2
3
4
5
function observe (value, asRootData) {
<!--创建Observer实例-->
ob = new Observer(value);
return ob
}

通过精简的两行代码看到主要的逻辑就是创建Observer实例,但observe函数用于递归将传递进来的data定义为响应式,直到传递来的value不是Object类型。那先让我们看看Observer里面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep(); // 用于记录value的依赖
this.vmCount = 0;
def(value, '__ob__', this); // __ob__标记,防止重复创建
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};

其中的if判断,主要是对数组判断,如果是数组,需要对数组中的每一个值进行递归observe,让数组中的数据被定义为响应式。

1
2
3
4
5
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};

显然如果不是,最终进入到walk函数中。那我们看一下walk函数里面代码

1
2
3
4
5
6
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};

这段代码主要逻辑是将对象的每个属性进行遍历,同时进行defineReactive$$1操作,defineReactive$$1函数里面用Object.defineProperty函数重新定义属性,通过getter/setter函数劫持读写操作,实现数据响应式。看到这个函数是不是很兴奋,我们已经来到了最核心的部分,也是Vue依赖收集的工作原理。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (obj,key,val,customSetter,shallow) {
var dep = new Dep();

var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}

// 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);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
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);
dep.notify();
}
});
}

前面我们提到observe函数用于递归,直到value不是Object类型;上面代码中变量dep用于val的依赖收集,变量childOb用于obeserve函数递归。上面可以看到getter函数里面调用了observe函数,实现对每个属性进行重新定义,通过getter/setter实现数据(响应)劫持。通过getter获取依赖收集,在属性中的get函数中判断是否存在Watcher依赖,会将其保存到每个属性中的dep内。

那我们继续看看set函数:

1
2
3
4
5
6
7
8
9
10
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}

我们删除一点警告性代码和if判断,主要代码其实做了两件事,一是如果存在自定义setter,就直接调用;二是通过dep.notify()来通知所有的监听器,告诉他更新数据。

总结

就今天的Vue的数据响应,主要是通过Object.defineProperty函数重新定义对象属性,数据通过getter来收集依赖,在函数中dep添加对应的对象属性的Watcher依赖。同时还介绍了Vue的初始化。这一部分只是依赖收集的准备部分。接下来的一章将介绍Watcher和Observer之间联系和触发过程。今天这一章还有些需要添加的地方,在写第二章之前会补充完毕。


本文由 Abert 创作,采用 知识共享署名 4.0 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。