本章会详细讲述在工作中踩到的一个关于vue-props的坑。在对级页面下,动态绑定props失效的问题以及解决办法。
起因
事情的起因是这样的:需求里要求从A页面中选择并获取到对象列表参数List[object],把List传给B页面中,在B页面中通过el-collapse将List展开,并且对每个List中的参数通过v-model进行赋值。如图所示:
自信满满地一顿骚操作后,发现在collapse-item中对于props值的操作并不生效。
于是又哗啦啦地开启了一顿排查。
排查
对于其中v-model绑定props更改并不生效的问题,
同时出现了一些比较奇怪的现象。
props的值可以正常传递到collspse中。
点击checkbox,边框会变红,但不会被勾中,之后“卡死”。
在对collspse-item的展开收起中,其实会刷新item里面的想要修改的值,但只会生效一个。
即点击红棋子,然后收起再展开item -》红棋子生效。
点击红棋子,再点击蓝棋子,然后收起再展开item -》只有蓝棋子生效。
第二列中的input也是,输入123,然后收起展开item -》只有3生效。
根据这些现象,再继续排查,先排除掉了这些原因:
- 将collspse组件去掉,发现仍然无法对list修改,排除collspse的问题。
- 通过checkbox的change方法,发现赋值是生效的,但是只生效最新的一个,覆盖掉之前生效的。并且视图不会更新。
- 将List数据复制到B页面当成本地数据去测试,发现没有问题。
则确定了是props的问题。
文档
由于之前踩过坑,知道无法直接对props值进行更改或者显示,
所以上述数据中的List都是使用以下形式使用的。
1 | computed: { |
查阅相关文档,最开始从官方vue文档入手,发现一下子就找到了答案:
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
而解决办法也非常清楚:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值:
1
2
3
4
5
6props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
1
2
3
4
5
6props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
原因是我试用了computed去做数据更改,而不是data。
所以解决办法是赋值给一个本地data即可。
其他
于是乎我很好奇,从最初的A页面开始,到底往下传递了多少层页面。
查看el-collapse源码,发现其大致结构如下:
A页面获取List -》 List到页面B -》到collapse -》(通过solt)到collapse-item
可以把组件理解成是一个子页面,组件内的solt又算是一个子页面。
而props数据流向只能向下,不能向上,当上层刷新的时候,下层数据也会刷新。
所以List到达最底层的item,item里的list数据更新后想向上传递(到collapse)被阻止,但item的展开收起(视图刷新)会对item下的props做一次刷新。
补充(翻车)
在测试中,按文档的写法是没有问题的。但之后问题就出现了。
在本次项目中,B页面是嵌套入A页面中的(nuxt的nuxt-child)。于是乎在就会出现,B页面已经载入了,而B页面中data初始化获取List的时候,List并没有数据。
在B载入完成后,需要A选择/变动后再流入B中,但是B并没有再次初始化获取值。
所以最后还是绕回我原来的写法:
- 通过localStorage进行中间存储List
- 每一次A对List的改动后都存储进localStorage中
- 并且每次改动,都通过ref调用B页面中的refresh方法去刷新getLocalStorage
- 曲线救国
参考文档: