经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
Vue.js 源码分析(十二) 基础篇 组件详解
来源:cnblogs  作者:大沙漠  时间:2019/6/26 11:41:51  对本文有异议

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

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

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <child title="Hello Wrold"></child>
  11. <hello></hello>
  12. <button @click="test">测试</button>
  13. </div>
  14. <script>
  15. Vue.component('child',{ //全局注册
  16. props:['title'],
  17. template:"<p>{{title}}</p>"
  18. })
  19. var app = new Vue({
  20. el:'#app',
  21. components:{
  22. hello:{template:'<p>Hello Vue</p>'} //局部组件
  23. },
  24. methods:{
  25. test:function(){
  26. console.log(this.$children)
  27. console.log(this.$children[1].$parent ===this)
  28. }
  29. }
  30. })
  31. </script>
  32. </body>
  33. </html>

渲染DOM为:

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

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

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

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

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

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

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <child title="Hello Wrold"></child>
  11. <child title="Hello Vue"></child>
  12. <child title="Hello Rose"></child>
  13. </div>
  14. <script>
  15. Vue.component('child',{
  16. props:['title'],
  17. template:"<p>{{title}}</p>"
  18. })
  19. var app = new Vue({el:'#app'})
  20. </script>
  21. </body>
  22. </html>

渲染为:

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

例1:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <child ></child>
  11. </div>
  12. <script>
  13. Vue.component('child',{
  14. data:{title:"Hello Vue"},
  15. template:"<p>{{title}}</p>"
  16. })
  17. var app = new Vue({el:'#app'})
  18. </script>
  19. </body>
  20. </html>

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

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

  1. strats.data = function ( //data的合并策略 第1196行
  2. parentVal,
  3. childVal,
  4. vm
  5. ) {
  6. if (!vm) { //如果vm不存在,对于组件来说是不存在的
  7. if (childVal && typeof childVal !== 'function') { //如果值不是一个函数,则报错
  8. "development" !== 'production' && warn(
  9. 'The "data" option should be a function ' +
  10. 'that returns a per-instance value in component ' +
  11. 'definitions.',
  12. vm
  13. );
  14. return parentVal
  15. }
  16. return mergeDataOrFn(parentVal, childVal)
  17. }
  18. return mergeDataOrFn(parentVal, childVal, vm)
  19. };

 

 源码分析


以这个例子为例:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <child title="Hello Wrold"></child>
  11. </div>
  12. <script>
  13. Vue.component('child',{
  14. props:['title'],
  15. template:"<p>{{title}}</p>"
  16. })
  17. var app = new Vue({el:'#app',})
  18. </script>
  19. </body>
  20. </html>

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

  1. function initAssetRegisters (Vue) { //初始化component、directive和filter函数 第4850行
  2. /**
  3. * Create asset registration methods.
  4. */
  5. ASSET_TYPES.forEach(function (type) { //遍历//ASSET_TYPES数组 ASSET_TYPES是一个数组,定义在339行,等于:['component','directive','filter']
  6. Vue[type] = function (
  7. id,
  8. definition
  9. ) {
  10. if (!definition) {
  11. return this.options[type + 's'][id]
  12. } else {
  13. /* istanbul ignore if */
  14. if ("development" !== 'production' && type === 'component') {
  15. validateComponentName(id);
  16. }
  17. if (type === 'component' && isPlainObject(definition)) { //如果是个组件
  18. definition.name = definition.name || id;
  19. definition = this.options._base.extend(definition); //则执行Vue.extend()函数 ;this.options._base等于大Vue,定义在5050行
  20. }
  21. if (type === 'directive' && typeof definition === 'function') {
  22. definition = { bind: definition, update: definition };
  23. }
  24. this.options[type + 's'][id] = definition;           //将definition保存到this.options[type + 's']里,例如组件保存到this.options['component']里面
  25. return definition
  26. }
  27. };
  28. });
  29. }

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

  1. Vue.extend = function (extendOptions) { //初始化Vue.extend函数 第4770行
  2. extendOptions = extendOptions || {};
  3. var Super = this;
  4. var SuperId = Super.cid;
  5. var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
  6. if (cachedCtors[SuperId]) {
  7. return cachedCtors[SuperId]
  8. }
  9. var name = extendOptions.name || Super.options.name;
  10. if ("development" !== 'production' && name) {
  11. validateComponentName(name);
  12. }
  13. var Sub = function VueComponent (options) { //定义组件的构造函数,函数最后会返回该函数
  14. this._init(options);
  15. };
  16. /*中间进行一些数据的合并*/
  17. // cache constructor
  18. cachedCtors[SuperId] = Sub;
  19. return Sub
  20. };
  21. }

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

可以看到child组件的构造函数被保存到Vue.options["components"]["child“]里面了。

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

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

  1. function createElement ( //创建vNode 第4335行
  2. context,
  3. tag,
  4. data,
  5. children,
  6. normalizationType,
  7. alwaysNormalize
  8. ) {
  9. if (Array.isArray(data) || isPrimitive(data)) { //如果data是个数组或者是基本类型
  10. normalizationType = children;
  11. children = data; //修正data为children
  12. data = undefined; //修正data为undefined
  13. }
  14. if (isTrue(alwaysNormalize)) {
  15. normalizationType = ALWAYS_NORMALIZE;
  16. }
  17. return _createElement(context, tag, data, children, normalizationType) //再调用_createElement
  18. }
  19. function _createElement ( //创建vNode
  20. context, //context:Vue对象
  21. tag, //tag:标签名或组件名
  22. data,
  23. children,
  24. normalizationType
  25. ) {
  26. /**/
  27. if (typeof tag === 'string') { //如果tag是个字符串
  28. var Ctor;
  29. ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
  30. if (config.isReservedTag(tag)) { //如果tag是平台内置的标签
  31. // platform built-in elements
  32. vnode = new VNode( //调用new VNode()去实例化一个VNode
  33. config.parsePlatformTagName(tag), data, children,
  34. undefined, undefined, context
  35. );
  36. } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { //如果该节点名对应一个组件,挂载组件时,如果某个节点是个组件,则会执行到这里
  37. // component
  38. vnode = createComponent(Ctor, data, context, children, tag); //创建组件Vnode
  39. } else {
  40. // unknown or unlisted namespaced elements
  41. // check at runtime because it may get assigned a namespace when its
  42. // parent normalizes children
  43. vnode = new VNode(
  44. tag, data, children,
  45. undefined, undefined, context
  46. );
  47. }
  48. } else {
  49. // direct component options / constructor
  50. vnode = createComponent(tag, data, context, children);
  51. }
  52. if (Array.isArray(vnode)) {
  53. return vnode
  54. } else if (isDef(vnode)) {
  55. if (isDef(ns)) { applyNS(vnode, ns); }
  56. if (isDef(data)) { registerDeepBindings(data); }
  57. return vnode //最后返回VNode
  58. } else {
  59. return createEmptyVNode()
  60. }
  61. }
  1. resolveAsset()用于获取资源,也就是获取组件的构造函数(在上面Vue.extend里面定义的构造函数),定义如下:
  1. function resolveAsset ( //获取资源 第1498行
  2. options,
  3. type,
  4. id,
  5. warnMissing
  6. ) {
  7. /* istanbul ignore if */
  8. if (typeof id !== 'string') {
  9. return
  10. }
  11. var assets = options[type];
  12. // check local registration variations first
  13. if (hasOwn(assets, id)) { return assets[id] } //先从当前实例上找id
  14. var camelizedId = camelize(id);
  15. if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } //将id转化为驼峰式后再找
  16. var PascalCaseId = capitalize(camelizedId);
  17. if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } //如果还没找到则尝试将首字母大写查找
  18. // fallback to prototype chain
  19. var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; //最后通过原型来查找
  20. if ("development" !== 'production' && warnMissing && !res) {
  21. warn(
  22. 'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
  23. options
  24. );
  25. }
  26. return res
  27. }

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

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

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

  1. function createComponent ( //创建组件Vnode 第4182行 Ctor:组件的构造函数 data:数组 context:Vue实例 child:组件的子节点
  2. Ctor,
  3. data,
  4. context,
  5. children,
  6. tag
  7. ) {
  8. /**/
  9. // install component management hooks onto the placeholder node
  10. installComponentHooks(data); //安装一些组件的管理钩子
  11.  
  12. /**/
  13. var vnode = new VNode(
  14. ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  15. data, undefined, undefined, undefined, context,
  16. { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  17. asyncFactory
  18. ); //创建组件Vnode
  19. return vnode //最后返回vnode
  20. }

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

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

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

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

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

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

  1. init: function init ( //组件的安装 第4110行
  2. vnode, //vnode:组件的占位符VNode
  3. hydrating, //parentElm:真实的父节点引用
  4. parentElm, //refElm:参考节点
  5. refElm
  6. ) {
  7. if ( //这是KeepAlive逻辑
  8. vnode.componentInstance &&
  9. !vnode.componentInstance._isDestroyed &&
  10. vnode.data.keepAlive
  11. ) {
  12. // kept-alive components, treat as a patch
  13. var mountedNode = vnode; // work around flow
  14. componentVNodeHooks.prepatch(mountedNode, mountedNode);
  15. } else {
  16. var child = vnode.componentInstance = createComponentInstanceForVnode( //调用该方法返回子组件的Vue实例,并保存到vnode.componentInstance属性上
  17. vnode,
  18. activeInstance,
  19. parentElm,
  20. refElm
  21. );
  22. child.$mount(hydrating ? vnode.elm : undefined, hydrating);
  23. }
  24. },

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

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

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

原文链接:http://www.cnblogs.com/greatdesert/p/11088574.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号