7. Vue中的父子組件間數據通訊

準備工做

爲了探究父子組件的數據傳遞以及事件觸發, 父組件選用模板:vue

<div id="app"><button-counter :count-total="count" v-on:increment="incremenTotal"></button-counter></div>
複製代碼

子組件選用模板node

<button v-on:click="incrementCounter">{{ counter }}</button>
複製代碼

父組件經過props傳遞參數count到子組件,子組件經過incrementCounter函數中的$emit觸發父組件的incremenTotal函數,具體過程以下:react

如何進行參數傳遞

button-counter組件中props傳遞的值,會在genData$2中通過genProps函數處理生成數組

"{"count-total":count}"
複製代碼

綁定的事件會通過genHandler函數處理生成bash

on:{"increment":incremenTotal}
複製代碼

因此生成的render函數爲app

"_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}})"
複製代碼

根據前面的編譯器原理生成完整的render函數爲:async

"_c('div',{attrs:{"id":"app"}},[_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}})],1)"
複製代碼

調用渲染函數函數

vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼

調用渲染函數,先建立子組件Vnodeui

[_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}}]
複製代碼

_c表示vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); },執行this

function createElement (
    context,
    tag,
    data,
    children,
    normalizationType,
    alwaysNormalize
  ) {
    if (Array.isArray(data) || isPrimitive(data)) {
      normalizationType = children;
      children = data;
      data = undefined;
    }
    if (isTrue(alwaysNormalize)) {
      normalizationType = ALWAYS_NORMALIZE;
    }
    return _createElement(context, tag, data, children, normalizationType)
  }
複製代碼

其中tag爲button-counter,data爲{attrs:{"count-total":count},on:{"increment":incremenTotal}},其他參數均爲undefined,而_createElement函數爲

...
if (typeof tag === 'string') {
    var Ctor;
    ns = (context.$vnode && context.$vnode.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 ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { //  判斷函數是否已經註冊,註冊則建立組件
    // component
    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 (Array.isArray(vnode)) {
    return vnode
} else if (isDef(vnode)) {
    if (isDef(ns)) { applyNS(vnode, ns); }
    if (isDef(data)) { registerDeepBindings(data); }
    return vnode
} else {
    return createEmptyVNode()
}
複製代碼

Ctor = resolveAsset(context.$options, 'components', tag)獲取已註冊組件button-counter的構造函數,建立組件createComponent函數爲

function createComponent (
    Ctor,
    data,
    context,
    children,
    tag
) {
    // 獲取Vue的構造函數VueComponent
    var baseCtor = context.$options._base;
    resolveConstructorOptions(Ctor);
    ...
    // extract props,處理props傳遞的數據,淺拷貝
    var propsData = extractPropsFromVNodeData(data, Ctor, tag);
    // extract listeners, since these needs to be treated as
    // child component listeners instead of DOM listeners
    // 獲取監聽函數的對象
    var listeners = data.on;
    // replace with listeners with .native modifier
    // so it gets processed during parent component patch.
    data.on = data.nativeOn;
    // install component management hooks onto the placeholder node
    // 給建立的子組件,添加鉤子
    installComponentHooks(data);
    // Core爲子組件button-counter的構造函數
    var name = Ctor.options.name || tag;
    // 建立新的Vnode
    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
    );

    return vnode
}
複製代碼

resolveConstructorOptions(Ctor)函數執行後,button子組件的參數Ctor.options爲

{
    components: {button-counter: ƒ},
    data: ƒ (),
    directives: {},
    filters: {},
    methods: {incrementCounter: ƒ},
    name: "button-counter",
    props: {countTotal: {type: ƒ, default: 0}},
    template: "<button v-on:click="incrementCounter">{{ counter }}</button>",
    _Ctor: {0: ƒ},
    _base: ƒ Vue(options)
}
複製代碼

接着,installComponentHooks函數爲button-counter組件添加的鉤子爲

destroy: ƒ destroy(vnode)
init: ƒ init(vnode, hydrating)
insert: ƒ insert(vnode)
prepatch: ƒ prepatch(oldVnode, vnode)
// data的格式以下
{
    attrs: {},
    hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
    on: undefined
}
複製代碼

組件button-counter生成新的Vnode以下:

{
    asyncFactory: undefined,
    asyncMeta: undefined,
    children: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: ƒ VueComponent(options), //  button-counter
        children: undefined,
        listeners: {increment: ƒ},
        propsData: {countTotal: 0},
        tag: "button-counter"
    },
    context: f Vue,
    data: {
        attrs: {},
        hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
        on: undefined
    },
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "vue-component-1-button-counter",
    text: undefined,
    child: undefined
}
複製代碼

button-counter組件的Vnode建立完畢,再回到建立id爲app的元素節點,整個Vnode爲

{
    asyncFactory: undefined,
    asyncMeta: undefined,
    children: [VNode],
    componentInstance: undefined,
    componentOptions: undefined,
    context: Vue實例,
    data: {attrs: {id: "app"}},
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "div",
    text: undefined,
    child: undefined
}
複製代碼

在patch過程當中,createElm函數建立元素

function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
    ...
    vnode.isRootInsert = !nested; // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }
    ...
}
複製代碼

