爱客仕-前端团队博客园

Vue源码解析之 Vue Class

这段时间折腾了一个vue的日期选择的组件,为了达成我一贯的使用舒服优先原则,我决定使用directive来实现,但是通过这个实现有一个难点就是我如何把时间选择的组件插入到dom中,所以问题来了,我是不是又要看Vue的源码?

vue2.0即将到来,改了一大堆,Fragment没了,所以vue社区中为数不多的组件又有一批不能在2.0中使用,vue的官方插件也是毁得只剩vuex兼容,所以在我正在折腾我的组件的时候看到这个消息我是崩溃的。。。但没办法,还是得继续。希望2.0出来之后官方能完善一下文档,1.0中太多东西根本没在文档里提到,比如Fragment,比如Vue的util方法,这给第三方组件以及插件开发者带来了无数的玛法,你只能去看源码了,费时费力,刚研究透又来个大更新,我真的想哭/(ㄒoㄒ)/~~

———-回归正题——–

Vue Class

vue的核心就是他的Vue class,component到最终其实也就是一个Vue的实例,包含了一些component独有的属性而已,我们来看看这个Class做了什么:

1
2
3
function Vue (options) {
this._init(options)
}

恩,他调用了_init,而在_init里面就是初始化了一大堆属性,这些不重要,最重要的是最下面他有这么一句代码:

1
2
3
if (options.el) {
this.$mount(options.el)
}

这个el是我们在调用new Vue({...})时传入的,即这个vue对象的挂载点,好了,我们找到办法去动态得把一个Vue的实例挂载到dom里面了,于是就有了如下代码:

1
2
3
4
5
6
7
const vm = new Vue({
template: '<div>我是天才</div>',
data: {
hehe: 'haha'
}
})
vm.$mount(document.body)

愉快得打开页面,等等,为什么整个页面上就剩下我是天才这句非常正确的话呢?哦~原来$mount默认是替换整个整个元素的,呵呵哒

那么我们要如何把节点插入到body里面呢?这里有很多办法,比如你直接调用$mount()不传任何参数,这个时候他不会执行插入操作,然后你把他编译过的节点(也就是vm.$el)拿出来手动通过dom操作来进行插入,当然我们肯定不能用这么low的方法O(∩_∩)O~,继续撸源码,很快我们找到了这么一个文件:

1
2
3
// instance/api/dom.js
Vue.prototype.$appendTo = function(target, cb, withTransition) {...}
Vue.prototype.$before = function(target, cb, withTransition) {...}

是的,Vue的实例自带一些dom操作的帮助,那么我们随便选一个用就是了,不细说

然而我们还是遇到了问题

使用这种方式动态插入的节点会有一个问题,那就是$el并不是我们真正想要的节点,而是一个注释节点,这是为啥?还是看源码,我们跟着$mount进去看看他做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue.prototype.$mount = function (el) {
if (this._isCompiled) {
process.env.NODE_ENV !== 'production' && warn(
'$mount() should be called only once.'
)
return
}
el = query(el)
if (!el) {
el = document.createElement('div')
}
this._compile(el)
this._initDOMHooks()
if (inDoc(this.$el)) {
this._callHook('attached')
ready.call(this)
} else {
this.$once('hook:attached', ready)
}
return this
}

显然我们的el是没有的,那么这里的el就变成了一个div,然后进行了_compile,再继续:

1
2
// 源码太长不贴了
// 文件位置:instance/internal/lifecycle.js

这里面他做了一个el = transclude(el, options),以及this._initElement(el),我们重点看一下this._initElement(el)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.prototype._initElement = function (el) {
if (el instanceof DocumentFragment) {
this._isFragment = true
this.$el = this._fragmentStart = el.firstChild
this._fragmentEnd = el.lastChild
// set persisted text anchors to empty
if (this._fragmentStart.nodeType === 3) {
this._fragmentStart.data = this._fragmentEnd.data = ''
}
this._fragment = el
} else {
this.$el = el
}
this.$el.__vue__ = this
this._callHook('beforeCompile')
}

我们发现这里的el以及不是之前我们可亲的div了,那么他是什么呢?我们倒回去看transclude

1
2
3
4
5
6
7
8
9
10
11
...
if (options) {
if (options._asComponent && !options.template) {
options.template = '<slot></slot>'
}
if (options.template) {
options._content = extractContent(el)
el = transcludeTemplate(el, options)
}
}
...

我们是有template的,所以执行了transcludeTemplate:

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
function transcludeTemplate (el, options) {
var template = options.template
var frag = parseTemplate(template, true)
if (frag) {
var replacer = frag.firstChild
var tag = replacer.tagName && replacer.tagName.toLowerCase()
if (options.replace) {
/* istanbul ignore if */
if (el === document.body) {
process.env.NODE_ENV !== 'production' && warn(
'You are mounting an instance with a template to ' +
'<body>. This will replace <body> entirely. You ' +
'should probably use `replace: false` here.'
)
}
// there are many cases where the instance must
// become a fragment instance: basically anything that
// can create more than 1 root nodes.
if (
// multi-children template
frag.childNodes.length > 1 ||
// non-element template
replacer.nodeType !== 1 ||
// single nested component
tag === 'component' ||
resolveAsset(options, 'components', tag) ||
hasBindAttr(replacer, 'is') ||
// element directive
resolveAsset(options, 'elementDirectives', tag) ||
// for block
replacer.hasAttribute('v-for') ||
// if block
replacer.hasAttribute('v-if')
) {
return frag
} else {
options._replacerAttrs = extractAttrs(replacer)
mergeAttrs(el, replacer)
return replacer
}
} else {
el.appendChild(frag)
return el
}
} else {
process.env.NODE_ENV !== 'production' && warn(
'Invalid template option: ' + template
)
}
}

这边生成了一个Fragment,好吧,我们最终还是回到了这里。。。因为这边返回的是一个Fragment,所以会执行如下代码:

1
2
3
4
5
6
7
if (el instanceof DocumentFragment) {
// anchors for fragment instance
// passing in `persist: true` to avoid them being
// discarded by IE during template cloning
prepend(createAnchor('v-start', true), el)
el.appendChild(createAnchor('v-end', true))
}

然后回到刚才的_initElement里面,this.$el = this._fragmentStart = el.firstChild,额,好吧。。。我表示无力吐槽

那么回到我们刚才的问题,想要让$el正确,只需要在new Vue({...})的时候传入replace: false就行了,但是外面就多包了一层div,怎么样都不觉得完美

到这里我们基本了解了初始化一个Vue对象时的一些方法的执行顺序,以及一个组件如何从字符串模板最终到一个节点的过程,讲得比较粗糙,建议有兴趣的各位还是自行去看源代码吧~