你了解 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 函数。

相关推荐
汪子熙22 分钟前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
杨荧23 分钟前
【JAVA开源】基于Vue和SpringBoot的旅游管理系统
java·vue.js·spring boot·spring cloud·开源·旅游
Envyᥫᩣ31 分钟前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.4 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
一 乐6 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
昨天;明天。今天。6 小时前
案例-表白墙简单实现
前端·javascript·css
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd6 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常6 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer6 小时前
Vite:为什么选 Vite
前端