阅读 1236
剖析vue中的nextTick

剖析vue中的nextTick

这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战

本文从nextTick API 的概念到使用,再到源码,层层剖析。系统地回顾nextTick的相关用法,以及内部调用逻辑。

概念

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

大白话:nextTick是vue中的批量异步更新策略。监听到组件的变化时,不会立即更新,会开启一个队列,同一watcher只入队一次。在下一次事件循环时,刷新队列执行更新。

原理

使用nextTick接收传入的回调函数,将回调函数暂时存放到一个队列中,开启异步更新(调用timerfuc函数)。在timerfuc中会优先使用Promise微任务去执行队列中的所有回调,在下一次事件循环时,更新页面。

在timerfuc中还存在一个降级的过程,是考虑浏览器的兼容设置的。 降级顺序: Promise => MutationObserver => setImmediate => setTimeout

image.png

可以看到nextTick其实是利用了浏览器的事件循环机制。会将nextTick中的任务存放到微任务队列中去(如果浏览器兼容微任务)。
当函数执行栈为空时,会去判断微任务队列中是否存在微任务,如果存在就会先去执行微任务队列中的任务(可以执行nextTick中的任务),然后再去执行宏任务。

image.png 关于JS运行机制的细节可以点击 JS运行机制 这篇文章。

如何工作

使用

nextTick会接收两个传入对象,一个是用户的回调,还有一个是上下文对象。在组件内,用户使用的时候,this 已经自动绑定到当前的 Vue 实例上,所有就不需要全局的Vue去调用 Vue.nextTick(cb)

  <div id="app">
    <h1>异步更新</h1>
    <p id="p1">{{count}}</p>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        count: '原始值'
      },
      mounted() {
        this.count = Math.random()
        console.log('第一次改动值:', this.count)
        this.$nextTick(() => {
          console.log('innerHTML', p1.innerHTML);
        })
      },
    })
  </script>
复制代码

image.png 我们可以看到在页面加载之后,count成功获取到了第一次改动的值,并在页面上更新。

源码剖析

那么,页面它是怎么渲染的呢?
老样子,我们通过控制台打断点来看看源码内部到底发生了什么吧 ^_^ !

image.png 首先从this.count的赋值,因为变量是通过this获取的,并不是使用this内部的$data,所以会走到变量的代理proxy中,然后进入了defineRective中的set拦截。 image.png 在set拦截中,回去通知dep做更新操作。

image.png

notify中执行watcher的更新函数update。在此之前,我们可以看出,存在一个watcher的排序,源码中将先创建的更高级的watcher放到前面,先执行。

image.png 通过遍历执行update,尝试将传入的watcher实例入队,启动异步任务。

image.png

在nextTick中添加一个flushSchedulerQueue回调。并将其放到callbacks数组中。 image.png 启动异步任务timerfuc

image.png 在异步任务中,执行flushCallbacks

image.png 在其内部就是遍历执行callbacks数组就相当于在执行之前的flushSchedulerQueue回调。
而在flushSchedulerQueue回调中,是遍历所有的watchers,执行他们的run函数。 image.png 在watcher的run函数中,会执行自身的get。真正会走到组件的更新updateComponent。在updateComponent内部,会先执行render()得到虚拟dom,执行_update()接收vnode,再执行patch(oldvnode,vnode),真实dom变更。

image.png

大家可以通过看这张nextTick流程图,再配合控制台流程进行解读。

未命名.png

应用

1、获取dom更新后最新数值。 根据本文案例,我们将count的修改n遍后,想获取dom更新后最新的count值时:

mounted() {
  this.count = Math.random()
  console.log('第一次改动值:', this.count)
  this.count = Math.random()
  console.log('第二次改动值:', this.count)
  this.count = Math.random()
  console.log('第三次改动值:', this.count)
  this.$nextTick(() => {
    console.log('innerHTML', p1.innerHTML);
  })
},
复制代码

image.png 注意点

  • 当我将nextTick放到最前面时,将会获取不到最新值,值会是原始值。

image.png 这是因为在nextTick时,想要的变量还未进入到异步更新的队列中。在异步更新队列中nextTick先进入,然后才是相关变量进入队列当中,所以nextTick输出的会是原始值。

  • 放到第一次或第二次修改值后面就不会发生变化。

image.png 这是因为相关watcher只入队一次。在第一次修改值的时候,count变量就会进入异步更新队列中,然后nextTick才会进入。根据队列特性,会先执行count变量的修改,再执行nextTick。所以仍然可以获取到count的最新值。

  • 如果前面有变量修改,nextTick会先于Promise执行。
mounted() {
  this.count = Math.random()
  console.log('第一次改动值:', this.count)
  Promise.resolve().then((res) => {
    console.log('Promise', p1.innerHTML);
  })
  this.$nextTick(() => {
    console.log('innerHTML', p1.innerHTML);
  })
  this.count = Math.random()
  console.log('第二次改动值:', this.count)
  this.count = Math.random()
  console.log('第三次改动值:', this.count)
},
复制代码

在修改相关值时,已经触发异步更新队列,会将相关变量先存放到队列当中,然后再去执行Promise,将其放入到异步队列中。 image.png

2、点击获取元素的宽高、滚动位置等

感兴趣的朋友可以关注 Vue源码初识专栏或者关注我哦,会持续输出vue相关知识哦(●'◡'●)。 如果不足,请多指教。

文章分类
前端
文章标签