Vue 组件 v-model 等价展开、computed 和 watch 三种写法谁更优雅?

书接上文 ref 还是 reactive,这是个问题

先介绍下我踩到的坑。 简单说,就是有一个输入框,即可以是其他组件联动改变输入框内容,也可以是用户直接输入改变输入框内容。

当时的我使用的是 watch 实现,突然点亮了无限循环,直接卡吧死机了。

有点慌,实际的工业项目中组件数量和交互逻辑很多,光打日志就查了半天,因为对 Vue 不熟,所以遇到问题时找到了诸多怀疑点,理解不透经验不够,就只能加班逐个排除,耗时费力。

眼瞅着需求要上线了,时不我待,只能先绕坑,临时采用了在 watch 里面加了些逐个考虑的分支判断,以及节流处理,才勉强压制住了无限循环。

现在腾出空来,得好好研究一下,免得下次快乐工作时又要慌张地加班填坑。

这篇文章的目的是找出 v-model 无限循环 的原因,以及探索组件 v-model 最佳实践写法

结论先行:

  1. 无限循环 的罪魁祸首是修改监听数据时直接重新赋值了新对象,而不是在原对象上改。

    举个栗子,如果监听对象 obj 数据为 {x: 123},现在要修改 obj.x 这个数据,我是这么做的,直接 obj = {x: 123},其实数据内容没变,但操作后 oldObj !== newObj ,进而触发 watch 监听,然后点亮潜在的无限循环。

    正确写法是 obj.x = 123,操作后数据没变,即 oldObj === newObj && oldObj.x === newObj.x ,不会触发 watch 监听调用,自然也不会有后面"一个bug改一天"的事了。

    所以,修改 watch 监听对象一定要注意了,只能改原对象。

  2. v-model 最佳实践写法是 官网推荐 v-model 实现方式二,"使用具有 getter 和 setter 属性的可写 computed",我当时第一眼就觉得"专业",今天再仔细一瞅,还"优雅"。

    1. "使用等价展开 :value 和 @input"虽然是官网推荐 v-model 实现方式一,但是得自己多写点代码,而且还嵌套在模版里面,看起来有点费劲,"不优雅"。

    2. "使用 innerValue 和 watch"是官网未推荐但用得比较多的 v-model 实现方式,这种从逻辑上比较好理解,但从逻辑上也会意识到有潜在的无限循环隐患。

      虽然看起来相比上面两种省去了一次局部的 Diff 算法的开销(咋看出来的文末有图),但是也引入了额外的 innerValue 变量,新带来了该变量的 watch 开销,但真正埋雷的是增加了双份数据,极易产出数据不一致问题。

如果对 Vue 源码理解已经在我这层(我称之为第二层)之上的大佬,对上述结论没有异议或者觉得有点扯,仍然可以当段子继续看。

书归正传,把我踩的坑抽象成最简单的题》

"有两个输入框,第一个输入框是1倍数,第二个输入框是2倍数,任意输入框输入,始终保证二倍数输入框的值是1倍数输入框值的2倍。"

如下👇

根据这个例子,我将问题场景代码移植过来精简后如下👇

源码详见 vue-v-model.endless-loop.html,运行后的确也触发了"无限循环",不过例子中因为把递归栈打爆了被迫中止。

如果非要试"真正的无限循环",只要把 watch 回调中数据修改加到任务队列里面去就行,感兴趣的朋友可以看看 vue-v-model.endless-loop-true.html 源码,我贴个代码 Diff。

能不能从代码找出无限循环问题所在,就得看诸位的功力了。

但本篇文章肯定不止于找出问题这么简单,我的目标是 Vue 理解上个台阶。

抛开问题,回到例子本身,我们先看看如果自己做,怎么做。

例子很简单,相信喜欢动手的朋友已经迫不及待地敲出了自己的代码。

不好意思,放错图了。

源码详见 vue-v-model.one-value.html。

简而言之,就是两个输入框共用一个公共变量,1倍数输入框可以直接使用 v-model="refCount" ,但2倍数输入框还使用 v-model 语法糖肯定不行,只能手写更冗长的等价展开 :value="multipleValue(multipleB, refCount)" @input="handleInput" 。

"是骡子是马,拉出来溜溜",运行一下。

毫无悬念地自测通过,打印调用关系日志也在意料之中。

只定义了一个数据 refCount ,比较好理解,不会涉及相互监听修改数据造成无限循环,不再赘述,详见下图:

当然了,手写 v-model 更冗长的等价展开肯定谈不上优雅,本着"能优雅就绝不将就"的倔强,坚决要使用 v-model 的朋友也很快交给出了自己的答卷。

源码详见vue-v-model.two-value.html,先跑跑看看。

运行自测通过,但打印调用关系日志信息虽在意料之中,却在情理之外。

因为使用了两个变量 refCountA 和 refCountB 相互监听相互修改,却没有触发无限循环。

相信熟悉 Vue 的朋友要抢答了, "Vue 官网上说,watch 仅在侦听源发生变化时才执行回调函数。现在是 newValue 和 oldValue 相等,不会触发回调,自然没有无限循环。"

的确,这就解释了上面双值相互监听却没有触发无限循环的情况。

更进一步,聪明的你应该也猜到了上面无限循环的例子错误在于使用了 refMultipleDataX.value = {},每次监听后重新赋值了新对象,而不是在原对象上面改,导致 watch 侦听源每次都发生变化,没完没了。

