code explore - vcalendar plugin

499 阅读2分钟

1、开发需要:要做一款日历展示

要求有三:

  • 1、即将一年所有日期,按照每个月,展示出来;
  • 2、点击其中一个日期,添加背景颜色,更新为选中状态,再点击取消;
  • 3、第一次加载或者数据更新时,将对应数据,在日历中显示为选中状态;

2、插件选择:

github中选中了vcalendar开源组件,比较满足当前的项目要求;

3、遇到问题:

按照文档说明使用,展示数据较多时,进行选中状态渲染,则出现卡顿延迟现象;

4、进行源码分析;

4.1 如何加载数据

multiple-dates

<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、具体代码不赘述;