专栏首页纸上得来终觉浅Vue3源码阅读笔记之$emit实现
原创

Vue3源码阅读笔记之$emit实现

// 问题:在vue子组件内部使用的方法中调用 this.$emit('some-event', ...args) 是如何触发父组件的方法呢?

// 先看下 this是什么:组件实例对象的proxy属性
// proxy属性时什么:组件实例对象ctx属性的代理
// 看下代理返回啥
/**
 * const publicPropertiesMap = extend(Object.create(null), {
        $: i => i,
        $el: i => i.vnode.el,
        $data: i => i.data,
        $props: i => (shallowReadonly(i.props) ),
        $attrs: i => (shallowReadonly(i.attrs) ),
        $slots: i => (shallowReadonly(i.slots) ),
        $refs: i => (shallowReadonly(i.refs) ),
        $parent: i => getPublicInstance(i.parent),
        $root: i => getPublicInstance(i.root),
        $emit: i => i.emit,
        $options: i => (resolveMergedOptions(i) ),
        $forceUpdate: i => () => queueJob(i.update),
        $nextTick: i => nextTick.bind(i.proxy),
        $watch: i => (instanceWatch.bind(i) )
    }); 

    而实例的emit在哪赋值的呢?
    createComponentInstance方法中: instance.emit = emit.bind(null, instance);

    看下emit的实现:
 */

// 打个断点 调用依次$emit就可以清晰看明白下面的逻辑了
function emit(instance, event, ...rawArgs) {
    const props = instance.vnode.props || EMPTY_OBJ;
    {
        // emitsOptions 放的是 在子组件声明的emits选项
        // propsOptions 放的是 在子组件声明的props选项
        const { emitsOptions, propsOptions: [propsOptions] } = instance;
        // 校验用户要触发的事件名字要在传入的参数范围内
        if (emitsOptions) {
            if (!(event in emitsOptions)) {
                if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
                    warn(`Component emitted event "${event}" but it is neither declared in ` +
                        `the emits option nor as an "${toHandlerKey(event)}" prop.`);
                }
            }
            else {
                // 对应文档中描述的 事件验证功能
                const validator = emitsOptions[event];
                if (isFunction(validator)) {
                    const isValid = validator(...rawArgs);
                    if (!isValid) {
                        warn(`Invalid event arguments: event validation failed for event "${event}".`);
                    }
                }
            }
        }
    }
    // emit调用的用户参数
    let args = rawArgs;
    const isModelListener = event.startsWith('update:');
    // for v-model update:xxx events, apply modifiers on args
    const modelArg = isModelListener && event.slice(7);
    // v-model内置的2个处理方法 格式化参数
    if (modelArg && modelArg in props) {
        const modifiersKey = `${modelArg === 'modelValue' ? 'model' : modelArg}Modifiers`;
        const { number, trim } = props[modifiersKey] || EMPTY_OBJ;
        if (trim) {
            args = rawArgs.map(a => a.trim());
        }
        else if (number) {
            args = rawArgs.map(toNumber);
        }
    }
    {
        // 忽略
        devtoolsComponentEmit(instance, event, args);
    }
    {
        // 事件名字格式 some-event toHandlerKey: => onChange 之类的格式
        const lowerCaseEvent = event.toLowerCase();
        if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) {
            warn(`Event "${lowerCaseEvent}" is emitted in component ` +
                `${formatComponentName(instance, instance.type)} but the handler is registered for "${event}". ` +
                `Note that HTML attributes are case-insensitive and you cannot use ` +
                `v-on to listen to camelCase events when using in-DOM templates. ` +
                `You should probably use "${hyphenate(event)}" instead of "${event}".`);
        }
    }
    // convert handler name to camelCase. See issue #2249
    let handlerName = toHandlerKey(camelize(event));
    // 我们在使用组件的时候按照 : @some-event="parent-cb" 那在子组件的prop就会得到对应的 {someEvent: parent-cb(已绑定好父组件的this) }这样的属性
    let handler = props[handlerName];
    // for v-model update:xxx events, also trigger kebab-case equivalent
    // for props passed via kebab-case
    if (!handler && isModelListener) {
        handlerName = toHandlerKey(hyphenate(event));
        handler = props[handlerName];
    }
    // 执行函数即可
    if (handler) {
        callWithAsyncErrorHandling(handler, instance, 6 /* COMPONENT_EVENT_HANDLER */, args);
    }
    // once类型的缓存操作 只执行一次
    const onceHandler = props[handlerName + `Once`];
    if (onceHandler) {
        if (!instance.emitted) {
            (instance.emitted = {})[handlerName] = true;
        }
        else if (instance.emitted[handlerName]) {
            return;
        }
        callWithAsyncErrorHandling(onceHandler, instance, 6 /* COMPONENT_EVENT_HANDLER */, args);
    }
}

// 上面的函数用到的 emitsOptions 就是被下面的函数解析出来的
function normalizeEmitsOptions(comp, appContext, asMixin = false) {
    if (!appContext.deopt && comp.__emits !== undefined) {
        return comp.__emits;
    }
    const raw = comp.emits;
    let normalized = {};
    // apply mixin/extends props
    let hasExtends = false;
    if (!isFunction(comp)) {
        const extendEmits = (raw) => {
            hasExtends = true;
            extend(normalized, normalizeEmitsOptions(raw, appContext, true));
        };
        // 合并minxins中的emits
        if (!asMixin && appContext.mixins.length) {
            appContext.mixins.forEach(extendEmits);
        }
        // 合并extends选项
        if (comp.extends) {
            extendEmits(comp.extends);
        }
        if (comp.mixins) {
            comp.mixins.forEach(extendEmits);
        }
    }
    if (!raw && !hasExtends) {
        return (comp.__emits = null);
    }
    if (isArray(raw)) {
        // 当前组件的emits选项
        raw.forEach(key => (normalized[key] = null));
    }
    else {
        // 当前组件的emits选项
        extend(normalized, raw);
    }
    return (comp.__emits = normalized);
}

// 总结一下:在子组件内部调用 $emit 其实就是触发props中的函数,这个函数的this早就在父组件的创建过程中绑定好this了,作为一个属性被传递给了子组件,子组件直接调用即可。

总结:组件实例上的 emit 方法其实就是调用props中从父组件传进来的一个箭头函数。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Vue3源码阅读笔记之组件是如何实现

    wanyicheng
  • Vue3源码阅读笔记之vnode定义

    wanyicheng
  • Vue3源码阅读笔记之事件队列

    总结一下:vue中的事件队列分3种,vue内部实现中主要是把render函数的包裹体effect放到queue队列中。

    wanyicheng
  • Vue3源码阅读笔记之异步组件

    wanyicheng
  • Vue3源码阅读笔记之数据响应式

    总结:Vue3中的数据响应式实现是一个较为独立的实现,适合单独分析学习哈。上文是删除了部分支线逻辑的版本,只保留了主线逻辑,大家如果想看完整的实现,还是建议去读...

    wanyicheng
  • Vue3 源码解析(九):setup 揭秘与 expose 的妙用

    在前几篇文章中我们一起学习了 Vue3 中新颖的 Composition API,而今天笔者要带大家一起看一下 Vue3 中的另一个新鲜的写法 —— setup...

    Originalee
  • 带你体验Vue2和Vue3开发组件有什么区别

    我们一直都有关注和阅读很多关于Vue3的新特性和功能即将到来。但是我们没有一个具体的概念在开发中会有如何的改变和不一样的体验。还有一些童鞋已经开始又慌又抓狂了 ...

    三钻
  • Vue3源码阅读笔记之整体执行顺序简介(1)

    从Vue官网得到源码(https://unpkg.com/vue@next),拷贝到本地文件,然后创建如下html:

    wanyicheng
  • Vue3源码阅读笔记之整体执行顺序简介(2)

    可以看到,目前只是直接对组件实例的data做了一次代理,handlers在普通对象情况下为 baseHandlers

    wanyicheng

扫码关注云+社区

领取腾讯云代金券