Vue.js 源碼分析(十二) 基礎篇 組件詳解

組件是可複用的Vue實例,一個組件本質上是一個擁有預約義選項的一個Vue實例,組件和組件之間經過一些屬性進行聯繫。html

組件有兩種註冊方式,分別是全局註冊和局部註冊,前者經過Vue.component()註冊,後者是在建立Vue實例的時候在components屬性裏指定,例如:vue

<!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爲:node

其中Hello World是全局註冊的組件渲染出來的,而Hello Vue是局部組件渲染出來的。react

咱們在測試按鈕上綁定了一個事件,點擊按鈕後輸出以下:api

能夠看到Vue實例的$children屬性是個數組,對應的是當前實例引用的全部組件的實例,其中$children[0]是全局組件child的實例,而children[1]是局部組件hello的實例。數組

而this.$children[1].$parent ===this輸出爲true則表示對於組件實例來講,它的$parent指向的父組件實例,也就是例子裏的根組件實例。瀏覽器

Vue內部也是經過$children和$parent屬性實現了組件和組件之間的關聯的。app

組件是能夠無限複用的,好比:異步

<!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>

渲染爲:async

注:對於組件來講,須要把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屬性,從而實現父組件和子組件的互連,組件的大體流程就是這樣子

相關文章
相關標籤/搜索