其他分享
首页 > 其他分享> > Vue中$nextTick原理

Vue中$nextTick原理

作者:互联网

1.$nextTick作用

如下图例子,文本改变后,响应式数据处理后,在mouted中获取到box高度都是0

  <div id='app'>
    <div class='box'>
      {{msg}}
    </div>
  </div>
  <script>
    let app = new Vue(
      {
        el: '#app',
        data: {
          msg: ''
        },
        mounted() {
          this.msg = '我是测试文字';
          console.log(document.querySelector('.box').offsetHeight);
        }
      }
    )
  </script>

但是直接去获取是可以获取到的
在这里插入图片描述
使用nextTick可以解决这个问题
实现异步更新回调延迟到下次DOM跟新周期之后执行

  <div id='app'>
    <div class='box'>
      {{msg}}
    </div>
  </div>
  <script>
    let app = new Vue(
      {
        el: '#app',
        data: {
          msg: ''
        },
        mounted() {
          this.msg = '我是测试文字';
          this.$nextTick(()=>{
		  	console.log(document.querySelector('.box').offsetHeight);
		  })
        }
      }
    )
  </script>

2.EventLoop事件循环机制

2.1 堆 栈 队列

a.堆(Heap)

利用完全二叉树维护一组数据
完全二叉树:1.除了最后一层其他都必须满 2.任何一个节点不能只有右子树没有左子树
二插搜索树:左子树都比节点小,右子树都比节点大
最大堆:根节点最大的堆叫最大堆大根堆
最小堆:根节点最小的叫做最小堆小根堆

b.栈(stack)

先进后出

c.队列(Queue)

先进先出

2.2 宏任务(MacroTask),微任务(MicroTask)

宏任务:setTimeout,setInterval,dom事件
微任务:promise.then()

在这里插入图片描述
JS的EventLoop事件循环机制,主要涉及到JS执行栈和任务队列
JS自上向下进行代码的编译执行,遇到同步代码压入JS执行栈执行后出栈,遇到异步代码放入任务队列,当JS执行栈清空,去执行异步队列中的回调函数,先去执行微任务队列,当微任务队列清空后,去检测执行宏任务队列中的回调函数,直至所有栈和队列清空

console.log(1);
setTimeout(() => {
  console.log(2);
  new Promise((resolve, reject) => {
    console.log(3);
    resolve();
  }).then(
    res => {
      console.log(4);
    }
  )
}, 0);
new Promise((resolve, reject) => {
  console.log(5);
  resolve();
}).then(
  res => {
   console.log(6);
  }
)
console.log(7);

//输出顺序:1,5,7,6,2,3,4

3.nextTick原理

我们不难发现,如果使用promise或settimeout都可以去实现上述需求
官网上叙述:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
因为 $nextTick() 返回一个 Promise 对象

  <div id='app'>
    <div class='box'>
      {{msg}}
    </div>
  </div>
  <script>
    let app = new Vue(
      {
        el: '#app',
        data: {
          msg: ''
        },
        mounted() {
          this.msg = '我是测试文字';
          this.$nextTick(()=>{
		  	console.log(document.querySelector('.box').offsetHeight);
		  })
		  // setTimeout(() => {
          //   console.log(document.querySelector('.box').offsetHeight);
          // }, 0)
          // new Promise((res, rej) => {
          //   res(1)
          // }).then(
          //   res => {
          //     console.log(document.querySelector('.box').offsetHeight);
          //   }
          // )
        }
      }
    )
  </script>

4.nextTick源码解析

在nextTick的外层定义变量就形成了一个闭包,所以我们每次调用$nextTick的过程其实就是在向callbacks新增回调函数的过程

流程:
1.把回调函数放入callbacks等待执行
2.将执行函数放到微任务或者宏任务中
3.事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

const callbacks=[];
let pending = false;
let timerFunc

export function nextTick(cb,ctx){
	let _resolve;
	callback.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;
			})
		}
	})
}

timerFunc函数的定义
做了四个判断,对当前环境进行不断的降级处理,尝试使用原生的Promise.then、MutationObserver和setImmediate,上述三个都不支持最后使用setTimeout

//isNative函数是用来判断所传参数是否在当前环境原生就支持
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  //判断1:是否原生支持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]'
)) {
  //判断2:是否原生支持MutationObserver
  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)) {
  //判断3:是否原生支持setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  //判断4:上面都不行,直接用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

flushCallbacks 函数的定义
把callbacks数组复制一份,然后把callbacks置为空,最后把复制出来的数组中的每个函数依次执行一遍;所以它的作用仅仅是用来执行callbacks中的回调函数

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

总结一下:
nextTick中维护了一个callbacks队列,一个pending锁,一个timerFunc
这个timerFunc就是根据浏览器环境判断得出的一个能够产生微任务或降级为宏任务的api调用,比如promise.then。
callbacks队列的作用是收集当前正在执行的宏任务中所有的nextTick回调,等当前宏任务执行完之后好一次性for循环啪执行完。 试想如果没有callback队列的话,每次调用nextTick都去创建一个timerFunc微任务(假设支持),那么也就不需要pending锁了。 现在有了callbacks队列的情况下就只需要创建一个timerFunc微任务,那问题是什么时候创建该微任务呢?
这里就要讲到pending了,在pending为false的时候表示第一次添加cb到callbacks中,这时候创建一个timerFunc微任务,并加锁。 后面调用nextTick就只是往callbacks添加回调。 等当前宏任务之后完之后,就会去执行timerFunc清空callbacks队列,并设置pending为false,一切归零

标签:nextTick,timerFunc,Vue,console,队列,callbacks,Promise,原理
来源: https://blog.csdn.net/Dimuzero/article/details/120875083