不使用keep-alive,记住列表的滚动位置(scrollTop的正确使用方式)

不使用keep-alive,记住列表的滚动位置(scrollTop的正确使用方式)不使用keep-alive,记住列表的滚动位置(scrollTop的正确使用方式)不使用keep-alive,记住列表的滚动位置(scrollTop的正确使用方式)不使用keep-alive,记住列表的滚动位置(scrollTop的正确使用方式)

需求如图,首页日常任务模块—>日常任务列表(父组件)–>任务列表(子组件)–>详情页(孙组件)。

就是从首页进入功能列表页父组件,滚动一段距离点击自立项目进入到任务子组件,再滚动一段距离点击第一条带日期的那一项进入任务详情,然后返回时要返回到刚才滚动的位置,而不是从列表第一条开始。

项目使用的vue框架和vant组件,我按照vue官方文档里介绍说把<router-view><keep-alive>包裹起来就可以实现,各组件内也定义了name属性,然而并没有卵用,每次返回都是从第一条显示。网上搜了下大概有两个原因:

1、<router-view>嵌套在层级不同的<router-view>中切换会出现缓存数据失效。

2、<keep-alive>只对直属的子组件有效,多个共用组件导致的<keep-alive>缓存失效

解决方法:

1、既然是多个router-view嵌套并且共用的情况下造成的,那么如果只存在一个router-view,也就是只需要app.vue作为框架内所有页面的容器,就不会有这个问题。然后将router转换一下,全部转换成一级路由。

2、不转换路由层级,使用scrollIntoView()或者获取父组件的scrollTop

Element接口的scrollIntoView()方法会滚动元素的父容器,使被调用scrollIntoView()的元素对用户可见。具体请参考

Element.scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数。一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0scrollTop

我在项目中使用了第二种解决方案,主要原因:

1、首先我觉得带层级的路由更清晰些。

2、在缓存的路由中引用封装的组件,缓存会造成其它列表项数据不正确,要作额外理。

3、因为scrollIntoView()只能设置滚动位置为start/center/end/nearest,滚动位置并不精确,体验不是太好,而且兼容性没有scrollTop好,所以选择了 Element.scrollTop

注意:这里有个坑,scrollTop一定要获取父组件的(或最外层router-view),即使是在子组件也要获取父组件的scrollTop,因为所有的内容都是包含在父组件里的,其实就是router-view。我一开始是获取的vant列表的scrollTop,不管怎么滚动scrollTop一直为0,纠结了不少时间。

具体操作步骤:

1、首先在父组件内用ref属性获取到最外层元素

// 父组件
<template>
  <div class="dailyTaskMain commonStyle" ref="dailyTaskMain">
    // 封装的通用组件,里面包含了vant列表,点击列表中的自立项目时会触发getScroll函数
    // 这里使用了v-show,如果是v-if要把滚动距离保存到vuex里
    <div v-show="isShowHeadAndTabBar">
    	<CommonParentPage @getScroll="getScroll"></CommonParentPage>
  	</div>
    <!-- 显示子路由 -->
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    data () {
      return {
        scroll: ''
      }
    },
    watch: {
    	// 监测路由路径变化,返回父路由时,刷新滚动距离
      $route (now, old) {
        if (old.name === 'dailyChildTask') {
          this.$nextTick(() => {
            this.$refs.dailyTaskMain.scrollTop = this.scroll
          })
        }
      }
    },
    methods: {
      // 获取滚动距离
      getScroll () {
        this.scroll = this.$refs.dailyTaskMain.scrollTop
      }
    }
  }
</script>

这样就拿到了父组件的内容的滚动距离,从子组件返回时,将滚动距离赋值给父组件最外层元素。完美解决!

2、在子组件内获取内容的滚动距离

此时,父组件的内容被隐藏了,只剩下子组件的内容。注意,子组件的内容也是包含在父组件内的,所以我们要想获取子组件内容的滚动距离,首先还是得拿到父组件的最外层元素。

vue提供了一个属性$parent,从字面意思就可以猜出,它是用来获取父组件信息的。

在子组件我们使用this.$parent.$el就可以得到父元素的最外层元素。

// 子组件
<template>
  <div class="dailyChildTask commonAddBtn">
    <div v-show="isShow">
      // 封装的通用组件
      <CommonChildPage @getParentScroll="getParentScroll"></CommonChildPage>
    </div>
  </div>
</template>

<script>
  export default{
    data(){
      return{
        scroll: ''
      }
    },
    watch: {
    	// 监测路由路径变化,返回父路由时,刷新滚动距离
      $route (now, old) {
        if ((old.name === 'dynamicForm')) {
            this.$parent.$el.scrollTop = this.scroll
        }
      }
    },
    methods:{
      // 获取父组件滚动距离(点击列表项时封装的子组件触发$emit从而触发getParentScroll,获取父组件内容的滚动距离)
      getParentScroll () {
        this.scroll = this.$parent.$el.scrollTop
      }
    }
  }
  
</script>

注意:v-show的妙用

项目中父、子组件切换时我使用的是v-show,因为v-show的特性(隐藏元素而不是销毁,相当于缓存了),子组件返回父组件、详情页返回子组件时不会重新刷新页面。所以this.scroll的值在上述情形下不会重置为0。

如果使用v-if(销毁元素),这时页面会重新刷新,this.scroll的值会重新初始化,所以返回时父元素的scrollTop始终为0。综上所述,使用v-if时不能用$emit方法来直接获取scrollTop,而是将scrollTop保存到vuex中,返回时从vuex获取scrollTop

当然使用v-show也可以使用vuex来保存scrollTop。这时就不需要$emit方法和getScroll等监听方法。直接从vuex里获取scrollTop的值。

使用v-if的注意事项

1、使用v-if页面会重新加载,所以我们要在数据请求成功后立即获取scrollTop

getList(){
	API.xxx().then(resp => {
		if(resp.httpCode === 200){
	      this.list = resp.xxx // 拿到列表数据
	      this.$nextTick(() => {
	        // 我这里封装了通用组件,在通用组件内获取父组件的scrollTop要用this.$parent.$el
	        this.$parent.$el.scrollTop = this.$store.state.xxx
	      })
	    }
	})
}

2、通过第一步我们是能拿到了scrollTop。但是有个问题,不管页面前进还是后退,都定位到了之前保存的位置。我们期待的效果是页面前进时scrollTop要为0,即从第一条数据开始。怎么办呢?

我们可以增加一个变量来控制scrollTop的获取。比如在vuex定义一个flag

在钩子函数里监听(路由全局前置守卫)

router.beforeEach((to, from, next) => {
  // 判断是否需要刷新滚动位置
  let name = from.name
  if (name === 'index') {// 如果是首页,即是页面前进
    store.commit('changeFlag', 0)
  } else {
    store.commit('changeFlag', 1)
  }
})

3、改写获取列表方法

// 获取列表
getList(){
	API.xxx().then(resp => {
		if(resp.httpCode === 200){
	      this.list = resp.xxx // 拿到列表数据
		    // 修改获取scrollTop的时机
		    if(flag){
		      this.$nextTick(() => {
		        // 我这里封装了通用组件,在通用组件内获取父组件的scrollTop要用this.$parent.$el
		        this.$parent.$el.scrollTop = this.$store.state.xxx
		       })
		     } else {
		       this.$parent.$el.scrollTop = 0
		     }
	    }
	})
}

4、下拉刷新时要将flag置为0

5、使用v-if页面会闪一下,因为在重新加载页面

上一篇:数据缓冲区详解


下一篇:Android代码混淆配置:ProGuard