背景
在我们的Taro项目中,有一个选择城市的列表页,城市列表数据大概2000条左右。在我们的头部输入框中输入内容进行搜索时,会出现输入内容自动来回跳动的情况。
效果如下图所示,我们在输入框中快速输入了12:
在连续输入多个数字时,比如12,输入框内容会先显示成12,再自动变更为1,接着又自动变更为12。
页面代码如下:
<!--dom部分-->
<Input
type='text'
className='search-input'
value={inputValue}
placeholder='请输入城市名称'
onInput={handleInput}
/>
// js部分
// 搜索框输入事件,这里为了做性能优化,在快速输入时,统一在最后一次更新数据(防抖处理)
const handleInput = (ev: any) => {
if (!!inputTimeout.current) {
clearTimeout(inputTimeout.current);
}
inputTimeout.current = setTimeout(() => {
const value = ev.detail.value;
setInputValue(value);
}, 300);
};
上面这种方式,在原生的小程序中是可以正常执行的,不会出现上述这种情况,并且在高频输入时也是最后统一设置一次值,符合我们的预期。为什么在Taro中会出现这种情况?
原因分析
我们先在代码中添加页面数据更新的字段和更新内容的打印。进入 Taro 项目的 dist/taro.js 文件,搜索定位 .setData 的调用位置,然后对数据进行打印,查看每次数据更新时,setData的具体内容。
再来看看我们每次输入框内容变更时,打印的数据内容。
setData的值是一个对象,对象的key就是指向我们的input输入框,value就是输入框的值。
发现存在2个问题:
- 每次输入一个值都会触发2次数据更新,并且更新的值都是input框的value值,理论来说,我们输入一次值,应该只会执行一次setData,并且setData的最终值就是输入框中最后的值;
- 从输入完成到数据更新的打印,间隔时间较长。输入完成大概
400ms左右出现第一次setData打印,再过300ms左右出现第二次打印。并不是输入完就立即更新了数据。
我们再来分析下为什么会出现上面2个问题:
1、输入框输入时会触发2次数据更新
从上面的input输入事件中,可以看到每次输入时我们都做了防抖处理,理论来说应该是在我们输入完内容300ms后只执行一次数据更新。
为了防止我们自定义事件对数据更新的影响,先把我们自定义的onInput事件监听移除。再来输入测试:
根据上面的动图我们可以发现,输入内容时只会执行一次数据更新了,但是在代码中,我们并没有对输入事件进行赋值操作。
而相同的代码在原生的小程序中,并不会触发setData。由此可见,应该是Taro内部做了处理。
查看Taro中Input组件的api,发现有个controlled属性,设置Input组件是否为受控组件,但是尝试修改这个属性值不管是true还是false,这里都还是会执行一次setData操作
在页面中显示我们input绑定的inputValue值,发现此时该值并没有变更。所以判断,Taro的这个默认setData事件,其实是用于更新界面中input框的显示内容的,但是其实就算没有这个事件,input框输入时显示的值也会自动变更。(这个事件目前来看是多余的,并且在页面数据量较大,更新较慢时,反而会导致这种卡顿的情况,还不太确定Taro中为什么要做这种处理)
在Taro的官网,debug指南中看到了相关的说明:
再回过头来看我们最开始的输入场景,每次输入时,就会先执行的Taro的Input框默认的setData,后面又通过定时器执行了我们自己的修改绑定值事件,所以看起来就是执行了2次。但是执行2次也不是我们出现这种情况的真正原因。因为通过上面的动图我们可以看到,在没有自定义回调事件的情况下,快速输入值,也同样的会出现上面的这种情况。
2、从输入完成到数据更新的打印,间隔时间较长
最开始的分析,我们发现了2个问题,第2个问题就是输入完成,到setData的打印,间隔的时间比较长,大概有400ms左右,这是因为当前页面的数据量比较大,有一个选择城市的长列表,大概2000条数据左右。
我们刚上面提到,Taro中,input输入会自动更新input的显示内容,而触发的默认setData也会更新input的显示内容,当我们的默认setData触发的足够快时,这样也没有什么问题,用户是感知不到会有2次更新的。
而如果我们的页面数据量较大时,setData触发就会比较慢,在快速输入2次值时,如12,首先input的输入会自动显示该内容(12),然后才会依次触发2次input的默认setData,第一次的值为1(此时再更新input框显示内容为1),第二次的值为12(此时再更新input框显示内容为12)。所以在我们看来,快速输入时,显示的内容就是12 > 1 > 12,这种跳动的情况。从上面的动图中的输入效果和打印时间,我们也可以看出来。
解决方案
综上我们可以发现,页面中Input输入框输入卡顿的原因主要是2个方面:
- input输入框输入时,Taro也会默认执行一次setData更新视图中input的值,页面数据量不大时是没有问题的,页面数据量较大时出现setData延迟,就会出现卡顿的情况。
这个问题看后续Taro是否会做优化处理
- 页面数据量大,导致setData延迟,input已经显示为最新内容了,延迟的setData,又会导致变更为原来的内容。
针对这个问题,可以通过Taro的
CustomWrapper组件,隔离数据操作,提高更新性能,将我们的input输入框的赋值操作与页面的大数据列表隔离开来;下面的思考内容会分析为什么可以这么处理的原因。
所以最终的解决方案:我们可以将input输入框及相关的内容,通过CustomWrapper组件包裹起来,隔离数据操作,解决这种卡顿问题。 在Taro中,大部分遇到的更新性能问题,都可以通过这种方式解决。
思考:为什么整个页面的数据量大时,setData只更新其中一个内容,也会很慢?
我们从小程序官网可以看到, setData 的过程,大致可以分成几个阶段:
- 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
- 将 data 从逻辑层传输到视图层;
- 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。
每次setData都会触发逻辑层虚拟 DOM 树的遍历和更新,所以当我们的DOM结构足够复杂、足够大时,这里的耗时就会越长,所以导致哪怕只是简单的更新了一个Input的value值,也会触发整个页面虚拟 DOM 树的遍历。
而在Taro中,Taro3 使用小程序的 template 进行渲染,一般情况下并不会使用原生自定义组件。这会导致一个问题,所有的 setData 更新都是由页面对象调用,如果我们的页面结构比较复杂,更新的性能就会下降。
层级过深时 setData 的数据结构:
page.setData({
"root.cn.[0].cn.[0].cn.[0].cn.[0].markers": []
})
针对这个问题,主要的思路是借用小程序的原生自定义组件,以达到局部更新的效果,从而提升更新性能。
期望的 setData 数据结构:
component.setData({
"cn.[0].cn.[0].markers": []
})
可以通过使用CustomWrapper这个基础组件,去包裹遇到更新性能问题的模块,提升更新时的性能,对后代节点的 setData 将由此自定义组件进行调用,达到局部更新的效果。
<View className='index'>
<Text>Demo</Text>
<CustomWrapper>
<GoodsList />
</CustomWrapper>
</View>
在我们的demo中,修改后页面代码如下:
<!--dom部分:将Input和其值变更影响的其他dom使用CustomWrapper包裹,形成自定义组件-->
<CustomWrapper>
<!--其他和inputValue值变更相关内容-->
<>...</>
<Input
type='text'
className='search-input'
value={inputValue}
placeholder='请输入城市名称'
onInput={handleInput}
/>
<!--其他和inputValue值变更相关内容-->
<>...</>
</CustomWrapper>
需要注意的时,使用这个组件形成自定义组件后,部分样式或js的交互,就会被隔离开来,相当于在小程序中使用了一个自定义组件。
原理:
组件的 setData 只会引起当前组件和子组件的更新,可以降低虚拟 DOM 更新时的计算开销。对于需要频繁更新的页面元素(例如:秒杀倒计时),可以封装为独立的组件,在组件内进行 setData 操作。
参考链接:
为什么相同的内容,在原生小程序中不卡顿,而在Taro中卡顿?
我们相同的代码,在原生小程序中,并不会出现这种卡顿的情况,并且整体的性能也比Taro中要好,为什么会出现这种情况,可以参考另外一篇文章: