vue源码分析nextTick()

1、vue页面更新简介

在这里粗略的描述下当数据变化后页面更新流程:
1、通过Observe数据劫持监听数据变化,当数据变化后通知触发闭包内的dep执行dep.notify,
2 、接着执行Watcher的update()
3、update()中并没有立即执行dom的更新,而是将更新事件推送到一个任务队列中。
4、执行任务队列中的方法。
下图是我对数据双向绑定的一个理解。
在这里插入图片描述
这篇文章重点不是理解双向绑定,只要明白每次数据变化,或执行一个函数将更新事件推送到一个任务队列中。

2、macrotasks和microtasks基本了解

参考文章:
javascript中的异步 macrotask 和 microtask 简介
Promise的队列与setTimeout的队列有何关联?–知乎
JavaScript 运行机制详解:再谈Event Loop
Macrotasks和Microtasks 其实都是是属于异步任务。常见的有:
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise, MutationObserver

我的理解:
  • macrotasks(宏任务)microtacks(微任务)。
  • 所有的同步任务都是在主线程上执行。
  • 1先会从宏任务中取出一个任务执行
  • 2宏任务执行完了,会将microtacks(微任务)队列全部取出,依次执行
  • 3然后再去取下一个宏任务

3、源码分析

1. 当数据变化时,触发watcher.update
 /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {    //watcher作为订阅者的update方法
    /* istanbul ignore else */
    // debugger
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 同步则执行run直接渲染视图
      this.run()
    } else {
      // 异步推送到观察者队列中,下一个tick时调用。
      queueWatcher(this)
    }
  }
  
2. queueWatcher 将观察者对象推入任务队列中
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
// 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送 
export function queueWatcher (watcher: Watcher) {
  // 获取watcher的id
  const id = watcher.id
  // 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验
  // console.log('has',has)
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 如果没有flush掉,直接push到队列中即可
      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.
      let 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 (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

从代码中可以看到,flushSchedulerQueue被放入nextTick中,此时处于waiting状态,期间还会不断的有Watcher对象被push进队列queue中(若原先已有相同的Watcher对象存在此次队列中,就不进行重复推送,这里用了has[id]来进行筛选),等待下一个tick。flushSchedulerQueue其实就是watcher视图更新。
关于waiting变量,这是很重要的一个标志位,它保证flushSchedulerQueue回调只允许被置入callbacks一次。只有在下一个tick中执行完flushSchedulerQueue,才会resetSchedulerState()重置调度器状态将waitting置为false,允许重新被推入callbacks。
正常情况callbacks长度一直是1,只有主动去调用Vue.nextTick( [callback, context] )。

3、nextTick内部方法
/**
 * 
 *  推送到队列中下一个tick时执行
    cb 回调函数
    ctx 上下文
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  /*cb存到callbacks中*/
  debugger
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 是否有任务在执行,pending 会在任务执行完更改状态false
  if (!pending) {
    pending = true
    if (useMacroTask) {
      //在Vue执行绑定的DOM事件,导致dom更新的任务,会被推入宏任务列表。例:v-on的一些事件绑定
      macroTimerFunc()
    } else {
      // 微任务
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Vue对任务进行了兼容的写法按顺序 优先监测。对宏任务,和微任务进行声明。
宏任务: setImmediate => MessageChannel =>setTimeout

/**
 * 而对于macroTask的执行,Vue优先检测是否支持原生setImmediate(高版本IE和Edge支持),
不支持的话再去检测是否支持原生MessageChannel,如果还不支持的话为setTimeout(fn, 0)。
 */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  // MessageChannel与原先的MutationObserver异曲同工
/**
在Vue 2.4版本以前使用的MutationObserver来模拟异步任务。
而Vue 2.5版本以后,由于兼容性弃用了MutationObserver。
Vue 2.5+版本使用了MessageChannel来模拟macroTask。
除了IE以外,messageChannel的兼容性还是比较可观的。
**/
 /**
  可见,新建一个MessageChannel对象,该对象通过port1来检测信息,port2发送信息。
  通过port2的主动postMessage来触发port1的onmessage事件,
  进而把回调函数flushCallbacks作为macroTask参与事件循环。
  **/
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  //上面两种都不支持,用setTimeout
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

微任务 :Promise=> macroTimerFunc (setImmediate => MessageChannel =>setTimeout)

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
// 微观任务 microTask 延迟执行
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

源码中有一段注释比较重要:
其大概的意思就是:在Vue2.4之前的版本中,nextTick几乎都是基于microTask实现的,但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终nextTick采取的策略是默认走icroTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。

从上述两个任务声明可以看出,都会传入一个flushCallbacks的函数。下面看下这个函数里面具体执行了什么,主要就是遍历任务队列,执行nextTick 中推入的函数,即flushSchedulerQueue或者开发者手动调用的 Vue.nextTick传入的回调方法。

/*下一个tick时的回调*/
function flushCallbacks () {
  // 一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程
  pending = false
  //复制callback
  const copies = callbacks.slice(0)
   //清除callback
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

参考

【Vue源码】Vue中DOM的异步更新策略以及nextTick机制
Vue.js异步更新DOM策略及nextTick

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值