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

相关推荐
面向星辰38 分钟前
html各种常用标签
前端·javascript·html
梦65040 分钟前
HTML新属性
前端
东风西巷3 小时前
PDFgear:免费全能的PDF处理工具
前端·pdf·软件需求
森之鸟3 小时前
Mac电脑上如何打印出字体图标
前端·javascript·macos
mCell4 小时前
GSAP 入门指南
前端·javascript·动效
gnip4 小时前
组件循环引用依赖问题处理
前端·javascript
Aotman_5 小时前
el-input textarea 禁止输入中文字符,@input特殊字符实时替换,光标位置保持不变
前端·javascript·vue.js·前端框架·es6
Nan_Shu_6145 小时前
Web前端面试题(1)
前端·面试·职场和发展
EveryPossible5 小时前
选择数据展示
javascript
lypzcgf5 小时前
Coze源码分析-资源库-创建知识库-前端源码-核心组件
前端·typescript·react·coze·coze源码分析·ai应用平台·agent开发平台