乐趣区

关于vue.js:VuenextTick核心原理

置信大家在写 vue 我的项目的时候,肯定会发现一个神奇的 api,Vue.nextTick。为什么说它神奇呢,那是因为在你做某些操作不失效时,将操作写在 Vue.nextTick 内,就神奇的失效了。那这是什么起因呢?

让咱们一起来钻研一下。

简述

  • vue 实现响应式并不是数据发生变化后 DOM 立刻变动,而是依照肯定策略异步执行 DOM 更新的
  • vue 在批改数据后,视图不会立即进行更新,而是要等 同一事件循环机制 内所有数据变动实现后,再对立进行 DOM 更新
  • nextTick 能够让咱们在下次 DOM 更新循环完结之后执行提早回调,用于取得更新后的 DOM。

事件循环机制

在探讨 Vue.nextTick 之前,须要先搞清楚事件循环机制,算是实现的基石了,那咱们来看一下。

在浏览器环境中,咱们能够将咱们的执行工作分为宏工作和微工作,

  • 宏工作:包含 整体代码 scriptsetTimeoutsetIntervalsetImmediate、I/O 操作、UI 渲染
  • 微工作:Promise.thenMuationObserver

事件循环的程序,决定 js 代码的执行程序。事件循环如下:

用代码解释,浏览器中事件循环的程序同如下代码:

for (macroTask of macroTaskQueue) { 
    // 1. 执行一个宏工作
    handleMacroTask();
    // 2. 执行所有的微工作
    for (microTask of microTaskQueue) {handleMicroTask(microTask);
    }
 }

vue 数据驱动视图的解决(异步变动 DOM)

<template>
  <div>
    <div>{{count}}</div>
    <div @click="handleClick">click</div>
  </div>
</template>
export default {data () {
        return {number: 0};
    },
    methods: {handleClick () {for(let i = 0; i < 10000; i++) {this.count++;}
        }
    }
}

剖析上述代码:

  • 当点击按钮时,count 会被循环扭转 10000 次。那么每次 count+1,都会触发 count 的 setter 办法,而后批改实在 DOM。按此逻辑,这整个过程,DOM 会被更新 10000 次,咱们都晓得 DOM 的操作是十分低廉的,而且这样的操作齐全没有必要。所以 vue 外部在派发更新时做了优化
  • 也就是,并不会每次数据扭转都触发 watcher 的回调,而是把这些 watcher 先增加到一个队列 queueWatcher 里,而后在 nextTick 后执行 flushSchedulerQueue 解决
  • 当 count 减少 10000 次时,vue 外部会先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行。并不需要在下一个 tick 的时候执行 10000 个同样的 Watcher 对象去批改界面,而是只须要执行一个 Watcher 对象,使其将界面上的 0 变成 10000 即可

Vue.nextTick原理

由上一节咱们晓得,Vue 中 数据变动 => DOM 变动 是异步过程,一旦察看到数据变动,Vue 就会开启一个工作队列,而后把在同一个事件循环 (Event loop) 中察看到数据变动的 Watcher(Vue 源码中的 Wacher 类是用来更新 Dep 类收集到的依赖的)推送进这个队列。

如果这个 watcher 被触发屡次,只会被推送到队列一次。这种缓冲行为能够无效的去掉反复数据造成的不必要的计算和 DOM 操作。而在下一个事件循环时,Vue 会清空队列,并进行必要的 DOM 更新。

nextTick的作用是为了在数据变动之后期待 Vue 实现更新 DOM,能够在数据变动之后立刻应用 Vue.nextTick(callback),JS 是单线程的,领有事件循环机制,nextTick的实现就是利用了事件循环的宏工作和微工作。

vue 中 next-tick.js 的源码如下

参考 vue 实战视频解说:进入学习

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

export let isUsingMicroTask = false

// 首先定义一个 callbacks 数组用来存储 nextTick,在下一个 tick 解决这些回调函数之前,// 所有的 cb 都会被存在这个 callbacks 数组中
const callbacks = []
// pending 是一个标记位,代表一个期待的状态
let pending = false

// 最初执行 flushCallbacks() 办法,遍历 callbacks 数组,顺次执行里边的每个函数
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {copies[i]()}
}

let timerFunc

/* 判断采纳哪种异步回调形式因为微工作优先级高,首先尝试微工作模仿 1. 首先尝试应用 Promise.then(微工作)2. 尝试应用 MuationObserver(微工作)回调 3. 尝试应用 setImmediate(宏工作)回调 4. 最初尝试应用 setTimeout(宏工作)回调 */
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]'
)) {
  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()}
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {_resolve = resolve})
  }
}

目前浏览器平台并没有实现 nextTick 办法,所以 Vue.js 源码中别离用 Promise、setTimeout、setImmediate 等形式在 microtask(或是 task)中创立一个事件,目标是在以后调用栈执行结束当前(不肯定立刻)才会去执行这个事件。

nextTick 的调用形式

  1. 回调函数形式:Vue.nextTick(callback)
  2. Promise 形式:Vue.nextTick().then(callback)
  3. 实例形式:vm.$nextTick(callback)

Vue.nextTick的利用

created 生命周期中操作 DOM

created 钩子函数执行的时候 DOM 其实并未进行挂载和渲染,此时就是无奈操作 DOM 的,咱们将操作 DOM 的代码中放到 nextTick 中,期待下一轮事件循环开始,DOM 就曾经进行挂载好了,而与这个操作对应的就是 mounted 钩子函数,因为在 mounted 执行的时候所有的 DOM 挂载已实现。

created(){vm.$nextTick(() => {// 不应用 this.$nextTick()办法操作 DOM 会报错
      this.$refs.test.innerHTML="created 中操作了 DOM"
  });
}

批改数据,获取 DOM 值

当咱们批改了 data 里的数据时,并不能立即通过操作 DOM 去获取到外面的值

<template>
  <div class="test">
    <p ref='msg' id="msg">{{msg}}</p>
  </div>
</template>

<script>
export default {name: 'Test',  data () {return {      msg:"hello world",}  },  methods: {changeMsg() {      this.msg = "hello Vue"  // vue 数据扭转,扭转了 DOM 里的 innerText
      let msgEle = this.$refs.msg.innerText  // 后续 js 对 dom 的操作
      console.log(msgEle)  // hello world
      // 输入能够看到 data 里的数据批改后 DOM 并没有立刻更新,后续的 DOM 不是最新的
            this.$nextTick(() => {        console.log(this.$refs.msg.innerText) // hello Vue
      })      this.$nextTick().then(() => {console.log(this.$refs.msg.innerText) // hello Vue
      })    },    changeMsg2() {      this.$nextTick(() => {console.log(this.$refs.msg.innerText) // 1.hello world 
      })      this.msg = "hello Vue" // 2.
      console.log(this.$refs.msg.innerText) // hello world
      this.$nextTick().then(() => {console.log(this.$refs.msg.innerText) // hello Vue
      })      // nextTick 中先增加的先执行,执行 1 后,才会执行 2(Vue 操作 Dom 的异步)}  }}
</script>

v-show/v-if 由暗藏变为显示

点击按钮显示本来以 v-show=false 或 v -if 暗藏起来的输入框,并获取焦点或者取得宽低等的场景

退出移动版