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


Posted in Javascript onSeptember 07, 2020

前言

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深入浅出

总结

到此这篇关于Vue.js原理分析之nextTick实现详解的文章就介绍到这了,更多相关Vue.js原理之nextTick实现内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
如何在父窗口中得知window.open()出的子窗口关闭事件
Oct 15 Javascript
JavaScript继承基础讲解(原型链、借用构造函数、混合模式、原型式继承、寄生式继承、寄生组合式继承)
Aug 16 Javascript
html的DOM中document对象images集合用法实例
Jan 21 Javascript
JavaScript实现同一页面内两个表单互相传值的方法
Aug 12 Javascript
Angular2 环境配置详细介绍
Sep 21 Javascript
jquery实时获取时间的简单实例
Jan 26 Javascript
JavaScript使用链式方法封装jQuery中CSS()方法示例
Apr 07 jQuery
AngularJS实现的获取焦点及失去焦点时的表单验证功能示例
Oct 25 Javascript
spring+angular实现导出excel的实现代码
Feb 27 Javascript
JavaScript实现轮播图效果代码实例
Sep 28 Javascript
Vue执行方法,方法获取data值,设置data值,方法传值操作
Aug 05 Javascript
vue form表单post请求结合Servlet实现文件上传功能
Jan 22 Vue.js
小程序实现可拖动的悬浮按钮
Sep 07 #Javascript
vue 修改 data 数据问题并实时显示操作
Sep 07 #Javascript
nginx部署多个vue项目的方法示例
Sep 06 #Javascript
js实现简单的无缝轮播效果
Sep 05 #Javascript
JS+CSS实现炫酷光感效果
Sep 05 #Javascript
js实现炫酷光感效果
Sep 05 #Javascript
js实现搜索提示框效果
Sep 05 #Javascript
You might like
非常好用的Zend Framework分页类
2014/06/25 PHP
php获取本周开始日期和结束日期的方法
2015/03/09 PHP
Windows平台实现PHP连接SQL Server2008的方法
2017/07/26 PHP
javascript 装载iframe子页面,自适应高度
2009/03/20 Javascript
javascript 正则替换 replace(regExp, function)用法
2010/05/22 Javascript
JavaScript表达式:URL 协议介绍
2013/03/10 Javascript
各种常用的JS函数整理
2013/10/25 Javascript
JavaScript控制listbox列表框的项目上下移动的方法
2015/03/18 Javascript
JSON格式的时间/Date(2367828670431)/格式转为正常的年-月-日 格式的代码
2016/07/27 Javascript
jQuery插件扩展实例【添加回调函数】
2016/11/26 Javascript
AngularJS 在同一个界面启动多个ng-app应用模块详解
2016/12/20 Javascript
JS实现的系统调色板完整实例
2016/12/21 Javascript
javascript深拷贝和浅拷贝详解
2017/02/14 Javascript
jQuery插件echarts实现的去掉X轴、Y轴和网格线效果示例【附demo源码下载】
2017/03/04 Javascript
JavaScript模拟文件拖选框样式v1.0的实例
2017/08/04 Javascript
JavaScript的数据类型转换原则(干货)
2018/03/15 Javascript
react配合antd组件实现的管理系统示例代码
2018/04/24 Javascript
JavaScript数组及常见操作方法小结
2019/11/13 Javascript
利用Python脚本在Nginx和uwsgi上部署MoinMoin的教程
2015/05/05 Python
Python编程之变量赋值操作实例分析
2017/07/24 Python
python3写爬取B站视频弹幕功能
2017/12/22 Python
python中for循环输出列表索引与对应的值方法
2018/11/07 Python
python爬虫项目设置一个中断重连的程序的实现
2019/07/26 Python
Python 使用threading+Queue实现线程池示例
2019/12/21 Python
Docker部署Python爬虫项目的方法步骤
2020/01/19 Python
基于python实现可视化生成二维码工具
2020/07/08 Python
CSS3实现可翻转的hover效果
2018/05/23 HTML / CSS
玩具反斗城西班牙网上商城:ToysRUs西班牙
2017/01/19 全球购物
为什么需要版本控制?
2013/08/08 面试题
实习单位接收函
2014/01/11 职场文书
市政管理求职信范文
2014/05/07 职场文书
中学教师个人总结
2015/02/10 职场文书
2015夏季作息时间调整通知
2015/04/24 职场文书
建议书的格式及范文
2015/09/14 职场文书
解决Laravel使用验证时跳转到首页的问题
2021/11/17 PHP
在SQL Server中使用 Try Catch 处理异常的示例详解
2022/07/15 SQL Server