书接上文 ref 还是 reactive,这是个问题。
先介绍下我踩到的坑。 简单说,就是有一个输入框,即可以是其他组件联动改变输入框内容,也可以是用户直接输入改变输入框内容。
当时的我使用的是 watch 实现,突然点亮了无限循环,直接卡吧死机了。
有点慌,实际的工业项目中组件数量和交互逻辑很多,光打日志就查了半天,因为对 Vue 不熟,所以遇到问题时找到了诸多怀疑点,理解不透经验不够,就只能加班逐个排除,耗时费力。
眼瞅着需求要上线了,时不我待,只能先绕坑,临时采用了在 watch
里面加了些逐个考虑的分支判断,以及节流处理,才勉强压制住了无限循环。
现在腾出空来,得好好研究一下,免得下次快乐工作时又要慌张地加班填坑。
这篇文章的目的是找出 v-model 无限循环 的原因,以及探索组件 v-model 最佳实践写法。
结论先行:
-
无限循环 的罪魁祸首是修改监听数据时直接重新赋值了新对象,而不是在原对象上改。
举个栗子,如果监听对象 obj 数据为 {x: 123},现在要修改 obj.x 这个数据,我是这么做的,直接 obj = {x: 123},其实数据内容没变,但操作后 oldObj !== newObj ,进而触发 watch 监听,然后点亮潜在的无限循环。
正确写法是 obj.x = 123,操作后数据没变,即 oldObj === newObj && oldObj.x === newObj.x ,不会触发 watch 监听调用,自然也不会有后面"一个bug改一天"的事了。
所以,修改 watch 监听对象一定要注意了,只能改原对象。
-
v-model 最佳实践写法是 官网推荐 v-model 实现方式二,"使用具有 getter 和 setter 属性的可写
computed
",我当时第一眼就觉得"专业",今天再仔细一瞅,还"优雅"。-
"使用等价展开 :value 和 @input"虽然是官网推荐 v-model 实现方式一,但是得自己多写点代码,而且还嵌套在模版里面,看起来有点费劲,"不优雅"。
-
"使用 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 加微信,期待和大牛的你做朋友,教学相长。
我是美团小象超市营销前端负责人盛书强,欢迎加入我们大前端团队。
㊗️大家新的一年龙行虎步,日进斗金!