vue2 nextTick源码分析以及实现原理详解

参考资料

vue2源码

github

问题描述

写这篇博客的原因是因为我的朋友问我vue中nextTick的实现原理,我大概知道是个什么情况,但是要给别人讲清楚还是差的远,因此去读了一下vue2中关于nextTick部分的源码。

此处Que一下小伟要认真学习啊hhhhh

nextTick源码

首先先附上vue(v2.7.7)中nextTick部分的源码(去掉了注释)

/* globals MutationObserver */

import {
    
     noop } from 'shared/util'
import {
    
     handleError } from './error'
import {
    
     isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks: Array<Function> = []
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]()
  }
}

let timerFunc


if (typeof Promise !== 'undefined' && isNative(Promise)) {
    
    
  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]')
) {
    
    
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {
    
    

  timerFunc = () => {
    
    
    setImmediate(flushCallbacks)
  }
} else {
    
    
  // Fallback to setTimeout.
  timerFunc = () => {
    
    
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
 * @internal
 */
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
    
    
  let _resolve
  callbacks.push(() => {
    
    
    if (cb) {
    
    
      try {
    
    
        cb.call(ctx)
      } catch (e: any) {
    
    
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
    
    
      _resolve(ctx)
    }
  })
  if (!pending) {
    
    
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    
    
    return new Promise(resolve => {
    
    
      _resolve = resolve
    })
  }
}

分析源码

export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
    
    
  let _resolve
  callbacks.push(() => {
    
    
    if (cb) {
    
    
      try {
    
    
        cb.call(ctx)
      } catch (e: any) {
    
    
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
    
    
      _resolve(ctx)
    }
  })
  if (!pending) {
    
    
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    
    
    return new Promise(resolve => {
    
    
      _resolve = resolve
    })
  }
}

nextTick的源码其实很简单,最终在暴露了一个nextTick方法给外界。在这个方法里面,cb是传进来的回调函数,ctx是this指向的对象。当存在传进来的回调函数cb的时候,将回调函数push到存储回调函数的数组里面,所有的回调函数都会被push到这个数组里面,并且统一调用执行。

const callbacks: Array<Function> = []

在push进数组后,先判断pending(异步锁)的状态,初始状态是false,然后进入后将pending置为true,调用函数timerFunc。当nextTick没有传入回调函数的时候回,返回一个promise化的调用。

nextTick里面就做了这么多,接下来我们看一下函数timerFunc做了什么

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    
    
  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]')
) {
    
    
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {
    
    

  timerFunc = () => {
    
    
    setImmediate(flushCallbacks)
  }
} else {
    
    
  // Fallback to setTimeout.
  timerFunc = () => {
    
    
    setTimeout(flushCallbacks, 0)
  }
}

别看这个函数好像做了很多东西,其实他就是判断了当前的环境,当当前环境存在promise的时候,用promise作为异步包装方案。当不存在promise的时候用MutationObserver作为异步包装方案,当也不存在MutationObserver的时候,用setImmediate,当setImmediate还不存在的时候就用最保险的setTimeout。

包装成异步后去调用flushCallbacks函数

function flushCallbacks() {
    
    
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    
    
    copies[i]()
  }
}

flushCallbacks就是重置异步锁然后循环调用被压入callbacks里面的回调函数。

总结一下来说nextTick其实就是将传入的回调函数统一压入一个数组里面,然后把这个数组里面的回调函数的调用放在一个异步任务里面去。

分析完源码后接下来就来分析一下nextTick实现的原理

准备工作

要弄清楚nextTick的原理,首先我们需要弄清楚几件事情。
1. nextTick是干什么的
我们知道,在vue里面其实是不鼓励去过多的操作dom的,但是假如我们一定要去操作dom呢?那么可能就会遇到一个问题。那就是dom还没更新完毕,我们就去操作了dom,这样的话,我们可能是拿不到我们想要的结果的。这个时候我们给在操作dom的外面包上nextTick,往往就能解决问题。

this.$nextTick(_ => {
    
    
	document.getElementById('box').innerHtml
});

2. js事件循环是什么(event loop)
我们都知道js是单线程的,至于为什么js是单线程的,是因为发明这个语言的目的就是为了作为浏览器的脚本语言。js的主要用途是与用户进行交互,以及操作dom,这决定了js只能是单线程的,否则会带来很复杂的同步的问题。
那么由于js是单线程的,因此只有当js上一个任务执行完成后才会执行下一个任务。可是假如其中有一个任务耗时很长(例如有一个10s的定时器),那么后面一个任务就不得不等上一个任务执行完再去执行,这显然是不科学的。
js在这种情况下,将任务分为了同步任务和异步任务,当js从上到下执行的时候,遇到了异步任务的时候,会将异步任务依次按照顺序放到异步执行队列里面,然后等待主线程(同步任务)全部执行完毕后,才会回头去执行异步执行队列里面的任务。执行的顺序原则上是按照先进先出的的方式去执行的。主线程从任务队列中读取事件,这个过程是不断循环的,所以这个的运行机制被称为事件循环

3. 异步宏任务和异步微任务
在明白了上面的事件循环后,可以看到在执行的顺序那里我写了原则上是按照先进先出的顺序执行,那么难道异步任务队列里面的执行顺序还有例外吗?其实是有的,因为异步任务,是分为异步微任务和异步宏任务的,异步微任务要先于异步宏任务执行,当然在异步微任务执行的时候,必然也是按照异步微任务进入异步执行队列里面的顺序执行的。同样的,异步宏任务也是这样,那么异步微任务和异步宏任务都有哪些呢?

常见的有:
微任务:promise.then MutationObserver promise.nextTick
宏任务:setInterval setTimeout postMessage setImmediate

问题分析

在确保已经明白了上面三点后,我们来正式分析nextTick实现的原理。

要明白nextTick实现的原理,无非就是弄明白为什么nextTick这个东西,他会在dom更新完毕后才来执行他。我们想一想,我们其实就是在代码里面写了这样一句代码。

this.$nextTick(_ => {
    
    });

可是这个东西为什么就这么神奇,一定是在dom更新完毕之后去执行的呢?他是怎么知道dom更新完毕了呢?是不是一脸懵逼,又很好奇。别着急,要弄懂这个东西,我们又得去搞懂vue的渲染的机制。

首先我们知道,在构建vue对象时,会对传入的数据进行一个getter/setter的转换(vue响应式的原理),我们将其标记为data,然后当一个组件渲染的时候,该组件对应的watcher(观察者)会将使用到的data中的那些数据记录为依赖dep,然后当data中的数据改变的时候,会看哪些watcher记录了改变了的那部分的依赖,然后通知到watcher,告诉他你手下有数据变了,赶紧给我更新视图。但如果一个watcher被多次触发,那么每次触发,都会去触发更新视图吗?答案是不会的,因为这样非常的耗费性能,如果一个watcher被多次触发,那么他只会更新最后一次触发的结果。

而watcher去更新视图又是调用了update方法,我们打开vue源码找到watcher的update部分的代码。

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    
    
    /* istanbul ignore else */
    if (this.lazy) {
    
    
      this.dirty = true
    } else if (this.sync) {
    
    
      this.run()
		/*同步则执行run直接渲染视图*/
      // 基本不会用到sync
    } else {
    
    
      queueWatcher(this)
    }
  }

