Vue源码阅读笔记——$nextTick原理

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

一、用法

Vue.nextTick( [callback, context] )
复制代码

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

二、为什么需要$nextTick

我们先看一个例子

<template>
    <h1 ref="msgRef">{{message}}</h1>
    <button @click="handleChangeMsg">点击</button
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      message: '小Y'
    }
  },
  methods: {
    handleChangeMsg() {
      this.message = 'DanDan'
      console.log(222, this.$refs['msgRef'].textContent)
      this.$nextTick(() => {
        console.log(1111, this.$refs['msgRef'].textContent)
      })
    }
  },
}
</script>
复制代码

image.png

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

对于上面例子的问题,我们就清楚了,Vue 在更新 DOM 时是异步执行的,在我们修改完成message的数据后,我们无法马上就拿到最新的DOM,这时我们需要应用nextTick来处理。

三、源码分析

在分析源码前,你需要明白什么是事件循环,如果你还不清楚,可以先了解一下什么是事件循环。

nextTick的源码在/src/core/util/next-tick.js中,代码不多就上百行代码,大家可以读一下。

首先我们看nextTick方法的定义

export function nextTick (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 => {
      _resolve = resolve
    })
  }
}

复制代码

我们可以看到在执行nextTick的时候,将回调函数cb追加到callbacks这个数组里面,然后有一个pending,我们看到在外层定义pending为false,false时会去执行timerFunc,我们再看timerFunc。

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
复制代码

对于timerFunc函数,我们看到对他的赋值,有四层判断。我们知道,异步处理会涉及到微任务和宏任务。源码里面我们看到先使用原生的Promise.thenMutationObserversetImmediate,都不支持的时候最后选择setTimeout,采用了一个降级的处理将flushCallbacks延迟执行。

对于flushCallbacks,它写的很简单,先是将pending置为false,然后将callbacks复制一份然后去循环执行,将callbacks的长度置为0。回过头我们看pending,它其实就是一个标识,在一个事件循环里面只执行一次timerFunc。

四、$nextTick原理总结

  • 首先将调用nextTick的回调添加到callbacks数组中,我们将它看作一个队列。
  • 然后有一个标识pending,这个标识的目的就是,在一次事件循环里面,只执行一次timerFunc函数。
  • 由于Vue对于DOM的更新是异步的,当我们修改完数据,不能马上拿到最新的DOM,所以我们需要nextTick去处理这些异步,我们自然想到微任务和宏任务去处理。源码里面Promise.thenMutationObserversetImmediatesetTimeout去降级处理去处理,将flushCallbacks函数包裹在timerFunc去执行。

猜你喜欢

转载自juejin.im/post/7082604301129875463