藏宝游

 找回密码
 立即注册
查看: 73|回复: 0

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

[复制链接]

 成长值: 530

  • TA的每日心情

    2024-3-8 00:13
  • 签到天数: 4 天

    [LV.2]圆转纯熟

    灌水成绩
    4356
    6
    23459
    主题
    帖子
    积分

    等级头衔

    ID : 3

    版主

    积分成就 威望 : 0
    贡献 : 0
    杰币 : 10385
    在线时间 : 21 小时
    注册时间 : 2022-1-8
    最后登录 : 2024-3-10

    荣誉勋章
    最佳新人热心会员推广达人宣传达人灌水之王突出贡献优秀版主荣誉管理论坛元老大富翁龙年大吉
    发表于 2024-3-10 13:20:43 | 显示全部楼层 |阅读模式
    前言
    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只会将回调添加到回调列表缓存起来,当任务触发时,会清空回调列表并依次执行所有回调 ,具体代码如下:
    1. const callbacks = []let pending = falsefunction flushCallbacks(){ //执行回调  pending = false  const copies = callbacks.slice(0)  callbacks.length = 0 //清空回调队列  for(let i = 0; i < copies.length; i++) {    copies[i]()  }}let microTimerFuncconst 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。具体实现如下:
    1. const callbacks = []let pending = falsefunction 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 = falseconst 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。具体实现如下:
    1. 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,当不支持的时候采取降级为宏任务方式
    1. if(typeof Promise !== 'undefined' && isNative(Promise)){  const p = Promise.resolve()  microTimerFunc = ()=>{    p.then(flushCallbacks)  }} else {  microTimerFunc = macroTimerFunc}
    复制代码
    若未提供回调且环境支持Promise情况下,nextTick会返回一个Promise,具体实现如下:
    1. 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运行原理的设计,完整代码如下:
    1. const callbacks = []let pending = falsefunction flushCallbacks(){ //执行回调  pending = false  const copies = callbacks.slice(0)  callbacks.length = 0 //清空回调队列  for(let i = 0; i < copies.length; i++) {    copies[i]()  }}let microTimerFunclet 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实现内容请搜索咔叽论坛以前的文章或继续浏览下面的相关文章希望大家以后多多支持咔叽论坛!

    原文地址:https://www.jb51.net/article/195028.htm
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    小黑屋|手机版|Archiver|RSS|藏宝游 ( 豫ICP备2021017492号 )|网站地图

    GMT+8, 2024-4-29 08:06 , Processed in 0.034818 second(s), 10 queries , Redis On.

    Powered by Discuz! X3.4

    本站不储存任何资源,所有资源均来自用户分享的网盘链接。
    本站为非盈利性站点,不会收取任何费用,所有内容不作为商业行为。

    快速回复 返回顶部 返回列表