从源码我们可以看到watcher的update方法。因为vue数据变化的时候不会马上更新页面,而是需要去做去重等等事情(上文说道的多次触发watcher只会更新最后一次因此需要去重),所以这个update走的是queueWatcher方法。

然后我们找到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.
 */
export function queueWatcher(watcher: Watcher) {
    
    
  const id = watcher.id
  if (has[id] != null) {
    
    
    return
  }

  if (watcher === Dep.target && watcher.noRecurse) {
    
    
    return
  }

  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.
    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 (__DEV__ && !config.async) {
    
    
      flushSchedulerQueue()
      return
    }
    nextTick(flushSchedulerQueue)
  }
}

前面的代码先不管,我们可以看到queueWatcher中。最终是调用了nextTick这个方法让视图更新的。

到这个地方我们就大概可以知道nextTick的运行的原理了。

  1. 首先数据更新了,侦听到数据的变化后,watcher会立马去触发update方法(这个是会先于我们自己在代码里面写的nextTick执行的),然后再触发queueWatcher,最后再调用nextTick方法,将函数的调用经过nextTick的异步包装后,放到异步执行队列里面。

  2. 上一步完成后,会将我们在代码里面写的nextTick的回调函数的调用经过异步包装后放入异步执行队列里面,再结合之前讲到的事件循环异步任务的执行顺序。由于他们的异步包装方式是相同的,并且视图更新的方法是先进入队列里面的。因此,我们的代码里面写到的nextTick回调的执行,总是会在dom更新完毕后才会去执行。

综上,就是nextTick实现的原理。

猜你喜欢

转载自blog.csdn.net/yangxbai/article/details/125931148