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

相关推荐
昔人'3 小时前
css`text-wrap:pretty`
前端·css
勇敢di牛牛3 小时前
Vue+mockjs+Axios 案例实践
前端·javascript·vue.js
Sheldon一蓑烟雨任平生3 小时前
Vue3 Class 与 Style 绑定
vue.js·vue3·class与style绑定·绑定class·绑定style·vue3绑定class·vue3绑定style
詩句☾⋆᭄南笙3 小时前
HTML列表、表格和表单
服务器·前端·html·表格·列表·表单
IT_陈寒4 小时前
Python性能翻倍的5个冷门技巧:从GIL逃逸到内存视图的实战优化指南
前端·人工智能·后端
南城巷陌4 小时前
错误边界:用componentDidCatch筑起React崩溃防火墙
前端·react.js·前端框架
FinClip4 小时前
OpenAI推出Apps SDK,你的企业App跟上了吗?
前端·app·openai
馨谙4 小时前
Linux中的管道与重定向:深入理解两者的本质区别
前端·chrome
Rhys..4 小时前
JS - npm init
开发语言·javascript·npm
夏天想4 小时前
复制了一个vue的项目然后再这个基础上修改。可是通过npm run dev运行之前的老项目,发现运行的竟然是拷贝后的项目。为什么会这样?
前端·vue.js·npm