Vue源碼分析(11)--實例分析component,props,slot

前言

本文是vue2.x源碼分析的第十一篇,主要看component,props,slot的處理過程!

實例代碼

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue</title>
  <script src="./vue11.js" type="text/javascript" charset="utf-8" ></script>
</head>
<body>
  <div id="app">
      <child :name='message'>
        <!-- <span>span from parent</span> -->
      </child>
  </div>
  <script>
    debugger;
    var child=Vue.component('child',{
        template:'<div>{{name}}<slot></slot></div>',
        props:['name']
    })
    var vm=new Vue({
      el:'#app',
      name:'app',
      data:{
        message:'message from parent'
      },
    });
  </script>
</body>
</html>

1、三個全局API(Vue.component、Vue.directive、Vue.filter)

initAssetRegisters函數在initGlobalAPI時被調用

    function initAssetRegisters (Vue) {
      //config._assetTypes=['component','directive','filter']
      config._assetTypes.forEach(function (type) {
        Vue[type] = function (id,definition) {
          if (!definition) {
            return this.options[type + 's'][id]
          } else {
            {
              if (type === 'component' && config.isReservedTag(id)) {
                warn(
                  'Do not use built-in or reserved HTML elements as component ' +
                  'id: ' + id
                );
              }
            }
            if (type === 'component' && isPlainObject(definition)) {
              definition.name = definition.name || id;
              definition = this.options._base.extend(definition);
            }
            if (type === 'directive' && typeof definition === 'function') {
              definition = { bind: definition, update: definition };
            }
            this.options[type + 's'][id] = definition;
            return definition
          }
        };
      });
    }

首先要知道,當vue庫文件加載完后,vue的初始化中已有這個東西:

Vue.options={
    components:{
        KeepAlive:Object,
        Transition:Object,
        TransitionGroup:Object
    },
    directives:{
        show:Object,
        model:Object
    },
    filter:{},
    _base:function Vue$3(options){...}
}

這些都是vue庫內置的組件和指令,當執行Vue.component、Vue.directive、Vue.filter時就是在對這些內置組件、指令、過濾器進行擴充,所以:

    var child=Vue.component('child',{
        template:'<div>child</div>',
        props:['name']
    })

執行完后

    Vue.options.components={
        KeepAlive:Object,
        Transition:Object,
        TransitionGroup:Object,
        child:function VueComponent(options)
    }

child的配置項存放在VueComponent.options中,在該函數中還對props,computed進行了處理:

    //對child的props的處理,key='name'
    proxy(VueComponent.prototype, "_props", key);
        //proxy函數如下:
        function proxy (target, sourceKey, key) {
          sharedPropertyDefinition.get = function proxyGetter () {
            return this[sourceKey][key]
          };
          sharedPropertyDefinition.set = function proxySetter (val) {
            this[sourceKey][key] = val;
          };
          //在VueComponent.prototype定義存取器屬性'name'
          Object.defineProperty(target, key, sharedPropertyDefinition);
        }

順便提一句,當定義全局指令時

    Vue.directive('v-focus',function(){...})
    //會將定義的函數當作bind和update函數,運行完是這樣:
    Vue.options.directives={
        show:Object,
        model:Object,
        v-focus:{
            bind:function(){...},
            update:function(){...},
        }
    }

2、詳細分析

vue的渲染過程可分為四步:


模板解析.jpg

當child=Vue.component('child',...)執行完后,開始new Vue的過程,經過一系列的處理,得到根節點的AST結構:

    attrs:Array(1) //這里存放了id:'app'
    attrsList:Array(1)
    attrsMap:Object
    children:Array(1)
    parent:undefined
    plain:false
    static:false
    staticRoot:false
    tag:"div"
    type:1
    __proto__:Object
    //其中的children[0],即child的AST如下:
    attrs:Array(1)   //這里存放了name:'message'
    attrsList:Array(1)
    attrsMap:Object
    children:Array(0)
    hasBindings:true
    parent:Object
    plain:false
    static:false
    staticRoot:false
    tag:"child"
    type:1      //得到AST時,vue將child看成普通html標簽,未做特殊處理
    __proto__:Object

此時得到的render函數如下:

    with(this){
        return _c(
            'div',
            {attrs:{"id":"app"}},
            [_c(
                'child',
                {attrs:{"name":message}}
                )
            ],
        1)
    }

render函數執行時,message取到了'message from parent',下面主要看_c函數的處理:

    vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
    //createElement函數如下:
    function createElement (context,tag,data,children,normalizationType,alwaysNormalize) {
      if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children;
        children = data;
        data = undefined;
      }
      if (alwaysNormalize) { normalizationType = ALWAYS_NORMALIZE; }
      return _createElement(context, tag, data, children, normalizationType)
    }
    function _createElement (context,tag,data,children,normalizationType) {
      ...
      // support single function children as default scoped slot
      //
      ... 暫時略過slot的處理
      //
      var vnode, ns;
      if (typeof tag === 'string') {
        var Ctor;
        ns = config.getTagNamespace(tag);
        if (config.isReservedTag(tag)) {
          // platform built-in elements
          vnode = new VNode(
            config.parsePlatformTagName(tag), data, children,
            undefined, undefined, context
          );
        } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
          //這里取到了child的構造函數
          // 創建child的vnode
          vnode = createComponent(Ctor, data, context, children, tag);
        } 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 (vnode) {
        if (ns) { applyNS(vnode, ns); }
        return vnode
      } else {
        return createEmptyVNode()
      }
    }

看下createComponent函數

function createComponent (Ctor,data,context,children,tag) {
  ...
  // 異步組件處理
  ...
  // 處理option
  resolveConstructorOptions(Ctor);
  data = data || {};
  // 將組件v-model的data轉成props & events
  ...
  // 提取props
  var propsData = extractProps(data, Ctor, tag);
  // 函數組件
  ...
  // 提取事件監聽
  var listeners = data.on;
  // 用.native modifier替換
  data.on = data.nativeOn;
  // 合并組件管理鉤子到placeholder vnode
  mergeHooks(data);
  // 返回placeholder vnode
  var name = Ctor.options.name || tag;
  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 }
  );
  return vnode
}

最終child的Vnode結構如下:

    children:undefined
    componentInstance:undefined
    componentOptions:Object
    context:Vue$3
    data:Object
    elm:undefined
    functionalContext:undefined
    isCloned:false
    isComment:false
    isOnce:false
    isRootInsert:true
    isStatic:false
    key:undefined
    ns:undefined
    parent:undefined
    raw:false
    tag:"vue-component-1-child"
    text:undefined
    child:(...)
    __proto__:Object
    //componentOptions對象如下:
        Ctor:function VueComponent(options)
        children:undefined
        listeners:undefined
        propsData:Object //包含name:'message from parent'
        tag:"child"
        __proto__:Object

組件的Vnode和普通html標簽的Vnode的主要差異在componentOptions
在得到child的Vnode后,再得到根節點的Vnode,然后根節點的render函數執行完畢,返回根節點的Vnode(children里存放了child的Vnode)。然后執行vm._update函數,該函數調用vm.__patch__,patch又調用createElm函數:

    function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
        vnode.isRootInsert = !nested; // for transition enter check
        //注意createComponent函數,對于普通html標簽對應的vnode會返回false,但對于組件的vnode會返回true
        if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
          return
        }
        var data = vnode.data;
        var children = vnode.children;
        var tag = vnode.tag;
        if (isDef(tag)) {
          ...
          vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            : nodeOps.createElement(tag, vnode);
          setScope(vnode);
          /* istanbul ignore if */
          {
            createChildren(vnode, children, insertedVnodeQueue);
            if (isDef(data)) {
              invokeCreateHooks(vnode, insertedVnodeQueue);
            }
            insert(parentElm, vnode.elm, refElm);
          }
          ...
        }
        ...
      }

首先是處理根節點的Vnode,當執行到createChildren(vnode, children, insertedVnodeQueue)時,開始處理child的Vnode,createChildren是直接調用createElm函數的,所以createElm函數再次執行,不同的是這次處理child的Vnode,來看下createComponent函數:

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data; //
    if (isDef(i)) {
      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        //這里開始執行vnode.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
      }
    }
  }

