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 相关文章推荐
循环 vs 递归浅谈
Feb 28 Javascript
简单的jquery左侧导航栏和页面选中效果
Aug 21 Javascript
javascript正则表达式基础知识入门
Apr 20 Javascript
分享JavaScript与Java中MD5使用两个例子
Dec 23 Javascript
关于原生js中bind函数的简单实现
Aug 10 Javascript
js基本算法:冒泡排序,二分查找的简单实例
Oct 08 Javascript
js实现界面向原生界面发消息并跳转功能
Nov 22 Javascript
详解angular中如何监控dom渲染完毕
Jan 03 Javascript
关于定制FileField中的上传文件名称问题
Aug 22 Javascript
微信小程序实现图片预览功能
Jan 31 Javascript
vue实现键盘输入支付密码功能
Aug 18 Javascript
JS/HTML5游戏常用算法之路径搜索算法 随机迷宫算法详解【普里姆算法】
Dec 13 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
PHP 网络开发详解之远程文件包含漏洞
2010/04/25 PHP
PHP读取网页文件内容的实现代码(fopen,curl等)
2011/06/23 PHP
PHP网页游戏学习之Xnova(ogame)源码解读(十一)
2014/06/25 PHP
Fleaphp常见函数功能与用法示例
2016/11/15 PHP
PHP实现的简单留言板功能示例【基于thinkPHP框架】
2018/12/07 PHP
Javascript日期对象的dateAdd与dateDiff方法
2008/11/18 Javascript
js 实现无干扰阴影效果 简单好用(附文件下载)
2009/12/27 Javascript
Jquery实现无刷新DropDownList联动实现代码
2010/03/08 Javascript
用jquery实现下拉菜单效果的代码
2010/07/25 Javascript
高效率JavaScript编写技巧整理
2013/08/23 Javascript
探讨JavaScript中声明全局变量三种方式的异同
2013/12/03 Javascript
js创建表单元素并使用submit进行提交
2014/08/14 Javascript
JavaScript中的parse()方法使用简介
2015/06/12 Javascript
javascript的几种继承方法介绍
2016/03/22 Javascript
基于bootstrap-datetimepicker.js不支持IE8的快速解决方法
2016/11/07 Javascript
快速将Vue项目升级到webpack3的方法步骤
2017/09/14 Javascript
jquery动态添加带有样式的HTML标签元素方法
2018/02/24 jQuery
vue实现点击关注后及时更新列表功能
2018/06/26 Javascript
json字符串传到前台input的方法
2018/08/06 Javascript
JavaScript闭包与作用域链实例分析
2019/01/21 Javascript
vue中input的v-model清空操作
2019/09/06 Javascript
vue-video-player视频播放器使用配置详解
2020/10/23 Javascript
[01:05]主宰至宝剑心之遗
2017/03/16 DOTA
python列表生成式与列表生成器的使用
2018/02/23 Python
python调用Matplotlib绘制分布点并且添加标签
2018/05/31 Python
python requests.post带head和body的实例
2019/01/02 Python
浅谈spring boot 集成 log4j 解决与logback冲突的问题
2020/02/20 Python
详解HTML5 LocalStorage 本地存储
2016/12/23 HTML / CSS
现代生活方式的家具和装饰:Dot & Bo
2018/12/26 全球购物
探亲邀请信范文
2014/01/30 职场文书
先进事迹报告会主持词
2014/04/02 职场文书
乡镇保密工作责任书
2014/07/28 职场文书
学习焦裕禄精神践行三严三实心得体会
2014/10/13 职场文书
python实现简易名片管理系统
2021/04/11 Python
go语言使用Casbin实现角色的权限控制
2021/06/26 Golang
什么是clearfix (一文搞清楚css清除浮动clearfix)
2023/05/21 HTML / CSS