// 模板
<div id="app">
<my-component></my-component>
<p>{{appVal}}</p>
</div>
// script
Vue.component('my-component', {
template: '<div>this is my component<child></child></div>',
components: {
'child': {
template: '<p>hello</p>'
}
}
})
new Vue({
el: '#app',
data () {
appVal: 'this is app val'
}
})
首先进行根实例化,从Vue.prototype._init方法开始:
Vue.prototype._init = function () {
var vm = this
...
initRender(vm)
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在这个内部,通过调用initRender方法给实例挂载生成vnode节点的方法:
function initRender () {
...
// 在内部render函数执行生成vnode的时候调用
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
...
// 供开发者调用的生成vnode的方法
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
...
}
接下来根据是否有el配置选项来将vue component挂载到真实dom节点上。
// public mount method
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
var template = options.template;
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
/* istanbul ignore if */
if ("development" !== 'production' && !template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
// 获取真实dom元素的字符串内容
template = getOuterHTML(el);
}
if (template) {
// 获取到模板的字符串内容后,调用compileToFunctions方法将模板字符编译成render函数
// 需要注意的是编译的时候只编译模板下的字符串,并不能直接编译当前模板的子组件的模板内容
var ref = compileToFunctions(template, {
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
// 生成render函数
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render; // 挂载render函数
options.staticRenderFns = staticRenderFns; // 挂载staticRenderFns函数
}
}
return mount.call(this, el, hydrating)
}
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
...
}
// 挂载前
callHook(vm, 'beforeMount');
var updateComponent;
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
...
} else {
updateComponent = function () {
// vm._render首先构建完成vnode
// 然后调用vm._update方法,更vnode挂载到真实的DOM节点上
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render; // 获取render函数
var _parentVnode = ref._parentVnode;
...
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
var vnode;
try {
// 开始调用render函数,用以生成vnode
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleError(e, vm, "render");
// return error render result,
// or previous vnode to prevent render error causing blank component
...
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
...
vnode = createEmptyVNode();
}
// set parent
vnode.parent = _parentVnode;
return vnode
};
}
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var prevActiveInstance = activeInstance;
activeInstance = vm;
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 页面初始化渲染
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
);
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null;
} else {
// updates
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
...
}
Vue组件系统简析
Vue version: 2.5.16
Vue
的组件系统是Vue
最为核心的功能之一。它也是构建大型的复杂的web应用的基础能力。接下来就通过这篇文章去分析下Vue
组件系统是如何工作的。这篇文章主要是讲组件系统的渲染。组件注册
在组件的注册使用过程当中,有2种使用方式:
通过全局方式注册的组件,可在模板根实例下使用。
通过局部注册方式注册的
child
组件只能在parent
组件内部使用。全局组件
首先,我们来看下全局组件:
Vue.component
方法提供了全局注册组件的能力,这也是Vue
在初始化的过程中,通过内部的initGlobalAPI
方法,在Vue
这个全局唯一个根constructor
上挂载的一个方法。通过
Vue.component
方法注册的组件最终还是调用的Vue.extend
方法来完成子组件对父组件的一系列的继承的初始化的工作。主要是将根 Vue 构造函数上的 options 配置和组件定义所传入的 options 进行合并,对组件,同时在根constructor
的options
属性上挂载这个全局子组件的constructor
。Vue.extend
方法在整个Vue
组件系统中算是一个建立起父子组件之间联系的作用。大家可以看到在
Vue.extend
方法内部,实际上就是创建了一个VueComponent
的constructor
,同时还需完成这个构造函数和根constructor
方法、原型链的继承工作,其中有一点关于options
配置属性合并的工作。// TODO: 讲解下options合并的过程 调用
mergeOptions
方法,将根constructor
(superCtor)的options
属性和子constructor
的options
(subCtor)属性进行一次合并。就拿components
属性来说,最终的结果就是subCtor.options.components.prototype = superCtor.options.components
,同时通过mixin
,使得subCtor.options.components
可直接访问全局组件和局部组件。这样调用
Vue.component
方法后完成全局组件的注册。Vue
的局部组件和全局组件注册的方法还不太一样,首先在注册的阶段,局部组件并非和全局组件一样在代码初始化的阶段就完成了全局组件的注册,局部组件是在父组件在实例化的过程中动态的进行注册的(后面的内容会讲到这个地方)。VueComponent实例化
当全局组件在注册完毕后,开始根实例化的过程。(这篇文章主要将组件渲染过程,所以其他关于
Vue
的部分内容会略去不展开)从一个实例开始:
首先进行根实例化,从
Vue.prototype._init
方法开始:在这个内部,通过调用
initRender
方法给实例挂载生成vnode节点的方法:接下来根据是否有
el
配置选项来将vue component
挂载到真实dom
节点上。在上面的实例当中,
Vue
的编译系统会将html
模板内容转化为render
函数:完成模板编译,生成
render
函数后,接下来调用mountComponent
方法:!!! 前方高能:
在
mountComponent
方法内部首先定义updateComponent
方法,这个方法内部首先会调用vm._render
,将模板编译后生成的渲染函数转化成vnode
,然后再调用vm._update
完成真实dom
的更新,新实例化一个watcher
,开始进行页面的渲染工作。首先来看下
vm._render
是如何将渲染函数转化成vnode
的:在上面已经提到了关于最后关于编译生成的
render
函数:在实际的执行过程当中,首先完成
children
节点的vnode
的生成工作。这里首先生成my-component
子组件的vnode
,我们来看下vm._c
方法,这个方法内部最终是调用_createElement
方法来生成vnode
:因此上面提到的
render
函数,最终生成一个vnode
当根节点的
VNode
生成完毕后,让我们再回到mountComponent
方法内部:当
vm._render
函数生成完vnode
后,执行vm._update(vnode)
,将vnode
渲染为真实的DOM
节点:当页面进行首次渲染的时候:
可查阅关于patch的方法:
再让我们看下上面说的产生的根
VNode
节点:这个根
VNode
有3个VNode
子节点,这个时候开始调用createChildren
方法递归的完成子VNode
的实例化,以及将VNode
渲染成真实的DOM
节点,并插入到父节点当中。