7. Vue中的父子组件间数据通信

356 阅读4分钟

准备工作

为了探究父子组件的数据传递以及事件触发, 父组件选用模板:

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

子组件选用模板

<button v-on:click="incrementCounter">{{ counter }}</button>

父组件通过props传递参数count到子组件,子组件通过incrementCounter函数中的$emit触发父组件的incremenTotal函数,具体过程如下:

如何进行参数传递

button-counter组件中props传递的值,会在genData$2中经过genProps函数处理生成

"{"count-total":count}"

绑定的事件会经过genHandler函数处理生成

on:{"increment":incremenTotal}

所以生成的render函数为

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

根据前面的编译器原理生成完整的render函数为:

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

调用渲染函数

vnode = render.call(vm._renderProxy, vm.$createElement);

调用渲染函数,先创建子组件Vnode

[_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); },执行

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存在,则循环它,依次调用每一个监听器回调,并将所有参数传递给监听器回调。