基于Vuex实现更好的数据流请求和渲染

18年年底我的最后一次迭代任务,其中之一就是对我们社区首页做优化改版。我们社区工程是基于vue的服务端渲染脚手架工程——nuxt开发的,目的是为了首屏服务端渲染,进而保证SEO。所以,时至今日我都不是太理解为啥掘金没有做服务端渲染。可能本身就足够NB而不需要SEO了吧,嘿嘿嘿。

可能有很多朋友说,滚动加载很简单,完全没有必要看。但我在写代码的过程中还是觉得有很多地方是值得借鉴的。当然,大牛绕道哈,谢谢您。

首先先看一下效果:

首页(index.vue)都是以卡片流的形式展示,分栏为推荐、关注和热门类,其中,推荐类卡片比较复杂,其内容涉及多个接口的调取和数据的粘合,在此不做业务上的详述,总之明确一点:数据处理比较繁琐。

先来整理一些我所理解的vuex中的概念和一些平时容易被忽略但却很实用的方法:

(1) Vuex是用于状态管理,其核心目的在于集中式存储,实现多个组件共享状态。但到底什么才算是状态数据?如果不存在多个组件共享数据,那还要不要用vuex呢?我个人认为如果数据获取和处理机制比较复杂,那将这部分工作代理到vuex上也是很棒的,且效果会更好;

(2) vuex中绝对不能直接更改state中的值,而是必须要通过提交mutation来变更。其中mutation执行的是同步任务,action执行的是异步任务,且action通过dispatch方法触发;

(3) action类似于mutation,不同在于:action提交的是mutation,而非直接变更状态;action可以包含任意异步操作。这样,我们就可以把请求的执行放到action中去,得到数据后再提交mutation来更新store。

代码规划

下面整理一些内容给大家

index.vue

这里我做的规划是,由于数据的处理环节比较繁琐,因此可以把请求数据和整合数据的过程统统从index.vue中抽离出来,即index.vue不做数据请求和处理,而把请求数据的工作交给vuex,index.vue只在computed中实时获取vuex的数据。这样做的意义在于功能思路清晰,便于维护,进一步简化了index.vue,其部分核心代码如下:

// index.vue
<template>
    <div class="topstoryHeader-nav">
      <a @click="_handleClick('recommend');">
        <span :class="{'hight-light-span':activeName=='recommend'}">推荐</span>
      </a>
      <a @click="_handleClick('attention')">
        <span :class="{'hight-light-span':activeName=='attention'}">关注</span>
      </a>
      <a @click="_handleClick('hot')">
        <span :class="{'hight-light-span':activeName=='hot'}">热门</span>
      </a>
    </div>
    
    <section v-if="activeName=='recommend'" v-kscroll="loadRecommend">
       <card-render v-for="(item,index) in cards_topstories" :key="index" :dataItem="item"/>  //卡片渲染组件
    </section>
    <section v-show="activeName=='attention'" v-kscroll="loadAttentioned">
       <card-render v-for="(item,index) in cards_attention" :key="index" :dataItem="item"/> 
    </section>
    <section v-show="activeName=='hot'" v-kscroll="loadHot">
       <card-render v-for="(item,index) in cards_hot" :key="index" :dataItem="item" />
    </section>
</template>
<script>
import { mapGetters } from "vuex";
export default {
    data() {
        return {
            activeName: "recommend" //初始化设置为推荐栏,
            page:1, //用于推荐栏的数据请求
            pageSize:10, //用于推荐栏的数据请求,
            
            //关注栏参数
            attentPayload: {
              page: 1,
              pageSize: 10
            },
            //热门栏参数
            hotPayload: {
              page: 1,
              pageSize: 10
            }
        }
    },
    computed: { },
    methods: { },
}
</script>
复制代码
滚动加载机制

写一个比较基础的滚动加载钩子:

Vue.directive('kscroll', {
    bind: function(el, binding) {
        window.addEventListener('scroll', _.debounce(() => {
            if (Math.max(window.pageYOffset || 0, document.documentElement.scrollTop) + window.innerHeight +
                10 >= document.body.offsetHeight) {
                binding.value.call();
            }
        }, 150))
    },
    unbind: function() { }
})
复制代码

但是目前该方法有一个缺陷是当浏览器缩放时就会失效,如果有更好的办法,希望您能教教我,谢谢。

数据加载机制

上述index.vue实现了基本的DOM渲染,接下来即获取数据机制,本文设计思路即是将所有请求都放在vuex中,每次页面通过分发dispatch来执行请求。以本文为例,在store文件夹下创建一个专门给首页用的中央数据处理机制home.js,核心代码如下:

// home.js
import {$fetch} from '../plugins/fetch'
import {$api} from '../plugins/api'
export const state = () => ({
  cards_topstories:[],
  cards_attention:null,
  cards_hot:null,

  recommond_last:null,
  attention_last:null,
  hot_last:null
})

export const getters = {
   cards_topstories: (state) => state.cards_topstories,
   cards_attention: (state) => state.cards_attention,
   cards_hot: (state) => state.cards_hot,

   recommond_last: (state) => state.recommond_last,
   attention_last: (state) => state.attention_last,
   hot_last: (state) => state.hot_last
}

export const mutations = {
    //保存推荐卡片集
   SET_STORIES: (state , payload) => {
       if(payload.curArray) { 
         state.cards_topstories = state.cards_topstories.concat(payload.curArray);
         state.recommond_last = payload.last;
       }
   },
   //保存关注卡片集
   SET_ATTENTIONED_STORIES: (state , payload) => {
       if(payload === 0) { //清空cards_attention列
         state.cards_attention = null;
         return;
       }
       if(payload.curArray && !state.cards_attention) {
          state.cards_attention = [];
       }
       state.cards_attention = state.cards_attention.concat(payload.curArray.content);
       state.attention_last = payload.isLast
   },
   //保存热门卡片集
   SET_HOT_STORIES: (state , payload) => {
       if(payload.curArray && !state.cards_hot) {
         state.cards_hot = [];
       }
       state.cards_hot = state.cards_hot.concat(payload.curArray.content);
       state.hot_last =  payload.isLast;
   }
}
export const actions = {
    loadTopStories:({ state, commit }, payload) => {
      let tempArrays = [];//临时存放
      //加载推荐卡片
      $fetch.get($api.TOP_STORIES,{
          page: payload.page , 
          pageSize:payload.pageSize,
          types: ["Answer", "Article", "News", "Course"]
      }).then( res => {
        if(res.errorCode) return
        tempArrays = res.content;
         commit({
             type:'SET_STORIES',
             curArray:tempArrays,
             last:res.last
         })
      })
  },
  loadAttentionedStories: ({state, commit}, payload) => {
      let params = 
      $fetch.get($api.TOP_ATTENTION_STORIES , payload).then(res => {
          if(res.errorCode) return
          commit({
              type:'SET_ATTENTIONED_STORIES',
              curArray:res,
              isLast:res.last
          })
      })
  },
  loadHotStories: ({state, commit}, payload) => {
      $fetch.get($api.TOP_HOT_STORIES,{
        page: payload.page , 
        pageSize:payload.pageSize,
      }).then(res => {
          if(res.errorCode) return
          commit({
            type:'SET_HOT_STORIES',
            curArray:res,
            isLast:res.last
          })
      })
  }
}
复制代码

如上代码,每次当页面要执行加载数据的时候,就需要重新调整好请求的参数,并通过dispatch分发机制执行action中的方法。数据获取后,再通过提交mutation更新数据,之后再在页面中通过实时获取getters中的数据来更新视图。

在index.vue中写入dispatch分发方法

index.vue不直接参与数据请求,但是也要多少间接参与一下,即通过dispatch分发调取home.js中的action,之后action再通过将数据提交mutation。index.vue 的methods中写入方法:

// index.vue
methods: {
    loadRecommend() {
      //在这里就可以做一些业务层级的限制操作,如未登录时只能滚动加载3次等
      //限制只能是加载推荐类卡片流,以及当前不是最后的结果
      if (this.activeName != "recommend" || this.recommond_last) return;
      
      this.$store.dispatch("home/loadTopStories", {
        page: ++this.page,
        pageSize: this.pageSize
      });
    },
    loadAttentioned() {
      if (this.activeName != "attention" || this.attention_last) return;
      let params = {
        page: this.attentPayload.page,
        pageSize: this.attentPayload.pageSize
      };
      if (!this.attentAll) {
        params.insulation = true;
      }
      this.$store.dispatch("home/loadAttentionedStories", params);
      this.attentPayload.page++;
    },
    loadHot() {
      if (this.activeName != "hot" || this.hot_last) return;
      this.$store.dispatch("home/loadHotStories", this.hotPayload);
      this.hotPayload.page++;
    }
}
复制代码

数据分发好后,接下来就是当数据在vuex中整理好后index.vue要实时获取到最新的数据,即通过计算属性获取getters,而当一个组件需要获取多个数据状态时,将这些状态都声明为计算属性会有些重复和冗余,在此采用mapGetters形式简化代码:

// index.vue
import { mapGetters } from 'vuex'
export default {
   computed: {
        ...mapGetters({
          cards_topstories: "home/cards_topstories",
          cards_attention: "home/cards_attention",
          cards_hot: "home/cards_hot",
          recommond_last: "home/recommond_last",
          attention_last: "home/attention_last",
          hot_last: "home/hot_last"
        })
    }   
}
复制代码

这样,就实现了请求、处理数据与获取、渲染数据完全隔离的效果。最后总结一下就是:页面组件通过计算属性获取最新的数据,触发滚动加载时,只是提供好参数(如page和pageSize),剩下的都交给vuex来干,这样就将功能进一步的分离,便于后期的代码维护。

<think>好的,用户之前已经问过关于Vue3中propsemit的问题,现在他们又提出了“单向数据流 emit props”,看起来他们想更深入地理解单向数据流的概念以及propsemit在其中的作用。首先,我需要确认用户是否已经了解基础,或者是否需要从更基础的部分开始解释。 用户可能是在学习Vue的过程中遇到了对数据流管理的困惑,特别是在父子组件之间的数据传递。他们可能已经知道propsemit的基本用法,但想理解为什么Vue推荐单向数据流,以及如何正确使用emit来维持这种数据流。 接下来,我需要回顾单向数据流的基本原则:父组件通过props传递数据给子组件,子组件不能直接修改props,而是通过emit事件通知父组件进行修改。这样做的目的是为了维护数据流动的可预测性,避免多个子组件同时修改同一数据导致的混乱。 用户可能存在的疑问点包括:为什么不能直接修改props?如果不小心修改了props会有什么后果?emit的具体工作机制是怎样的?另外,用户可能想知道在复杂场景下如何有效管理状态,比如多个子组件需要修改同一数据时,或者当传递的是对象或数组这类引用类型时,如何处理才能保持单向数据流。 同时,用户可能对Vue3中的一些新特性感兴趣,比如使用组合式API(Composition API)时如何结合propsemit,或者如何利用TypeScript进行类型定义以增强代码的可维护性。之前的回复中已经提到了TypeScript的支持,可能需要进一步展开或举例说明。 另外,用户可能在实际开发中遇到了问题,比如直接修改了props导致意料之外的bug,或者emit的事件没有被正确监听处理。这时候需要强调单向数据流的重要性,并提供实际的错误示例正确做法,帮助用户避免常见陷阱。 还需要考虑用户的学习阶段,是否是初学者还是有一定经验的开发者。如果是前者,可能需要用更简单的例子类比来解释概念;如果是后者,可以深入讨论最佳实践高级用法,比如使用v-model的语法糖来简化双向绑定,或者结合Pinia等状态管理工具来处理跨组件通信。 最后,要确保回答结构清晰,分点说明,结合代码示例注意事项,帮助用户全面理解单向数据流与props、emit的关系,并能够应用到实际项目中。同时,提醒用户相关资源文档,方便他们进一步学习。</think>在 Vue.js 中,**单向数据流**是组件间数据传递的核心设计原则,而 `props` `emit` 是实现这一原则的关键工具。以下是它们的详细解释协作方式: --- ### 一、**单向数据流是什么?** - **核心思想**:数据从父组件“向下流动”到子组件,子组件不能直接修改父组件传递的数据。若需要修改,必须通过**事件(`emit`)通知父组件**,由父组件更新数据。 - **目的**:确保数据变化的可预测性可维护性,避免多个子组件随意修改同一数据导致的混乱。 --- ### 二、**`props`:父组件向子组件传递数据** #### 1. 基本用法 - **父组件**通过 `props` 传递数据: ```vue <!-- Parent.vue --> <template> <Child :title="parentTitle" /> </template> ``` - **子组件**声明并接收 `props`: ```vue <!-- Child.vue --> <script setup> const props = defineProps({ title: { type: String, required: true } }); </script> <template> <h1>{{ title }}</h1> </template> ``` #### 2. 单向性的体现 - **禁止直接修改**:子组件不能直接修改 `props` 的值(如 `props.title = 'New Title'`),否则会触发 Vue 的警告。 - **对象/数组的陷阱**:若 `props` 是对象或数组,直接修改其属性(如 `props.user.age = 30`)虽然不会报错,但会破坏单向数据流,导致父组件数据意外变更。 --- ### 三、**`emit`:子组件向父组件发送事件** #### 1. 基本用法 - **子组件**触发事件并传递数据: ```vue <!-- Child.vue --> <script setup> const emit = defineEmits(['updateTitle']); const handleClick = () => { emit('updateTitle', 'New Title'); // 发送事件数据 }; </script> <template> <button @click="handleClick">修改标题</button> </template> ``` - **父组件**监听并处理事件: ```vue <!-- Parent.vue --> <template> <Child :title="parentTitle" @update-title="handleUpdate" /> </template> <script setup> const parentTitle = ref('Old Title'); const handleUpdate = (newTitle) => { parentTitle.value = newTitle; // 父组件更新数据 }; </script> ``` #### 2. 实现数据流的闭环 - **子组件**通过 `emit` 通知父组件数据需要更新。 - **父组件**接收事件并修改原始数据,触发重新渲染,新的数据再通过 `props` 传递给子组件。 --- ### 四、**关键注意事项** #### 1. 为什么坚持单向数据流? - **可维护性**:数据变化的源头唯一(父组件),便于调试追踪。 - **避免副作用**:防止子组件意外修改父组件状态,导致难以预测的 Bug。 #### 2. 特殊场景的解决方案 - **需要“双向绑定”时**:使用 `v-model` 语法糖(本质是 `props` + `emit`): ```vue <!-- 父组件 --> <Child v-model:title="parentTitle" /> <!-- 子组件 --> <script setup> const props = defineProps(['title']); const emit = defineEmits(['update:title']); const updateValue = (newVal) => { emit('update:title', newVal); }; </script> ``` - **复杂状态管理**:当多个组件需要共享状态时,使用 Pinia 或 Vuex 集中管理。 #### 3. 引用类型数据的处理 - 若传递的是对象或数组,且需要子组件修改父组件数据: - **推荐方式**:通过 `emit` 通知父组件修改。 - **替代方案**:使用 `reactive` 或 `toRefs` 保持响应性,但仍需谨慎操作(官方不推荐直接修改)。 --- ### 五、**总结:数据流动图** ``` 父组件 —props→ 子组件 ↑ | |— emit 事件 ——| ``` - **数据流方向**:单向(父 → 子)。 - **修改权限**:只有父组件能直接修改数据,子组件通过事件“请求”修改。 合理使用 `props` `emit`,能有效维护组件间的清晰边界,符合 Vue 的设计哲学。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值