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

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui