Vue.js原理分析之nextTick实现详解

作者:minijun2333 时间:2024-05-13 09:38:08 

前言

tips:第一次发技术文章,篇幅比较简短,主要采取文字和关键代码表现的形式,希望帮助到大家。(若有不正确还请多多指正)

nextTick作用和用法

用法:nextTick接收一个回调函数作为参数,它的作用是将回调延迟到下一次DOM更新之后执行,如果没有提供回调函数参数且在支持Promise的环境中,nextTick将返回一个Promise。
适用场景:开发过程中,开发者需要在更新完数据之后,需要对新DOM做一些操作,其实我们当时无法对新DOM进行操作,因为这时候还没有重新渲染,这时候nextTick就派上了用场。

nextTick实现原理

下面我们介绍下nextTick工作原理:

首先我们应该了解到更新完数据(状态)之后,DOM更新这个动作并不是同步进行的,而是异步的。Vue.js中有一个队列,每当需要渲染时,会将Watcher推送到这个队列中,等下一次事件循环中再让Watcher触发渲染流程。这里我们可能会有两个疑问: 

**1.为什么更新DOM是异步的?**

我们知道从Vue2.0开始使用虚拟DOM进行渲染,变化侦测只发送到组件级别,组件内部则通过虚拟DOM的diff(比对)而进行局部渲染,而在同一次事件循环中组件假如收到两份通知,组件是否会进行两次渲染呢?事实上一次事件循环组件会在所有状态修改完毕之后只进行一次渲染操作。

**2.什么是事件循环?**

javascript是单线程脚本语言,它具有非阻塞特性,之所以非阻塞是由于在处理异步代码时,主线程会挂起这个任务,当异步任务处理完毕之后会根据一定的规则去执行异步任务的回调,异步任务分宏任务(macrotast)和微任务(microtast),它们会被分配到不同的队列中,当执行栈所有任务执行完毕之后,会先检查微任务队列中是否有事件存在,优先执行微任务队列事件对应的回调,直至为空。然后再执行宏任务队列中事件的回调。无限重复这个过程,形成一个无限循环就叫做事件循环。

常见微任务包括:Promise 、MutationObserver、Object.observer、process.nextTick等

常见宏任务包括:setTimeout、setInterval、setImmediate、MessageChannel、requestAnimation、UI交互事件等

微任务如何注册?

nextTick会将回调添加到异步任务队列中延迟执行,在执行回调前,反复调用nextTick,Vue并不会反复添加到任务队列中,只会向任务队列添加一个任务,多次使用nextTick只会将回调添加到回调列表缓存起来,当任务触发时,会清空回调列表并依次执行所有回调 ,具体代码如下: 


const callbacks = []
let pending = false

function flushCallbacks(){ //执行回调
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0 //清空回调队列
 for(let i = 0; i < copies.length; i++) {
   copies[i]()
 }
}
let microTimerFunc
const p = Promise.resolve()
microTimerFunc = () => { //注册微任务
 p.then(flushCallbacks)
}

export function nextTick(cb,ctx){
 callbacks.push(()=>{
   if(cb){
     cb.call(ctx)
   }
 })
 if(!pending){
   pending = true //将pending设置为true,保证任务在依次事件循环中不会重复添加
   microTimerFunc()
 }
}

由于微任务优先级太高,可能在某些场景下需要使用到宏任务,所以Vue提供了可以强制使用宏任务的方法withMacroTask。具体实现如下:


const callbacks = []
let pending = false

function flushCallbacks(){ //执行回调
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0 //清空回调队列
 for(let i = 0; i < copies.length; i++) {
   copies[i]()
 }
}
let microTimerFunc
//新增代码
let macroTimerFunc = function(){
 ...
}

let useMacroTask = false
const p = Promise.resolve()
microTimerFunc = () => { //注册微任务
 p.then(flushCallbacks)
}

//新增代码
export function withMacroTask(fn){
 return fn._withTask || fn._withTask = function()=>{
   useMacroTask = true
   const res = fn.apply(null,arguments)
   useMacroTask = false
   return res
 }
}

export function nextTick(cb,ctx){
 callbacks.push(()=>{
   if(cb){
     cb.call(ctx)
   }
 })
 if(!pending){
   pending = true //将pending设置为true,保证任务在依次事件循环中不会重复添加
   //修改代码
   if(useMacroTask){
     macroTimerFunc()
   }else{
     microTimerFunc()
   }
 }
}

上面提供了一个withMacroTask方法强制使用宏任务,通过useMacroTask变量进行控制是否使用注册宏任务执行,withMacroTask实现很简单,先将useMacroTask变量设置为true,然后执行回调,回调执行之后再改回false。

宏任务是如何注册?

注册宏任务优先使用setImmediate,但是存在兼容性问题,只能在IE中使用,所以使用MessageChannel作为备选方案,若以上都不支持则最后会使用setTimeout。具体实现如下:


if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){
 macroTimerFunc = ()=>{
   setImmediate(flushCallbacks)
 }
} else if(
 typeof MessageChannel !== 'undefined' &&
 (isNative(MessageChannel) || MessageChannel.toString() === '[Object MessageChannelConstructor]')
){
 const channel = new MessageChannel()
 const port = channel.port2
 channel.port1.onmessage = flushCallbacks
 macroTimerFunc = ()=>{
   port.postMessage(1)
 }
} else {
 macroTimerFunc = ()=>{
   setTimout(flushCallbacks,0)
 }
}

