zoukankan      html  css  js  c++  java
  • 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属性,从而实现父组件和子组件的互连,组件的大致流程就是这样子

  • 相关阅读:
    locate命令详解
    python 爬虫获取文件式网站资源完整版(基于python 3.6)
    python 爬虫获取文件式网站资源(基于python 3.6)
    利用python 获取网址中的href(基于python 3.6)
    springmvc+font-awesome开发出的页面显示方框乱码的解决方法
    2017年6月短学期培训代码总结 -----springMvc
    2017年6月短学期培训代码总结 -----JDBC
    构建之法 第十章 典型用户和场景
    构建之法 第九章 项目经理
    构建之法 第八章 需求分析
  • 原文地址:https://www.cnblogs.com/greatdesert/p/11088574.html
Copyright © 2011-2022 走看看