你了解 Vue 源码中的 getNow 函数吗?

getNow 函数的源码如下:

ts 复制代码
let cachedNow: number = 0
const p = /*#__PURE__*/ Promise.resolve()
const getNow = () =>
  cachedNow || (p.then(() => (cachedNow = 0)), (cachedNow = Date.now()))
  • /*#__PURE__*/ 注释的作用是与 rollup.js 配合使用,用于告诉 rollup.js ,该函数不会产生副作用,可以放心地对其进行 Tree-Shaking 。

  • 缓存机制:将获取当前时间戳的操作结果缓存到 cachedNow 中,避免多次获取当前时间戳造成性能浪费。

  • 延迟时间戳计算:在微任务(Promise.then 回调)中重置 cacheNow ,可以保证在同一事件循环周期内调用 getNow 函数获取到的是同一时间戳,避免在同一个事件循环周期内重复计算时间戳。

  • 逗号表达式 (冷门知识点):逗号表达式会将最后的一个表达式作为返回的结果。Javascript中的 "," 『逗号表达式』

    ts 复制代码
    p.then(() => (cachedNow = 0)), (cachedNow = Date.now())

短短 4 行代码(实际上是 3 行),就包含了那么多知识点,尤大可以的👍。

getNow 函数是与 Vue.js 的事件处理机制紧密联系一起的。

Vue.js 为了提升事件处理的性能,伪造了事件处理函数 invoker 。

ts 复制代码
// 省略了无关代码
function createInvoker(
  initialValue: EventValue,
  instance: ComponentInternalInstance | null
) {
  const invoker: Invoker = (e: Event & { _vts?: number }) => {
    if (!e._vts) {
      // 记录事件处理函数执行的时间
      e._vts = Date.now()
    } else if (e._vts <= invoker.attached) {
      // 屏蔽绑定时间晚于事件处理函数执行时间的事件处理函数的执行
      return
    }
  }
  invoker.value = initialValue
  // 记录事件处理函数绑定的时间
  invoker.attached = getNow()
  return invoker
}

当更新操作发生在事件冒泡之前,即为 dom 元素绑定事件处理函数发生在事件冒泡之前时,会导致不该执行的事件被提前执行的情况。如下面的例子:

js 复制代码
const { effect, ref } = VueReactivity

const bol = ref(false)

effect(() => {
  // 创建 vnode
  const vnode = {
    type: 'div',
    props: bol.value ? {
      onClick: () => {
        alert('父元素 clicked')
      }
    } : {},
    children: [
      {
        type: 'p',
        props: {
          onClick: () => {
            bol.value = true
          }
        },
        children: 'text'
      }
    ]
  }
  // 渲染 vnode
  renderer.render(vnode, document.querySelector('#app'))
})

👆上面例子源自《Vue.js 设计与实现》,8.8 节:事件冒泡与更新时机问题

Vue.js 利用事件处理函数被绑定到 DOM 元素的时间与事件触发时间之间的差异来解决该问题。

当事件触发的时间早于事件处理函数被绑定的时间时,意味着该事件触发时,目标元素上还没有绑定相关的事件处理函数。因此我们可以屏蔽所有绑定时间晚于事件触发时间的事件处理函数的执行来解决此问题。具体实现在上文的 createInvoker 函数中。

Vue.js 绑定与移除事件的处理在 patchEvent 函数中。

ts 复制代码
export function patchEvent(
  el: Element & { _vei?: Record<string, Invoker | undefined> },
  rawName: string,
  prevValue: EventValue | null,
  nextValue: EventValue | null,
  instance: ComponentInternalInstance | null = null
) {
  // vei = vue event invokers
  const invokers = el._vei || (el._vei = {})
  const existingInvoker = invokers[rawName]
  if (nextValue && existingInvoker) {
    // patch
    existingInvoker.value = nextValue
  } else {
    const [name, options] = parseName(rawName)
    if (nextValue) {
      // add
      const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
      addEventListener(el, name, invoker, options)
    } else if (existingInvoker) {
      // remove
      removeEventListener(el, name, existingInvoker, options)
      invokers[rawName] = undefined
    }
  }
}

可见,Vue.js 中使用原生的 addEventListener 函数来绑定事件,使用 removeEventListener 函数来移除绑定的事件。

同时,Vue.js 为了提升性能,Vue.js 伪造了一个事件处理函数 invoker (具体可见上文的 createInvoker 函数)。然后把真正的事件处理函数设置为 invoker.value 属性的值。在绑定事件时,绑定的是 invoker ,当更新事件的时候,我们将不再需要调用 removeEventListener 函数来移除上一次绑定的事件,只需要更新 invoker.value 的值即可。这样,在更新事件时可以避免一次 removeEventListener 函数的调用,从而提升了性能。

由于 JS 的执行速度很快,难免会发生更新操作发生在事件冒泡之前的情况,所以为了保证在同一个事件循环周期内触发或绑定的事件处理函数使用的是相同的时间戳,所以结合了微任务(Promise.then 回调)和缓存的机制来实现 getNow 函数。

相关推荐
IT_陈寒20 分钟前
React实战技巧总结
前端·人工智能·后端
陈随易8 小时前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
Dream-Y.ocean10 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn11 小时前
性能监控
前端
Highcharts.js11 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn11 小时前
构建工具
前端
FRing0312 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷12 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen14 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒14 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端