1、开发需要:要做一款日历展示
要求有三:
- 1、即将一年所有日期,按照每个月,展示出来;
- 2、点击其中一个日期,添加背景颜色,更新为选中状态,再点击取消;
- 3、第一次加载或者数据更新时,将对应数据,在日历中显示为选中状态;
2、插件选择:
github中选中了vcalendar开源组件,比较满足当前的项目要求;
3、遇到问题:
按照文档说明使用,展示数据较多时,进行选中状态渲染,则出现卡顿延迟现象;
4、进行源码分析;
4.1 如何加载数据
<v-calendar :attributes="attributes" @dayclick="onDayClick" />
export default {
data() {
return {
days: [],
};
},
computed: {
dates() {
// 根据days 计算得到dates
return this.days.map(day => day.date);
},
attributes() {
// dates 变动以后触发到了attributes计算属性
return this.dates.map(date => ({
highlight: true,
dates: date, // 这里是传递数据的地方
}));
},
},
methods: {
onDayClick(day) {
// 点击日历,控制days数据
const idx = this.days.findIndex(d => d.id === day.id);
if (idx >= 0) {
this.days.splice(idx, 1);
} else {
this.days.push({
id: day.id,
date: day.date,
});
}
},
},
};
4.2 查看组件源码
// Calendar.vue
传给组件的attributes,做了监听,当attributes的数据发生变动时,
watch: {
...
attributes: {
handler(val) {
const { adds, deletes } = this.store.refresh(val);
// console.time()
this.refreshAttrs(this.pages, adds, deletes);
// console.timeEnd()
//经过排查这个方法非常耗费时间
//大概每一次都要1-2秒时间
},
deep: true,
},
}
refreshAttrs(pages = [], adds = [], deletes = [], reset) {
if (!arrayHasItems(pages)) return;
// For each page...
console.time()
pages.forEach(p => {
// **** 嵌套双循环 O(N2)
// For each day...
p.days.forEach(d => {
let map = {};
// If resetting...
if (reset) {
d.refresh = true;
} else if (hasAny(d.attributesMap, deletes)) {
// Delete attributes from the delete list
map = omit(d.attributesMap, deletes);
// Flag day for refresh
d.refresh = true;
} else {
// Get the existing attributes
map = d.attributesMap || {};
}
// For each attribute to add...
adds.forEach(attr => {
// Add it if it includes the current day
const targetDate = attr.intersectsDay(d);
if (targetDate) {
const newAttr = {
...attr,
targetDate,
};
map[attr.key] = newAttr;
// Flag day for refresh
d.refresh = true;
}
});
// Reassign day attributes
if (d.refresh) {
d.attributesMap = map;
}
});
});
console.timeEnd()
// Refresh pages
this.$nextTick(() => {
// 调用子组件 pages(CalendarPane.vue) // 实际为月的组件
this.$refs.pages.forEach(p => p.refresh());
});
},
// CalendarPane.vue
refresh() {
// days 为 CalendarDay.vue 日的组件
this.$refs.days.forEach(d => d.refresh());
},
//CalendarDay.vue
render(h) {
// Backgrounds layer
const backgroundsLayer = () =>
// 这里进行渲染 选中背景色
this.hasBackgrounds &&
h(
'div',
{
class: 'vc-highlights vc-day-layer',
},
this.backgrounds.map(({ key, wrapperClass, class: bgClass, style }) =>
h(
'div',
{
key,
class: wrapperClass,
},
[
h('div', {
class: bgClass,
style,
}),
],
),
),
);
....
},
computed: {
...
backgrounds() {
return this.glyphs.backgrounds;
},
hasBackgrounds() {
return !!arrayHasItems(this.backgrounds);
},
},
methods:{
refresh() {
// debugger
if (!this.day.refresh) return;
this.day.refresh = false;
const glyphs = {
backgrounds: [],
dots: [],
bars: [],
popovers: [],
content: [],
};
// Use $set to trigger reactivity in popovers, if needed
// // debugger
this.$set(
this.day,
'attributes',
Object.values(this.day.attributesMap || {}).sort(
(a, b) => a.order - b.order,
),
);
this.day.attributes.forEach(attr => {
// Add glyphs for each attribute
// 对每一天的属性进行处理
const { targetDate } = attr;
// 从targetDate中取出isDate
const { isDate, isComplex, startTime, endTime } = targetDate;
const onStart = this.startTime <= startTime;
const onEnd = this.endTime >= endTime;
const onStartAndEnd = onStart && onEnd;
const onStartOrEnd = onStart || onEnd;
const dateInfo = {
isDate,
isComplex,
onStart,
onEnd,
onStartAndEnd,
onStartOrEnd,
};
// 用取出的属性 来进行数据更新
this.processHighlight(attr, dateInfo, glyphs);
this.processNonHighlight(attr, 'content', dateInfo, glyphs.content);
this.processNonHighlight(attr, 'dot', dateInfo, glyphs.dots);
this.processNonHighlight(attr, 'bar', dateInfo, glyphs.bars);
this.processPopover(attr, glyphs);
});
this.glyphs = glyphs;
},
processHighlight(
{ key, highlight },
{ isDate, isComplex, onStart, onEnd, onStartAndEnd },
{ backgrounds, content },
) {
// debugger
if (!highlight) return;
const { base, start, end } = highlight;
if (isDate || isComplex) {
// 这里this.glyphs.backgrounds 数据更新以后,计算 backgrounds
backgrounds.push({
key,
wrapperClass: 'vc-day-layer vc-day-box-center-center',
class: ['vc-highlight', start.class],
style: start.style,
});
content.push({
key: `${key}-content`,
class: start.contentClass,
style: start.contentStyle,
});
}.........
}
}
4.3 至此分析完毕
-
1、attributes 更新,refreshAttrs方法会对日历中每一日数据进行重新更新计算,耗时最多;
-
2、数据更新完毕,则对每一天组件进行重新渲染;
-
3、展示全年365日,数据就会较大,造陈卡顿;
5、解决思路和方法
-
解决思路
- 1 不再采用attributes更新,来驱动数据变化进行更新;
- 2 当改变一日数据的时候,则只更新对应数据即可,减少不必要的计算和渲染;
-
解决方法
- 1、数据变动后,只更改对应的数据;
- 2、找到数据对应日的组件或div,只改变对应css样式,开销最少;
- 3、具体代码不赘述;