首頁 > web前端 > Vue.js > 主體

乾貨分享,帶你了解Vue中的Vue.nextTick

青灯夜游
發布: 2022-03-22 11:38:00
轉載
2236 人瀏覽過

這篇文章跟大家分享一下Vue純乾貨,介紹一下你不知道的Vue.nextTick,希望對大家有幫助!

乾貨分享,帶你了解Vue中的Vue.nextTick

用過Vue的朋友多多少少都知道$nextTick~ 在正式講解nextTick之前,我想你應該清楚知道Vue在更新DOM 時是異步執行的,因為接下來講解過程會結合元件更新一起講~ 事不宜遲,我們直轉主題吧(本文以v2.6.14版本的Vue原始碼進行講解)【相關推薦:vuejs影片教學

#一、nextTick小測驗

你真的了解nextTick嗎?來,直接上題~

<template>
  <div id="app">
    <p ref="name">{{ name }}</p>
    <button @click="handleClick">修改name</button>
  </div>
</template>

<script>
  export default {
  name: &#39;App&#39;,
  data () {
    return {
      name: &#39;井柏然&#39;
    }
  },
  mounted() {
    console.log(&#39;mounted&#39;, this.$refs.name.innerText)
  },
  methods: {
    handleClick () {
      this.$nextTick(() => console.log(&#39;nextTick1&#39;, this.$refs.name.innerText))
      this.name = &#39;jngboran&#39;
      console.log(&#39;sync log&#39;, this.$refs.name.innerText)
      this.$nextTick(() => console.log(&#39;nextTick2&#39;, this.$refs.name.innerText))
    }
  }
}
</script>
登入後複製

請問上述程式碼中,當點選按鈕「修改name」時,'nextTick1''sync log' 'nextTick2'對應的this.$refs.name.innerText分別會輸出什麼? 注意,這裡印出來的是DOM的innerText~(文章結尾處會貼出答案)

如果此時的你有非常堅定的答案,那你可以不用繼續往下看了~但如果你對自己的答案有所顧慮,那不如跟著我,接著往下看。相信你看完,不需要看到答案都能有個肯定的答案了~!


二、nextTick原始碼實作

#源碼位於core/util/next-tick。可以將其分為4個部分來看,直接上程式碼

1. 全域變數

callbacks佇列、pending狀態

const callbacks = [] // 存放cb的队列
let pending = false // 是否马上遍历队列,执行cb的标志
登入後複製

2. flushCallbacks

#遍歷callbacks執行每個cb

function flushCallbacks () {
  pending = false // 注意这里,一旦执行,pending马上被重置为false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 执行每个cb
  }
}
登入後複製

3. nextTick的非同步實作

根據執行環境的支援程度採用不同的非同步實作策略

let timerFunc // nextTick异步实现fn

if (typeof Promise !== &#39;undefined&#39; && isNative(Promise)) {
  // Promise方案
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks) // 将flushCallbacks包装进Promise.then中
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== &#39;undefined&#39; && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === &#39;[object MutationObserverConstructor]&#39;
)) {
  // MutationObserver方案
  let counter = 1
  const observer = new MutationObserver(flushCallbacks) // 将flushCallbacks作为观测变化的cb
  const textNode = document.createTextNode(String(counter)) // 创建文本节点
  // 观测文本节点变化
  observer.observe(textNode, {
    characterData: true
  })
  // timerFunc改变文本节点的data,以触发观测的回调flushCallbacks
  timerFunc = () => { 
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== &#39;undefined&#39; && isNative(setImmediate)) {
  // setImmediate方案
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最终降级方案setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
登入後複製
  • 這裡用個真實案例加深對MutationObserver的理解。畢竟比起其他三種非同步方案,這個應該是大家最陌生的
    const observer = new MutationObserver(() => console.log(&#39;观测到文本节点变化&#39;))
    const textNode = document.createTextNode(String(1))
    observer.observe(textNode, {
        characterData: true
    })
    
    console.log(&#39;script start&#39;)
    setTimeout(() => console.log(&#39;timeout1&#39;))
    textNode.data = String(2) // 这里对文本节点进行值的修改
    console.log(&#39;script end&#39;)
    登入後複製
  • 知道對應的輸出會是怎麼樣的嗎?
    • script startscript end會在第一輪巨集任務中執行,這點沒問題

    • setTimeout會被放入下一輪巨集任務執行

    • #MutationObserver是微任務,所以會在本輪巨集任務後執行,所以先於setTimeout

  • 結果如下圖:
    乾貨分享,帶你了解Vue中的Vue.nextTick

4. nextTick方法實作

cbPromise方式

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 往全局的callbacks队列中添加cb
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, &#39;nextTick&#39;)
      }
    } else if (_resolve) {
      // 这里是支持Promise的写法
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 执行timerFunc,在下一个Tick中执行callbacks中的所有cb
    timerFunc()
  }
  // 对Promise的实现,这也是我们使用时可以写成nextTick.then的原因
  if (!cb && typeof Promise !== &#39;undefined&#39;) {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
登入後複製
  • 深入細節,理解pending有什麼用,如何運作?

案例1,同一輪Tick中執行2次$nextTicktimerFunc只會執行一次

this.$nextTick(() => console.log(&#39;nextTick1&#39;))
this.$nextTick(() => console.log(&#39;nextTick2&#39;))
登入後複製
  • 用圖看看比較直覺?

乾貨分享,帶你了解Vue中的Vue.nextTick


三、Vue元件的非同步更新

這裡如果有對Vue元件化派發更新不是十分了解的朋友,可以先戳這裡,看圖解Vue響應式原理了解下Vue組件化和派發更新的相關內容再回來看噢~

Vue的非同步更新DOM其實也是使用nextTick來實現的,跟我們平時使用的$nextTick其實是同一個~

這裡我們回顧一下,當我們改變一個屬性值的時候會發生什麼事?

乾貨分享,帶你了解Vue中的Vue.nextTick

根據上圖派發更新過程,我們從watcher.update開時講起,以渲染Watcher為例,進入到queueWatcher

1. queueWatcher做了什麼?

// 用来存放Wathcer的队列。注意,不要跟nextTick的callbacks搞混了,都是队列,但用处不同~
const queue: Array<Watcher> = []

function queueWatcher (watcher: Watcher) {
  const id = watcher.id // 拿到Wathcer的id,这个id每个watcher都有且全局唯一
  if (has[id] == null) {
    // 避免添加重复wathcer,这也是异步渲染的优化做法
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    }
    if (!waiting) {
      waiting = true
      // 这里把flushSchedulerQueue推进nextTick的callbacks队列中
      nextTick(flushSchedulerQueue)
    }
  }
}
登入後複製

2. flushSchedulerQueue做了什麼?

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // 排序保证先父后子执行更新,保证userWatcher在渲染Watcher前
  queue.sort((a, b) => a.id - b.id)
  // 遍历所有的需要派发更新的Watcher执行更新
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    // 真正执行派发更新,render -> update -> patch
    watcher.run()
  }
}
登入後複製
  • 最後,一張圖搞懂元件的非同步更新過程

乾貨分享,帶你了解Vue中的Vue.nextTick


#、回歸題目本身

相信經過上文對nextTick源碼的剖析,我們已經揭開它神秘的面紗了。這時的你一定可以堅定地把答案說出來了~話不多說,我們一起核實下,看看是不是如你所想!

1、如图所示,mounted时候的innerText是“井柏然”的中文

乾貨分享,帶你了解Vue中的Vue.nextTick

2、接下来是点击按钮后,打印结果如图所示

乾貨分享,帶你了解Vue中的Vue.nextTick

  • 没错,输出结果如下(意不意外?惊不惊喜?)

    • sync log 井柏然

    • nextTick1 井柏然

    • nextTick2 jngboran

  • 下面简单分析一下每个输出:

    this.$nextTick(() => console.log(&#39;nextTick1&#39;, this.$refs.name.innerText))
    this.name = &#39;jngboran&#39;
    console.log(&#39;sync log&#39;, this.$refs.name.innerText)
    this.$nextTick(() => console.log(&#39;nextTick2&#39;, this.$refs.name.innerText))
    登入後複製
    • sync log:这个同步打印没什么好说了,相信大部分童鞋的疑问点都不在这里。如果不清楚的童鞋可以先回顾一下EventLoop,这里不多赘述了~

    • nextTick1:注意其虽然是放在$nextTick的回调中,在下一个tick执行,但是他的位置是在this.name = 'jngboran'的前。也就是说,他的cb会App组件的派发更新(flushSchedulerQueue)更先进入队列,当nextTick1打印时,App组件还未派发更新,所以拿到的还是旧的DOM值。

    • nextTick2就不展开了,大家可以自行分析一下。相信大家对它应该是最肯定的,我们平时不就是这样拿到更新后的DOM吗?

  • 最后来一张图加深理解

  • 乾貨分享,帶你了解Vue中的Vue.nextTick


    写在最后,nextTick其实在Vue中也算是比较核心的一个东西了。因为贯穿整个Vue应用的组件化、响应式的派发更新与其息息相关~深入理解nextTick的背后实现原理,不仅能让你在面试的时候一展风采,更能让你在日常开发工作中,少走弯路少踩坑!好了,本文到这里就暂告一段落了,如果读完能让你有所收获,就帮忙点个赞吧~画图不易、创作艰辛鸭~

    (学习视频分享:vuejs教程web前端

    以上是乾貨分享,帶你了解Vue中的Vue.nextTick的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
vue
來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!