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.mapState
import { 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
即从store
的state
中派生出的状态,例如我们需要对列表进行过滤并计数,如果有多个组件需要用到某个属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,这两种方式无论哪种方式都不是很理想。而Vuex
允许我们在store
中定义getter
(可以认为是store
的计算属性),就像计算属性一样getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
访问getters
getters
接收state
作为其第一个参数,接受其他getters
作为第二个参数,如不需要则第二个参数可以省略,与state
一样,我们也可以通过Vue
的Computed
获得Vuex
的getters
。
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
,这个选项更像是事件注册,当触发一个类型为increment
的mutation
时,调用此函数,要唤醒一个mutation handler
,你需要以相应的type
调用store.commit
方法。
/无提交荷载
this.$store.commit("increment");
//提交荷载
this.$store.commit("incrementN", { n: 100 })
Mutations需遵守Vue的响应规则
既然Vuex
的store
中的状态是响应式的,那么当我们变更状态时,监视状态的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
来改变状态,你无法知道什么时候回调和哪个先回调,这就是为什么要区分Mutation
和Action
这两个概念,在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.state
和context.getters
来获取state
和getters
。
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()需要返回Promise
actions: {
actionA: async function({ commit }) {
commit("gotData", await getData());
},
actionB: async function({ dispatch, commit }) {
await dispatch("actionA");
commit("gotOtherData", await getOtherData());
}
}
// ...
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store
对象就有可能变得相当臃肿,为了解决以上问题,Vuex
允许我们将store
分割成模块
模块分割
当进行模块分割时,每个模块拥有自己的state
、mutation
、action
、getter
,甚至是嵌套子模块,即从上至下进行同样方式的分割。
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 的状态
模块的局部状态
对于模块内部的mutation
和getter
,接收的第一个参数是模块的局部状态,对于模块内部的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");
}
}
}
}
命名空间
默认情况下,模块内部的action
、mutation
和getter
是注册在全局命名空间的——这样使得多个模块能够对同一mutation
或action
作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true
的方式使其成为带命名空间的模块,当模块被注册后,它的所有getter
、action
及mutation
都会自动根据模块注册的路径调整命名。
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']
}
}
}
}
}
})
启用了命名空间的getter
和action
会收到局部化的getter
,dispatch
和commit
。换言之,你在使用模块内容module assets
时不需要在同一模块内额外添加空间名前缀,更改namespaced
属性后不需要修改模块内的代码。
如果你希望使用全局state
和getter
,rootState
和rootGetters
会作为第三和第四参数传入getter
,也会通过context
对象的属性传入action
。若需要在全局命名空间内分发action
或提交mutation
,将{ root: true }
作为第三参数传给dispatch
或commit
即可。
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: {
dispatch }) {
dispatch("someAction")
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
payload) { /* ... */ } // -> "someAction"
}
}
}
}
}
当使用mapState
、mapGetters
、mapActions
和mapMutations
这些函数来绑定带命名空间的模块时,写起来可能比较繁琐,对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。或者你可以通过使用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.myModule
和store.state.nested.myModule
访问模块的状态。模块动态注册功能使得其他Vue
插件可以通过在store
中附加新模块的方式来使用Vuex
管理状态。例如vuex-router-sync
插件就是通过动态注册模块将vue-router
和vuex
结合在一起,实现应用的路由状态管理。你也可以使用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
中的数据都会增加getter
和setter
,并且会收集watcher
,这样还占内存,不需要响应式的数据我们可以直接定义在实例上。
Copy<template>
<view>
</view>
</template>
<script>
export default {
components: {},
data: () => ({
}),
beforeCreate: function(){
this.timer = null;
}
}
</script>
<style scoped>
</style>
函数式组件
函数组是一个不包含状态和实例的组件,简单的说,就是组件不支持响应式,并且不能通过this
关键字引用自己。因为函数式组件没有状态,所以它们不需要像Vue
的响应式系统一样需要经过额外的初始化,这样就可以避免相关操作带来的性能消耗。当然函数式组件仍然会对相应的变化做出响应式改变,比如新传入新的props
,但是在组件本身中,它无法知道数据何时发生了更改,因为它不维护自己的状态。很多场景非常适合使用函数式组件:
一个简单的展示组件,也就是所谓的
dumb
组件。例如buttons
、pills
、tags
、cards
等,甚至整个页面都是静态文本,比如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-if
,v-for
比v-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
方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。对于需要修改的长列表的优化大列表两个核心,一个分段一个区分,具体执行分为:仅渲染视窗可见的数据、进行函数节流、 减少驻留的VNode
和Vue
组件,不使用显示的子组件slot
方式,改为手动创建虚拟DOM
来切断对象引用。
Copyexport default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
路由懒加载
Vue
是单页面应用,可能会有很多的路由引入,这样使用webpcak
打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效。对于Vue
路由懒加载的方式有Vue
异步组件、动态import
、webpack
提供的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
,是用于提取公共代码的工具,CommonsChunkPlugin
于4.0
及以后被移除,使用SplitChunksPlugin
替代。
使用treeShaking
tree shaking
是一个术语,通常用于描述移除JavaScript
上下文中的未引用代码dead-code
,其依赖于ES2015
模块系统中的静态结构特性,例如import
和export
,这个术语和概念实际上是兴起于ES2015
模块打包工具rollup
。
第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助babel-plugin-component
,然后可以只引入需要的组件,以达到减小项目体积的目的,以项目中引入element-ui
组件库为例。
Copy{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
Copyimport 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脱离文档流,高度塌陷
<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>
效果:
可以看到上面效果给box
设置完float
结果脱离文档流,使container
高度没有被撑开,从而背景颜色没有颜色出来,解决此问题可以给container
触发BFC
,上面我们所说到的触发BFC
属性都可以设置。
修改代码
<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>
效果:
2.Margin边距重叠
<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>
效果:
可以看到上面我们为两个盒子的margin
外边距设置的是10px
,可结果显示两个盒子之间只有10px
的距离,这就导致了margin
塌陷问题,这时margin
边距的结果为最大值,而不是合,为了解决此问题可以使用BFC
规则(为元素包裹一个盒子形成一个完全独立的空间,做到里面元素不受外面布局影响),或者简单粗暴方法一个设置margin
,一个设置padding
。
修改代码
<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>
效果:
3.两栏布局
<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>
效果:
可以看到上面元素,第二个div
元素为300px
宽度,但是被第一个div
元素设置Float
脱离文档流给覆盖上去了,解决此方法我们可以把第二个div
元素设置为一个BFC
。
修改代码
<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>
效果:
后端一次给你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秒
,非常消耗时间
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
标签先放进文档碎片
中,然后一次性appendChild
到container
中,这样减少了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>
虚拟列表
虚拟列表需要讲解的比较多,在这里我分享一下我的一篇虚拟列表
的文章,哈哈我自认为讲的不错吧哈哈哈哈哈哈