组件是可复用的Vue实例,一个组件本质上是一个拥有预定义选项的一个Vue实例,组件和组件之间通过一些属性进行联系。

组件有两种注册方式,分别是全局注册和局部注册,前者通过Vue.component()注册,后者是在创建Vue实例的时候在components属性里指定,例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<child title="Hello Wrold"></child>
<hello></hello>
<button @click="test">测试</button>
</div>
<script>
Vue.component('child',{ //全局注册
props:['title'],
template:"<p>{{title}}</p>"
})
var app = new Vue({
el:'#app',
components:{
hello:{template:'<p>Hello Vue</p>'} //局部组件
},
methods:{
test:function(){
console.log(this.$children)
console.log(this.$children[1].$parent ===this)
}
}
})
</script>
</body>
</html>

渲染DOM为:

writer by:大沙漠 QQ:22969969

其中Hello World是全局注册的组件渲染出来的,而Hello Vue是局部组件渲染出来的。

我们在测试按钮上绑定了一个事件,点击按钮后输出如下:

可以看到Vue实例的$children属性是个数组,对应的是当前实例引用的所有组件的实例,其中$children[0]是全局组件child的实例,而children[1]是局部组件hello的实例。

而this.$children[1].$parent ===this输出为true则表示对于组件实例来说,它的$parent指向的父组件实例,也就是例子里的根组件实例。

Vue内部就是通过$children和$parent属性实现了父组件和子组件之间的关联的。

组件是可以无限复用的,比如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<child title="Hello Wrold"></child>
<child title="Hello Vue"></child>
<child title="Hello Rose"></child>
</div>
<script>
Vue.component('child',{
props:['title'],
template:"<p>{{title}}</p>"
})
var app = new Vue({el:'#app'})
</script>
</body>
</html>

渲染为:

注:对于组件来说,需要把data属性设为一个函数,内部返回一个数据对象,因为如果只返回一个对象,当组件复用时,不同的组件引用的data为同一个对象,这点和根Vue实例不同的,可以看官网的例子:点我点我

例1:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<child ></child>
</div>
<script>
Vue.component('child',{
data:{title:"Hello Vue"},
template:"<p>{{title}}</p>"
})
var app = new Vue({el:'#app'})
</script>
</body>
</html>

运行时浏览器报错了,如下:

报错的内部实现:Vue注册组件时会先执行Vue.extend(),然后执行mergeOptions合并一些属性,执行到data属性的合并策略时会做判断,如下:

strats.data = function (              //data的合并策略          第1196行
parentVal,
childVal,
vm
) {
if (!vm) { //如果vm不存在,对于组件来说是不存在的
if (childVal && typeof childVal !== 'function') { //如果值不是一个函数,则报错
"development" !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
); return parentVal
}
return mergeDataOrFn(parentVal, childVal)
} return mergeDataOrFn(parentVal, childVal, vm)
};

源码分析


以这个例子为例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<child title="Hello Wrold"></child>
</div>
<script>
Vue.component('child',{
props:['title'],
template:"<p>{{title}}</p>"
})
var app = new Vue({el:'#app',})
</script>
</body>
</html>

Vue内部会执行initGlobalAPI()函数给大Vue增加一些静态方法,其中会执行一个initAssetRegisters函数,该函数会给Vue的原型增加一个Vue.component、Vue.directive和Vue.filter函数函数,分别用于注册组件、指令和过滤器,如下

function initAssetRegisters (Vue) {       //初始化component、directive和filter函数 第4850行
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(function (type) { //遍历//ASSET_TYPES数组 ASSET_TYPES是一个数组,定义在339行,等于:['component','directive','filter']
Vue[type] = function (
id,
definition
) {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if ("development" !== 'production' && type === 'component') {
validateComponentName(id);
}
if (type === 'component' && isPlainObject(definition)) { //如果是个组件
definition.name = definition.name || id;
definition = this.options._base.extend(definition); //则执行Vue.extend()函数 ;this.options._base等于大Vue,定义在5050行
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition };
}
this.options[type + 's'][id] = definition;           //将definition保存到this.options[type + 's']里,例如组件保存到this.options['component']里面
return definition
}
};
});
}

Vue.extend()将使用基础Vue构造器,创建一个“子类”。参数是一个包含组件选项的对象,也就是注册组件时传入的对象,如下:

  Vue.extend = function (extendOptions) {       //初始化Vue.extend函数  第4770行
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
} var name = extendOptions.name || Super.options.name;
if ("development" !== 'production' && name) {
validateComponentName(name);
} var Sub = function VueComponent (options) { //定义组件的构造函数,函数最后会返回该函数
this._init(options);
};
/*中间进行一些数据的合并*/
// cache constructor
cachedCtors[SuperId] = Sub;
return Sub
};
}

以例子为例,当加载完后,我们在控制台输入console.log(Vue.options["components"]),输出如下:

可以看到child组件的构造函数被保存到Vue.options["components"]["child“]里面了。其他三个KeepAlive、Transition和TransitionGroup是Vue的内部组件

当vue加载时会执行模板生成的render函数,例子里的render函数等于:

执行_c('child',{attrs:{"title":"Hello Wrold"}})函数时会执行vm.$createElement()函数,也就是Vue内部的createElement函数,如下

function createElement (      //创建vNode 第4335行
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) { //如果data是个数组或者是基本类型
normalizationType = children;
children = data; //修正data为children
data = undefined; //修正data为undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType) //再调用_createElement
} function _createElement ( //创建vNode
context, //context:Vue对象
tag, //tag:标签名或组件名
data,
children,
normalizationType
) {
/*略*/
if (typeof tag === 'string') { //如果tag是个字符串
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) { //如果tag是平台内置的标签
// platform built-in elements
vnode = new VNode( //调用new VNode()去实例化一个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); //创建组件Vnode
} 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 (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) { applyNS(vnode, ns); }
if (isDef(data)) { registerDeepBindings(data); }
return vnode //最后返回VNode
} else {
return createEmptyVNode()
}
}
resolveAsset()用于获取资源,也就是获取组件的构造函数(在上面Vue.extend里面定义的构造函数),定义如下:
function resolveAsset (       //获取资源 第1498行
options,
type,
id,
warnMissing
) {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
var assets = options[type];
// check local registration variations first
if (hasOwn(assets, id)) { return assets[id] } //先从当前实例上找id
var camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } //将id转化为驼峰式后再找
var PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } //如果还没找到则尝试将首字母大写查找
// fallback to prototype chain
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; //最后通过原型来查找
if ("development" !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
);
}
return res
}

例子里执行到这里时就可以获取到在Vue.extend()里定义的Sub函数了,如下:

我们点击这个函数时会跳转到Sub函数,如下:

回到_createElement函数,获取到组件的构造函数后就会执行createComponent()创建组件的Vnode,这一步对于组件来说很重要,它会对组件的data、options、props、自定义事件、钩子函数、原生事件、异步组件分别做一步处理,对于组件的实例化来说,最重要的是安装钩子吧,如下:

function createComponent (      //创建组件Vnode 第4182行 Ctor:组件的构造函数  data:数组 context:Vue实例  child:组件的子节点
Ctor,
data,
context,
children,
tag
) {
/*略*/
// install component management hooks onto the placeholder node
installComponentHooks(data); //安装一些组件的管理钩子 /*略*/
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
); //创建组件Vnode
return vnode //最后返回vnode
}

installComponentHooks()会给组件安装一些管理钩子,如下:

function installComponentHooks (data) {         //安装组件的钩子 第4307行
var hooks = data.hook || (data.hook = {}); //尝试获取组件的data.hook属性,如果没有则初始化为空对象
for (var i = 0; i < hooksToMerge.length; i++) { //遍历hooksToMerge里的钩子,保存到hooks对应的key里面
var key = hooksToMerge[i];
hooks[key] = componentVNodeHooks[key];
}
}

componentVNodeHooks保存了组件的钩子,总共有四个:init、prepatch、insert和destroy,对应组件的四个不同的时期,以例子为例执行完后data.hook等于如下:

