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中的 "," 『逗号表达式』
tsp.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 函数。