Vue源碼解析(四)-components組件

組件初始化渲染

本文以局部組件的註冊方式介紹組件的初始化渲染,demo以下vue

new Vue({
  el: '#app',
  template: 
  `<div>
     <div>father component!</div>
     <my-component></my-component>
  </div>`,
  components:{
      'my-component': {
          template: '<div>children component!</div>'
      }
  }
})

一、Vue源碼解析(一)-模版渲染介紹過,vue初始化時根據template函數生成render函數,本文render函數會調用vm._c('my-component'),_createElement判斷'my-component是註冊過的組件,所以以組件的方式生成vnodenode

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

//template生成的render函數vm._render會調用vm._c('my-component')
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

function _createElement(){
    //本例tag=‘my-component’,‘my-component’在components屬性中註冊過,所以以組件的方式生成vnode
    if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag);
    }
}

//本例Ctor參數{template: '<div>children component1!</div>'}
function createComponent (Ctor){
    //Vue構造函數
    var baseCtor = context.$options._base;
    if (isObject(Ctor)) {
      //生成VuComponent構造函數
      //此處至關於Ctor = Vue.extend({template: '<div>children component1!</div>'}), Vue.extend後面有介紹;
      Ctor = baseCtor.extend(Ctor);
    }
    //將componentVNodeHooks上的方法掛載到vnode上,組件初次渲染會用到componentVNodeHooks.init
    var data = {}
    mergeHooks(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 } );
}
//component初始化和更新的方法,此處先介紹init
var componentVNodeHooks = {
    init(vnode){
        //根據Vnode生成VueComponent實例
        var child = vnode.componentInstance = createComponentInstanceForVnode(vnode);
        //將VueComponent實例掛載到dom節點上,本文是掛載到<my-component></my-component>節點
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
}

二、調用vm._update將vnode渲染爲瀏覽器dom,主要方法是遍歷vnode的全部節點,根據節點類型調用相關的方法進行解析,本文主要介紹components的解析方法createComponent:根據vnode生成VueComponent(繼承Vue)對象,
調用Vue.prototype.$mount方法渲染domreact

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    //組件vnode節點渲染方法
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    //Vue源碼解析(一)中介紹過普通vnode節點渲染步驟 
    //根據vnode節點生成瀏覽器Element對象
    vnode.elm = nodeOps.createElement(tag, vnode);
    var children = vnode.children;
    //遞歸將vnode子節點生成Element對象
    createChildren(vnode, children, insertedVnodeQueue);
    //將生成的vnode.elm插入到瀏覽器的父節點當中
    insert(parentElm, vnode.elm, refElm);
}

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      //i就是上面的componentVNodeHooks.init方法
      i(vnode, false /* hydrating */, parentElm, refElm);
    }
    if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
    }
}  

function createComponentInstanceForVnode (){
    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
    };
    //上面提到的VueComponent構造函數Ctor,至關於new VueComponent(options)
    return new vnode.ComponentOptions.Ctor(options)
}

3 、new VueComponent和new Vue的過程相似,本文就再也不作介紹segmentfault

全局註冊組件

上文提到過 Vue.extend方法(繼承Vue生成VueComponent構造函數)此處單獨介紹一下api

Vue.extend = function (extendOptions) {
    var Super = this;
    var Sub = function VueComponent (options) {
      this._init(options);
    };
    //經典的繼承寫法
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    );
    return Sub
  };

經過Vue.component也能夠全局註冊組件,不須要每次new vue的時候單獨註冊,demo以下:瀏覽器

var globalComponent = Vue.extend({
    name: 'global-component',
    template: '<div>global component!</div>'
});
Vue.component('global-component', globalComponent);
new Vue({
    el: '#app',
    template:
    `<div>
         <global-component></global-component>
         <my-component></my-component>
    </div>`,
    components:{
        'my-component': {
            template: '<div>children component!</div>'
        }
    }
})

vue.js初始化時會先調用一次initGlobalAPI(Vue),給Vue構造函數掛載上一些全局的api,其中又會調用到
initAssetRegisters(Vue),其中定義了Vue.component方法,具體看下其實現app

