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 相关文章推荐
Jquery异步请求数据实例代码
Dec 28 Javascript
JQuery学习笔录 简单的JQuery
Apr 09 Javascript
Js和JQuery获取鼠标指针坐标的实现代码分享
May 25 Javascript
jQuery仿360导航页图标拖动排序效果代码分享
Aug 24 Javascript
bootstrap3 兼容IE8浏览器!
May 02 Javascript
浅谈js常用内置方法和对象
Sep 24 Javascript
关于Jquery中的bind(),on()绑定事件方式总结
Oct 26 Javascript
jquery滚动条插件(可以自定义)
Dec 11 Javascript
JS重载实现方法分析
Dec 16 Javascript
JavaScript文件的同步和异步加载的实现代码
Aug 19 Javascript
动态创建Angular组件实现popup弹窗功能
Sep 15 Javascript
JS数组的常用方法整理
Mar 31 Javascript
小程序实现可拖动的悬浮按钮
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
收音机的保养
2021/03/01 无线电
用PHP开发GUI
2006/10/09 PHP
浅谈PHP各环境下的伪静态配置
2019/03/13 PHP
js 编写规范
2010/03/03 Javascript
关于js获取radio和select的属性并控制的代码
2011/05/12 Javascript
jquery mobile页面跳转后样式丢失js失效的解决方法
2014/09/06 Javascript
使用js dom和jquery分别实现简单增删改
2014/09/11 Javascript
JS实现鼠标滑过链接改变网页背景颜色的方法
2015/10/20 Javascript
js获取及判断键盘按键的方法
2015/12/01 Javascript
jQuery实现的调整表格行tr上下顺序
2016/01/10 Javascript
JavaScript html5 canvas绘制时钟效果
2016/03/01 Javascript
js添加绑定事件的方法
2016/05/15 Javascript
jQuery tagsinput在h5邮件客户端中应用详解
2016/09/26 Javascript
jQuery EasyUI tree 使用拖拽时遇到的错误小结
2016/10/10 Javascript
深入理解Angular4中的依赖注入
2017/06/07 Javascript
JavaScript用二分法查找数据的实例代码
2017/06/17 Javascript
vue中实现methods一个方法调用另外一个方法
2018/02/08 Javascript
vue props传值失败 输出undefined的解决方法
2018/09/11 Javascript
JavaScript时间与时间戳的转换操作实例分析
2018/12/07 Javascript
three.js搭建室内场景教程
2018/12/30 Javascript
微信网页登录逻辑与实现方法
2019/04/29 Javascript
echarts多条折线图动态分层的实现方法
2019/05/24 Javascript
Openlayers实现地图的基本操作
2020/09/28 Javascript
详解Vue.js3.0 组件是如何渲染为DOM的
2020/11/10 Javascript
[48:39]Ti4主赛事胜者组第一天 EG vs NEWBEE 2
2014/07/19 DOTA
python 从远程服务器下载日志文件的程序
2013/02/10 Python
Python md5与sha1加密算法用法分析
2017/07/14 Python
利用python编写一个图片主色转换的脚本
2017/12/07 Python
Python3读取Excel数据存入MySQL的方法
2018/05/04 Python
在Qt中正确的设置窗体的背景图片的几种方法总结
2019/06/19 Python
python实现大文件分割与合并
2019/07/22 Python
Python动态声明变量赋值代码实例
2019/12/30 Python
matplotlib之pyplot模块坐标轴标签设置使用(xlabel()、ylabel())
2021/02/22 Python
学生自我评价范文
2014/02/02 职场文书
党员民主评议个人总结
2014/10/20 职场文书
群众路线党员个人整改措施
2014/10/27 职场文书