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 #44

Open
SKing7 opened this issue Jun 26, 2018 · 0 comments
Open

Vue $nextTick #44

SKing7 opened this issue Jun 26, 2018 · 0 comments

Comments

@SKing7
Copy link
Owner

SKing7 commented Jun 26, 2018

有人在vue里提issue,vue的nextTick是阻塞UI render的。
image
大意是,nextTick是把任务更新到了microTask,因为microTask在本次Event Loop结束前,执行当前task结束后会执行microTask Queue,也就阻塞了UI Render。

那我们看下vue源码来分析下,先看1.x版本的这块代码:

export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

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

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var noop = function () {}
    timerFunc = () => {
      p.then(nextTickHandler)
      // 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 if (typeof MutationObserver !== 'undefined') {
    // use MutationObserver where native Promise is not available,
    // e.g. IE11, iOS7, Android 4.4
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = setTimeout
  }

  return function (cb, ctx) {
    var func = ctx
      ? function () { cb.call(ctx) }
      : cb
    callbacks.push(func)
    if (pending) return
    pending = true
    timerFunc(nextTickHandler, 0)
  }
})()

get一个知识点,根据文档中,可以看到在当前task执行完后,render之前,还会执行高优先级的microtask。
再看上面的代码,代码不是很复杂。代码中先判断有没有Native Promise,再判断有没有MO,如果两个都不支持就降级为timeout。这两个都是microtask。其实vue就是把nextTick封装成了microTask,
MO在这里的代码逻辑是,他创建一个textNode,然后为这个textNode Observer Listener,然后改变这个节点的值,就实现了把当前事件push到microTask Queue中。

为什么是microTask

看到这,你可能会疑问,为什么nextTick要是microTask?
另外在nextTick中经常会出现阻塞UI Render和拿不到Dom节点的情况?

尤雨溪也尝试过修改,vue后面的版本,尝试把microTask换为macroTask。
看下2.0.0-rc.7的源码:

export const nextTick = (function () {
  let callbacks = []
  let pending = false
  let timerFunc

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

  /* istanbul ignore else */
  if (inBrowser && window.postMessage &&
    !window.importScripts && // not in WebWorker
    !(isAndroid && !window.requestAnimationFrame) // not in Android <= 4.3
  ) {
    const NEXT_TICK_TOKEN = '__vue__nextTick__'
    window.addEventListener('message', e => {
      if (e.source === window && e.data === NEXT_TICK_TOKEN) {
        nextTickHandler()
      }
    })
    timerFunc = () => {
      window.postMessage(NEXT_TICK_TOKEN, '*')
    }
  } else {
    timerFunc = (typeof global !== 'undefined' && global.setImmediate) || setTimeout
  }

  return function queueNextTick (cb: Function, ctx?: Object) {
    const func = ctx
      ? function () { cb.call(ctx) }
      : cb
    callbacks.push(func)
    if (pending) return
    pending = true
    timerFunc(nextTickHandler, 0)
  }
})()

发现这个版本,把nextTick使用postMessage实现,但postMessage任务是task,他提交的任务将插入到下个任务队列。这样就会导致一个问题:
看下面两个fiddler:
2.0.0-rc.7
之前的版本
代码完全一样,唯一的区别就是引用vue的版本。
rc7版本,当你滚动的时候,你会发现,render总是有延迟卡顿的感觉。这就是因为在这个版本用的postMessage,执行的任务为task,任务执行在当前响应scroll的下一次render,所以出现画面不同步的情况。
所以得出结论:
因为根据HTML Standard,在每个 task 运行完以后,UI 都会rerender,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。但是如果新建一个 task ,那么渲染就会进行两次并且会延迟更新。

后面的版本就改回promise,MO来实现。
上面的nextTick就是在值修改,计算computed的值,Vue.$nextTick和这里隐式的是一个实现。
更新dom,vue也会把它放到一个microtask里,这样,nextTick的回调追加到队列,

计算完miscroTask,就是render
其中可以注意到有

10.For each fully active Document in docs, run the animation frame callbacks for that Document, passing in now as the timestamp.
11.For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]
12.For each fully active Document in docs, update the rendering or user interface of that Document and its browsing context to reflect the current state.

在第十步执行run the animation frame callbacks:requestAnimationFrame回调和update intersection observations steps :IntersectionObserver回调
最后一步,渲染引擎渲染UI;

具体为什么经常在vue中修改了一个响应式数据,但是通过访问dom节点,拿不到更新后的数据呢?但是通过nextTick就可以;
vue中更新节点都是把任务作为microtask,在显式调用nextTick时,同样会作为microtask push到队尾,这样前面执行完之后,也就能拿到更新后的值了;

再看最新版本的vue(25.16)

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
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
}
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

可以看出nextTick已经支持设置使用macroTask,除非设置使用macroTask,否则默认使用microTask,fallback to macroTask;
设置使用macroTask,通过暴露的接口:withMacroTask
那我们接下来看看哪些事件的处理是使用macroTask;

function add (
  event: string,
  handler: Function,
  once: boolean,
  capture: boolean,
  passive: boolean
) {
  handler = withMacroTask(handler)
  if (once) handler = createOnceHandler(handler, event, capture)
  target.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

事件处理函数用了macroTask,事件处理函数执行前useMacroTask = true;
那这个函数执行期间,nextTick使用的是macroTask

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant