vlambda博客
学习文章列表

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

Vuex是一个专为Vue.js应用程序开发的状态管理模式,其采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。每一个Vuex应用的核心就是store仓库,store基本上就是一个容器,它包含着你的应用中大部分的状态state

在大量的业务场景下,不同的模块组件之间确实需要共享数据,也需要对其进行修改操作。也就引发软件设计中的矛盾:模块组件之间需要共享数据和数据可能被任意修改导致不可预料的结果。为了解决其矛盾,软件设计上就提出了一种设计和架构思想,将全局状态进行统一的管理,并且需要获取、修改等操作必须按我设计的套路来,就好比马路上必须遵守的交通规则,右行斑马线就是只能右转一个道理,统一了对全局状态管理的唯一入口,使代码结构清晰、更利于维护。状态管理模式从软件设计的角度,就是以一种统一的约定和准则,对全局共享状态数据进行管理和操作的设计理念。你必须按照这种设计理念和架构来对你项目里共享状态数据进行CRUD,所以所谓的状态管理模式就是一种软件设计的一种架构模式。
关于Vuex的五个核心概念,在这里可以简单地进行总结:

  • state: 基本数据。

  • getters: 从基本数据派生的数据。

  • mutations: 提交更改数据的方法,同步操作。

  • actions: 像一个装饰器,包裹mutations,使之可以异步。

  • modules: 模块化Vuex

State

Vuex使用单一状态树,即用一个对象就包含了全部的状态数据,state作为构造器选项,定义了所有我们需要的基本状态参数,也就是说state便是唯一数据源SSOT,同样每个应用将仅仅包含一个store实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。此外单状态树和模块化并不冲突,我们仍然可以将状态和状态变更事件分布到各个子模块中。使用Vuex并不意味着你需要将所有的状态放入Vuex,虽然将所有的状态放到Vuex会使状态变化更显式和易调试,但也会使代码变得冗长和不直观,如果有些状态严格属于单个组件,最好还是作为组件的局部状态。

在Vue组件中获得Vuex状态

 store实例中读取状态最简单的方法就是在计算属性中返回某个状态,由于Vuex的状态存储是响应式的,所以在这里每当store.state.count变化的时候,都会重新求取计算属性,进行响应式更新。

const store = new Vuex.Store({ state: { count: 0 }})const vm = new Vue({ //.. store, computed: { count: function(){ return this.$store.state.count; } }, //..})

mapState辅助函数

mapState函数返回的是一个对象,当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余,为了解决这个问题,我们可以使用mapState辅助函数帮助我们生成计算属性。

// 在单独构建的版本中辅助函数为 Vuex.mapStateimport { mapState } from "vuex";
export default { // ... computed: mapState({ // 箭头函数 count: state => state.count,
// 传字符串参数 count 等同于 state => state.count countAlias: "count",
// 使用 this countPlusLocalState: function(state) { return state.count + this.localCount; } }) // ...}

如果当前组件中还有局部计算属性需要定义,通常可以使用对象展开运算符...将此对象混入到外部对象中。

import { mapState } from "vuex";
export default { // ... computed: { localComputed: function() { /* ... */ }, // 使用对象展开运算符将此对象混入到外部对象中 ...mapState({ // ... }) // ... } // ...}

Getter

getters即从storestate中派生出的状态,例如我们需要对列表进行过滤并计数,如果有多个组件需要用到某个属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,这两种方式无论哪种方式都不是很理想。而Vuex允许我们在store中定义getter(可以认为是store的计算属性),就像计算属性一样getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

访问getters

getters接收state作为其第一个参数,接受其他getters作为第二个参数,如不需要则第二个参数可以省略,与state一样,我们也可以通过VueComputed获得Vuexgetters

const store = new Vuex.Store({ state: { count:0 }, getters: { // 单个参数 countDouble: function(state){ return state.count * 2 }, // 两个参数 countDoubleAndDouble: function(state, getters) { return getters.countDouble * 2 } }})
const vm = new Vue({ //.. store, computed: { count: function(){ return this.$store.state.count; }, countDouble: function(){ return this.$store.getters.countDouble; }, countDoubleAndDouble: function(){ return this.$store.getters.countDoubleAndDouble; } }, //..})

mapGetters辅助函数

mapGetters辅助函数是将store中的getters映射到局部计算属性,与state类似。

import { mapGetters } from "vuex";
export default { // ... computed: { // 使用对象展开运算符将 getters 混入 computed 对象中 ...mapGetters([ "countDouble", "CountDoubleAndDouble", //.. ]), ...mapGetters({ // 映射 this.double 为 store.getters.countDouble double: "countDouble" }) } // ...}

Mutation

提交mutation是更改Vuex中的store中的状态的唯一方法,mutation必须是同步的,如果要异步需要使用action

定义mutation

每个mutation都有一个字符串的事件类型type和一个回调函数handler,这个回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数,提交载荷作为第二个参数(提交荷载在大多数情况下应该是一个对象),提交荷载也可以省略的。

const store = new Vuex.Store({ state: { count: 1 }, mutations: { // 无提交荷载 increment: function(state) { state.count++; }, // 提交荷载 incrementN: function(state, payload) { state.count += payload.n; } }})

你不能直接调用一个mutation handler,这个选项更像是事件注册,当触发一个类型为incrementmutation时,调用此函数,要唤醒一个mutation handler,你需要以相应的type调用store.commit方法。

/无提交荷载this.$store.commit("increment");//提交荷载this.$store.commit("incrementN", { n: 100 })

Mutations需遵守Vue的响应规则

既然Vuexstore中的状态是响应式的,那么当我们变更状态时,监视状态的Vue组件也会自动更新,这也意味着Vuex中的mutation也需要与使用Vue一样遵守一些注意事项:

  • 最好提前在你的store中初始化好所有所需属性。

  • 当需要在对象上添加新属性时,应该使用Vue.set(obj, "newProp", 1), 或者以新对象替换老对象,例如state.obj = { ...state.obj, newProp: 1 }

Mutation必须是同步函数

一条重要的原则就是mutation必须是同步函数,假如我们正在debug一个app并且观察devtool中的mutation日志,每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照,然而如果在mutation中使用异步函数中的回调让这不可能完成,因为当mutation触发的时候,回调函数还没有被调用,devtools不知道什么时候回调函数实际上被调用,实质上任何在回调函数中进行的状态的改变都是不可追踪的。
mutation中混合异步调用会导致你的程序很难调试,当你调用了两个包含异步回调的mutation来改变状态,你无法知道什么时候回调和哪个先回调,这就是为什么要区分MutationAction这两个概念,在Vuex中,mutation都是同步事务,任何由提交的key导致的状态变更都应该在此刻完成。

mapMutations辅助函数

与其他辅助函数类似,你可以在组件中使用this.$store.commit("xxx")提交mutation,或者使用mapMutations辅助函数将组件中的methods映射为store.commit调用

import { mapMutations } from "vuex";
export default { //.. methods: { ...mapMutations([ "increment" // 映射 this.increment() 为 this.$store.commit("increment") ]), ...mapMutations({ add: "increment" // 映射 this.add() 为 this.$store.commit("increment") }) } // ...}

Action

Action类似于mutation,不同在于Action提交的是mutation,而不是直接变更状态,而且Action可以包含任意异步操作。

注册actions

Action函数接受一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.statecontext.getters来获取stategetters

const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment: function(state) { state.count++; } }, actions: { increment: function(context) { setInterval(() => context.commit("increment"), 1000); } }})

分发actions

Action通过store.dispatch方法触发,同样也支持以载荷方式和对象方式进行分发。

// 分发this.$store.dispatch("increment");
// 以载荷形式分发store.dispatch("incrementN", { n: 10 });
// 以对象形式分发store.dispatch({ type: "incrementN", n: 10 });

mapActions辅助函数

使用mapActions辅助函数可以将组件的methods映射为store.dispatch调用。

import { mapActions } from "vuex";
export default { //.. methods: { ...mapActions([ "incrementN" //映射 this.incrementN() 为 this.$store.dispatch("incrementN") ]), ...mapActions({ add: "incrementN" //映射 this.add() 为 this.$store.dispatch("incrementN") }) } // ...}

组合Action

Action通常是异步的,在一些场景下我们需要组合Action用以处理更加复杂的异步流程,store.dispatch可以处理被触发的action的处理函数返回的Promise,并且store.dispatch仍旧返回Promise。一个store.dispatch在不同模块中可以触发多个action函数,在这种情况下,只有当所有触发函数完成后,返回的Promise才会执行。

// ...actions: { actionA: function({ commit }) { return new Promise((resolve, reject) => { setTimeout(() => { commit("someMutation"); resolve(); }, 1000) }) }}// ...
// 在触发Actions时// ...store.dispatch("actionA").then(() => { // ...})// ...
// 在另外一个 action 中// ...actions: { // ... actionB: function({ dispatch, commit }) { return dispatch("actionA").then(() => { commit("someOtherMutation"); }) }}// ...
// 使用 async/await// 当然此时getData()和getOtherData()需要返回Promiseactions: { actionA: async function({ commit }) { commit("gotData", await getData()); }, actionB: async function({ dispatch, commit }) { await dispatch("actionA"); commit("gotOtherData", await getOtherData()); }}// ...

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store对象就有可能变得相当臃肿,为了解决以上问题,Vuex允许我们将store分割成模块

模块分割

当进行模块分割时,每个模块拥有自己的statemutationactiongetter,甚至是嵌套子模块,即从上至下进行同样方式的分割。

const moduleA = { state: () => ({ /* ... */ }), mutations: { /* ... */ }, actions: { /* ... */ }, getters: { /* ... */ }}
const moduleB = { state: () => ({ /* ... */ }), mutations: { /* ... */ }, actions: { /* ... */ }}
const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB }})
store.state.a // -> moduleA 的状态store.state.b // -> moduleB 的状态

模块的局部状态

对于模块内部的mutationgetter,接收的第一个参数是模块的局部状态,对于模块内部的getter,根节点状态会作为第三个参数。

const moduleA = { state: { count: 0 }, mutations: { increment: function(state) { // state 模块的局部状态 state.count++; } }, getters: { doubleCount: function(state) { return state.count * 2 }, sumWithRootCount: function(state, getters, rootState) { return state.count + rootState.count; } }}

同样对于模块内部的action,局部状态通过context.state暴露出来,根节点状态则为context.rootState

const moduleA = { // ... actions: { incrementIfOddOnRootSum: function({ state, commit, rootState }) { if ((state.count + rootState.count) % 2 === 1) { commit("increment"); } } }}

命名空间

默认情况下,模块内部的actionmutationgetter是注册在全局命名空间的——这样使得多个模块能够对同一mutationaction作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true的方式使其成为带命名空间的模块,当模块被注册后,它的所有getteractionmutation都会自动根据模块注册的路径调整命名。

const store = new Vuex.Store({ modules: { account: { namespaced: true,
// 模块内容(module assets) state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响 getters: { isAdmin: function() { ... } // -> getters['account/isAdmin'] }, actions: { login: function() { ... } // -> dispatch('account/login') }, mutations: { login: function() { ... } // -> commit('account/login') },
// 嵌套模块 modules: { // 继承父模块的命名空间 myPage: { state: () => ({ ... }), getters: { profile: function() { ... } // -> getters['account/profile'] } }, // 进一步嵌套命名空间 posts: { namespaced: true, state: () => ({ ... }), getters: { popular: function() { ... } // -> getters['account/posts/popular'] } } } } }})

启用了命名空间的getteraction会收到局部化的getterdispatchcommit。换言之,你在使用模块内容module assets时不需要在同一模块内额外添加空间名前缀,更改namespaced属性后不需要修改模块内的代码。
如果你希望使用全局stategetterrootStaterootGetters会作为第三和第四参数传入getter,也会通过context对象的属性传入action。若需要在全局命名空间内分发action或提交mutation,将{ root: true }作为第三参数传给dispatchcommit即可。

modules: { foo: { namespaced: true,
getters: { // 在这个模块的 getter 中,`getters` 被局部化了 // 你可以使用 getter 的第四个参数来调用 `rootGetters` someGetter(state, getters, rootState, rootGetters) { getters.someOtherGetter // -> "foo/someOtherGetter" rootGetters.someOtherGetter // -> "someOtherGetter" }, someOtherGetter: state => { /* ... */ } },
actions: { // 在这个模块中, dispatch 和 commit 也被局部化了 // 他们可以接受 `root` 属性以访问根 dispatch 或 commit someAction({ dispatch, commit, getters, rootGetters }) { getters.someGetter // -> "foo/someGetter" rootGetters.someGetter // -> "someGetter"
dispatch("someOtherAction") // -> "foo/someOtherAction" dispatch("someOtherAction", null, { root: true }) // -> "someOtherAction"
commit("someMutation") // -> "foo/someMutation" commit("someMutation", null, { root: true }) // -> "someMutation" }, someOtherAction(ctx, payload) { /* ... */ } } }}

若需要在带命名空间的模块注册全局action,你可添加root: true,并将这个action的定义放在函数handler中。

{ actions: { someOtherAction({ dispatch }) { dispatch("someAction") } }, modules: { foo: { namespaced: true,
actions: { someAction: { root: true, handler(namespacedContext, payload) { /* ... */ } // -> "someAction" } } } }}

当使用mapStatemapGettersmapActionsmapMutations这些函数来绑定带命名空间的模块时,写起来可能比较繁琐,对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。或者你可以通过使用createNamespacedHelpers创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数

// ...computed: { ...mapState({ a: state => state.some.nested.module.a, b: state => state.some.nested.module.b }) }, methods: { ...mapActions([ "some/nested/module/foo", // -> this["some/nested/module/foo"]() "some/nested/module/bar" // -> this["some/nested/module/bar"]() ]) }// ...
// ...computed: { ...mapState("some/nested/module", { a: state => state.a, b: state => state.b }) }, methods: { ...mapActions("some/nested/module", [ "foo", // -> this.foo() "bar" // -> this.bar() ]) }// ...
// ...import { createNamespacedHelpers } from "vuex"const { mapState, mapActions } = createNamespacedHelpers("some/nested/module")export default { computed: { // 在 `some/nested/module` 中查找 ...mapState({ a: state => state.a, b: state => state.b }) }, methods: { // 在 `some/nested/module` 中查找 ...mapActions([ "foo", "bar" ]) }}// ...

模块动态注册

store创建之后,你可以使用store.registerModule方法注册模块,之后就可以通过store.state.myModulestore.state.nested.myModule访问模块的状态。模块动态注册功能使得其他Vue插件可以通过在store中附加新模块的方式来使用Vuex管理状态。例如vuex-router-sync插件就是通过动态注册模块将vue-routervuex结合在一起,实现应用的路由状态管理。你也可以使用store.unregisterModule(moduleName)来动态卸载模块,注意你不能使用此方法卸载静态模块,即创建store时声明的模块。此外你可以通过store.hasModule(moduleName)方法检查该模块是否已经被注册到store

import Vuex from "vuex";
const store = new Vuex.Store({ /* 选项 */ })
// 注册模块 `myModule`store.registerModule("myModule", { // ...})// 注册嵌套模块 `nested/myModule`store.registerModule(["nested", "myModule"], { // ...})

Vue常用性能优化

Vue常用的一些优化方式,主要是在构建项目过程需要注意的方面。

编码优化

避免响应所有数据

不要将所有的数据都放到data中,data中的数据都会增加gettersetter,并且会收集watcher,这样还占内存,不需要响应式的数据我们可以直接定义在实例上。

 
   
   
 
Copy
<template>
<view>

</view>
</template>

<script>
export default {
components: {},
data: () => ({

}),
beforeCreate: function(){
this.timer = null;
}
}
</script>

<style scoped>
</style>

函数式组件

函数组是一个不包含状态和实例的组件,简单的说,就是组件不支持响应式,并且不能通过this关键字引用自己。因为函数式组件没有状态,所以它们不需要像Vue的响应式系统一样需要经过额外的初始化,这样就可以避免相关操作带来的性能消耗。当然函数式组件仍然会对相应的变化做出响应式改变,比如新传入新的props,但是在组件本身中,它无法知道数据何时发生了更改,因为它不维护自己的状态。很多场景非常适合使用函数式组件:

  • 一个简单的展示组件,也就是所谓的dumb组件。例如buttonspillstagscards等,甚至整个页面都是静态文本,比如About页面。

  • 高阶组件,即用于接收一个组件作为参数,返回一个被包装过的组件。

  • v-for循环中的每项通常都是很好的候选项。

区分computed和watch使用场景

computed是计算属性,依赖其它属性值,并且computed的值有缓存,只有它依赖的属性值发生改变,下一次获取computed的值时才会重新计算computed的值。
watch更多的是观察的作用,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
当我们需要进行数值计算,并且依赖于其它数据时,应该使用computed,因为可以利用computed的缓存特性,避免每次获取值时,都要重新计算。当我们需要在数据变化时执行异步或开销较大的操作时,应该使用watch,使用watch选项允许我们执行异步操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。

v-for添加key且避免同时使用v-if

  • v-for遍历必须为item添加key,且尽量不要使用index而要使用唯一id去标识item,在列表数据进行遍历渲染时,设置唯一key值方便Vue.js内部机制精准找到该条列表数据,当state更新时,新的状态值和旧的状态值对比,较快地定位到diff

  • v-for遍历避免同时使用v-ifv-forv-if优先级高,如果每一次都需要遍历整个数组,将会影响速度。

区分v-if与v-show使用场景

  • 实现方式: v-if是动态的向DOM树内添加或者删除DOM元素,v-show是通过设置DOM元素的display样式属性控制显隐。

  • 编译过程: v-if切换有一个局部编译卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件,v-show只是简单的基于CSS切换。

  • 编译条件: v-if是惰性的,如果初始条件为假,则什么也不做,只有在条件第一次变为真时才开始局部编译, v-show是在任何条件下都被编译,然后被缓存,而且DOM元素保留。

  • 性能消耗: v-if有更高的切换消耗,v-show有更高的初始渲染消耗。

  • 使用场景: v-if适合条件不太可能改变的情况,v-show适合条件频繁切换的情况。

长列表性能优化

Vue会通过Object.defineProperty对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要Vue来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,可以通过Object.freeze方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。对于需要修改的长列表的优化大列表两个核心,一个分段一个区分,具体执行分为:仅渲染视窗可见的数据、进行函数节流、 减少驻留的VNodeVue组件,不使用显示的子组件slot方式,改为手动创建虚拟DOM来切断对象引用。

 
   
   
 
Copy
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};

路由懒加载

Vue是单页面应用,可能会有很多的路由引入,这样使用webpcak打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效。对于Vue路由懒加载的方式有Vue异步组件、动态importwebpack提供的require.ensure,最常用的就是动态import的方式。

 
   
   
 
Copy
{
path: "/example",
name: "example",
//打包后,每个组件单独生成一个chunk文件
component: () => import("@/views/example.vue")
}

服务端渲染SSR

如果需要优化首屏加载速度并且首屏加载速度是至关重要的点,那么就需要服务端渲染SSR,服务端渲染SSR其实是优缺点并行的,需要合理决定是否真的需要服务端渲染。

优点

  • 更好的SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面,如果SEO对站点至关重要,而页面又是异步获取内容,则可能需要服务器端渲染SSR解决此问题。

  • 更快的内容到达时间time-to-content,特别是对于缓慢的网络情况或运行缓慢的设备,无需等待所有的JavaScript都完成下载并执行,用户将会更快速地看到完整渲染的页面,通常可以产生更好的用户体验,并且对于那些内容到达时间time-to-content与转化率直接相关的应用程序而言,服务器端渲染SSR至关重要。

缺点

  • 开发条件所限,浏览器特定的代码,只能在某些生命周期钩子函数lifecycle hook中使用,一些外部扩展库external library可能需要特殊处理,才能在服务器渲染应用程序中运行。

  • 涉及构建设置和部署的更多要求,与可以部署在任何静态文件服务器上的完全静态单页面应用程序SPA不同,服务器渲染应用程序,通常需要处于Node.js server运行环境。

  • 更大的服务器端负载,在Node.js中渲染完整的应用程序,显然会比仅仅提供静态文件的server更加大量占用CPU资源CPU-intensive-CPU密集型,因此如果预料在高流量环境high traffic下使用,需要准备相应的服务器负载,并明智地采用缓存策略。

使用keep-alive组件

当在组件之间切换的时候,有时会想保持这些组件的状态,以避免反复重渲染导致的性能等问题,使用<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。重新创建动态组件的行为通常是非常有用的,但是在有些情况下我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来,此时使用<keep-alive>包裹组件即可缓存当前组件实例,将组件缓存到内存,用于保留组件状态或避免重新渲染,和<transition>相似它,其自身不会渲染一个DOM元素,也不会出现在组件的父组件链中。

 
   
   
 
Copy
<keep-alive>
<component v-bind:is="currentComponent" class="tab"></component>
</keep-alive>

打包优化

模板预编译

当使用DOM内模板或JavaScript内的字符串模板时,模板会在运行时被编译为渲染函数,通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。如果使用webpack,并且喜欢分离JavaScript和模板文件,可以使用vue-template-loader,其可以在构建过程中把模板文件转换成为JavaScript渲染函数。

SourceMap

在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此sourceMap出现了,它就是为了解决不好调式代码问题的,在线上环境则需要关闭sourceMap

配置splitChunksPlugins

Webpack内置了专门用于提取多个Chunk中的公共部分的插件CommonsChunkPlugin,是用于提取公共代码的工具,CommonsChunkPlugin4.0及以后被移除,使用SplitChunksPlugin替代。

使用treeShaking

tree shaking是一个术语,通常用于描述移除JavaScript上下文中的未引用代码dead-code,其依赖于ES2015模块系统中的静态结构特性,例如importexport,这个术语和概念实际上是兴起于ES2015模块打包工具rollup

第三方插件的按需引入

我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助babel-plugin-component,然后可以只引入需要的组件,以达到减小项目体积的目的,以项目中引入element-ui组件库为例。

 
   
   
 
Copy
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

 
   
   
 
Copy
import Vue from 'vue';
import { Button, Select } from 'element-ui';

Vue.use(Button)
Vue.use(Select)

BFC到底是什么东西

BFC 全称:Block Formatting Context, 名为 "块级格式化上下文"。

W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。

简单来说就是,BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。那么怎么使用BFC呢,BFC可以看做是一个CSS元素属性

怎样触发BFC

这里简单列举几个触发BFC使用的CSS属性

  • overflow: hidden

  • display: inline-block

  • position: absolute

  • position: fixed

  • display: table-cell

  • display: flex

BFC的规则

  • BFC就是一个块级元素,块级元素会在垂直方向一个接一个的排列

  • BFC就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签

  • 垂直方向的距离由margin决定, 属于同一个BFC的两个相邻的标签外边距会发生重叠

  • 计算BFC的高度时,浮动元素也参与计算

BFC解决了什么问题

1.使用Float脱离文档流,高度塌陷

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>高度塌陷</title> <style> .box { margin: 100px; width: 100px; height: 100px; background: red; float: left; } .container { background: #000; }</style></head><body> <div class="container"> <div class="box"></div> <div class="box"></div> </div></body></html>

效果:

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

可以看到上面效果给box设置完float结果脱离文档流,使container高度没有被撑开,从而背景颜色没有颜色出来,解决此问题可以给container触发BFC,上面我们所说到的触发BFC属性都可以设置。

修改代码

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>高度塌陷</title> <style> .box { margin: 100px; width: 100px; height: 100px; background: red; float: left; } .container { background: #000; display: inline-block; }</style></head><body> <div class="container"> <div class="box"></div> <div class="box"></div> </div></body></html>

效果:

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

2.Margin边距重叠

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .box { margin: 10px; width: 100px; height: 100px; background: #000; }</style></head><body> <div class="container"> <div class="box"></div> <div class="box"></div> </div></body></html>

效果:

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

可以看到上面我们为两个盒子的margin外边距设置的是10px,可结果显示两个盒子之间只有10px的距离,这就导致了margin塌陷问题,这时margin边距的结果为最大值,而不是合,为了解决此问题可以使用BFC规则(为元素包裹一个盒子形成一个完全独立的空间,做到里面元素不受外面布局影响),或者简单粗暴方法一个设置margin,一个设置padding

修改代码

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Margin边距重叠</title> <style> .box { margin: 10px; width: 100px; height: 100px; background: #000; }</style></head><body> <div class="container"> <div class="box"></div> <p><div class="box"></div></p> </div></body></html>

效果:

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

3.两栏布局

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>两栏布局</title> <style> div { width: 200px; height: 100px; border: 1px solid red; }</style></head><body> <div style="float: left;"> 两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局 </div> <div style="width: 300px;"> 我是蛙人,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭 </div></body></html>

效果:

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

可以看到上面元素,第二个div元素为300px宽度,但是被第一个div元素设置Float脱离文档流给覆盖上去了,解决此方法我们可以把第二个div元素设置为一个BFC

修改代码

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>两栏布局</title> <style> div { width: 200px; height: 100px; border: 1px solid red; }</style></head><body> <div style="float: left;"> 两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局两栏布局 </div> <div style="width: 300px;display:flex;"> 我是蛙人,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭,如有帮助请点个赞叭 </div></body></html>

效果:

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

后端一次给你10万条数据,如何优雅展示,到底考察我什么?

前置工作

先把前置工作给做好,后面才能进行测试

后端搭建

新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务

没有安装nodemon的同学可以先全局安装npm i nodemon -g

// server.js
const http = require('http')const port = 8000;
http.createServer(function (req, res) { // 开启Cors res.writeHead(200, { //设置允许跨域的域名,也可设置*允许所有域名 'Access-Control-Allow-Origin': '*', //跨域允许的请求方法,也可设置*允许所有方法 "Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS", //允许的header类型 'Access-Control-Allow-Headers': 'Content-Type' }) let list = [] let num = 0
// 生成10万条数据的list for (let i = 0; i < 100000; i++) { num++ list.push({ src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image', text: `我是${num}号嘉宾林三心`, tid: num }) } res.end(JSON.stringify(list));}).listen(port, function () { console.log('server is listening on port ' + port);})
复制代码

前端页面

先新建一个index.html

// index.html
// 样式<style> * { padding: 0; margin: 0; } #container { height: 100vh; overflow: auto; } .sunshine { display: flex; padding: 10px; } img { width: 150px; height: 150px; }</style>
// html部分<body> <div id="container"> </div> <script src="./index.js"></script></body>

然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据

// index.js
// 请求函数const getList = () => { return new Promise((resolve, reject) => { //步骤一:创建异步对象 var ajax = new XMLHttpRequest(); //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数 ajax.open('get', 'http://127.0.0.1:8000'); //步骤三:发送请求 ajax.send(); //步骤四:注册事件 onreadystatechange 状态改变就会调用 ajax.onreadystatechange = function () { if (ajax.readyState == 4 && ajax.status == 200) { //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的 resolve(JSON.parse(ajax.responseText)) } } })}
// 获取container对象const container = document.getElementById('container')


直接渲染

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间

Vuex中的核心方法-----后端一次给你10万条数据,如何优雅展示

const renderList = async () => {
console.time('列表时间')
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img /><span>${item.text}</span>`
container.appendChild(div)
})
console.timeEnd('列表时间')
}
renderList()
复制代码

setTimeout分页渲染

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了

const renderList = async () => { console.time('列表时间') const list = await getList() console.log(list) const total = list.length const page = 0 const limit = 200 const totalPage = Math.ceil(total / limit)
const render = (page) => { if (page >= totalPage) return setTimeout(() => { for (let i = page * limit; i < page * limit + limit; i++) { const item = list[i] const div = document.createElement('div') div.className = 'sunshine' div.innerHTML = `<img code-snippet__subst">${item.src}" /><span>${item.text}</span>` container.appendChild(div) } render(page + 1) }, 0) } render(page) console.timeEnd('列表时间')}

requestAnimationFrame

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

const renderList = async () => { console.time('列表时间') const list = await getList() console.log(list) const total = list.length const page = 0 const limit = 200 const totalPage = Math.ceil(total / limit)
const render = (page) => { if (page >= totalPage) return // 使用requestAnimationFrame代替setTimeout requestAnimationFrame(() => { for (let i = page * limit; i < page * limit + limit; i++) { const item = list[i] const div = document.createElement('div') div.className = 'sunshine' div.innerHTML = `<img code-snippet__subst">${item.src}" /><span>${item.text}</span>` container.appendChild(div) } render(page + 1) }) } render(page) console.timeEnd('列表时间')}

文档碎片 + requestAnimationFrame

文档碎片的好处

  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片

const renderList = async () => { console.time('列表时间') const list = await getList() console.log(list) const total = list.length const page = 0 const limit = 200 const totalPage = Math.ceil(total / limit)
const render = (page) => { if (page >= totalPage) return requestAnimationFrame(() => { // 创建一个文档碎片 const fragment = document.createDocumentFragment() for (let i = page * limit; i < page * limit + limit; i++) { const item = list[i] const div = document.createElement('div') div.className = 'sunshine' div.innerHTML = `<img code-snippet__subst">${item.src}" /><span>${item.text}</span>` // 先塞进文档碎片 fragment.appendChild(div) } // 一次性appendChild container.appendChild(fragment) render(page + 1) }) } render(page) console.timeEnd('列表时间')}

懒加载

为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着

其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。

至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性

IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例

<script setup lang="ts">import { onMounted, ref, computed } from 'vue'const getList = () => { // 跟上面一样的代码}
const container = ref<HTMLElement>() // container节点const blank = ref<HTMLElement>() // blank节点const list = ref<any>([]) // 列表const page = ref(1) // 当前页数const limit = 200 // 一页展示// 最大页数const maxPage = computed(() => Math.ceil(list.value.length / limit))// 真实展示的列表const showList = computed(() => list.value.slice(0, page.value * limit))const handleScroll = () => { // 当前页数与最大页数的比较 if (page.value > maxPage.value) return const clientHeight = container.value?.clientHeight const blankTop = blank.value?.getBoundingClientRect().top if (clientHeight === blankTop) { // blank出现在视图,则当前页数加1 page.value++ }}
onMounted(async () => { const res = await getList() list.value = res})</script>
<template> <div id="container" @scroll="handleScroll" ref="container"> <div class="sunshine" v-for="(item) in showList" :key="item.tid"> <img :src="item.src" /> <span>{{ item.text }}</span> </div> <div ref="blank"></div> </div></template>

虚拟列表

虚拟列表需要讲解的比较多,在这里我分享一下我的一篇虚拟列表的文章,哈哈我自认为讲的不错吧哈哈哈哈哈哈