對於div元素,createComponent函數返回false,button-counter組件則會執行如下代碼:

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)) {
            i(vnode, false /* hydrating */);
        }
        // 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.
        ...
    }
}
複製代碼

只有組件data屬性中才具備鉤子函數,子組件初始化以下:

var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
      if (
        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(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    }
複製代碼

根據組件Vnode建立組件實例

function createComponentInstanceForVnode (
    vnode, // we know it's MountedComponentVNode but flow doesn't
    parent // activeInstance in lifecycle state
  ) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // check inline-template render functions
    var inlineTemplate = vnode.data.inlineTemplate;
    if (isDef(inlineTemplate)) {
      options.render = inlineTemplate.render;
      options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    return new vnode.componentOptions.Ctor(options)
  }
複製代碼

new vnode.componentOptions.Ctor(options) 其中options爲

{
    parent: id爲app的Vue實例,
    _isComponent: true,
    _parentVnode: 父組件中button-counter組件的Vnode
}
複製代碼

新建子組件實例

新建子組件button實例,進入到了

var Sub = function VueComponent (options) {
    this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
複製代碼

子組件的構造函數繼承了父組件。接着初始化子組件的參數

initInternalComponent(vm, options);
複製代碼

合併父子組件的參數後vm.$options參數爲

{
    parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}.
    propsData: {countTotal: 0},
    _componentTag: "button-counter",
    _parentListeners: {increment: ƒ},
    _parentVnode: VNode {tag: "vue-component-1-button-counter", data: {…}, children: undefined, text: undefined, elm: undefined, …},
    _renderChildren: undefined
}
複製代碼

獲取props值

button子組件的參數爲

{
    components: {button-counter: ƒ}
    data: ƒ (),
    directives: {},
    filters: {},
    methods: {incrementCounter: ƒ},
    name: "button-counter",
    props: {
        countTotal: {type: ƒ, default: 0}
    },
    template: "<button v-on:click="incrementCounter">{{ count }}</button>",
    _Ctor: {0: ƒ},
    _base: ƒ Vue(options)
}
複製代碼

button子組件會在原型上繼承該對象,button子組件爲

{
    parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
    propsData: {countTotal: 0},
    _componentTag: "button-counter",
    _parentListeners: {increment: ƒ},
    _parentVnode: VNode {tag: "vue-component-1-button-counter", data: {…}, children: undefined, text: undefined, elm: undefined, …},
    _renderChildren: undefined,
    __proto__: Object // 繼承上面的對象
}
複製代碼

上面的參數通過初始化props

if (opts.props) { 
    initProps(vm, opts.props); 
}
複製代碼

具體爲

var propsData = vm.$options.propsData || {};
    var props = vm._props = {};
    // cache prop keys so that future props updates can iterate using Array
    // instead of dynamic object key enumeration.
    var keys = vm.$options._propKeys = [];
    var isRoot = !vm.$parent;
    // root instance props should be converted
    // root實例的props屬性應該被轉成響應式數據
    if (!isRoot) {
      toggleObserving(false);
    }
      // static props are already proxied on the component's prototype // during Vue.extend(). We only need to proxy props defined at // instantiation here. if (!(key in vm)) { proxy(vm, "_props", key); } }; toggleObserving(true); } 複製代碼

規格化後的props從其父組件傳入的props數據中或者使用new建立的propsData參數中,篩選出須要的數據保存在vm._props中,而後在vm上設置一個代理,經過vm.x訪問vm._props.x。

button組件的解析

button子組件根據編譯器解析生成的render函數爲:

with(this){return _c('button',{on:{"click":incrementCounter}},[_v(_s(counter))])}
複製代碼

this指向button-counter組件的構造函數,接着button子組件會調用$mount函數,進行模板解析,生成Vnode

{
    asyncFactory: undefined,
    asyncMeta: undefined,
    children: [VNode],
    componentInstance: undefined,
    componentOptions: undefined,
    context: button-counter子組件的構造函數,
    data: {on: {click: f}},
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "button",
    text: undefined,
    child: undefined
}
複製代碼

context表示父組件中button-counter組件的構造實例,接着進入patch過程

// 此時oldVnode爲空
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
}
複製代碼

