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 相关文章推荐
两个select之间option的互相添加操作(jquery实现)
Nov 12 Javascript
JavaScript中的无阻塞加载性能优化方案
Oct 10 Javascript
详解jQuery中的元素的属性和相关操作
Aug 14 Javascript
Javascript中的arguments对象
Jun 20 Javascript
jquery easyui dataGrid动态改变排序字段名的方法
Mar 02 Javascript
seajs模块压缩问题与解决方法实例分析
Oct 10 Javascript
Vue中使用webpack别名的方法实例详解
Jun 19 Javascript
jQuery插件实现弹性运动完整示例
Jul 07 jQuery
解决vue-cli webpack打包后加载资源的路径问题
Sep 25 Javascript
在webstorm开发微信小程序之使用阿里自定义字体图标的方法
Nov 15 Javascript
js实现for循环跳过undefined值示例
Jul 02 Javascript
vue中实现点击变成全屏的多种方法
Sep 27 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
MySQL时间字段究竟使用INT还是DateTime的说明
2012/02/27 PHP
详解WordPress开发中的get_post与get_posts函数使用
2016/01/04 PHP
php 从一个数组中随机的取出若干个不同的数实例
2016/12/31 PHP
js cookies实现简单统计访问次数
2009/11/24 Javascript
js 获取时间间隔实现代码
2014/05/12 Javascript
JS实现单行文字不间断向上滚动的方法
2015/01/29 Javascript
js实现拖拽效果
2015/02/12 Javascript
javascript实现完美拖拽效果
2015/05/06 Javascript
jQuery调用Webservice传递json数组的方法
2016/08/06 Javascript
require.js与bootstrap结合实现简单的页面登录和页面跳转功能
2017/05/12 Javascript
JQuery EasyUI的一些常用组件
2017/07/12 jQuery
jquery实现倒计时小应用
2017/09/19 jQuery
使用vue 国际化i18n 实现多实现语言切换功能
2018/10/11 Javascript
JS事件流与事件处理程序实例分析
2019/08/16 Javascript
highcharts.js数据绑定方式代码实例
2019/11/13 Javascript
JavaScript多种滤镜算法实现代码实例
2019/12/10 Javascript
小程序角标的添加及绑定购物车数量进行实时更新的实现代码
2020/12/07 Javascript
[02:29]完美世界高校联赛上海赛区回顾
2015/12/15 DOTA
Python将图片批量从png格式转换至WebP格式
2020/08/22 Python
python 对多个csv文件分别进行处理的方法
2019/01/07 Python
selenium获取当前页面的url、源码、title的方法
2019/06/12 Python
pyhton中__pycache__文件夹的产生与作用详解
2019/11/24 Python
python 微信好友特征数据分析及可视化
2020/01/07 Python
html5 Canvas画图教程(6)—canvas里画曲线之arcTo方法
2013/01/09 HTML / CSS
整理HTML5的一些新特性与Canvas的常用属性
2016/01/29 HTML / CSS
初三物理教学反思
2014/01/21 职场文书
大学生个人求职口试自我评价
2014/02/16 职场文书
自主招生推荐信范文
2014/05/10 职场文书
环境工程专业自荐信范文
2014/06/24 职场文书
导航工程专业自荐信
2014/09/02 职场文书
房产公证书样本
2015/01/23 职场文书
趵突泉导游词
2015/02/03 职场文书
校长师德表现自我评价
2015/03/04 职场文书
学校勤俭节约倡议书
2015/04/29 职场文书
大学生如何逃脱“毕业季创业队即散伙”魔咒?
2019/08/19 职场文书
详解python中[-1]、[:-1]、[::-1]、[n::-1]使用方法
2021/04/25 Python