Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从JavaScript的运行机制来了解Vue的nextTick #5

Open
dravenww opened this issue Feb 8, 2021 · 0 comments
Open

从JavaScript的运行机制来了解Vue的nextTick #5

dravenww opened this issue Feb 8, 2021 · 0 comments
Labels
js javascript vue

Comments

@dravenww
Copy link
Owner

dravenww commented Feb 8, 2021

前篇

nextTick(flushSchedulerQueue)

nextTick是Vue里面一个比较核心的概念;不过在讲nextTick之前就必须要讲到JavaScript的运行机制和任务队列。

JavaScript的运行机制

众所周知,浏览器的脚本语言是JavaScript,这个语言最大的特点就是单线程,也就是在同一时间只能干一件事情。

为什么是单线程的呢?假定有两个线程,一个操作dom,一个删除dom,岂不就乱套了~

当然为了充分利用CPU,Html5提出了web worker,允许开发人员创建多个线程,但是子线程完全受主线程控制,但是不得操作DOM,这也是遵循了单线程的标准。

单线程呢,也就意味着所有的任务,都需要排队运行,一个任务运行结束后,才会去执行下一个任务;

熟悉JavaScript的开发人员都明白,有异步回调这个概念,也就是说会挂起等待中的任务,去执行下一个任务,等回调回来再去执行被挂起的任务。

综上所述,任务分为两种,一个是同步任务(synchronous,简称sync),一个是异步任务(asynchronous,简称async)。

  • 同步任务指的是,在主线程上面排队执行的任务,一个任务的结束,才能执行下一个任务;
  • 异步任务指的是,不在主线程上面的任务,而是在任务队列中,主线程执行完成后询问任务队列,从任务队列中取的一个任务,放到主线程中执行。
    所以简要图示一下,就是这样的:

主进程会不断重复获取步骤,执行完一个qtask,则继续询问qtask任务队列,获取qtask,放到主线程来执行。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制

任务队列

任务队列,也就是异步任务的队列。分为两种类型的任务:微任务(microtask)和宏任务(macrotask)

宏任务(macrotask):

  • 包括:setTimeout、setInterval、setImmediate、I/O、UI renderingmacrotask事件;
  • 可以理解为浏览器执行完当前宏任务后,在下一个宏任务执行之前,浏览器就会开始进行渲染;
  • 宏任务一般是当前事件循环的最后一个任务,浏览器的ui绘制会插在每个宏任务之间,阻塞宏任务会导致浏览器ui不渲染;
  • 其实也可以把主线程的任务当作第一个宏任务来看待。

微任务(microtask):

  • 包括:Promises(浏览器实现的原生Promise)、MutationObserver、process.nextTick;
  • 浏览器进行ui渲染之前执行的任务,也就是ui渲染是在微任务执行完成后才开始的;
  • 值得注意的是,过多的微任务会阻塞浏览器的渲染;给microtask队列添加过多回调阻塞macrotask队列的任务;
  • 鉴于上面问题,浏览器考虑性能的问题,也会对微任务的数量进行限制;
  • 事件的冒泡行为,也是在微任务后执行,微任务的优先级是最高的;
    举个例子:
console.log('main start');

setTimeout(() => {
  console.log('macrotask');
  Promise.resolve().then(() => {
    console.log('microtask 1');
  })
}, 0);

Promise.resolve().then(() => {
  console.log('microtask 2');
  Promise.resolve().then(() => {
    console.log('microtask 3');
  })
})

console.log('main end');

上面模仿了一下微任务(Promise)和宏任务(setTimeout);微任务里面套了个微任务;宏任务里面套了个微任务;
输出如下:

main start
main end
microtask 2
microtask 3
macrotask
microtask 1

可以分析下上面代码的执行顺序:

  • 第一步:先执行的是主线程的代码main start和main end;
  • 第二步:开始执行微任务microtask2,执行microtask2过程中,又添加了一个microtask3的微任务,
  • 第三步:执行完microtask2后,继续从microtask队列中取微任务,发现有刚在执行microtask2过程中放进去的3,取出microtask3来执行microtask3;
  • 第四步:执行完microtask3后,继续从microtask队列中去取微任务,此时微任务队列为空,则去宏任务队列中取任务,取到macrotask;
  • 第五步:执行macrotask,执行的过程中,又往微任务队列存了个microtask1;
  • 第六步:执行完macrotask后,此时一个宏任务执行完成,开始下一轮重复,也就回到了上面的步骤2,微任务队列获取微任务,发现了microtask1;
  • 第七步:执行microtask1,执行完成,程序运行完成。

综上分析任务队列完成

nextTick

经过上面的过程,相信大家都对浏览器的运行机制和任务队列有了足够的了解,也明白了任务队列中任务的执行顺序,接下来咱们看下nextTick的实现。文件位于/src/core/util/next-tick.js,先看下Vue里面任务的代码:

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) ||
  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
    console.log('counter', counter)
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

上面代码是Vue对timerFun的定义,Vue倾向于微任务,毕竟微任务优先级是最高的,咱们来看下实现:

  • 最优先采用的是Promise,直接使用的也是咱们上面的例子:Promise.resolve().then(flushCallbacks);
  • 如果浏览器不支持原生的Promise,退而求其次,使用浏览器自带的MutationObserver;MutationObserver,它会在指定的DOM发生变化时被调用;Vue的实现方式是创建一个dom节点,通过改变节点的内容,来触发MutationObserver的回调:new MutationObserver(flushCallbacks);
  • 如果浏览器也不支持MutationObserver,那没办法了,只能使用宏任务了setImmediate和setTimeout,这两个在Vue里面使用方式是一样的,两者的执行顺序在无I/O的时候说不准,不过在有I/O的时候setImmediate是会先被执行的,这可能也是Vue先考虑使用setImmediate的原因吧。

来看下nextTick的代码吧:

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) {
      console.log('_resolve')
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

上面部分代码,会对传进来的cb进行存储,放到全局变量callbacks里面,然后判断当前执行的状态,是否属于pending(类似Promise的pending状态)状态,可以理解为忙着呢,如果不忙,就让它忙起来,执行上面部分讲到的timerFun;这部分也就是咱们经常用到的

$nextTick(function() {
	dosomething....
})

下面咱们来看下调用timerFun后,执行的flushCallbacks;

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

执行开始,置为不忙状态,因为浏览器是单线程的,执行这段代码的时候,就不会执行别的代码,不用担心这时候会有别的事情影响此处代码的执行,也不用担心此时会有nextTick的调用,也就不用担心pending状态的此处改变会不会影响nextTick部分的逻辑;

此处代码很简单,获取回调的拷贝,然后把回调栈清空;依次执行回调。

#记得star

@dravenww dravenww added js javascript vue labels Feb 8, 2021
@dravenww dravenww reopened this Mar 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
js javascript vue
Projects
None yet
Development

No branches or pull requests

1 participant