再次進入createElm函數的createComponent函數,此時vnode.data中是不存在鉤子函數的,故 能夠直接跳過這個函數,遞歸將button組件的子元素掛載到button元素上來即vnode.elm,最後返回給vm.$el,子組件的建立過程結束,因而又再次來到createComponent函數,進行組件的初始化

if (isDef(vnode.componentInstance)) {
    initComponent(vnode, insertedVnodeQueue);
    insert(parentElm, vnode.elm, refElm);
    if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
    }
    return true
}
複製代碼

並返回true,即createElm函數return結束,此時id=app的節點已建立完畢,最後掛載到其父節點body上 ,代碼以下

if (isDef(data)) {
    invokeCreateHooks(vnode, insertedVnodeQueue);
}
    insert(parentElm, vnode.elm, refElm);
...
// destroy old node
if (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode);
}
複製代碼

刪除以前的老節點後,整個父子組件的渲染結束。

事件觸發

button組件初始化事件initEvent函數爲

function initEvents (vm) {
    vm._events = Object.create(null);
    vm._hasHookEvent = false;
    // init parent attached events
    var listeners = vm.$options._parentListeners;
    if (listeners) {
      updateComponentListeners(vm, listeners);
    }
}
複製代碼

此時vm.$options._parentListeners爲:

{
    increment: ƒ ()
}
複製代碼

繼續執行

function updateComponentListeners (
    vm,
    listeners,
    oldListeners
  ) {
    target = vm;
    updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
    target = undefined;
}
複製代碼

updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm)這個過程當中由target.$on(event, fn)註冊了函數,$on函數爲:

Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
    for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
    }
    } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn);
    // optimize hook:event cost by using a boolean flag marked at registration
    // instead of a hash lookup
    if (hookRE.test(event)) {
        vm._hasHookEvent = true;
    }
    }
    return vm
};
複製代碼

將監聽函數push到vm._events數組中,再點擊按鈕,觸發$emit函數

Vue.prototype.$emit = function (event) {
      var vm = this;
      var cbs = vm._events[event];
      if (cbs) {
        cbs = cbs.length > 1 ? toArray(cbs) : cbs;
        var args = toArray(arguments, 1);
        var info = "event handler for \"" + event + "\"";
        for (var i = 0, l = cbs.length; i < l; i++) {
          invokeWithErrorHandling(cbs[i], vm, args, vm, info);
        }
      }
      return vm
    };
複製代碼

將事件監聽器從vm._events中取出,賦值給cbs,若cbs存在,則循環它,依次調用每個監聽器回調,並將全部參數傳遞給監聽器回調。

相關文章
相關標籤/搜索