useDateFormat带我复习了JS字符串的几个重要API

714 阅读1分钟

很多时候我们前端页面展示都涉及对后台返回的日期数据进行格式化,我们当然不想在只用到一个格式化日期函数的情况下就引入整个moment,所以我们可以自己写日期格式化的工具函数,比如类似下面的代码:

const formateTime = (data) => {
  const da = data || new Date().getTime();
  const dt = new Date(da);
  const y = dt.getFullYear();
  const m = (`${dt.getMonth() + 1}`).padStart(2, '0');
  const d = (`${dt.getDate()}`).padStart(2, '0');
  const hh = (`${dt.getHours()}`).padStart(2, '0');
  const mm = (`${dt.getMinutes()}`).padStart(2, '0');
  const ss = (`${dt.getSeconds()}`).padStart(2, '0');
  return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
};

但如果您的项目中使用了vueuse,那么请注意它提供了格式化日期和时间的方法了,那就是useDateFormat,我们不用自己再写了,我们今天就一起了解一下其源码实现。一张图预览今日学习内容:

1.示例

官方文档示例demo对应的源代码如下:

<script setup lang="ts">
import { ref } from 'vue'
import { useDateFormat, useNow } from '@vueuse/core'

const formatter = ref('YYYY-MM-DD HH:mm:ss')
const formatted = useDateFormat(useNow(), formatter)
</script>

<template>
  <p class="text-20px font-bold text-emerald-500">
    {{ formatted }}
  </p>
  <div class="flex items-center">
    <span class="mr-5px text-18px">
      Formatter Editor :
    </span>
    <input v-model="formatter" type="text">
  </div>
</template>

useNow获取当前时间,使用useDateFormat根据响应式的格式字符串formatter对当前时间进行转换。代码运行结果如下:

修改formatter, 转换结果可以同步变化:

2.源码

将代码折叠起来,预览一下源码:

2.1 useDateFormat函数

export function useDateFormat(date: MaybeRef<DateLike>, formatStr: MaybeRef<string> = 'HH:mm:ss') {
  return computed(() => formatDate(normalizeDate(unref(date)), unref(formatStr)))
}

使用计算属性computed返回转换结果,我们在上面的例子中看到当修改formatter的时候,转换结果可以同步变化,其原因就在于使用了computed。formatDate用于将日期进行格式转换,normalizeDate是对日期进行规范化返回Date类型的对象。

2.2 formatDate函数

export const formatDate = (date: Date, formatStr: string) => {
  const years = date.getFullYear()
  const month = date.getMonth()
  const days = date.getDate()
  const hours = date.getHours()
  const minutes = date.getMinutes()
  const seconds = date.getSeconds()
  const milliseconds = date.getMilliseconds()
  const day = date.getDay()
  const matches: Record<string, string | number> = {
    YY: String(years).slice(-2),
    YYYY: years,
    M: month + 1,
    MM: `${month + 1}`.padStart(2, '0'),
    D: String(days),
    DD: `${days}`.padStart(2, '0'),
    H: String(hours),
    HH: `${hours}`.padStart(2, '0'),
    h: `${hours % 12 || 12}`.padStart(1, '0'),
    hh: `${hours % 12 || 12}`.padStart(2, '0'),
    m: String(minutes),
    mm: `${minutes}`.padStart(2, '0'),
    s: String(seconds),
    ss: `${seconds}`.padStart(2, '0'),
    SSS: `${milliseconds}`.padStart(3, '0'),
    d: day,
  }
  return formatStr.replace(REGEX_FORMAT, (match, $1) => $1 || matches[match])
}

根据日期对象获取日期对应的年、月、日、时、分、秒、毫秒、星期几;matches的键对应各种模式,值对应日期值,简单说一下slice方法,其作用是从一个字符串中提取并返回新的字符串,不影响原字符串,传入负值说明要从倒数的位置提取。再说一下padStart方法,其第一个参数为目标长度,第二个参数为填充字符。

formatDate函数的返回结果是对日期的模式字符串进行替换,用相应的日期值替换字符串中的模式。这里使用的是replace方法,其函数签名如下:

str.replace(regexp|substr, newSubStr|function)

第一个参数可以是正则表达式或者一个字符串,第二个参数可以是一个字符串也可以是一个函数。在formatDate中第一个参数是正则表达式REGEX_FORMAT,第二个参数是一个函数。

REGEX_FORMAT的定义如下:

const REGEX_FORMAT = /* #__PURE__ */ /[([^]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g

其含义不难理解,是一个能够匹配常用日期格式的正则表达式。

再看(match, $1) => $1 || matches[match],match是匹配的子串例如'YYYY'或者'mm'等,matches[match]就代表对应的日期值。由于replace方法的第一参数是一个正则表达式,则$1代表第一个括号匹配来的字符串。

2.3 normalizeDate函数

export const normalizeDate = (date: DateLike) => {
  if (date === null)
    return new Date(NaN) // null is invalid
  if (date === undefined)
    return new Date()
  if (date instanceof Date)
    return new Date(date)
  if (typeof date === 'string' && !/Z$/i.test(date)) {
    const d = date.match(REGEX_PARSE) as any
    if (d) {
      const m = d[2] - 1 || 0
      const ms = (d[7] || '0').substring(0, 3)
      return new Date(d[1], m, d[3]
          || 1, d[4] || 0, d[5] || 0, d[6] || 0, ms)
    }
  }

  return new Date(date!)
}

normalizeDate对所有可能指日期的值进行规范化,注意如果参数date为null,则new Date(NaN)得到的是一个非法日期,如下所示:

如果是date是字符串则调用match方法检索其对于正则表达式REGEX_PARSE的匹配结果。REGEX_PARSE的定义如下:

const REGEX_PARSE = /* #__PURE__ */ /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/

其含义是能够匹配像'1970-01-20 11:47'这样的日期字符串,例如下图所示的匹配结果:

3.总结

今天我们分析了useDateFormat的源码,发现其实现中两个重要的函数formatDate和normalizeDate,在这两个函数的实现中用到了常用的字符串API:match、replace、padStart和slice。之所以能够通过修改模式字符串就能够得到响应式的转换结果是因为useDateFormat返回了computed。