修改也很简单,直接在原对象修改即可,运行自测通过。

修改 colordiff 如下👇

补充个小知识点,git 不支持文件比较,需要安装 colordiff,命令行如下:

bash 复制代码
# mac 安装 colordiffyarn global add colordiff
# colordiff 对比两个文件内容colordiff -u  vue-v-model.endless-loop.html vue-v-model.endless-loop.v1.html

写到这,简单总结一下。

当过码农的朋友都知道,工业代码复杂度远高于上述精简 Demo,正常数据联动的标准作业程序是通过组件进行模块化封装,相信从上面例子的两种实现,我们已经知道 v-model 才是今天真正的主角。

我对 v-model 的第一印象是"清爽",工程实践后不禁探着脑袋仔细瞅了瞅,"真绕"。

那 Vue 官方推荐组件 v-model 实现是什么?为什么这样推荐?

接下来,我们展开说说。

v-model 可以在组件上使用以实现双向绑定。

Vue 官网《组件 v-model》

按官网介绍, v-model 是块语法糖,实质上是对 v-bind 和 v-on 的一个简化写法,为的使用起来更方便。

本文中例子的第一种实现就使用了比较小众的等价写法,第二种实现直接使用的是大部分朋友熟悉的 v-model 写法。

来,我们把上述例子的二种写法改写成组件实现。

源码详见 vue-v-model.one-value.value-input.v1.html。

眼神犀利的朋友会注意到,36 行注释了 "考考你:为什么不需要给props.count赋值?"

这就是为啥我对 v-model 第二印象是"真绕"的来源。但这题我不想回答。

真不是我不会。

因为下面的运行日志&数据流图可以很清晰地看出来逻辑,无须多言。

同理,将上述坚持用 v-model 的实现封装到组件代码如下:

代码详见 vue-v-model.two-value.value-input.v1.html,这只是一种实现,逻辑同上使用一个值的实现,自己跑着看看吧,我不赘述了。

到这,有必要再总结一下。其实,定义组件里面的 v-model 等价展开 :value="multipleValue()" @input="handleInput" 实现,就是官网《组件 v-model》的推荐写法之一。

简而言之,就是组件内部不独立存储数据,数据直接使用 props 属性传递过来计算后显示,如果内部有输入框修改,则直接通过 emit 发送事件给到外部组件的 v-model 修改,再通过 props 属性传回来。如下🏮图所示👇

接下来,给大家介绍一下第二种使用具有 getter 和 setter 属性的可写 computed 实现。

源码详见 vue-v-model.one-value.computed-setter-getter.v1.html,运行页面如下:

简而言之,使用具有 getter 和 setter 属性的可写 computed 实现更加简洁方便,不需要等价展开 v-model,两个字,"优雅"!

相信一些不太爱看官方文档的朋友多少有些意外,因为自己实现的是另外一种方式,即按思维惯性顺推的 innerValue 加 watch 实现,我也是。

我写这篇文章的两个初衷,一是意识到多个监听来回改值存在无限循环隐患,二是这种符合逻辑惯性的 innerValue 加 watch 实现没被推荐,难道有坑?

实践出真知,来!先上代码实现。

源码详见 vue-v-model.one-value.watch-inner-value.v1.html,运行自测如下:

咋一看,好像也行,没啥问题呀!

因为我是Android客户端转的前端,22年4月份才接触 Vue,前端经验少了点,所以工业代码迭代时我一般会关键字全局搜索当前代码仓库,看有没有类似实现,毕竟已有实现哪怕不优雅,但也是经过线上考验的,在自己未能驾驭时跟随经过考验的代码是一个有效规避风险的选择。

搜索到的结果也是,实现总共一石,watch 独得八斗,等价展开和 computed 共分二斗。

从上面梳理的数据流图看,三种方式大同小异,甚至说惊人的相似。有必要把三种实现放在一起比较一下了。

源码详见 vue-v-model.one-value.3-v-model-vs.html。

把上面的数据流图拉上来对比再总结一下:

不对,图又放错了。

总结已经放在顶部先行了,这就不再复制。

上述代码均在 github.com/shengshuqia... Star 不迷路。

今天已经是 2024 年元旦,原计划是想在新年钟声敲响前给 2023 年划上句号,没想到写着写着却发现了越来越多的草蛇灰线,挺有意思,但工作量也大得惊人。

借用美团川哥的一句话,"流水不争先,争的是滔滔不绝。"

如果超过了 ref 还是 reactive,这是个问题 409 的阅读量(是不是少的可怜),春节前再写篇"图解 watch 和 computed 源码"或者"图解 ref 和 reactive 源码"。

欢迎评论区讨论、交流,或者微信搜索"书强号"公众号关注、shengshuqiang01 加微信,期待和大牛的你做朋友,教学相长。

我是美团小象超市营销前端负责人盛书强,欢迎加入我们大前端团队。

㊗️大家新的一年龙行虎步,日进斗金!

相关推荐
时清云26 分钟前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学34 分钟前
宏队列和微队列
前端·javascript
持久的棒棒君1 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_857297911 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
一 乐1 小时前
租拼车平台|小区租拼车管理|基于java的小区租拼车管理信息系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·微信·notepad++·拼车
undefined&&懒洋洋2 小时前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者4 小时前
React 19 新特性详解
前端
小程xy4 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6324 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6324 小时前
WebGL编程指南之进入三维世界
前端·webgl