根据$nextTick一个怪异的现象经过窥探源码发现vue惊天地泣鬼神的神来之笔

事情是这样的, 这是一个在某天的默默的开发中, 笔者发现了一个惊天地泣鬼神的抓破脑壳都想不破的问题, 然后在这个月黑风高的晚上, 通过对源码的窥探终于发现原因的悲惨故事

我们先来看一个demo, 关于$nextTick的使用这里就不再赘述了

<!-- html结构非常的easy,  在vue接管的id为app的dom区域内渲染了msg -->
<div id='#app'>
    {{ msg }}
</div>
const vm = new Vue({
    el: '#app',
    data: {
        msg: 'helloWorld'
    }
})

// 1. 首先页面中一定会渲染出helloWorld

// 2. 第一个$nextTick
vm.$nextTick(() => {
    console.log('我是第一个$nextTick的输出', vm.msg, vm.$el.innerHTML);
})

// 3. 更改msg的值
vm.msg = 'yes i do';
console.log(vm.msg, vm.$el.innerHTML);

// 4. 第二个$nextTick
vm.$nextTick(() => {
    console.log('我是第二个$nextTick的输出',vm.msg, vm.$el.innerHTML);
})

console.log(vm.msg, vm.$el.innerHTML);

我们可以看到输出结果如下

$nextTick输出结果

笔者一开始是真实的一脸懵逼, 我当时唯一可以确定的是$nextTick是异步的, 当然Vue对于界面的更新本来就是异步的, 这个等于是在说废话, 但是两个nextTick的结果竟然不一样?说好的会延迟到下一次dom更新才执行呢? 于是笔者去Vue的官网仔细的翻了翻

来自vue官方:

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

翻译成人话就是 vue的每次数据更新都会在下一个事件循环(eventloop)中执行

那么结合我们上方的输出结果, 笔者短暂认为$nextTick是在下一次事件循环中dom更新完毕后执行


那么问题来了: 既然$nextTick会在dom更新后执行, 为何第一个中打印dom的值依旧没有发生改变呢?既然没改变就意味着他没有在dom更新后执行啊? 这到底啥情况

如果你不看源码 一定是百思不得其解的, 因为vue有时候确实设计的非常精妙

笔者来用自己的方法给你写一写你能够看的明白的$nextTick, 跟着注释看我相信你是不会迷路的

const nextTick = (function() {
   let callbacks = [];  // 最后所有在nextTick中传递过来的函数都会进入这个数组 
   
   let timerHandler = () => { // 这个函数用来延迟nexTick传递进来的函数的执行
     // Promise.resolve这句话往这里一站, 你就知道这哥们后面的那行then代码要等待了,
       const p = Promise.resolve(); 
       p.then(releaseCallbacks); // 等同步任务执行完毕这哥们会执行
   }
   
  function releaseCallbacks() { // 作为p.then的回调 releaseCallbacks肯定也会在微队列中等待
      
      for(let i = 0; i < callbacks.length; i++) {
          callbacks[i](); // 所有存储进callbacks的函数挨个执行
      }
      callbacks.length = 0;
  }

  // 真正暴露给用户的回调函数
  return function(cb) {
      callbacks.push(cb);
      timerHandler();
  } 
}())

调用我们自己的的nextTick方法,其他语句都不变 我们走一遍输出发现输出结果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M06r2HMg-1591801267706)(./自己写的nextTick.png)]

确实发现所有交付给nextTick的函数都按照异步执行了, 但是并没有如我们所想象的那样, 相反连nextTick真正的作用都发挥不上了, 我们不再可以监听到msg被更改, 于是我们来看看被笔者进行注释过后的真正的$nextTick源码(当然, 前提是上面笔者的这份简化版源码你已经看懂了, 不然vue源码会更加头大)

export let isUsingMicroTask = false // 这是vue用来判断是否启用微任务的锁, 如果不懂没关系他不重要

const callbacks = [] // 同样, 最后所有在nextTick中传递过来的函数都会进入这个数组
let pending = false // 异步锁, 如果同步任务未执行完, 异步锁肯定是锁住的

function flushCallbacks () { // 最终执行callbacks的函数
  pending = false // 重置异步锁

  // 这里我们发现将callbacks复制了一份给copies, 最终循环操作的也是copies, 这是因为不想造成nextTick嵌套调用的冲突
  const copies = callbacks.slice(0) 
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc // 相当于上面的timerHandler


// 判断当前环境支不支持原生的Promise构造函数
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    // 如果支持会走上面的timerHandler的流程
  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]'
)) {
    // 判断是不是IE
  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)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 真正暴露出去的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 这个是新加的, 如果没有传递cb参数则返回一个新的promoise出去
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

抛开一些兼容性写法和一些容错机制来说, vue的nextTick和我们写的nextTick没有什么差别, 但是为什么会产生截然不同的效果呢?

继续阅读源码笔者有发现, vue中还存在一个queueWatcher方法, 如下

  function queueWatcher (watcher) {
    var 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.
        var 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;

        if (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }

你不用将他看懂, 但是笔者可以告诉你这哥们的作用就是用来更改nextTick的执行顺序的

本身我们执行nextTick他的效果跟一般的异步任务没什么太大的区别, 无非就是nextTick会被置于微任务, 而queueWatcher方法和他带来的一些骚操作则改变了nextTick的运行轨迹

  1. 如果在$nextTick前没有更改vue监控的属性值的情况发生, 那么nextTick中的代码按照正常异步微任务走掉

  2. 如果在$nextTick前有更改了vue所监控的属性值的情况, 则queueWatcher会调换nextTick的执行顺序, nextTick将会在下一次事件循环vue刷新页面后执行

有些东西你不看源码是真的想破头都想不出来他到底是什么原因, 这也是我们作为开发者一直要追逐的事情, 共勉

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/106677239