microTimerFunc的实现方法是通过Promise.then,但是并不是所有浏览器都支持Promise,当不支持的时候采取降级为宏任务方式


if(typeof Promise !== 'undefined' && isNative(Promise)){
 const p = Promise.resolve()
 microTimerFunc = ()=>{
   p.then(flushCallbacks)
 }
} else {
 microTimerFunc = macroTimerFunc
}

若未提供回调且环境支持Promise情况下,nextTick会返回一个Promise,具体实现如下:


export function nextTick(cb, ctx) {
 let _resolve
 callbacks.push(()=>{
   if(cb){
     cb.call(ctx)
   }else{
     _resolve(ctx)
   }
 })

if(!pending){
   pending = true
   if(useMacroTask){
     macroTimerFunc()
   }else{
     microTimerFunc()
   }
 }

if(typeof Promise !== 'undefined' && isNative(Promise)){
   return new Promise(resolve=>{
     _resolve = resolve
   })
 }
}

以上是nextTick运行原理的设计,完整代码如下:


const callbacks = []
let pending = false

function flushCallbacks(){ //执行回调
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0 //清空回调队列
 for(let i = 0; i < copies.length; i++) {
   copies[i]()
 }
}
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

//注册宏任务
if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){
 macroTimerFunc = ()=>{
   setImmediate(flushCallbacks)
 }
} else if(
 typeof MessageChannel !== 'undefined' &&
 (isNative(MessageChannel) || MessageChannel.toString() === '[Object MessageChannelConstructor]')
){
 const channel = new MessageChannel()
 const port = channel.port2
 channel.port1.onmessage = flushCallbacks
 macroTimerFunc = ()=>{
   port.postMessage(1)
 }
} else {
 macroTimerFunc = ()=>{
   setTimout(flushCallbacks,0)
 }
}

//微任务注册
if(typeof Promise !== 'undefined' && isNative(Promise)){
 const p = Promise.resolve()
 microTimerFunc = ()=>{
   p.then(flushCallbacks)
 }
} else {//降级处理
 microTimerFunc = macroTimerFunc
}

export function withMacroTask(fn){
 return fn._withTask || fn._withTask = function()=>{
   useMacroTask = true
   const res = fn.apply(null,arguments)
   useMacroTask = false
   return res
 }
}

export function nextTick(cb,ctx){
 let _resolve
 callbacks.push(()=>{
   if(cb){
     cb.call(ctx)
   }else{
     _resolve(ctx)
   }
 })
 if(!pending){
   pending = true //将pending设置为true,保证任务在依次事件循环中不会重复添加
   //修改代码
   if(useMacroTask){
     macroTimerFunc()
   }else{
     microTimerFunc()
   }
 }

if(typeof Promise !== 'undefined' && isNative(Promise)){
   return new Promise(resolve=>{
     _resolve = resolve
   })
 }
}

以上便是对nextTick的实现原理的全部介绍。

参考资料

Vue.js深入浅出

来源:https://juejin.im/post/6869220072074936334

标签:vue.js,原理,nexttick
0
投稿

猜你喜欢

  • Django中文件上传和文件访问微项目的方法

    2021-04-15 10:44:45
  • SqlServer修改数据库文件及日志文件存放位置

    2024-01-15 15:17:22
  • PHP使用自定义key实现对数据加密解密的方法

    2023-08-21 12:48:37
  • Python-docx 实现整体修改或者部分修改文字的大小和字体类型

    2022-07-19 10:07:25
  • SNS用户体验和互动性浅析

    2011-01-17 17:56:00
  • SQL基础语句总结

    2011-03-11 14:54:00
  • python数据结构之链表的实例讲解

    2021-09-24 17:58:46
  • IE6,7下实现white-space:pre-wrap;

    2009-12-31 18:30:00
  • python+Django+pycharm+mysql 搭建首个web项目详解

    2024-01-18 22:18:07
  • iOS开发runloop运行循环机制学习

    2024-01-24 19:03:04
  • Python中List.count()方法的使用教程

    2023-08-12 11:56:15
  • python GUI库图形界面开发之PyQt5图片显示控件QPixmap详细使用方法与实例

    2023-05-31 17:41:29
  • vscode中使用Autoprefixer3.0无效的解决方法

    2023-10-05 11:03:46
  • 一位网友的DIV CSS编码笔记——XieBiji

    2008-09-12 12:22:00
  • 30秒学会30个超实用Python代码片段【收藏版】

    2021-08-04 17:13:32
  • 写入cookie的JavaScript代码库 cookieLibrary.js

    2024-04-16 10:41:08
  • 关于你不想知道的所有Python3 unicode特性

    2022-03-03 13:06:40
  • chr(9)、chr(10)、chr(13)、chr(32)与特殊空格

    2009-07-03 15:26:00
  • Python 短视频爬虫教程

    2022-02-13 00:17:33
  • 解决TensorFlow调用Keras库函数存在的问题

    2023-11-20 15:04:25
  • asp之家 网络编程 m.aspxhome.com