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 加微信,期待和大牛的你做朋友,教学相长。

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

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

相关推荐
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
吹牛不交税6 小时前
admin.net-v2 框架使用笔记-netcore8.0/10.0版
vue.js·.netcore