前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3源码阅读笔记之$emit实现

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

原创
作者头像
wanyicheng
修改2021-04-14 10:09:58
2.8K0
修改2021-04-14 10:09:58
举报
代码语言:javascript
复制
// 问题:在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中从父组件传进来的一个箭头函数。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档