Vue.js 源码分析(十二) 基础篇 组件详解
组件是可复用的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 源码分析(十二) 基础篇 组件详解的更多相关文章
- Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现 组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事 ...
- Vue.js 源码分析(十八) 指令篇 v-for 指令详解
我们可以用 v-for 指令基于一个数组or对象来渲染一个列表,有五种使用方法,如下: <!DOCTYPE html> <html lang="en"> & ...
- Vue.js 源码分析(十九) 指令篇 v-html和v-text指令详解
双大括号会将数据解释为普通文本,而非 HTML 代码.为了输出真正的 HTML,你需要使用 v-html 指令,例如: <!DOCTYPE html> <html lang=&quo ...
- Vue.js 源码分析(十六) 指令篇 v-on指令详解
可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,例如: <!DOCTYPE html> <html lang="en"& ...
- Vue.js 源码分析(十五) 指令篇 v-bind指令详解
指令是Vue.js模板中最常用的一项功能,它带有前缀v-,比如上面说的v-if.v-html.v-pre等.指令的主要职责就是当其表达式的值改变时,相应的将某些行为应用到DOM上,先介绍v-bind指 ...
- Vue.js 源码分析(十) 基础篇 ref属性详解
ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例,例如: ...
- jQuery 源码分析(十) 数据缓存模块 data详解
jQuery的数据缓存模块以一种安全的方式为DOM元素附加任意类型的数据,避免了在JavaScript对象和DOM元素之间出现循环引用,以及由此而导致的内存泄漏. 数据缓存模块为DOM元素和JavaS ...
- jQuery 源码分析(十九) DOM遍历模块详解
jQuery的DOM遍历模块对DOM模型的原生属性parentNode.childNodes.firstChild.lastChild.previousSibling.nextSibling进行了封装 ...
- Vue.js 源码分析(一) 代码结构
关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...
随机推荐
- Java面试题:Java中的集合及其继承关系
关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们看这张图即可: 1.List.Set.Map是否继承自Collection接口? List.Set ...
- 面向对象的六大原则之 接口隔离原则——ISP
ISP = Interface Segregation Principle ISP的定义如下: 1.客户端不应该依赖他不需要的接口 2.一个类对另外一个类的依赖性应该是建立在最小的接口上 3.不应 ...
- WinRAR命令行版本 rar.exe使用详解(适用Linux)
RAR 命令行语法: RAR.exe <命令> [ -<开关> ] <压缩文件> [ <@列表文件...> ] [ <文件...> ] [ ...
- delphi消息发送字符串
delphi消息发送字符串 其实不论什么方法,归根揭底都是通过传递对象的指针来达到效果的. 方法一: procedure SendString(strMSG: string);var Data: t ...
- elasticsearch bulk
情景介绍 公司2000W的数据从mysql 迁移至elasticsearch,以提供微服务.本文基于elasticsearch-py bulk操作实现数据迁移.相比于elasticsearch-dum ...
- LeetCode——Employees Earning More Than Their Managers
The Employee table holds all employees including their managers. Every employee has an Id, and there ...
- Spark(4)
Spark Core官网学习回顾 Speed disk 10x memory 100x Easy code interactive shell Unified Stack Batch Streamin ...
- 跟着 Alex 学python 1.安装
声明 : 文档内容学习于 http://www.cnblogs.com/xiaozhiqi/ 参考文档: http://www.runoob.com/python/python-tutorial.ht ...
- 201871010107-公海瑜《面向对象程序设计(java)》第十五周学习总结
201871010107-公海瑜<面向对象程序设计(java)>第十五周学习总结 项目 内容 这个作业属于 ...
- win7系统中右键新建没有写字板
问题描述: win7系统中右键新建没有写字板 解决方案: 1. 按下Win+R后输入regedit打开注册表. (可以使用组合键ALT+ 键盘上的左键, 对展开的注册表项进行折叠方可查看) 2.定位到 ...