前言
几个月前的一个早上,天空下着小雨,天气不算热。我吃完早饭便出发去一个电商公司面试。我到的时候已经有几个人在了,我就填了基本信息,等了一会儿到我了。我进去坐下,做了简短的自我介绍。面试官并没有看我,他看着简历,眉头紧皱,喃喃自语“会原理、说自己很懂原理,哼哼”
面试官:你说一下vue的响应式,可以深入一下,api可以带过一下就好了。
我挪了挪椅说:好的,vue的响应式 主要是通过 effect 副作用函数作为入口,向其传入使用者的副作用函数。该函数中的响应式数据会被 proxy 追踪,被追踪的响应式数据与传入的副作用函数将作为key和value,被 bucket桶结构所维护。从而实现了将需要执行的操作和对应的数据所关联。在 get 中会执行track入桶操作,在set中会执行 trigger出桶操作。 而computed与watch都是通过 effect函数创建出来的。大概就是这样。您还需要我具体说吗?
面试官:不用了,你说下 为什么Vue有个nextTick函数,难道js的dom更新不是同步执行的吗?
我挠挠头:呃,这个我不太清楚,不好意思啊,我对 响应式和diff算法比较熟。
面试官摇摇头:因为你在简历中既然写了“理解Vue响应式、diff算法等原理”,那我可能就会问其他方面的。你既然写了等,就应该要会才能往上写,对吧?
我憨笑了一下,然后点点头。
面试官:那先这样吧,结果会在一周左右通知你。
尴尬又气
我对自己很生气,我知道肯定没戏了,这才第二个问题就凉了,问的也太少了。唉!不过要是再问其他的原理,我确实可能还答不出来。我怎么就不知道 nextTick 相关的呢。我回家看看吧,别再像这次这样尴尬了。
学习
在使用 Vue 的过程中,总是避免不了的要使用 nextTick 这个 api。比如在编辑功能中,在弹窗组件弹出后里面嵌套了一个 form 表单,如果我们想要获取到 form表单的 实例对象,此时立即获得可能就会出现拿不到值的情况。多半是因为 dialog 虽然已经改变了 visible 的值,但dom中还没有出现 form。而这个时候就会用到 nextTick了。(如果你不存在这种情况,那是因为你的dialog 在显示前,其内容就已经渲染成 Dom 了,有属性可以控制)
nextTick 使用方式
nextTick 有两种使用方式,一种是 await nextTick(),这种方式可以看出来,nextTick肯定返回了一个 Promise。
await nextTick()
另一种方式是传一个回调过去,这种方式可以看出 nextTick 还支持传入一个函数作为参数,在Dom 更新后会调用这个函数。
nextTick(()=>{
console.log(formRef.value,'formRef.value);
})
上面这样正常使用是没有什么问题的,但我遇到过几次这样做没用的。但是加了定时器就可以了,如果还不可以,就把定时器的时间设置长一点就可以了。那这么说来还是执行时机问题。我上网上搜了相关答案,也去ai问了,说是Dom还没更新完,又或者是受网络、电脑硬件等多种影响,反正就是还没更新完。
那我的定时器要设置多久?一秒还不够,两秒吗? 但说实话,用定时器心里还是有些担心,时间大于500,就感觉到延迟了。但是时间短了,又担心之后出问题。
nextTick
我们先看下不使用nextTick的情况
此时我准备点击最右侧的按钮
可以看到,使用和不使用nextTick 的区别。不使用时,即使页面上已经更改了,但获取到的数据还是之前的。
nextTick 极简版实现
下面这个函数,就已经可以在上面这个场景中解决问题了。
// 创建一个成功的 Promise, 其身上的 .then方法中的回调函数,会被放入微任务队列。
const resolvedPromise = Promise.resolve();
function myNextTick(fn) {
return fn ? resolvedPromise.then(fn) : resolvedPromise;
}
而且有意思的是,如果我们通过vue的响应式数据更改,则不通过 nextTick 就无法立即获取到更改后的值。但如果是通过 javaScript 原生去修改 DOM,就不会出现这个问题
const inputContainer = document.querySelector('.input-container')
inputContainer.style.width = '500px'
console.log(inputContainer.style.width) // 500px
这种方式完全不需要 nextTick ,那也就是说关于 DOM的变化,vue没有立即修改,而是延迟修改了dom。如果是这样的话,当你去获取修改后的值时,实际上 vue还没去修改 dom 的值,那当然就获取不到新的值了。
js的DOM更新是同步,Vue异步更新DOM
js 更新DOM是直接同步更新的,但Vue修改响应式数据后,并不会立即修改,而是会缓存在一个队列中,等到下一个事件循环中才进行一次性更新 。其中一个原因是修改DOM比较消耗性能,因为每一次修改都可能会导致 重排与重绘。
事件循环
js是单线程的,所有的代码都是运行在一个线程上,如果某个代码执行时间过长,就会阻塞这个线程。而异步执行就是不会阻塞主线程的代码。
异步分为宏任务与微任务,每个任务可能有很多,便将对应的任务放入对应的队列中等待执行。在事件循环中会先执行 同步任务,然后执行一个宏任务,接着执行所有的微任务,如果中途有微任务添加,则也执行。
注意:像 setTimeout 这样的函数是会立即执行的,只是其中的回调在时间到的时候会被放入宏任务队列,等待执行。
而vue的异步更新则也是通过微任务队列执行的。而此时我们只需要添加一个微任务,该微任务则与vue更新dom一同进入列队中,并且该微任务也一定是在dom更新后才执行。既然js中更新dom是同步的。那执行完dom操作后,也一定可以能获取到值了。
nextTick不止我写的这么简单
vue原码中的 nextTick函数远不止我的这几行代码,它需要处理处理更多的任务,比如更新 DOM、执行 watch 回调、触发生命周期钩子等。而这些不能检测到就立即执行,而是需要合理的按顺序执行。
它需要维护一个队列,并按序取出执行,还需要考虑递归、错误信息和生命周期钩子的正确执行,因为钩子的执行处于特定时机的执行,而非是在跟主任务队列一起执行。
结语
这下问到nextTick的,我还是能说几句话的了,不过要再深入,我还得歇菜。