vue-nextTick原理和实现

1,230 阅读4分钟

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

注:以下是个人理解、如有不对还望指正!

nextTick使用😁

方法描述:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM、也就意味着你在data初始化数据进行页面渲染成功后走的一个回调钩子、它支持在生命周期created、mounted或者任意方法内部使用

nextTick案例😄

需求、进入页面我们通过一系列操作成功后显示一个滚动元素、然后对显示的元素进行滚动事件监听

    //html
    <div id="app">
        <div id="name" v-if="show">滚动条内容测试</div>
        <button @click="changeStatus">操作成功按钮</button>
    </div>
    //js
    data(){
      return{
        show:false
      }
     },
     methods:{
       changeStatus(){
          this.show = true;
          //获取不到节点而报错
          let dom = document.getElementById('name')
          dom.addEventListener('scroll',(e) =>{ console.log(e) })
          //使用nextTick方式获取dom进行监听
          this.$nextTick( () =>{
             let dom = document.getElementById('name')
             dom.addEventListener('scroll',(e) =>{ console.log(e) })
          })
       }
     }
    

上面案例我们知道当我们去修改data的show状态值、立刻在同步代码执行获取dom、发现我们并不能马上获取、这是因为vue不会即时进行更新dom、而是把需要更新dom的操作存放在异步更新的队列、试想如果同步每次一修改就去更新操作、堵塞同步代码、页面进行多次渲染、很浪费性能 当然异步也会有其他问题的引入、vue也给我们提供了dom更新后的钩子$nextTick为了就是解决异步更新获取dom的操作

nextTick思考🤔

这里我们引入一段nextTick实现的思考、为什么他这个API就能实现dom更新后进行触发呢??有没有其他的原生方法呢、答案是:有的

setTimeout( () =>{
   console.log('setTimeout' ,document.getElementById('name') )
   let dom = document.getElementById('name')
   dom.addEventListener('scroll',(e) =>{ console.log(e) })
})

发现也能正常获取到dom、这是什么原理呢?下面我们开始nextTick的源码分析

nextTick源码分析🤔

定位到源码的/src/core/util/next-tick.js 我们把核心的粘贴出来

  • 保存nextTick里面的回调方法集合、每次页面渲染后去清空执行这些回调
const callbacks = []
//定义一个状态标记、确保每次更新后只会有一个flushCallbacks函数在运行
let pending = false
  • 执行清空操作
function flushCallbacks () {
  //初始化标记
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
  • 核心、利用eventLoop原理去判断环境、执行清空方法
//优先判断是否存在Promise、利用微任务去执行清空回调
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true

  //没有promsie的情况、利用h5提供给我们dom变化后的回调去实现
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  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
  //还没有就判断 setImmediate是否有、作者说setImmediate依然比setTimeout更好
  //该方法用于分解长时间运行的操作,并在浏览器“完成其他操作“(后立即运行回调函数
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
  // 最后才去用setTimeout
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

通过源码我们发现、vue利用js事件循环机制“eventLoop“原理去做的这一个操作、所以为什么我们在代码里面加入setTimeout也能获取到dom的原因

疑惑🤔

那为什么vue能过准确的知道什么时候更新好了呢、顺着源码往下看、 在/src/core/observer/scheduler.js有一段

// queue 队列
export function queueWatcher (watcher: Watcher) {
     //...忽略
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      //执行了nextTick、那么就有可能触发flushCallbacks回调
      nextTick(flushSchedulerQueue)
    }
  }
}

这里他把需要更新的组件watcher队列方法丢到nextTick、利用队列先进先出原理、等组件执行更新完后在执行nextTick剩下开发者定义的钩子

总结

大概流程是、组件里面执行方法、第一行代码去修改data变量、修改后会进入vue的Set方法触发dep.notify方法、通知收集的watcher去执行update方法、update方法会把自己放入watcher队列、然后把队列放入nextTick钩子这个时候会去判断nextTick里面是否已经有在执行的flushCallbacks、如果没有则会触发next-tick.js的timerFunc执行(此时执行会把callbacks放到微任务或者宏任务、然后回到我们的代码第一行结尾,继续往下执行、微或宏暂时放入队列等待方法同步执行完后调用、这个时候就是执行更改data数据下一行代码、this.$nextTick(...)、又往callbacks添加了一个我们自己定义的回调方法、记住此时里面还有一个组件更新的方法还没开始执行、他是比我们先进队列所以先执行、所以为什么我们能在下一个回调里面拿到dom元素、因为这个时候vue已经把dom更新上去了才去执行剩下的callback方法、所以利用这个规则其实我们在自己代码里面写Promise.resolve().then( () => { ...获取dom } ,也能拿到dom!