为了 Vue 组件测试,你需要为每个事件绑定的方法加上括号吗?

VueJS简明教程(一)之基本使用方法 简介:这是一篇超级简单的入门文章 如果说是JQuery是手工作坊,那么Vue.js就像是一座工厂,虽然Vue.js做的任何事情JQuery都可以做,但无论是代码量还是流程规范性都是前者较优。 Vue.js的官方中文教程其实也是一个不错的教程,不过相比于一次性把所有概念掌握,我更倾向于先会用,之后再在实际应用中把未涉及到的知识点逐步补全。 就像开车,不是非要知道发动机的工作原理才能上路... 阅读详情

本文由华为云体验技术团队松塔同学分享

先说结论,当然不是!Vue 组件测试,尤其是组件触发事件的测试,有成熟的示例。我们同样要关注测试的原则,例如将组件当成黑盒,不关心其内部实现,而只关心与其交互。本文是借由一次 Vue 组件测试,引发对 Vue 源码和 Spy 函数的延伸探讨。

假设你写了一个 Vue 组件,它大概长这样:

<MyComponent
  :disabled="!valid"
  :data="someTestData"
  @confirm="handleConfirm"
/>

它定义了data和disabled作为 props,前者作为组件的数据输入,后者用来定义组件的功能开关。组件被点击时,会抛出confirm事件,不过当disabled为true时,confirm事件不会被触发。

当你想为这个组件写一些单元测试时,可能会这样写:

describe('MyComponent on the page', () => {
  // ...
  it('confirm event', async () => {
    const instance = wrapper.findComponent({ name: 'MyComponent' })
    const spy = vi
      .spyOn(wrapper.vm, 'handleConfirm')
      .mockImplementation(() => null)
    await instance.trigger('click')
    expect(spy).not.toHaveBeenCalled()
    // ... change valid
    await instance.trigger('click')
    expect(spy).toHaveBeenCalledTimes(1)
  })
})

valid初始化时为false,即MyComponent一开始不会抛出confirm事件,当valid被改变后,点击MyComponent,confirm事件才被抛出。

这段单元测试会在最后一句报错,显示spy实际被触发 0 次。实际上,spy永远不会被触发,即使valid初始化时为true也是如此。

然而,将模板里的方法调用调整一下,加上括号,单元测试就按照预期通过了:

<MyComponent
  :disabled="!valid"
  :data="someTestData"
  @confirm="handleConfirm()"
/>

为什么加不加括号会引起单元测试的逻辑变化?

模板语法

首先我们需要看一看模板在编译时,处理@confirm="handleConfirm()"和@confirm="handleConfirm"有什么不同。

从@vue/compiler-sfc的compileTemplate方法开始一路往下分析,会发现模板编译的核心方法是@vue/compiler-core这个包中的baseCompile方法。这个方法主要干三件事:

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  // ...
    
  // 1. 生成基础ast
  const ast = isString(template) ? baseParse(template, options) : template
  
  // ...
  
  // 2. 对ast做转换
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )
  // 3.生成渲染函数
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}
  1. 调用baseParse方法解析 HTML,生成基础的 AST。由于 Vue 在 HTML 上增加了许多语法特性(v-if、v-for、v-bind 等等),需要做对应解析。
<div @click="handleConfirm()" /> 生成的 AST<div @click="handleConfirm" /> 生成的 AST
11.18图1.png11.18图2.png

查看生成的 AST 结构后可以发现,加不加括号对结构并不会产生影响。二者都生成了 v-on 的 prop,exp中的 content 未对原始内容做出改动。

  1. 进一步对 AST 做解析和转换。这一步引入了nodeTransforms和directiveTransforms对象,其实是在./transforms目录下的一系列函数:
export function getBaseTransformPreset(
    prefixIdentifiers?: boolean
): TransformPreset {
    return [
        [
            transformOnce,
            transformIf,
            transformMemo,
            transformFor,
            ...(__COMPAT__ ? [transformFilter] : []),
            ...(!__BROWSER__ && prefixIdentifiers
                ? [
                    // order is important
                    trackVForSlotScopes,
                    transformExpression
                ]
                : __BROWSER__ && __DEV__
                    ? [transformExpression]
                    : []),
            transformSlotOutlet,
            transformElement,
            trackSlotScopes,
            transformText
        ],
        {
            on: transformOn,
            bind: transformBind,
            model: transformModel
        }
    ]
}