var ASSET_TYPES = [
      'component',
      'directive',
      'filter'
  ];
  //循環註冊ASSET_TYPES中的全局方法
  ASSET_TYPES.forEach(function (type) {
    Vue[type] = function (
      id,
      definition
    ) {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(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 };
        }
        //全局的組件、指令和過濾器都掛載在Vue.options上
        this.options[type + 's'][id] = definition;
        return definition
      }
    };
  });

  Vue.prototype._init = function (options) {
      vue初始化時將options參數和Vue.options組裝爲vm.$options
      vm.$options = mergeOptions(
        //Vue.options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
  }

本例組裝後的vm.$option.components值以下,proto中前3個屬性是內置全局組件
clipboard.pngdom

組件通訊

prop

在 Vue 中,父子組件的關係能夠總結爲 prop 向下傳遞,事件向上傳遞。父組件經過 prop 給子組件下發數據,子組件經過事件給父組件發送消息.先看看prop是怎麼工做的。demo以下:函數

new Vue({
  el: '#app',
  template:
  `<div>
     <div>father component!</div>
     <my-component message="hello!"></my-component>
  </div>`,
  components:{
      'my-component':{
          props: ['message'],
          template: '<span>{{ message }}</span>'
      }
  }
})

一、template生成的render函數包含:_c('my-component',{attrs:{"message":"hello!"}})]
二、render => vnode => VueComponent,上文提到的VueComponent的構造函數調用了Vue.prototype._init,而且入參option.propsData:{message: "hello!"}
三、雙向綁定中介紹過Vue初始化時會對data中的全部屬性調用defineReactive方法,對data屬性進行監聽;
VueComponent對propsData也是相似的處理方法,initProps後propsData中的屬性和data同樣也是響應式的,propsData變化,相應的view也會發生改變this

function initProps (vm, propsOptions) {
    for (var key in propsOptions){
        //defineReactive參照Vue源碼解析(二)
        defineReactive(props, key, value);
        //將propsData代理到vm上,經過vm[key]訪問propsData[key]
        proxy(vm, "_props", key);
    }
}

四、propsData是響應式的了,但更經常使用的是動態props,按官網說法:「咱們能夠用v-bind來動態地將prop綁定到父組件的數據。每當父組件的數據變化時,該變化也會傳導給子組件」,那麼vue是如何將data的變化傳到給自組件的呢,先看demo

var vm = new Vue({
  el: '#app',
  template:
  `<div>
    <my-component :message="parentMsg"></my-component>
  </div>`,
  data(){
    return{
      parentMsg:'hello'
    }
  },
  components:{
      'my-component':{
          props: ['message'],
          template: '<span>{{ message }}</span>'
      }
  }
})
vm.parentMsg = 'hello world'

五、雙向綁定中介紹過vm.parentMsg變化,會觸發dep.notify(),通知watcher調用updateComponent;
又回到了updateComponent,以後的dom更新過程能夠參考上文的組件渲染邏輯,只是propsData值已是最新的vm.parentMsg的值了

//又見到了。。全部的dom初始化或更新都會用到
updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

Vue.prototype._update = function (vnode, hydrating) {
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    //Vue源碼解析(一)介紹過dom初始化渲染的源碼
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
      // 本文介紹dom更新的方法
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
}

Vue源碼解析(一)介紹過vm.__patch__中dom初始化渲染的邏輯,本文再簡單介紹下vm.__patch關於component更新的邏輯:

function patchVnode (oldVnode, vnode){
    //上文介紹過componentVNodeHooks.init,此處i=componentVNodeHooks.prepatch
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode);
    }
}
var componentVNodeHooks = {
  init(){},
  prepatch: function prepatch (oldVnode, vnode) {
    var options = vnode.componentOptions;
    var child = vnode.componentInstance = oldVnode.componentInstance;
    //更新組件
    updateChildComponent(
      child,
      //此時的propsData已是最新的vm.parentMsg
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  }
}

function updateChildComponent (vm, propsData){
    //將vm._props[key]設置爲新的propsData[key]值,從而觸發view層的更新
    var props = vm._props;
    props[key] = validateProp(key, vm.$options.props, propsData, vm);
}

emit

子組件向父組件通訊須要用到emit,先給出demo

var vm = new Vue({
  el: '#app',
  template:
  `<div>
    <my-component @rf="receiveFn"></my-component>
  </div>`,
  methods:{
    receiveFn(msg){
      console.log(msg)
    }
  },
  components:{
      'my-component':{
          template: '<div>child</div>',
          mounted(){
            this.$emit('rf','hello')
          }
      }
  }
})

本例中子組件mount結束會觸發callHook(vm, 'mounted'),調用this.$emit('rf','hello'),從而調用父組件的receiveFn方法

Vue.prototype.$emit = function (event) {
    //本例cbs=vm._events['rf'] = receiveFn,vm._events涉及v-on指令解析,之後有機會詳細介紹下
    var cbs = vm._events[event];
    //截取第一位以後的參數
    var args = toArray(arguments, 1);
    //執行cbs
    cbs.apply(vm, args);
}

event bus

prop和emit是父子組件通訊的方式,非父子組件能夠經過event bus(事件總線)實現

var bus = new Vue();
var vm = new Vue({
  el: '#app',
  template:
  `<div>
    <my-component-1></my-component-1>
    <my-component-2></my-component-2>
  </div>`,
  components:{
      'my-component-1':{
          template: '<div>child1</div>',
          mounted(){
              bus.$on('event',(msg)=>{
                  console.log(msg)
              })
          }
      },
      'my-component-2':{
          template: '<div>child2</div>',
          mounted(){
              bus.$emit('event','asd')
          }
      }
  }
})

emit方法上文已經介紹過,主要看下on方法,其實就是將fn註冊到vm._events上

Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
    }
    return vm
  };
相關文章
相關標籤/搜索