Vue之nextTick原理

vue

前言

我们都知道vue是数据驱动视图,而vue中视图更新是异步的。在业务开发中,有没有经历过当改变了数据,视图却没有按照我们的期望渲染?而需要将对应的操作放在nextTick中视图才能按照预期的渲染,有的时候nextTick也不能生效,而需要利用setTimeout来解决?

搞清楚这些问题,那么就需要搞明白以下几个问题:
1、vue中到底是如何来实现异步更新视图;
2、vue为什么要异步更新视图;
3、nextTick的原理;
4、nextTick如何来解决数据改变视图不更新的问题的;
5、nextTick的使用场景。

以下分享我的思考过程。

Vue中的异步更新DOM

Vue中的视图渲染思想

vue中每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

如果对vue视图渲染的思想还不是很清楚,可以参考这篇defineProperty实现视图渲染用defineProty模拟的Vue的渲染视图,来了解整个视图渲染的思想。

Vue异步渲染思想和意义

但是Vue的视图渲染是异步的,异步的过程是数据改变不会立即更新视图,当数据全部修改完,最后再统一进行视图渲染。

在渲染的过程中,中间有一个对虚拟dom进行差异化的计算过程(diff算法),大量的修改带来频繁的虚拟dom差异化计算,从而导致渲染性能降低,异步渲染正是对视图渲染性能的优化。

Vue异步渲染视图的原理

  • 依赖数据改变就会触发对应的watcher对象中的update

 /**

* Subscriber interface.

* Will be called when a dependency changes.

*/

update () {

/* istanbul ignore else */

if (this.lazy) {

this.dirty = true

} else if (this.sync) {

this.run()

} else {

queueWatcher(this)

}

}

  • 默认的调用queueWatcher将watcher对象加入到一个队列中

/**

* Push a watcher into the watcher queue.

* Jobs with duplicate IDs will be skipped unless it's

* pushed when the queue is being flushed.

*/

export function queueWatcher (watcher: Watcher) {

const id = watcher.id

if (has[id] == null) {

has[id] = true

if (!flushing) {

queue.push(watcher)

} else {

// if already flushing, splice the watcher based on its id

// if already past its id, it will be run next immediately.

let i = queue.length - 1

while (i > index && queue[i].id > watcher.id) {

i--

}

queue.splice(i + 1, 0, watcher)

}

// queue the flush

if (!waiting) {

waiting = true

nextTick(flushSchedulerQueue)

}

}

}

当第一次依赖有变化就会调用nextTick方法,将更新视图的回调设置成微任务或宏任务,然后后面依赖更新对应的watcher对象都只是被加入到队列中,只有当nextTick回调执行之后,才会遍历调用队列中的watcher对象中的更新方法更新视图。

这个nextTick和我们在业务中调用的this.$nextTick()是同一个函数。

if (!waiting) {

waiting = true

nextTick(flushSchedulerQueue)

}

flushSchedulerQueue刷新队列的函数,用于更新视图

function flushSchedulerQueue () {

flushing = true

let watcher, id

// Sort queue before flush.

// This ensures that:

// 1. Components are updated from parent to child. (because parent is always

// created before the child)

// 2. A component's user watchers are run before its render watcher (because

// user watchers are created before the render watcher)

// 3. If a component is destroyed during a parent component's watcher run,

// its watchers can be skipped.

queue.sort((a, b) => a.id - b.id)

// do not cache length because more watchers might be pushed

// as we run existing watchers

for (index = 0; index < queue.length; index++) {

watcher = queue[index]

id = watcher.id

has[id] = null

watcher.run()

// in dev build, check and stop circular updates.

if (process.env.NODE_ENV !== 'production' && has[id] != null) {

circular[id] = (circular[id] || 0) + 1

if (circular[id] > MAX_UPDATE_COUNT) {

warn(

'You may have an infinite update loop ' + (

watcher.user

? `in watcher with expression "${watcher.expression}"`

: `in a component render function.`

),

watcher.vm

)

break

}

}

}

那么nextTick到底是个什么东西呢?

nextTick的原理

vue 2.5中nextTick的源码如下(也可以跳过源码直接看后面的demo,来理解nextTick的用处):

/**

* Defer a task to execute it asynchronously.

*/

export const nextTick = (function () {

const callbacks = []

let pending = false

let timerFunc

function nextTickHandler () {

pending = false

const copies = callbacks.slice(0)

callbacks.length = 0

for (let i = 0; i < copies.length; i++) {

copies[i]()

}

}

// An asynchronous deferring mechanism.

// In pre 2.4, we used to use microtasks (Promise/MutationObserver)

// but microtasks actually has too high a priority and fires in between

// supposedly sequential events (e.g. #4521, #6690) or even between

// bubbling of the same event (#6566). Technically setImmediate should be

// the ideal choice, but it's not available everywhere; and the only polyfill

// that consistently queues the callback after all DOM events triggered in the

// same loop is by using MessageChannel.

/* istanbul ignore if */

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {

timerFunc = () => {

setImmediate(nextTickHandler)

}

} else if (typeof MessageChannel !== 'undefined' && (

isNative(MessageChannel) ||

// Phantomjs

MessageChannel.toString() === '[object MessageChannelConstructor]'

)) {

const channel = new MessageChannel()

const port = channel.port2

channel.port1.onmessage = nextTickHandler

timerFunc = () => {

port.postMessage(1)

}

} else

/* istanbul ignore next */

if (typeof Promise !== 'undefined' && isNative(Promise)) {

// use microtask in non-DOM environments, e.g. Weex

const p = Promise.resolve()

timerFunc = () => {

p.then(nextTickHandler)

}

} else {

// fallback to setTimeout

timerFunc = () => {

setTimeout(nextTickHandler, 0)

}

}

return function queueNextTick (cb?: Function, ctx?: Object) {

let _resolve

callbacks.push(() => {

if (cb) {

try {

cb.call(ctx)

} catch (e) {

handleError(e, ctx, 'nextTick')

}

} else if (_resolve) {

_resolve(ctx)

}

})

if (!pending) {

pending = true

timerFunc()

}

// $flow-disable-line

if (!cb && typeof Promise !== 'undefined') {

return new Promise((resolve, reject) => {

_resolve = resolve

})

}

}

})()

用下面这个demo来感受依赖更新时和nextTick的关系以及nextTick的用处:

 function isNative(Ctor) {

return typeof Ctor === 'function' && /native code/.test(Ctor.toString())

}

const nextTick = (function () {

let pending = false;

let callbacks = []

let timerFunc

function nextTickHandler() {

pending = false

const copies = callbacks.slice(0)

callbacks.length = 0

for (let i = 0; i < copies.length; i++) {

copies[i]()

}

}

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {

timerFunc = () => {

setImmediate(nextTickHandler)

}

} else if (typeof MessageChannel !== 'undefined' && (

isNative(MessageChannel) ||

// Phantomjs

MessageChannel.toString() === '[object MessageChannelConstructor]'

)) {

const channel = new MessageChannel()

const port = channel.port2

channel.port1.onmessage = nextTickHandler

timerFunc = () => {

port.postMessage(1)

}

} else

/* istanbul ignore next */

if (typeof Promise !== 'undefined' && isNative(Promise)) {

// use microtask in non-DOM environments, e.g. Weex

const p = Promise.resolve()

timerFunc = () => {

p.then(nextTickHandler)

}

} else {

// fallback to setTimeout

timerFunc = () => {

setTimeout(nextTickHandler, 0)

}

}

console.log('timerFunc:', timerFunc)

return function queueNextTick(cb, ctx) {

callbacks.push(() => {

if (cb) {

cb.call(ctx)

}

})

// console.log('callbacks:', callbacks)

if (!pending) {

pending = true

console.log('pending...', true)

timerFunc()

}

}

})()

// 模拟异步视图更新

// 第一次先将对应新值添加到一个数组中,然后调用一次nextTick,将读取数据的回调作为nextTick的参数

// 后面的新值直接添加到数组中

console.time()

let arr = []

arr.push(99999999)

nextTick(() => {

console.log('nextTick one:', arr, arr.length)

})

function add(len) {

for (let i = 0; i < len; i++) {

arr.push(i)

console.log('i:', i)

}

}

add(4)

// console.timeEnd()

// add()

// add()

nextTick(() => {

arr.push(888888)

console.log('nextTick two:', arr, arr.length)

})

add(8)的值之后

console.timeEnd()

在chrome运行结果如下:

可以看到第二个nextTick中push的值最后渲染在add(8)的值之后,这也就是nextTick的作用了,nextTick的作用就是用来处理需要在数据更新(在vue中手动调用nextTick时对应的是dom更新完成后)完才执行的操作。

nextTick的原理:
首先nextTick会将外部传进的函数回调存在内部数组中,nextTick内部有一个用来遍历这个内部数组的函数nextTickHandler,而这个函数的执行是异步的,什么时候执行取决于这个函数是属于什么类型的异步任务:微任务or宏任务。

主线程执行完,就会去任务队列中取任务到主线程中执行,任务队列中包含了微任务和宏任务,首先会取微任务,微任务执行完就会取宏任务执行,依此循环。nextTickHandler设置成微任务或宏任务就能保证其总是在数据修改完或者dom更新完然后再执行。(js执行机制可以看promise时序问题&js执行机制)

为什么vue中对设置函数nextTickHandler的异步任务类型会有如下几种判断?

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {

timerFunc = () => {

setImmediate(nextTickHandler)

}

} else if (typeof MessageChannel !== 'undefined' && (

isNative(MessageChannel) ||

// PhantomJS

MessageChannel.toString() === '[object MessageChannelConstructor]'

)) {

const channel = new MessageChannel()

const port = channel.port2

channel.port1.onmessage = nextTickHandler

timerFunc = () => {

port.postMessage(1)

}

} else

/* istanbul ignore next */

if (typeof Promise !== 'undefined' && isNative(Promise)) {

// use microtask in non-DOM environments, e.g. Weex

const p = Promise.resolve()

timerFunc = () => {

p.then(nextTickHandler)

}

} else {

// fallback to setTimeout

timerFunc = () => {

setTimeout(nextTickHandler, 0)

}

}

浏览器环境中常见的异步任务种类,按照优先级:

  • macro task:同步代码、setImmediate、MessageChannel、setTimeout/setInterval
  • micro task:Promise.then、MutationObserver

而为什么最后才判断使用setTimeout?
vue中目的就是要尽可能的快地执行回调渲染视图,而setTimeout有最小延迟限制:如果嵌套深度超过5级,setTimeout(回调,0)就会有4ms的延迟。

所以首先选用执行更快的setImmediate,但是setImmediate有兼容性问题,目前只支持Edge、Ie浏览器:

可以用同样执行比setTimeout更快的宏任务MessageChannel来代替setImmediate。MessageChannel兼容性如下:

当以上都不支持的时候,就使用new Promise().then(),将回调设置成微任务,Promise不支持才使用setTimeout。

资源搜索网站大全 https://www.renrenfan.com.cn

广州VI设计公司https://www.houdianzi.com

总结:

nextTick就是利用了js机制执行任务的规则,将nextTick的回调函数设置成宏任务或微任务来达到在主线程的操作执行完,再执行的目的。

在vue中主要提供对依赖Dom更新完成后再做操作的情况的支持

 

nextTick的使用场景

当改变数据,视图没有按预期渲染时;都应该考虑是否是因为本需要在dom执行完再执行,然而实际却在dom没有执行完就执行了代码,如果是就考虑使用将逻辑放到nextTick中,有的时候业务操作复杂,有些操作可能需要更晚一些执行,放在nextTick中仍然没有达到预期效果,这个时候可以考虑使用setTimeout,将逻辑放到宏任务中。

基于以上分析,可以列举几个nextTick常用到的使用场景:

  • 在created、mounted等钩子函数中使用时。
  • 对dom进行操作时,例如:使用$ref读取元素时

        // input 定位

scrollToInputBottom() {

this.$nextTick(() => {

this.$refs.accept_buddy_left.scrollTop =

this.$refs.accept_buddy_left.scrollTop + 135

this.$refs.accept_buddy_ipt[

this.$refs.accept_buddy_ipt.length - 1

].$refs.ipt.focus()

})

},

  • 计算页面元素高度时:

        // 监听来自 url 的期数变化,跳到该期数

urlInfoTerm: {

immediate: true,

handler(val) {

if (val !== 0) {

this.$nextTick(function() {

// 计算期数所在位置的高度

this.setCellsHeight()

//设置滚动距离

this.spaceLenght = this.getColumnPositionIndex(

this.list,

)

setTimeout(() => {

this.setScrollPosition(val)

}, 800)

})

}

},

以上是 Vue之nextTick原理 的全部内容, 来源链接: utcz.com/z/379509.html

回到顶部