以往Get一些技能之后,总以为记在脑子里就万事大吉了,随着年纪增大越来越发现记忆力是最不靠谱的东西,所以往后的学习我都想记录下来,方便日后翻看。
接触前端前前后后也有很长时间,一直没有特别深入地学习。最近因为工作原因面了很多前端,自己也顺便花了点时间把Vue的源码从头看了一遍,方便面试提问(阴险笑容)。
Vue真的是一个非常了不起的前端框架,源码不压缩情况下一共就1w行+,作者自己也说比较react和vue孰优孰劣是没有意义的。Vue的优势在于他的轻量快捷,像我这样没啥经验的小白都能很快整个项目出来。
废话不多说,我下的是的源码v2.6.11。建个工程,目录特别简单
真的很简单!平时vue-cli与webpack会帮我们做掉很多事情。我这里都没用,简单点接下去阅读会更加直观, 简单说两句,el属性就是指定页面上控制的区域,可以是实例也可以是css选择器。其实这部分代码就是创建了一个名叫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
//初始化
function Vue (options) {
console.log("第一句代码");
if (!(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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);
}
};
}
可以看到这里类的申明完全使用了es5的写法来向下兼容。
然后入口这里call了两个熟悉的生命周期的函数beforeCreat、Created。而在beforeCreat之前,还有一些操作,例如会进行mergeOptions操作,得到一个$options对象,里面会做一些组件的命名检查,必须以字母开头、中间可以有下划线,数字,但某些关键字是不可以使用的如‘slot,component’,包括html标签也不可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Validate component names
*/
function checkComponents (options) {
for (var key in options.components) {
validateComponentName(key);
}
console.log("检查组件完毕");
}
function validateComponentName (name) {
if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}
之后有初始化代理对象、给vue实例添加与生命周期相关的属性、初始化与父级组件相关的事件等操作。再然后来到了created阶段。
1
2
3
initInjections(vm);
initState(vm);
initProvide(vm);
这里的概念:provide/inject,这个是Vue在2.2.0版本新增的一个属性。
引用:这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
平时没怎么用,这里不赘述了。
让我们来到我认为vue初始化最核心的代码initState()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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做了一些初始化的操作.
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
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
console.log("key",key);
{
if (typeof methods[key] !== 'function') {
warn(
"Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
之后通过一些规则合并methods对象,合并过程增加了一些策略比如:不能与props选项对象中的某个键同名;不能与vue实例自带的以$或_开关的属性同名;否则它都会给我们一个错误的警告。最后通过bind(methods[key], vm);绑定到vm对象,因此我们可以直接通过this.xx()调用方法。
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
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
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];
{
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
observe(data, true /* asRootData */);
}
同样的,可以看到这里拿到data之后,做一些检查,一切正常之后来到
1
proxy(vm, "_data", key);
1
2
3
4
5
6
7
8
9
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
通过Object.defineProperty给传入的参数添加对应的存取器属性。
这步其实很好理解,平时我们我们定义data如:
1
2
3
4
data: {
name: "曹达华",
age: 37
}
如果我想获取name
属性,明明应该这么写this.data.name
,但是我们通过this.name
也能获取到,原因就在这,以前一直不理解,看了源码就一目了然了,当调用this.name
时 get
方法会去拿data
里面的name
。同样的,当调用this.name="曹达华"
时,也会修改this.data.name
的值。
之后就是第二步,给data添加观察者对象。
1
observe(data, true /* asRootData */);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
第一次进入这个方法,我们是将整个data对象作为value作为参数传入了方法中,所以第一步判断是否不是个对象data显然是个object,所以会来到下一步,判断该对象是否含有观察者属性,一旦vue监听了对象,就会给属性添加一个__ob__
属性。变量用来保存 Observer
实例 贴下代码(第5行):
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();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
ok,第一次data显然没有被监听过。来到下一步判断,这个数据对象是否是一个对象或数组,并且处理可”观察“模式,同时不能是服务器端渲染(这里不是特别理解,没怎么用过这个)也要求数据对象是可扩展的,只有这些条件同时满足,才会为这个数据对象添加一个”观察器“对象
1
ob = new Observer(value);
上面Observer类的构造方法里基本就是采用递归的思想(因为对象里可能包含对象、数组,数组里面又可能有数组和对象),把所有有资格的属性带到这里的,下面就是vue实现数据双向绑定的秘密,其实就是重写了属性的get和set方法进行监听,一旦set方法被call到,发现值改变就会去更新node,下面会讲到。好像所有的数据绑定理念都差不多,包括iOS的KVO一样,本质也是重写set方法去监听值的改变。扯远了,先上代码。
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
58
59
60
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();
}
});
}
1
var dep = new Dep();
第一句,怎么理解这个dep,后续会讲到,我第一次碰到也理解了半天,先来看后面
1
2
3
4
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
这句挺好理解,如果这个属性property.configurable === flase
就不做后续处理了。
1
2
3
4
5
6
7
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
这段代码的前两句定义了 getter
和 setter
常量,分别保存了来自 property
对象的 get
和 set
函数,我们知道 property
对象是属性的描述对象,一个对象的属性很可能已经是一个访问器属性了,所以该属性很可能已经存在 get
或 set
方法。由于接下来会使用 Object.defineProperty
函数重新定义属性的 setter/getter
,这会导致属性原有的 set
和 get
方法被覆盖,所以要将属性原有的 setter/getter
缓存,并在重新定义的 set
和 get
方法中调用缓存的函数,从而做到不影响属性的原有读写操作,如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//省略..
var value = getter ? getter.call(obj) : val;
//省略..
return value
},
set: function reactiveSetter (newVal) {
//省略..
var value = getter ? getter.call(obj) : val;
//省略..
}
});
而 var childOb = !shallow && observe(val);
这句话也挺好理解,就是上面说的递归调用了,因为这里的val可能也是对象或者数组,又会再次调用observe()
,当然递归一定会有临界点,那就是该方法的第一句话做了边界的判断,这样就和前面串起来了。
1
2
3
if (!isObject(value) || value instanceof VNode) {
return
}
好了,做完这些事情,终于来到核心的核心了,重写get\set
方法。
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
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();
}
});
里面一大坨是个啥我们先不管,总之先搞清楚至此我们已经完成了最开始initData()
的全部逻辑。而且到目前为止,我们还没有调用属性的get\set
方法,所以里面的逻辑,我们暂时还没有触发。什么时候触发后续会说到。
先来看get
方法
1
2
3
4
5
6
7
8
9
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
这个到底在干嘛呢。 之前说到每个属性都会创建一个var dep = new Dep();
这个dep对象里面其实是封装一个数组。
1
2
3
4
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
里面保存了值与被调用地方的关系
,一说也可以叫依赖
。啥意思呢?
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>从零阅读</title>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<p></p>
<div></div>
<button @click="test()"></button>
</div>
<script>
// 创建一个Vue的实例对象
let vue = new Vue({
// 将来需要控制界面上的哪个区域
el: '#app',
// 告诉Vue的实例对象, 被控制区域的数据是什么
data: {
name: "曹达华",
},
methods:{
test(){
this.name = "周星星"
}
},
});
</script>
</body>
</html>
举个最最简单的例子,这里的p标签,div标签全部使用了我name的属性,那么我在点击按钮后通过test()方法修改了name的值,为什么两个标签都会改变呢。原因就在这,这就好比,小太监收到了皇帝的指令,他要告诉大臣A,又要告诉大臣B,他需要打两个电话,通知的人越多,他要打的电话也就越多。因为get
方法会在每个标签第一次获取的值的时候多次调用,所以通过dep.depend();
这里会保存多个依赖关系。这里注意每个依赖关系只会收集1次
,收集的流程后面会讲到,继续看这个方法实现。
ok,明白了这个原因,让我们先来看看这个依赖是怎么添加的,首先 Dep.target
存在的话说明有依赖需要被收集,有人会问这里会不会重复收集,作者在这里做了很多层的保护,所以每个依赖关系只会收集1次,如果 Dep.target
不存在就意味着不需要收集,Dep.target
是一个全局的变量,怎么赋值规则后面说。
接下来有点绕了,首先第一句dep.depend();
很好理解,就是把当前的属性的依赖收集一次,那么
1
2
3
4
5
6
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
这里又把子对象依赖又收集了一次,我举个例子:
1
2
3
4
5
6
data: {
person:{
name: "曹达华",
age: 37
}
}
depend
完长这样:
1
2
3
4
5
6
7
8
data: {
person:{
name: "曹达华",
age: 37,
__ob__: (Observer){value, dep, vmCount}//childOb.dep是这里的dep
}
__ob__: (Observer){value, dep, vmCount}//
}
为什么要这么设计呢?刚开始我也被绕晕了,同样的依赖person对象内部的观察者的dep里面也要放一份呢。
收集的依赖的触发时机是在使用
$set
或Vue.set
给数据对象添加新属性时触发,我们知道由于js
语言的限制,在没有Proxy
之前Vue
没办法拦截到给对象添加属性的操作。所以Vue
才提供了$set
和Vue.set
等方法让我们有能力给对象添加新属性的同时触发依赖。
这时来到这里源码大概1000多行的位置。
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
function set (target, key, val) {
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val
}
哦。。明白了,为啥明明get/set
方法里已经有dep对象在闭包里了,Observer类为什么还有个dep属性,因为当我们在外部调用Vue.set(this.person, 'height', 160)
给person对象新增属性的时候,根本无法触发get
方法,所以我们必须有个另外的变量dep把这个依赖缓存起来,方便后续调用。
总结,如果现有属性发生变化,会直接触发set
中闭包中的dep
的进行notify
。如果是新增,删除属性这种操作,无法触发set
方法,那么开发人员必须通过Vue.set
方法去手动触发notify
,作为初学者的我刚开始不解我明明console.log数据变化了呀,页面怎么没变化啊
就是因为直接通过this.person.xx == xxx
这种方式新增属性,无法触发数据绑定。
好了弄懂这个,后面就很简单了。
1
2
3
if (Array.isArray(value)) {
dependArray(value);
}
这里就是如果读取的属性值是数组,那么需要调用 dependArray
函数逐个触发数组每个元素的依赖收集。
收集完依赖,来说说set
方法是如何触发的了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
}
前面几句很好理解,如果值没变化,就retrun了,不鸟他。
如果之前实现了set
的话,会call之前定义的方法,确保逻辑的缜密。
有时候塞进来的数值可能也是一个对象或者数组,这个时候我们需要继续对他们进行观测,所以我们看到这里再次调用了observe(newVal)
。
最后就是对之前我们收集的依赖进行通知,dep.notify()
去进行各个dom
元素数据的刷新。
剩下就是notify里到底做了什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
1
2
3
4
5
6
7
8
9
10
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
可以看到就是对直接收集的依赖原来是一个个Watch
对象,而notify()
就是进行update()
,至于watch
对象是什么时候初始化的,并且是是怎么初始化,包括后面是怎么刷新(异步,同步)的。这就关系到一些抽象语法树的解析,渲染队列的知识。又是一大堆复杂的逻辑,感觉又能写很久,所以有机会再写。
回到最初的initData()
方法,执行完最后就是对计算属性computed
和watch
属性的解析,这很简单,就不展开了。之后就是callcreated
生命周期方法,至此代码完成了所有初始化的工作。之后就是挂载元素和上面的创建Watch
对象了。
以上就是Vue组件初始化的全过程,漏了很多,有机会再补。
参考资料
[HcySunYang](https://github.com/HcySunYang)/vue-design