loo41/Blog

浅析 NextTick

loo41 opened this issue · 3 comments

loo41 commented

Next-tick

NextTick 是做什么到?

来自 Vue 官网讲述: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

是否似曾相似

Vue.nextTick(() => {})
this.$nextTick(() => {})
在 vue 中 created 函数钩子函数执行的时候DOM 其实并未进行任何渲染,所以得放在 nextTick 中去获取 dom,与其对于得生命周期钩子函数是 mounted
BUT 中 Vue 中 for 渲染 dom 就算是中 mounted 调用 nextTick也不能获取到具体到 dom,为什么
现在来一探究竟

先来看一下下面这个问题

打印的是什么,请先思考【下面有答案】

console.log(1)

setTimeout(() => {
  console.log(8)
}, 2000)

setTimeout(() => {
  console.log(3)
  Promise.resolve().then(() => {
    console.log(4)
  })
  setTimeout(() => {
    console.log(6)
  }, 3000)
}, 1000)

new Promise((resolve, reject) => {
  console.log(5)
  resolve()
}).then(() => {
  console.log(7)
})

console.log(2)

谈 Event-Loop

js 是单线程执行,当然,现在又有了一个 worker 创造了多线程环境,但是 worker 受限很多,
js 执行是有一个执行栈,主要分了,宏任务(macro-task)和 微任务(micro-task)

  • 宏任务有那些

    1. setTimeout
    2. I/O
    3. setInterval
    4. setImmediate
    5. 主线程
    6. MessageChannel
  • 微任务有那些

    1. Promise 系列 .then .catch .finally
    2. process.nexttick
    3. MutationObserver
// 执行流程
              (执行一个宏任务,产生宏任务,入栈)
                    ↑
--------↑------- 宏任务 ←--—--
        |           |         |
        |           |         |
        |           |         |
        |           ↓         | 没有
        |         微任务 ------
        |           |↘
        |           |(执行所有微任务过程中产生微任务,继续执行)
        |           |
        |           | 有 执行完所以微任务
        |           |
        |___________↓

--------------------↓------------------
                直到栈为空

我们在来看上面的问题

  • 主线程 (宏任务) 打印 1 - 5 - 2 【new Promise 会立即执行,不属于,微任务】
  • 执行所有微任务 promise.then 打印 7
  • 在执行栈中抛出一个【可以执行的 -> 到时间,虽然,第一个 setTimeout 首先注册,在任务队列栈底】宏任务执行 3 - 4
  • 在执行所有微任务 【没有】
  • 在抛出一个宏任务执行 8
  • 在执行所有微任务 【没有】
  • 在抛出一个宏任务执行 6
  • 任务执行完毕
  • 1 5 2 7 3 4 8 6

在谈 NextTick

我去.......,上面这一大堆和 NextTick 有什么关系?
还真有点关系,看源码,我只剪取了主要部分,源码来自 wepy

// 微任务
let microTimerFunc
// 宏任务
let macroTimerFunc
// 默认不用宏任务,因为微任务到优先级高于宏任务
let useMacroTask = false
// 判断 setImmediate 能不能使用
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 能使用注册宏任务
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
  // 是否支持 MessageChannel
} 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 {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// 判断是否可以使用 Promise ie 8 以下不能吧.....
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  // 注册微任务
  microTimerFunc = () => {
    p.then(flushCallbacks)
  }
} else {
  // 如果不存在,微任务注册微宏任务
  microTimerFunc = macroTimerFunc
}

/**

执行流程就是
宏任务 检测 setImmediate ----- 不能 ----> 降级 MessageChannel ------不能-----> 降级 setTimeout
微任务 Promise ---- 不能 ---> 微任务注册微宏任务
上面的 flushCallbacks 就是你要执行的函数

在 Vue 整个 nextTick 的作用

  // 盗用官方的一个例子
  Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
// 在 updateMessage 方法中,更新数据,立即获取更新后的 dom 是获取不到的,所以得把获取 dom 加到事件队列的栈,异步获取更新后的dom  

主线程更新前 ---> 遇到宏任务或微任务 ---> 放入栈 ---> 主线程执行完成,更新完成 ----> 执行栈 ---- > 获取更新后的dom

总结

  • 到此 nextTick 就结束了

看第一遍我的的结果是:1 2 5 7 3 4 8 6,看了你的答案,我错了-_- ~~,经常忽略 new Promise 在.then前面动作是同步的。谈 NextTick的源码写的很仔细,清晰明了。Vue.nextTick([callback, context])的第二个参数也可以说明一下,如果 callback换成箭头函数又需要注意什么

this.$nextTick( ()=> {
     console.log(this.$el.textContent) //报错
})

期待你写更多的文章哦
第一段结尾有两句话读不太通顺

loo41 commented

看第一遍我的的结果是:1 2 5 7 3 4 8 6,看了你的答案,我错了-_- ~~,经常忽略 new Promise 在.then前面动作是同步的。谈 NextTick的源码写的很仔细,清晰明了。Vue.nextTick([callback, context])的第二个参数也可以说明一下,如果 callback换成箭头函数又需要注意什么

this.$nextTick( ()=> {
     console.log(this.$el.textContent) //报错
})

期待你写更多的文章哦
第一段结尾有两句话读不太通顺

  • 嗯嗯 Vue.nextTick
  • 最后两句
    • 第一个是 vue.nextTick 和 生命周期钩子函数 mounted 相对应,一个获取更新后的 dom,一个页 面挂载完成
    • 第二个是,vue中页面挂载完成,也不一定获取得到 v-for 循环中的 dom,遇到这种情况如何去处理
loo41 commented

我从来没用过他的第二个参数 context