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 相关文章推荐
js 操作符实例代码
Oct 24 Javascript
JavaScript日期时间格式化函数分享
May 05 Javascript
jQuery操作元素css样式的三种方法
Jun 04 Javascript
移动Web中图片自适应的两种JavaScript解决方法
Jun 18 Javascript
简单纯js实现点击切换TAB标签实例
Aug 23 Javascript
Jquery promise实现一张一张加载图片
Nov 13 Javascript
jQuery实现ajax调用WCF服务的方法(附带demo下载)
Dec 04 Javascript
AngularJS表格样式简单设置方法示例
Mar 03 Javascript
javascript深拷贝的原理与实现方法分析
Apr 10 Javascript
详解Koa中更方便简单发送响应的方式
Jul 20 Javascript
解决 window.onload 被覆盖的问题方法
Jan 14 Javascript
深入了解Vue3模板编译原理
Nov 19 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
使用VisualStudio开发php的图文设置方法
2010/08/21 PHP
php计算到指定日期还有多少天的方法
2015/04/14 PHP
优化WordPress中文章与评论的时间显示
2016/01/12 PHP
ThinkPHP5.1框架数据库链接和增删改查操作示例
2019/08/03 PHP
php+layui数据表格实现数据分页渲染代码
2019/10/26 PHP
Javascript在IE和FireFox中的不同表现简析
2012/12/03 Javascript
『jQuery』取指定url格式及分割函数应用
2013/04/22 Javascript
IE8对JS通过属性和数组遍历解析不一样的地方探讨
2013/05/06 Javascript
JavaScript字符串对象substring方法入门实例(用于截取字符串)
2014/10/17 Javascript
javascript实现九宫格相加数值相等
2020/05/28 Javascript
轻松理解vue的双向数据绑定问题
2017/10/30 Javascript
bootstrap select下拉搜索插件使用方法详解
2017/11/23 Javascript
浅谈vue同一页面中拥有两个表单时,的验证问题
2018/09/18 Javascript
JavaScript 高性能数组去重的方法
2018/09/20 Javascript
JavaScript寄生组合式继承原理与用法分析
2019/01/11 Javascript
JS根据json数组多个字段排序及json数组常用操作
2019/06/06 Javascript
koa2+vue实现登陆及登录状态判断
2019/08/15 Javascript
Vue父子传递实例讲解
2020/02/14 Javascript
[01:56]《DOTA2》中文配音CG
2013/04/22 DOTA
[51:30]OG vs LGD 2018国际邀请赛淘汰赛BO3 第二场 8.26
2018/08/30 DOTA
Python语言实现百度语音识别API的使用实例
2017/12/13 Python
matplotlib中legend位置调整解析
2017/12/19 Python
matplotlib设置legend图例代码示例
2017/12/19 Python
python读取csv和txt数据转换成向量的实例
2019/02/12 Python
Python基于pandas绘制散点图矩阵代码实例
2020/06/04 Python
Python使用xlrd实现读取合并单元格
2020/07/09 Python
Python通过递归函数输出嵌套列表元素
2020/10/15 Python
python接口自动化框架实战
2020/12/23 Python
节省高达65%的城市景点费用:Go City
2019/07/06 全球购物
大众服装店创业计划书范文
2014/01/01 职场文书
宿舍保安职务说明书
2014/02/25 职场文书
机械制造专业毕业生求职信
2014/03/02 职场文书
勤奋学习演讲稿
2014/05/10 职场文书
庆祝新中国成立65周年“向国旗敬礼”网上签名寄语
2014/09/27 职场文书
2014年党风廉政工作总结
2014/12/03 职场文书
十大最强格斗系宝可梦,超梦X仅排第十,第二最重格斗礼仪
2022/03/18 日漫