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

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

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

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

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

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

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

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

Vue 组件 v-model 等价展开、computed 和 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 监听对象一定要注意了,只能改原对象。

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

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

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

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

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

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

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

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

书归正传,把我踩的坑抽象成最简单的题》
“有两个输入框,第一个输入框是1倍数,第二个输入框是2倍数,任意输入框输入,始终保证二倍数输入框的值是1倍数输入框值的2倍。”
如下👇

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

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

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

源码详见 vue-v-model.endless-loop.html运行一下后的确也触发了“无限循环”,不过例子中因为把递归栈打爆了被迫中止,打开浏览器开发者工具控制台可以看到错误提示。

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

如果非要试“真正的无限循环”,只要把 watch 回调中数据修改加到任务队列里面去就行,感兴趣的朋友可以看看 vue-v-model.endless-loop-true.html 源码,运行一下,你会发现页面直接卡死了,要想看到无限循环,可以先打开浏览器开发者工具控制台,再在输入框输入个数字触发一下,可以看到一直在打印日志,完全没有停下来的意思,唯一能做的是关闭当前浏览器标签退出。我贴个代码 Diff。

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

能不能从代码找出无限循环问题所在,就得看诸位的功力了。
但本篇文章肯定不止于找出问题这么简单,我的目标是 Vue 理解上个台阶。

抛开问题,回到例子本身,我们先看看如果自己做,怎么做。
例子很简单,相信喜欢动手的朋友已经迫不及待地敲出了自己的代码。

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

不好意思,放错图了。

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

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

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

“是骡子是马,拉出来溜溜”,运行一下
毫无悬念地自测通过,打印调用关系日志也在意料之中。
只定义了一个数据 refCount ,比较好理解,不会涉及相互监听修改数据造成无限循环,不再赘述,详见下图:

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

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

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

源码详见 vue-v-model.two-value.html运行一下先跑跑看看。
运行自测通过,但打印调用关系日志信息虽在意料之中,却在情理之外。
因为使用了两个变量 refCountArefCountB 相互监听相互修改,却没有触发无限循环。

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

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

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

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

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

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

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

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

# mac 安装 colordiff
yarn 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-bindv-on 的一个简化写法,为的使用起来更方便。

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

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

本文中例子的第一种实现就使用了比较小众的等价写法,第二种实现直接使用的是大部分朋友熟悉的 v-model 写法。
来,我们把上述例子的二种写法改写成组件实现。

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

源码详见 vue-v-model.one-value.value-input.v1.html
眼神犀利的朋友会注意到,36 行注释了 “考考你:为什么不需要给 props.count 赋值?”

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

源码详见 vue-v-model.one-value.3-v-model-vs.html运行一下

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

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

不对,图又放错了。

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

总结已经放在顶部先行了,这就不再复制。
上述代码均在 github.com/shengshuqia… ,我自测运行通过,有需自取,欢迎帮忙点亮 Star 不迷路。

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

借用美团川哥的一句话,“流水不争先,争的是滔滔不绝。”
如果超过了 ref 还是 reactive,这是个问题 的点赞收藏评论互动总数 147(是不是少的可怜😭),春节前再写篇“图解 watch 和 computed 源码”或者“图解 ref 和 reactive 源码”。
Vue 组件 v-model 等价展开、computed 和 watch 三种写法谁更优雅?
欢迎评论区讨论、交流,或者微信搜索“书强号”公众号关注、shengshuqiang01 加微信,期待和大牛的你做朋友,教学相长。
我是美团小象超市营销前端负责人盛书强,欢迎加入我们大前端团队。
㊗️大家新的一年龙行虎步,日进斗金!

原文链接:https://juejin.cn/post/7318446209064337435 作者:盛书强

(0)
上一篇 2024年1月1日 上午11:03
下一篇 2024年1月1日 下午4:07

相关推荐

发表回复

登录后才能评论