身为原来的jquery,angular使用者。后面接触了react和vue。渐渐的喜欢上了vue。抱着学习的态度呀。看看源码。果然菜要付出代价。一步步单步调试。头好疼。看到哪里记到哪里。来一点点心得。错误的地方请帮我指出来。谢谢。最近看的是vue component部分。
先上一段最简单的代码,来剖析component机制:
<div id="app">
<div>{{a}}</div>
<input v-model="heihei">
<button v-on:click="click1">
click1
</button>
<my-component>
<div slot='dudu'>111</div>
<Child>{{a}}</Child>
</my-component>
<button @click.stop="click2">
click2
</button>
</div>
</body>
<script src="vue.js"></script>
<script type="text/javascript">
var Child = {
template: '<div>A custom component!</div>'
}
Vue.component('my-component', {
name: 'my-component',
template: '<div>A custom component!<Child></Child><slot></slot></div>',
components: {
Child:Child
},
created(){
console.log(this);
},
mounted(){
console.log(this);
}
})
new Vue({
el: '#app',
data: function () {
return {
heihei:{name:3333},
a:1
}
},
components: {
Child:Child
},
created(){
},
methods: {
click1: function(){
console.log(this);
},
click2: function(){
alert('click2')
}
}
})
</script>
我们按照浏览器的思维逐行来。执行到脚本时。首先执行了
Vue.component('my-component', {
name: 'my-component',
template: '<div>A custom component!<Child></Child><slot></slot></div>',
components: {
Child:Child
},
created(){
console.log(this);
},
mounted(){
console.log(this);
}
})
我们来看看这个函数经历了什么:
vue.js初始化的时候。调用了initGlobalAPI(vue),为vue挂上了工具函数vue.component
经过initGlobalAPI(vue)中的initAssetRegisters (Vue) 后。变为
vue.component = function (
id,
definition
) {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
{
if (type === 'component' && config.isReservedTag(id)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + id
);
}
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id;
definition = this.options._base.extend(definition);
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition };
}
this.options[type + 's'][id] = definition;//全局的组件或者指令和过滤器。统一挂在vue.options上。等待init的时候利用策略合并侵入实例。供实例使用
return definition
}
};
this.options._base在initGlobalAPI(vue)中为Vue.options._base = Vue;
so vue.component调用了vue.extend。找到了源头。我们来好好看看这个vue.extend这个function。代码如下:
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]//如果组件已经被缓存在extendOptions上则直接取出
}
var name = extendOptions.name || Super.options.name;
{
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'//校验组件名
);
}
}
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;//将vue上原型的方法挂在Sub.prototype中,Sub的实例同时也继承了vue.prototype上的所有属性和方法。
Sub.cid = cid++;
Sub.options = mergeOptions(
Super.options,
extendOptions//通过vue的合并策略合并添加项到新的构造器上
);
Sub['super'] = Super;缓存父构造器
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps$1(Sub);
}
if (Sub.options.computed) { //处理props和computed相关响应式配置项
initComputed$1(Sub);
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
// create asset registers, so extended classes
// can have their private assets too. //在新的构造器上挂上vue的工具方法
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub;
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
// cache constructor
cachedCtors[SuperId] = Sub;//缓存组件构造器在extendOptions上
return Sub
};
}
function initProps$1 (Comp) {
var props = Comp.options.props;
for (var key in props) {
proxy(Comp.prototype, "_props", key);
}
}
function initComputed$1 (Comp) {
var computed = Comp.options.computed;
for (var key in computed) {
defineComputed(Comp.prototype, key, computed[key]);
}
}
总的来说vue.extend是返回了一个带有附加配置相的新的vue的构造器。在函数中,构造器叫做Sub,等待render时候初始化。
经过vue.component的调用。vue增加了一个全局组件my-component;此时vue.options.component如下图:
前三个是vue内置的三个组件,在initgloabalapi的时候初始化。
至此全局组件创建完成。全局组件放置在最底层。在以后的策略合并里会在子组件中的component项的__proto__中。
通过组件的递归创建渲染来看vue整体的生命周期(理解vue如何巧妙构建应用)
上图:
vue官方的生命周期图,其实也就是vue组件的构成的生命周期。沿着new Vue()我们来大概看看这些生命周期在什么阶段
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$1++;
var startTag, endTag;
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
startTag = "vue-perf-init:" + (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);//属性代理,即vm.xx = vm.data.xx
}
// expose real self
vm._self = vm;
initLifecycle(vm);//初始化生命周期状态变量,建立子父关系初始值,如$children,$parent.
initEvents(vm);// 初始化事件
initRender(vm);//初始化render核心函数_$createElement和$slots $scopedSlots等
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 ("development" !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(((vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);//如果有el配置相则主动挂载。触发之后的compile.render
}
};
介绍了大概的_init函数,我们继续往下看程序的运行。完成了vue.component()之后。开始执行new vue(),创建实例。
对照_init函数。我们知道它分别进行了对传入参数的合并。初始化实例参数。创建响应的响应式。最后挂载:vm.$mount(vm.$options.el);
简单说说挂载。好吧。我们还是往方法里面看,挂载的时候发生了什么:
// public mount method
Vue$3.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
var mount = Vue$3.prototype.$mount//缓存mount,用来触发render
Vue$3.prototype.$mount = function (//核心mount用来构建render函数
el,
hydrating
) {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
"development" !== 'production' && warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."//检测,排除不可挂载的元素
);
return this
}
var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
var template = options.template;//假如输入的是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;//输入的是dom节点时
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
template = getOuterHTML(el);//如果是一个id,如此次初始化挂载的id=app,会取到id=app的html
}
if (template) {
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
mark('compile');
}
var ref = compileToFunctions(template, {
shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函数。用于生成render函数。这里不细说
delimiters: options.delimiters
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;//挂载render到实例options中。待调用
options.staticRenderFns = staticRenderFns;//静态的元素区分开。提升性能,后续虚拟dom树比较时,不会比较静态节点
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
mark('compile end');
measure(((this._name) + " compile"), 'compile', 'compile end');
}
}
}
return mount.call(this, el, hydrating)//利用缓存的mount调用准备好的render
};
$mount方法的核心其实就是准备好组件的render函数。这里最核心的一个方法就是:
var ref = compileToFunctions(template, {
shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函数。用于生成render函数。这里不细说
delimiters: options.delimiters
}, this);
compileToFunctions这个函数中主要做了两件事:
1:对模版进行compile(按标签解析,生成ast(抽象语法树)
2:利用generate(ast, options),生成render函数语法
我们来看看最后实例生成的render函数:
没有错就是这个样子,很有感觉。生成的render函数保存在options中,等待调用
好吧。开始调用吧。
mount.call(this, el, hydrating)=》mountComponent(this, el, hydrating)=》updateComponent = function () {
vm._update(vm._render(), hydrating);
};=》vm._watcher = new Watcher(vm, updateComponent, noop);
new Watcher中会主动调用updateComponent去touch依赖(给页面中引用过的data中的变量假如监听)正式调用render函数。既然都说了。那就来看看render函数:
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
var _parentVnode = ref._parentVnode;
if (vm._isMounted) {
// clone slot nodes on re-renders
for (var key in vm.$slots) {
vm.$slots[key] = cloneVNodes(vm.$slots[key]);
}
}
vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject;
if (staticRenderFns && !vm._staticTrees) {
vm._staticTrees = [];
}
// 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 {
vnode = render.call(vm._renderProxy, vm.$createElement);//核心函数,调用render
} catch (e) {
handleError(e, vm, "render function");
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
{
vnode = vm.$options.renderError
? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
: vm._vnode;
}
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if ("development" !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
);
}
vnode = createEmptyVNode();
}
// set parent
vnode.parent = _parentVnode;
return vnode
};
_render()间接调用了vnode = render.call(vm._renderProxy, vm.$createElement);
然后结合render函数。看看发生了什么。vm.$createElement是核心的创建虚拟dom的函数。
继续看看核心构建虚拟dom函数:
function createElement (
context,
tag,
data,
children,//children是该元素下的所有子元素
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
if (isDef(data) && isDef((data).__ob__)) {
"development" !== 'production' && warn(
"Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
'Always create fresh vnode data objects in each render!',
context
);
return createEmptyVNode()
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function') {
data = data || {};
data.scopedSlots = { default: children[0] };
children.length = 0;
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {假如是组件则从上下文中取出组件的构造相关参数
// component
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (vnode !== undefined) {
if (ns) { applyNS(vnode, ns); }
return vnode
} else {
return createEmptyVNode()
}
}
这里其实我们不难看出vue在构造虚拟dom时。递归的去调用createElement去生成虚拟dom树。当children是组件或者是普通元素时。做不同的处理。这里我们关注的是。当子元素是组建时。这里调用了
vnode = createComponent(tag, data, context, children);
细心的人可以在去看看这个函数做了什么。简单来说这个函数将组件的构造参数取出来,放置在元素的componentOptions上。供后续创建真实dom时。标记该元素是组件。递归初始化。
跳过这些沉重的。我们直接看看我们的这个html生成的最终的虚拟dom长什么样。如下:
我们在来看看我们的my-component组件长什么样子:
componentOptios上存着初始化组件需要的参数。
构建好虚拟dom后。vue进入update阶段:
这个阶段vue会判断先前有无该元素。是否为第一次渲染。假如是第一次。那么直接创建。如果不是有先前的ovnode,则比较差异。最小化更新。看看具体函数:
nction lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
var prevEl = vm.$el;
var prevVnode = vm._vnode;//取出缓存的久的虚拟dom
var prevActiveInstance = activeInstance;
activeInstance = vm;
vm._vnode = 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
);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);//更新时。会比较差异
}
activeInstance = prevActiveInstance;
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
__patch__函数我们就不细看了。算了看一下:
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;//假如第一次渲染。直接创建
createElm(vnode, insertedVnodeQueue, parentElm, refElm);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {//假如更新并且前后虚拟dom相似,这里相似有自己的一个算法。比如tag,key必需一致。才会去diff比较
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode
} else {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
);
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
var oldElm = oldVnode.elm;
var parentElm$1 = nodeOps.parentNode(oldElm);
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm$1,
nodeOps.nextSibling(oldElm)
);
if (isDef(vnode.parent)) {
// component root element replaced.
// update parent placeholder node element, recursively
var ancestor = vnode.parent;
while (ancestor) {
ancestor.elm = vnode.elm;
ancestor = ancestor.parent;
}
if (isPatchable(vnode)) {
for (var i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode.parent);
}
}
}
if (isDef(parentElm$1)) {
removeVnodes(parentElm$1, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
patch方法中核心的是createElm:看懂这个函数非常重要代码如下
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {//根据之前保存的componentoptions来识别是否为组件。若是。则进这个逻辑
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
if (data && data.pre) {
inPre++;
}
if (
!inPre &&
!vnode.ns &&
!(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) &&
config.isUnknownElement(tag)
) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if ("development" !== 'production' && data && data.pre) {
inPre--;
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
我们这边还是先关注自己的组件部分。当children是组件元素时,很显然调用了createComponent(vnode, insertedVnodeQueue, parentElm, refElm);
var componentVNodeHooks = {
init: function init (
vnode,
hydrating,
parentElm,
refElm
) {
if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance,
parentElm,//调用了组件内部的_init方法递归创建子组件。正式进入子组件的生命周期
refElm
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);触发子组件的挂载。出发子组件的编译和render。又重新来一遍/直到子组件完全渲染好。再开始creelem下一个child
} else if (vnode.data.keepAlive) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
}
},
这里就是递归创建子组件的核心部分.
总结: 第一次写这个vue。失败了。切模块切的不够细。组件机制感觉用了好多东西。这个面太大了。自己讲的时候也不知道该细讲还是。。。
总的来说:vue在创建虚拟dom的时候,如果元素是组件。则准备好组件的构造参数。包括模版和数据等等。组件中的元素如slot,和child放在组件元素的children下。供后面的内容分发用组件中的元素也是在父组件的作用域内编译的。看—_render()函数就知道。然后在vue需要将虚拟dom变为真实dom时。遇到组件元素时。开始递归初始化。直到把组件compile,render构建完后。开始构建下一个元素。最后添加到真实id=app上。并且把旧的删了。哈哈。随便写了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。