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

Vue/nextTick #2

Open
canvascat opened this issue Nov 24, 2020 · 2 comments
Open

Vue/nextTick #2

canvascat opened this issue Nov 24, 2020 · 2 comments
Labels

Comments

@canvascat
Copy link
Owner

nextTick

Vue 的视图是异步更新的,一般情况下更新数据后等待一个$nextTick()即可保证视图完成更新。

JS 的执行是单线程的,且基于事件循环,具体介绍可以看 事件循环:微任务和宏任务这篇文章。事件循环中有 macro task 和 micro task 的概念,每个 macro task 结束后都要清空所有 micro task。即:

for (let macroTask of macroTaskQueue) {
  // 处理当前宏任务
  handleMacroTask(macroTask);
  // 处理所有微任务
  for (let microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}

在 vue 中,nextTick 的相关代码在src/core/util/next-tick.js中。

在浏览器环境中,常见的 macro task 有 setTimeoutMessageChannelpostMessagesetImmediate
常见的 micro task 有 MutationObseverPromise.then

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

export let isUsingMicroTask = false;

const callbacks = [];
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;

// nextTick利用了微任务队列,可以通过任何一个原生的 Promise.then 或 MutationObserver 访问该队列。
// MutationObserver的支持更广泛,但是在 iOS>=9.33 的 UIWebView中,当触发触摸事件回调时会出现bug,触发几次后完全停止工作…
// 因此优先使用原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
    // 在上述UIWebViews中,Promise.then 会陷入一种奇怪的状态,
    // 即回调被推入微任务队列,但队列没有被刷新,直到浏览器需要做一些其他的工作,比如处理一个计时器。
    // 因此,我们可以通过添加一个空计时器来“强制”刷新微任务队列。
    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 {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

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();
  }

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}

next-tick.js 中申明了一个异步延时器 timerFunc,它会优先使用原生的 Promise.then,若不支持则会使用 MutationObserver,通过更新观察的元素变化实现。若不支持这两种 micro task 则会降级为 macro task,优先使用 setImmediate,不支持则降级为 setTimeout 0

最后向外暴露一个nextTick方法,也就是我们平时使用的 this.$nextTick()。该方法逻辑比较简单,把传入的回调函数 push 到callbacks中,然后执行 timerFunc,会在下一个 tick 执行flushCallbacks,对 callbacks 遍历执行相应的函数并清空该数组。这里使用callbacks保存所有回调统一一次执行而不是放在nextTick中直接执行 cb 是为了保证在同一个 tick 内的多次执行nextTick不会开启多个异步任务,而是把这些异步任务压成一个同步任务在下一个 tick 中执行完毕。

另外该文件还向外暴露了一个变量 isUsingMicroTask,该变量只在src\platforms\web\runtime\modules\events.js中的用于v-onadd方法中使用过。

在 vue2.5 版本中,nextTick使用的是 mocro task 和 macro task 相结合的方式处理。

@canvascat
Copy link
Owner Author

@canvascat
Copy link
Owner Author

UI渲染相关JavaScript:event loop详解

关于 ResizeObserver

let counter = 1;

const textNode = document.createTextNode(String(9 + counter));
const divNode = document.createElement('div');
divNode.style.width = '10px';
divNode.style.wordBreak = 'break-all';
divNode.appendChild(textNode);
document.body.appendChild(divNode);


const r_ob = new ResizeObserver(() => {
  console.log('R');
});
r_ob.observe(divNode);
const m_ob = new MutationObserver(() => {
  console.log('M');
});
m_ob.observe(textNode, { characterData: true, });

divNode.addEventListener('click', () => {
  textNode.data = String(9 + (++counter) % 2);
  setTimeout(() => {
    console.log('T');
  });
  Promise.resolve().then(() => {
    console.log('P');
  });
});

点击改变divNode依次输出 M P R T,可以发现 MutationObserver 实在 micro tasks 和 macro task 之间执行,应该是属于上文提到的 dispatch event。

@canvascat canvascat added the vue label Apr 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant