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为:
其中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 源码分析(二十二) 指令篇 v-model指令详解
-
Vue.js 源码分析(十六) 指令篇 v-on指令详解
-
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
-
Vue.js 源码分析(十二) 基础篇 组件详解
-
Vue.js 源码分析(十五) 指令篇 v-bind指令详解
-
Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解
-
Vue 2.0 深入源码分析(八) 基础篇 依赖注入 provide/inject组合详解
-
Vue.js 源码分析(十一) 基础篇 过滤器 filters属性详解
-
Vue.js 源码分析(十八) 指令篇 v-for 指令详解
-
Vue.js 源码分析(十三) 基础篇 组件 props属性详解