光从名字就可以看出来,依旧是对 Vue 的语法特性做的一些工作,最终在 AST 的每个节点上增加codegenNode,这个属性将会被用在第三步生成渲染函数过程中。经过 transform 这一步后,生成的codegenNode如下:

<div @click="handleConfirm()" /> 的 codegenNode<div @click="handleConfirm" /> 的 codegenNode
11.18图3.png11.18图4.png

二者 prop 中的 value 值有所差异,type 是 typescript 定义的 enum,编译后变成了数字,还原后前者的类型从SIMPLE_EXPRESSION变成了COMPOUND_EXPRESSION,后者仍保持之前的SIMPLE_EXPRESSION。

造成二者差异的原因,需要深入transformOn这个对 v-on 语法转换的方法。它根据 AST 节点的exp和arg,生成codegenNode中props下的属性。简化一下有关exp的逻辑,核心代码如下:

const isMemberExp = isMemberExpression(exp.content, context)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
const hasMultipleStatements = exp.content.includes(`;`)
if (isInlineStatement || (shouldCache && isMemberExp)) {
    // wrap inline statement in a function expression
    exp = createCompoundExpression([
        `${isInlineStatement
            ? !__BROWSER__ && context.isTS
                ? `($event: any)`
                : `$event`
            : `${!__BROWSER__ && context.isTS ? `\n//@ts-ignore\n` : ``}(...args)`
        } => ${hasMultipleStatements ? `{` : `(`}`,
        exp,
        hasMultipleStatements ? `}` : `)`,
    ]);
}

首先对exp做判断,是否是 member expression、是否是 inline statement,是否有多个 statement。然后出现了exp的改写,根据判断生成了 compound expression,实际就是转换成了函数表达。看来isMemberExp、isInlineStatement这两个判断影响了最终codegenNode的生成。

Member Expression

这是个来源于 AST 定义的概念,JavaScript 中经常有对象属性的指向,例如:

const a = { x: 0 }
const b = a.x

这里a.x就是 member expression,transformOn中调用isMemberExpression来做判断,实际就是调用 babel parser 的能力分析,简化来说:

try {
    let ret: Expression = parseExpression(path, {
        plugins: context.expressionPlugins,
    });
    if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') {
        ret = ret.expression;
    }
    return (
        ret.type === 'MemberExpression' ||
        ret.type === 'OptionalMemberExpression' ||
        ret.type === 'Identifier'
    );
} catch (e) {
    return false;
}

这里 MemberExpression、OptionalMemberExpression、Identifier 都被认定成了 member expression。OptionalMemberExpression 即带有 optional chaining (?.) 的表达式。Identifier 也被包括的原因是,在模板中一般会省略主对象,如 this、或者 setup 中返回的对象。

<div @click="handleConfirm" />中handleConfirm就是 Identifier,它指向的就是我们在 script 中定义的函数。

isInlineStatement的判断中还出现了一个条件fnExpRE.test(exp.content),这是函数表达式的正则判断:

11.18图5.png

虽然直接在模板里声明函数很罕见,但是 Vue 并没有限制这种做法。

那exp如果既不是 member expression,也不是函数表达式,transformOn就把它当作 inline statement。实际上这是我们在日常使用时比较常见的作法,例如只是简单对变量赋值,那就无需在<script>中声明函数,而是简写为:

<MyComponent
    : disabled= "!valid"
    : data= "someTestData"
    @confirm ="hasConfirmed = $event"
/>

而让这段代码生效的原因,就在于transformOn编译时将exp包裹了一层函数声明。它调用createCompoundExpression,将$event 作为函数入参,从而使函数内能获取到:

($event) => (hasConfirmed = $event)
  1. 由上一步生成的codegenNode,转换成最终的渲染函数。重点看一下带括号的表达式生成的渲染函数:
const _Vue = Vue

return function render(_ctx, _cache) {
    with (_ctx) {
        const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

        return (_openBlock(), _createElementBlock("div", {
            onClick: $event => (handleConfirm())
        }, null, 8 /* PROPS */, ["onClick"]))
    }
}

with statement 是在模板中可以省略 this 的原因。

对比

将以上分析做一个总结,我们可以将编译后结果简化一下,那么带括号的函数表达:

const ctx = { handleConfirm: () => null }
const prop = { onClick: ($event) => { ctx.handleConfirm() } }

不带括号的函数表达:

const ctx = { handleConfirm: () => null }
const prop = { onClick: ctx.handleConfirm }

Mock Function

我们已经搞清楚在编译阶段,带不带括号的函数表达有什么区别。接下来就要研究这个区别对于 Mock 行为产生了什么影响。Vitest 内部利用 tinyspy 来实现 mock 功能,本文并不会深入 tinyspy 的具体实现,因为 JavaScript spy 库大同小异,而背后的 JavaScript 语言特性才是本文真正想分享的。spy 函数的基本功能就是提供对目标函数的监视,例如执行次数,出入参等。一个函数在声明后,JavaScript 无法让我们二次修改它的内容,因此通常来说 spy 库会将原本函数的引用指向新的实现。一个简单的 spy 函数可以是这样:

function spyOn(obj, method) {
    let spy = {
        args: [],
    };

    let original = obj[method];
    obj[method] = function () {
        let args = [].slice.apply(arguments);
        spy.count++;
        spy.args.push(args);
        return original.call(obj, args);
    };

    return Object.freeze(spy);
}

它将object[method]指向了新的函数,首先更新函数执行的次数、记录每次执行的入参,然后用call执行原始函数。

对应到本文的例子中,当我们声明const spy = vi.spyOn(wrapper.vm, 'handleConfirm')后,wrapper.vm.handleConfirm就被指向了 spy 生成的新函数,这个改动针对的是 Vue 实例对象,而我们由模板编译生成的渲染函数仍保持不变。因此const prop = { onClick: ctx.handleConfirm }中onClick仍指向原始函数的引用,无论 handleConfirm 之后怎么改变,其在渲染函数生成后就从始至终不变了。而const prop = { onClick: ($event) => { ctx.handleConfirm() } }中ctx.handleConfirm()会在点击回调触发后解析,此时就会指向spyOn定义的新函数了。

总结

当搞清楚模板语法生成事件回调的逻辑后,我们就会发现这其实是一个经典的对象引用指向的问题。受限于 JavaScript 语言特性,mock 行为实际上创建了一个新的函数,而上下文若仍保持着对原函数的引用,那 mock 行为不会按照预期运行也就可以理解了。

当你想要测试组件是否正确地 emit,也许应该尝试@vue/test-utils中的emitted()方法。或者将视角拉得更高,从最终页面呈现的内容来判断。

关于 OpenTiny

图片

OpenTiny 是一套企业级 Web 前端开发解决方案,提供跨端、跨框架、跨版本的 TinyVue 组件库,包含基于 Angular+TypeScript 的 TinyNG 组件库,拥有灵活扩展的低代码引擎 TinyEngine,具备主题配置系统TinyTheme / 中后台模板 TinyPro/ TinyCLI 命令行等丰富的效率提升工具,可帮助开发者高效开发 Web 应用。


欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~更多视频内容也可关注B站、抖音、小红书、视频号

OpenTiny 也在持续招募贡献者,欢迎一起共建

OpenTiny 官网:https://opentiny.design/

OpenTiny 代码仓库:https://github.com/opentiny/

TinyVue 源码:https://github.com/opentiny/tiny-vue

TinyEngine 源码: https://github.com/opentiny/tiny-engine

欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

codemod-get-to-optional-member-expressions:将Lodash的get函数更改为ES7可选成员表达式 codemod获取到可选成员表达式 该存储库包含一个与一起使用的codemod脚本。 概述 codemod用对ECMAScript的本机替换对Lodash的方法的调用。 这是一种可靠访问深度嵌套对象属性的新颖方法,现在可以与。 重要的是要注意,这目前仅适用于Babel 7 。 如果使用的是Babel 6.x,则需要将项目更新为Babel 7才能使用此codemod。 const foo = get ( data , 'crate.box.present.wrapping.color' ) ; const bar = get ( data , 'crate.box.present.wrapping.color' , 'green' ) ; // :backhand_index_pointing_down: Becomes :backhand_index_pointing_down: const foo = data ?. crate ?. box ?. present ?. wrapping ?. co 立即下载

相关推荐

2021年J题:基于STM32F103的周期信号波形识别与参数测量系统设计(立创EDA+SZPT_ZXL)

本文详细复盘了2021年全国大学生电子设计竞赛J题获奖项目,手把手教你如何基于STM32F103微控制器设计并实现周期信号波形识别与参数测量系统。文章深入剖析了硬件电路设计(包括程控放大、工作点抬升与滞回比较器)与核心软件算法(如动态采样、FFT频谱分析与波形识别逻辑),并分享了DMA传输、FFT库使用等实战要点与避坑指南,为嵌入式信号处理与电赛备赛提供了宝贵经验。

weixin_32673065的博客 230

vue之事件绑定加不加括号

学了一段时间的小程序,有重新回过头来学习vue, 突然对时间绑定加不加括号有些疑问; 经过测试,不加括号也是可以的, 但是在如果需要传递参数的时候还是有一些区别, 如果没有加括号, 默认传递参数为 MouseEvent, 如果加了括号,则需要在括号中使用$event, 才能获取到MouseEvent ...

ZHXT__的博客 1017

保姆级教程:用CST 2023一步步复现一个1-8GHz Vivaldi天线(含参数优化与结果分析)

本文提供了一份详细的CST 2023教程,指导读者如何从零开始设计并优化1-8GHz Vivaldi天线。内容涵盖工程设置、参数化建模、仿真加速技巧以及性能优化策略,特别适合需要实现宽带天线设计的工程师和研究人员。通过实战案例,帮助用户掌握Vivaldi天线的核心设计要点和CST高级功能应用。

weixin_30800987的博客 619

Vue事件绑定,绑定方法带括号与不带括号的区别。

Vue事件绑定,绑定方法带括号与不带括号的区别。

fyx2823773469的博客 396

从零开始:用君正T23ZN打造智能门锁双摄系统(含代码示例与调试技巧)

本文详细介绍了基于君正T23ZN视频处理芯片构建智能门锁双摄系统的完整实战方案。内容涵盖硬件选型与核心电路设计、软件开发环境搭建、双摄驱动框架、图像信号处理(ISP)调优、人脸识别算法集成与性能优化,以及系统调试与稳定性保障。文章提供了丰富的代码示例和调试技巧,旨在帮助嵌入式开发者高效实现具备人脸识别与监控功能的低功耗智能门锁视觉系统。

opencv7vision的博客 716

【详细】芯片设计全流程(包括每个流程需要用到的工具以及需要参与的工作人员)

一颗芯片的诞生经历了设计、制造和测试(分别对应集成电路产业链的设计业、制造业和封测业),而每一步都包含了复杂的步骤和流程。 图0-1 集成电路产业链 现在,我们重点介绍芯片设计的全流程以及每个流程需要用到的设计工具和需要参与的工作人员。 一、IC设计分类 首先对IC设计的分类做个介绍,如图1-1所示。 图1-1 IC设计分类 如图1-1所示,IC设计可以粗分为数字IC设计和射频/模拟IC设计,数字IC设计又可分为ASIC设计和FPGA/CPLD设计,此外还有一部分IC设计.

qq_36045093的博客 8万+

Vue学习记录20

本文介绍了Vue中的单文件组件,工具链,状态管理等基础概念。

cwtlw的博客 846

C语言实现TCP聊天室:Socket编程与并发模型实战解析

网络编程是构建分布式系统的核心技术,其核心在于理解进程间通信的底层机制。TCP/IP协议栈提供了可靠的字节流传输服务,通过Socket API这一抽象接口,开发者可以在应用层建立连接、收发数据。为了高效处理多个并发连接,服务器需要采用多线程或I/O复用模型,前者为每个连接创建独立线程,逻辑清晰但资源消耗大;后者通过select、poll或epoll等系统调用监控多个文件描述符,显著提升并发能力。这些技术是构建高性能网络服务(如即时通讯、在线游戏服务器)的基础。本文以C语言实现一个简洁的TCP聊天室为例,详细

weixin_32642109的博客 292

JS调用函数加括号和不加括号的区别 JS调用函数加()不加()的区别

区别: 不加括号的: 会等待事件触发再调用函数。 加括号的:不会等待事件触发直接调用函数。 实验: html <button class="btn btn-default" id="btn">确定</button> js function text(){ console.log(321); } $('#btn').on('click',text()) ...

QIingBo的博客 660

Spring AI Alibaba Graph进阶:如何用图算法优化你的推荐系统?

本文深入探讨了如何利用Spring AI Alibaba Graph的图算法优化推荐系统。通过分析用户行为数据的网络化特征,结合加权随机游走和社区发现算法,显著提升推荐精准度和用户点击率。文章还分享了混合推荐架构设计、性能优化技巧及生产环境中的实战经验,为开发者提供了一套完整的图算法推荐解决方案。

weixin_33738555的博客 128

STM32实战:如何快速移植野火PID调试助手协议到你的电机控制项目

本文详细解析了如何将野火PID调试助手协议快速移植到STM32电机控制项目中。通过深度剖析协议帧格式、数据解析逻辑及环形缓冲区应用,提供了从工程结构规划、核心函数适配到串口中断集成的完整移植步骤。文章还分享了协议健壮性增强、数据上报频率优化等高级调试技巧,帮助开发者构建稳定可靠的上位机调试通道,大幅提升PID参数整定效率。

weixin_29171129的博客 468

【Vue3 第二十七章】路由和状态管理

Vue 组件本身其实已经实现了对响应式状态的管理。如果你只需要一个简单的页面路由,而不想为此引入一整个路由库,你可以通过动态组件的方式,监听浏览器 hashchange 事件或使用 History API 来更新当前组件。Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。- 为 Vue.js 提供富有表现力、可配置的、方便的路由。

前端知识分享喵 1017

MIKE水动力笔记7_实测数据与模型输出结果的拟合对比

上一博文【MIKE水动力笔记6】主要讲解如何自己制作实测数据的时间序列文件dfs0,而本博文讲解如何进行实测数据与模型输出结果的拟合对比。

晏长街的博客 1万+

vue中事件绑定函数加括号和不加括号的区别

html代码 <div id="example-2"> <button @click="test">测试</button> </div> js代码 new Vue({ el: '#example-2', data(){ return { } }, methods: { test(id){ cons...

sinat_24918465的博客 6384

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

vue 事件绑定函数加括号和不加括号的区别

自定义事件加不加括号的区别 不加括号: <div> <button @click="test">测试</button> </div> test(id){ console.log(id); } 加括号: <div> <button @click="test()">测试</button> </div> test(id){ console.log(id); } 添加括号传值

weixin_56008510的博客 3330

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

vue之@click绑定的函数,如何实现加不加括号都可执行

vue@click 事件原理

yummry的博客 6017

学习Vue基本使用方法和组件

学习Vue基本使用方法和组件

李长渊的博客 520

Vue 方法中 $event 的用法

Vue 方法中 $event 的用法

猎人在吃肉 6537

vue事件绑定及传参相关(事件修饰符相关)

在vue中如果我们想处理事件绑定,可以使用v-on指令来进行事件的绑定 如下: <button v-on:click="handleClick1">默认传递事件对象</button> <button @click="handleClick2(1,2,$event)">传参及传递事件对象</button> 对应的参数接收的方式如下: methods:{ handleClick1(eve){ console..

木易的博客 848

js或者vue中点击触发事件函数,事件加括号与不加括号的区别

不加括号 <button @click="test">测试</button> test(a) { console.log(a); }, 输出指针事件:例如返回当事件被触发时鼠标指针相对于浏览器页面(或客户区)的水平坐标。 加括号 <button @click="test()">测试</button> test(a) { console.log(a); }, 输出undefined:函.

q1471729313的博客 894
上一篇: 专访|OpenTiny 社区 Mr 栋:结合兴趣,明确定位,在开源中给自己一些技术性挑战
下一篇: 原来 TinyVue 组件库跨框架(Vue2、Vue3、React、Solid)是这样实现的?
OpenTiny社区
博客等级 码龄4年 1540粉丝 266原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值