问题:1.vue中改变数据之后无法立马通过DOM来获取数据,官方推荐nextTick来解决
vue得数据更新,会开启一个异步队列,将所有得数据变化缓存进去,这里面会做一个去重处理,比如重复得watcher最后都只会执行1个,避免重复得DOM计算消耗性能。
解决疑问
- 1.nextTick得实现原理
- 2.怎么确定nextTick是在DOM更新完毕再执行(这个问题我看了很多文章大致都只是讲了nextTick得原理,并没有实际讲出此问题。)
前言:针对vue2.5之后得版本,2.5之前nextTick得实现有些不同,不过大致思路是一样得。具体哪里不同,可能就是执行nextTick在不同情况下使用用宏任务还是微任务。
1.首先看下nextTick源码,短小精悍
去掉了注释
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'----------------------------
第一部分:创建了一个函数flushCallbacks ,callbacks 是个数组,将nextTick传入得回调push到数组里,然后flushCallbacks 循环执行
----------------------------
const callbacks = []
let pending = falsefunction flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}
}----------------------------
第二部分:别看这里写得各种判断,其实总结下来就是如何去执行flushCallbacks 函数。
microTimerFunc 微任务 默认使用promise, 如果不支持 就使用macroTimerFunc 宏任务
macroTimerFunc 宏任务, 他也是一个优先级判断 setImmediate >MessageChannel>setTimeout
----------------------------
let microTimerFunc //微任务
let macroTimerFunc //宏任务
let useMacroTask = falseif (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {macroTimerFunc = () => {setImmediate(flushCallbacks)}
} else if (typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) ||MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {const channel = new MessageChannel()const port = channel.port2channel.port1.onmessage = flushCallbacksmacroTimerFunc = () => {port.postMessage(1)}
} else {/* istanbul ignore next */macroTimerFunc = () => {setTimeout(flushCallbacks, 0)}
}if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()microTimerFunc = () => {p.then(flushCallbacks)if (isIOS) setTimeout(noop)}
} else {microTimerFunc = macroTimerFunc
}----------------------------
第三部分:withMacroTask 方法来控制useMacroTask ,其实就是通过不同得情况去判断使用微任务还是宏任务。
----------------------------
export function withMacroTask (fn: Function): Function {return fn._withTask || (fn._withTask = function () {useMacroTask = truetry {return fn.apply(null, arguments)} finally {useMacroTask = false }})
}----------------------------
第四部分:暴露nextTick函数,第二个参数ctx,vue已经优化默认就是vue实例,所有this.$nextTick你使用箭头函数还是function,指向得都是vue实例,里面代码简单就是将cb回调函数放入callbacks,然后通过useMacroTask来判断用什么方式去执行。
----------------------------
export function nextTick (cb?: Function, ctx?: Object) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = trueif (useMacroTask) {macroTimerFunc()} else {microTimerFunc()}}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}
总结了里面得逻辑,代码其实还是比较简单得
第一部分:创建了一个函数flushCallbacks ,callbacks 是个数组,将nextTick传入得回调push到数组里,然后flushCallbacks 循环执行
第二部分:别看这里写得各种判断,其实总结下来就是如何去执行flushCallbacks 函数。
microTimerFunc 微任务 默认使用promise, 如果不支持 就使用macroTimerFunc 宏任务
macroTimerFunc 宏任务, 他也是一个优先级判断 setImmediate >MessageChannel>setTimeout
第三部分:withMacroTask 方法来控制useMacroTask ,其实就是通过不同得情况去判断使用微任务还是宏任务。
第四部分:暴露nextTick函数,第二个参数ctx,vue已经优化默认就是vue实例,所有this.$nextTick你使用箭头函数还是function,指向得都是vue实例,里面代码简单就是将cb回调函数放入callbacks,然后通过useMacroTask来判断用什么方式去执行。
这里补充一下一个事件循环机制得概念
- 事件循环机制:我们都知道js是单线程得,类似于排队任务,1个完成下一个,但是碰到耗时比较大得,就比较尴尬了,所以这时候会开启一个异步队列,等到主线程同步任务所有完成,就会执行异步队列,拿出一个执行一个,重复操作,直到所有异步任务都完成,
- 异步队列:分为宏任务和微任务,宏任务比微任务耗性能,所以浏览器一般都是优先使用微任务
- 微任务:promise MutationObserver promise.nextTick
- 宏任务:setInterval setTimeout postMessage setImmediate
挺多问题文章就分析到这得,但是我还是无法理解nextTick是如何监听到vue得DOM更新得,有得说是MutationObserver,但是在vue2.5版本之后,在上面文件中是没有它得
2.为了弄清楚原因,我得理解vue得DOM是如何渲染得
- vue当数据变化得时候,会促发它得set方法中得派发更新 notify(),这是Dep中得一个方法,它会去遍历触发当前Dep中存放得watcher。从而触发watcher中得update方法来更新页面
update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}
- 上面我们看到vue源码中watcher得update方法,因为vue数据变化得时候不会马上更新页面需要做些去重等事情,所以它走得是queueWatcher方法
- 最终结果
这里我就直接截图了,queueWatcher方法中最终调用nextTick这个方法来执行,所以到这应该差不多知道了,最终也是通过nextTick这个方法来触发watcher中得run(),所以同样是nextTick,他们使用得异步加载得方式是一样得,所以,比如同样两个宏任务,或者同样两个微任务,都是按顺序执行,优先执行DOM渲染,之后再执行我们得this.$nextTick中得回调!
最后,有什么不对得可以指出来,相互学习!