概述
社区 vue 基础教程用到自定义的 v-validator 指令, 首次加载表单登录或注册页,都需要点击两次才能正常跳转
对该问题,本文提供了两个解决办法,一个是 settimeout,另一个则利用 js 事件流原理,仅仅改变模板提交事件的绑定节点就可以了。
使用 setTimeout 延迟调用
之前方案
vue 最新版 2.6.10,用登录或注册方法,也隐式调用了验证指令中绑定的点击事件处理逻辑,而 nextTick 回调函数块内,有依赖该指令处理逻辑的代码 canSubmit。只要 nextTick 回调函数体内的代码在指令事件逻辑调用之后执行,就不需要点击两次了。例 如,用 setTimeout 建个宏任务 (目的让验证指令内中的事件绑定逻辑先执行),代码如下register(e) {
this.$nextTick(() => { const target = e.target.type === "submit" ? e.target : e.target.parentElement; setTimeout(() => { if (target.canSubmit) { this.submit(); } }); }); }
分析
开始的时候也认为与 nexTick 有关,查阅了大量资料,发现 vuejs 的 nexttick ,与 nodejs 中的 nextick 理念一致的,都是在下一次宏任务事件循环中,先处理掉当批次的微任务,再接着干,没毛病。vuejs 中的 nextick 存在的意义在于,确保其回调内的逻辑是在调用者对应的节点渲染完毕之后,再执行
虽然上述方案,也解决了两次点击的问题,但始终不优雅。方案很难让人信服,问题源自点击事件,理应从事件流的角度去解决,而非 setTimeout。
事件绑定
自定义的 validator 指令,在 v-validator 被绑定元素插入父节点时,会调用该指令的钩子函数 inserted。该钩子内,会寻找表单内 [type=submit] 元素节点,即提交按钮 button,并在该按钮上动态绑定了监听器
const submitBtn = form ? form.querySelector('[type=submit]') : null if (submitBtn) { const submitHandler = () => { validate(el, modifiers, value) const errors = form.querySelectorAll('.has-error') if (!errors.length) { submitBtn.canSubmit = true } else { submitBtn.canSubmit = false } } submitBtn.addEventListener('click', submitHandler, false) ....
以登录组件,提交按钮为例,显然也绑定了一个 click 事件
<button @click="login" type="submit" class="btn btn-lg btn-success btn-block"> <i class="fa fa-btn fa-sign-in"></i> 登录 </button>
使用了 v-validator 自定义指令的表单,在执行验证规则时,会隐式的在表单 button 提交按钮上添加点击事件
该点击事件内,会在表单验证不通过时,给 button 节点对象添加一个 canSubmit 标识 ,一个布尔值。
而该 canSubmit 标识 值是模板上绑定的 vue 点击 @click=“login" 事件,能否正常提交表单的条件
成因
- 提交 button 元素节点上,同时注册有两个点击事件,一个隐式的指令事件,一个模板绑定的 @click 事件
- 后者的处理逻辑依赖于前者,因此正常情况下,二者要有序,必须是先指令事件再模板事件
但对于教程代码而言其,同元素 button 上 click 类型的可能存在两个点击事件监听器,这样首次加载组件,总是先绑定了模板点击事件,验证指令在 button 上的点击事件监听器只在 input 输入验证存在错误时注册 , 这样 button 点击
- 第一次点击时,模板监听执行,canSubmit 布尔值不存在(false) ---> 验证指令监听执行,button 添加 cansubmit 标识。
- 第二次点击时,模板监听执行 由于第一次 cansubmit 已添加(若验证通过它值是 true), 执行表单提交
对于 dom2 级事件 addEventListener 添加,其本身节点的事件处理逻辑由注册顺序所决定,即对同一节点元素上绑定的多个同类事件,触发该节点事件,同类事件逻辑会依次执行。示例代码如下
<button id="myBtn">点我</button> <script> var x = document.getElementById("myBtn"); x.addEventListener("click", myFunction); x.addEventListener("click", someOtherFunction); function myFunction() { alert ("Hello World!") } function someOtherFunction() { alert ("函数已执行!") } </script>
- 同一节点一次点击,依次弹出 Hello World! ---> 函数已执行!
原理 事件流
向下捕获 --> 目标事件 ---> 向上冒泡
- 由成因可知,若都在 button 上注册点击事件,显然模板上注册的点击事件监听器肯定是第一个执行,需要点击两次是必然。因此,利用事件流特点,分开绑定才是正常解决方案,让 button 的验证指令点击事件冒泡,在他的上层节点捕获执行表单提交事件回调即可
- 既然顺序,如此重要,改变模板同节点上的事件布局,就可以实现事件有序执行。
推荐方案
- 仅仅只需要将模板提交按钮上的点击事件绑定到提交按钮的父节点上!!!
- 将模板上绑定的按钮提交点击事件,绑定在 [type="submit"] 父节点上
这样当点击登录时,产生点击事件,冒泡顺序,会先触发 button 按钮上的点击事件,先调用执行按钮上的隐式指令点击事件逻辑得到 canSubmit 值,后调用冒泡父节点 span 上的 click 事件,执行 login 回调。
<span @click="login"> <button type="submit" class="btn btn-lg btn-success btn-block"> <i class="fa fa-btn fa-sign-in"></i>登录 </button> </span>
上述增加无语义 span 标签,可能会带来一些副作用,破坏已有的 dom 结构。通常作法是,在符合条件的既有 Dom 节点元素上进行,比如将 login 提交事件逻辑绑定在表单 form 节点上。
小结
- 对于 vuejs 组件模板节点绑定的事件,在当前节点的同类事件中,模板绑定回调最先注册到当前队列
- 同样的在编辑个人用户资源时,首次进入该页保存也可以用同样的方法解决
补充
在看 react,有所悟,重读了 vue 事件系统 api。不同于原生的 js 事件源绑定,vue 和 react 的事件,本质上都是用了事件代理,即在当前根节点上代理监听内部事件,然后根据不同的事件源,作执行不同的回调。
© 著作权归作者所有
相关热门文章
发表评论