最后将虚拟VNode渲染为真实DOM节点的时候会执行n createelm()函数,该函数会优先执行createComponent()函数去创建组件,如下:

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {     //创建组件节点 第5590行   ;注:这是patch()函数内的createComponent()函数,而不是全局的createComponent()函数
var i = vnode.data; //获取vnode的data属性
if (isDef(i)) { //如果存在data属性(组件vnode肯定存在这个属性,普通vnode有可能存在)
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; //这是keepAlive逻辑,可以先忽略
if (isDef(i = i.hook) && isDef(i = i.init)) { //如果data里定义了hook方法,且存在init方法
i(vnode, false /* hydrating */, parentElm, refElm);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}

createComponent会去执行组件的init()钩子函数:

  init: function init (         //组件的安装 第4110行
vnode, //vnode:组件的占位符VNode
hydrating, //parentElm:真实的父节点引用
parentElm, //refElm:参考节点
refElm
) {
if ( //这是KeepAlive逻辑
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
var child = vnode.componentInstance = createComponentInstanceForVnode( //调用该方法返回子组件的Vue实例,并保存到vnode.componentInstance属性上
vnode,
activeInstance,
parentElm,
refElm
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},

createComponentInstanceForVnode会创建组件的实例,如下:

function createComponentInstanceForVnode (      //第4285行 创建组件实例 vnode:占位符VNode parent父Vue实例 parentElm:真实的DOM节点  refElm:参考节点
vnode, // we know it's MountedComponentVNode but flow doesn't
parent, // activeInstance in lifecycle state
parentElm,
refElm
) {
var options = {
_isComponent: true,
parent: parent,
_parentVnode: vnode,
_parentElm: parentElm || null,
_refElm: refElm || null
};
// check inline-template render functions
var inlineTemplate = vnode.data.inlineTemplate; //尝试获取inlineTemplate属性,定义组件时如果指定了inline-template特性,则组件内的子节点都是该组件的模板
if (isDef(inlineTemplate)) { //如果inlineTemplate存在,我们这里是不存在的
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options) //调用组件的构造函数(Vue.extend()里面定义的)返回子组件的实例,也就是Vue.extend()里定义的Sub函数
}

最后Vue.extend()里的Sub函数会执行_init方法对Vue做初始化,初始化的过程中会定义组件实例的$parent和父组件的$children属性,从而实现父组件和子组件的互连,组件的大致流程就是这样子

Vue.js 源码分析(十二) 基础篇 组件详解的更多相关文章

  1. Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解

    我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现 组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事 ...

  2. Vue.js 源码分析(十八) 指令篇 v-for 指令详解

    我们可以用 v-for 指令基于一个数组or对象来渲染一个列表,有五种使用方法,如下: <!DOCTYPE html> <html lang="en"> & ...

  3. Vue.js 源码分析(十九) 指令篇 v-html和v-text指令详解

    双大括号会将数据解释为普通文本,而非 HTML 代码.为了输出真正的 HTML,你需要使用 v-html 指令,例如: <!DOCTYPE html> <html lang=&quo ...

  4. Vue.js 源码分析(十六) 指令篇 v-on指令详解

    可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,例如: <!DOCTYPE html> <html lang="en"& ...

  5. Vue.js 源码分析(十五) 指令篇 v-bind指令详解

    指令是Vue.js模板中最常用的一项功能,它带有前缀v-,比如上面说的v-if.v-html.v-pre等.指令的主要职责就是当其表达式的值改变时,相应的将某些行为应用到DOM上,先介绍v-bind指 ...

  6. Vue.js 源码分析(十) 基础篇 ref属性详解

    ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例,例如: ...

  7. jQuery 源码分析(十) 数据缓存模块 data详解

    jQuery的数据缓存模块以一种安全的方式为DOM元素附加任意类型的数据,避免了在JavaScript对象和DOM元素之间出现循环引用,以及由此而导致的内存泄漏. 数据缓存模块为DOM元素和JavaS ...

  8. jQuery 源码分析(十九) DOM遍历模块详解

    jQuery的DOM遍历模块对DOM模型的原生属性parentNode.childNodes.firstChild.lastChild.previousSibling.nextSibling进行了封装 ...

  9. Vue.js 源码分析(一) 代码结构

    关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...

随机推荐

  1. Java面试题:Java中的集合及其继承关系

    关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们看这张图即可: 1.List.Set.Map是否继承自Collection接口? List.Set ...

  2. 面向对象的六大原则之 接口隔离原则——ISP

    ISP = Interface Segregation Principle   ISP的定义如下: 1.客户端不应该依赖他不需要的接口 2.一个类对另外一个类的依赖性应该是建立在最小的接口上 3.不应 ...

  3. WinRAR命令行版本 rar.exe使用详解(适用Linux)

    RAR 命令行语法: RAR.exe <命令> [ -<开关> ] <压缩文件> [ <@列表文件...> ] [ <文件...> ] [ ...

  4. delphi消息发送字符串

    delphi消息发送字符串 其实不论什么方法,归根揭底都是通过传递对象的指针来达到效果的. 方法一: procedure SendString(strMSG: string);var  Data: t ...

  5. elasticsearch bulk

    情景介绍 公司2000W的数据从mysql 迁移至elasticsearch,以提供微服务.本文基于elasticsearch-py bulk操作实现数据迁移.相比于elasticsearch-dum ...

  6. LeetCode——Employees Earning More Than Their Managers

    The Employee table holds all employees including their managers. Every employee has an Id, and there ...

  7. Spark(4)

    Spark Core官网学习回顾 Speed disk 10x memory 100x Easy code interactive shell Unified Stack Batch Streamin ...

  8. 跟着 Alex 学python 1.安装

    声明 : 文档内容学习于 http://www.cnblogs.com/xiaozhiqi/ 参考文档: http://www.runoob.com/python/python-tutorial.ht ...

  9. 201871010107-公海瑜《面向对象程序设计(java)》第十五周学习总结

    201871010107-公海瑜<面向对象程序设计(java)>第十五周学习总结             项目                            内容   这个作业属于 ...

  10. win7系统中右键新建没有写字板

    问题描述: win7系统中右键新建没有写字板 解决方案: 1. 按下Win+R后输入regedit打开注册表. (可以使用组合键ALT+ 键盘上的左键, 对展开的注册表项进行折叠方可查看) 2.定位到 ...