看下vnode.data.hook.init函數

    init: function init (vnode,hydrating,parentElm,refElm) {
        if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
        //這里開始創建vnode的組件實例
          var child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode,
            activeInstance,
            parentElm,
            refElm
          );
          child.$mount(hydrating ? vnode.elm : undefined, hydrating);
        } else if (vnode.data.keepAlive) {
          // kept-alive components, treat as a patch
          var mountedNode = vnode; // work around flow
          componentVNodeHooks.prepatch(mountedNode, mountedNode);
        }
    }

看下createComponentInstanceForVnode函數

function createComponentInstanceForVnode (
  vnode, // we know it's MountedComponentVNode but flow doesn't
  parent, // activeInstance in lifecycle state
  parentElm,
  refElm
) {
    //  vnode.componentOptions如下:
    //  Ctor:function VueComponent(options)
    //  children:undefined
    //  listeners:undefined
    //  propsData:Object
    //  tag:"child"
    //  __proto__:Object
  var vnodeComponentOptions = vnode.componentOptions;
  var options = {
    _isComponent: true,
    parent: parent,
    propsData: vnodeComponentOptions.propsData,
    _componentTag: vnodeComponentOptions.tag,
    _parentVnode: vnode,
    _parentListeners: vnodeComponentOptions.listeners,
    _renderChildren: vnodeComponentOptions.children,
    _parentElm: parentElm || null,
    _refElm: refElm || null
  };
  // check inline-template render functions
  var inlineTemplate = vnode.data.inlineTemplate;
  if (inlineTemplate) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  //這里的Ctor就是 function VueComponent(options){
  //                    this._init(options);
  //               }
  return new vnodeComponentOptions.Ctor(options)
}

接著就進入了和new Vue()一樣的處理過程了

    ...
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');
    //這里沒有vm.$options.el還未生成,故不執行
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
    ...

此時createComponentInstanceForVnode函數執行完畢,接著執行vnode.data.hook.init中的 child.$mount(hydrating ? vnode.elm : undefined, hydrating),然后又開始了這個過程:

模板解析.jpg

這個過程結束后vnode.data.hook.init函數執行完畢,接著執行createComponent函數中的 initComponent(vnode, insertedVnodeQueue)函數,然后createComponent函數執行完畢,從而createChildren執行完,然后
執行invokeCreateHooks(vnode, insertedVnodeQueue)和insert(parentElm, vnode.elm, refElm),頁面就渲染完畢了。

3、總結component的處理過程

  • 解析模板得到根節點的AST(根節點的AST的children包含了組件節點的AST);
  • 由根節點的AST和組件節點的AST得到render函數;
  • render函數在執行時,_c函數對普通html標簽和組件標簽的處理不一樣,不過最終都返回了Vnode結構;
  • 執行createElm函數,該函數處理兩種情況:組件Vnode和普通Vnode
  • 對于組件Vnode,會再次經歷模板解析->AST->render->Vnode->createElm->真實DOM

4、props和slot的原理

從component的處理過程不難發現props和slot的原理

4.1 props

  • Vue.component(child,{props:['name']})執行中執行了initProps$1(Sub),該函數執行了proxy(VueComponent.prototype, "_props", 'name'),即把name定義成VueComponent.prototype的存取器屬性,其中get函數是這樣的:
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };

注:這里尚未實現this._props

  • 組件Vnode在執行this._init過程中執行了initState,該函數又執行了initProps,這個函數實現了將'name'作為this._props的存取器屬性,于是在子組件中vm.name相當于vm._props.name

4.2 slot

  • 根節點AST
  • 根節點render
  • 根節點Vnode的children是子節點的Vnode,子節點Vnode的children應該是span節點,但實際上并不是,做了特殊處理,將span的Vnode放入了子節點Vnode.componentOptions.children中
  • 子節點模板的AST
  • 子節點render又做了特殊處理,slot應該是_v('slot',...),但實際被處理成_t('default')
    with(this){
        return _c('div',[_v(_s(name)),_t("default")],2)
    }
  • 子節點模板Vnode的children中已經有兩個Vnode,_t('default')被轉成了
    從this.$slots['default']中取的Vnode,該Vnode即是span的Vnode,this.$slots是在initRender函數中初始化的
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容