开头
我自己本身的技术栈是Vue,之前也多多少少在看了一些关于Vue的源码层面的东西。虽然进了公司以后用的是React,但是我觉得学什么技术栈并不是最重要的,最重要的是我们在学习的过程中去弄懂它的原理,我们看源码也不是为了看而去看,更重要的是去学习人家的设计思想和设计理念以及编码风格,我们每个人一开始不都是高手,在成长的路程中,我们都是通过学习别人和模仿别人得到不断的成长,就像你在小时候学习说话和走路一样,遇到困难的我们也不要去畏惧,不要听到源码就畏缩,勇敢面对就好了,这迟早也是我们学习的过程中要面对的一座大山。这篇文章其实之前已经写了一半了,然后今天趁着有空就把它给继续完善了,以后可能自己也主要转React技术栈了,关于React的一些原理性的东西以后再好好研究。也希望大家看完这篇文章能有所学习~
事件循环机制
在浏览器环境中,我们可以将我们的执行任务分为宏任务和微任务
- 宏任务: 包括
整体代码script
,setTimeout
,setInterval
、setImmediate
、 I/O 操作、UI 渲染 等 - 微任务:
Promise.then
、MuationObserver
特别说明的是new Promise
里面的内容是同步执行的,像new Promise(resolve(console.log('1')))
是同步执行的,resolve之后.then进入微任务队列
,具体的内容请往下继续看。
在浏览器环境中: 事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。大概就是先执行同步代码,然后就将宏任务放进宏任务队列,宏任务队列中有微任务就将其放进微任务队列,当宏任务队列执行完就检查微任务队列,微任务队列为空了就开始下一轮宏任务的执行,往复循环。 宏任务 -> 微任务 -> 宏任务 -> 微任务一直循环。
我们下面看个图有助于我们理解,另外如果要看事件循环相关的题目和讲解,可以参考我这篇文章里的事件循环小节
?前端基础知识大汇总(欢迎收藏)
当我们理解了基本的事件循环机制之后,我们就可以开始看看Vue里面的的nextTick
是怎么运作的了。
nextTick原理
Vue是异步执行DOM更新的,一旦观察到数据变化,Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher(Vue源码中的Wacher类是用来更新Dep类收集到的依赖的)
推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。这部分的详情可以参考Vue响应式实现的源码。
nextTick
的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback)
,JS是单线程的,拥有事件循环机制,nextTick
的实现就是利用了事件循环的宏任务和微任务。
当我们调用nextTick
的时候,传入一个回调函数,这时候会发生以下的步骤:
-
将回调函数传入一个
callbacks
数组中 -
判断采用哪种异步回调方式
- 首先尝试使用
Promise.then(微任务)
- 尝试使用
MuationObserver(微任务)
回调 - 尝试使用
setImmediate(宏任务)
回调 - 最后尝试使用
setTimeout(宏任务)
回调
- 首先尝试使用
-
到最后执行
flushCallbacks()
方法,遍历callbacks
数组,依次执行里边的每个函数
我们再看一眼源码,源码在(Vue2.6.12版本) src -> core -> util -> next-tick.js
//判断宏任务和微任务的变量
//true为正在使用微任务
export let isUsingMicroTask = false
//存放回调函数的数组
const callbacks = []
//该变量用来设置异步锁
let pending = false
//用来遍历执行回调函数数组里的函数
function flushCallbacks () {
//执行回调函数时将异步锁给重置
pending = false
//防止出现nextTick中包含nextTick时出现问题,浅拷贝callbacks数组之后将其清空
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判断是否支持Promise
const p = Promise.resolve()
timerFunc = () => {
//执行环境支持Promise则使用Promise.then(微任务)去执行回调函数
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
//设置为true,正在使用微任务
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//当执行环境不支持Promise的时候,我们就做此处的判断
//如果执行环境不是IE浏览器以及支持MutationObserver这个API的话就执行这里
//比如在IOS7,Android 4.4的环境下 (IE11中的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)) {
// 如果Promise和MutationObserver都不支持那就使用setImmediate
// 该方法优先级依然比setTimeout高,setImmediate属于宏任务
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//如果上面三种方法都不支持才使用setTimeout,setTimeout也属于宏任务
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()
}
//当我们不传回调函数的时候,提供一个Promise化的调用
//相当于可以nextTick().then(() => {})
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我们再来看一个在Vue中的使用例子:
Vue生命周期的created
钩子函数进行DOM操作的话我们就需要使用到nextTick
了。我们将DOM的操作放在Vue.nextTick()
的回调函数中,原因是在created
钩子函数执行的时候DOM 其实并未进行挂载和渲染,此时就是无法操作DOM的,我们将操作DOM的代码中放到nextTick中,等待下一轮事件循环开始,DOM就已经进行挂载好了,而与这个操作对应的就是mounted钩子函数,因为在mounted
执行的时候所有的DOM挂载已完成。
created(){
this.$nextTick(() => {
//不使用this.$nextTick()方法操作DOM会报错
this.$refs.test.innerHTML="created中操作了DOM"
});
},
另外,当我们修改了data里的数据时,并不能通过操作DOM去获取到里面的值,此时我们也需要用到nextTick
<template>
<div class="test">
<p id="msg">{{msg}}</p>
</div>
</template>
<script>
export default {
name: 'Test',
data () {
return {
msg:"你好,Vue~",
}
},
methods: {
changeMsg() {
this.msg = "你好,王大锤!" //vue数据改变,改变了DOM里的innerText
let msgEle = document.getElementById('msg').innerText //后续js对dom的操作
console.log(msgEle) // 输出可以看到data里的数据修改后DOM并没有立即更新,后续的DOM不是最新的
},
}
}
</script>
我们使用nextTick
对上面的代码进行一下改善就能看到正确的操作结果啦
methods: {
changeMsg() {
this.msg = "你好,王大锤!" //vue数据改变,改变了DOM里的innerText
this.$nextTick(() => {
let msgEle = document.getElementById('msg').innerText
console.log(msgEle) // 输出可以看到data里的数据修改后DOM已经更新完成
})
},
}
总结
文章就暂时先写到这了,其实很多时候我们学习一个东西,首先还是得学会如何去使用,等到用到一定程度的时候,就可以去学习它的设计原理了,因为我们学习一个东西不能总是浮于表层,要发展成一个T型人才,就得在某个领域里去纵向发展,然后其它领域里横向扩展,这样我们才能永远保持学习的热枕,对这个世界保持一种探索的欲望,永远不要满足于现状。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!