【VUE3源码学习】nextTick 实现原理

2023-11-13

什么是nextTick?

定义: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

看完这个定义不免心生疑问:

  • 下次 DOM 更新循环结束之后是什么时候?
  • 执行延迟回调?
  • 更新后的 DOM?

基于以上问题和平时的使用经验可以基本解答疑问:

  • vue 更新DOM的策略是异步更新
  • nextTick 可以接收一个函数做为入参
  • nextTick 后能拿到最新的数据

那么nextTick 是怎么实现的呢?既然是异步更新,这涉及到了 js 的执行机制,下面一起复习一下js执行机制。

JS 执行机制

我们都知道 JS 是单线程语言,即指某一时间内只能干一件事,即为同步

而JS为什么是单线程的呢?这就要提及JS的主要用途了。JS自诞生之日起,其主要用途是与用户互动和DOM操作,如果同一时间,一个添加了 DOM,一个删除了 DOM, 这个时候语言就不知道是该添还是该删了,所以从应用场景来看 JS 只能是单线程,否则会带来复杂的同步问题。

单线程就意味着所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来讲是不可接受的,所以JS中就出现了异步的概念。

概念

同步任务:指排队在主线程上依次执行的任务
异步任务:不进入主线程,而进入任务队列的任务,又分为宏任务和微任务
宏任务: 渲染事件、请求、script、setTimeout、setInterval、Node中的setImmediate 等
微任务: Promise.then、MutationObserver(监听DOM)、Node 中的 Process.nextTick等

执行机制

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。

当执行栈中的同步任务执行完后,就会去任务队列中拿一个宏任务放到执行栈中执行,执行完该宏任务中的所有微任务,再到任务队列中拿宏任务,即一个宏任务、所有微任务、渲染、一个宏任务、所有微任务、渲染…(不是所有微任务之后都会执行渲染),如此形成循环,即事件循环(EventLoop)

nextTick 就是创建一个异步任务,那么它自然要等到同步任务执行完成后才执行。

nextTick 用法

先看个例子,点击按钮更新 DOM 内容,并获取最新的 DOM 内容

 <template>
     <div ref="test">{{name}}</div>
     <el-button @click="handleClick">按钮</el-button>
 </template>
 <script setup>
     import { ref, nextTick } from 'vue'
     const name = ref("initName")
     const test = ref(null)
     async function handleClick(){
         name.value = 'newName'
         console.log(test.value.innerText) // initName
         await nextTick()
         console.log(test.value.innerText) // newName
     }
     return { name, test, handleClick }
 </script>

nextTick 源码剖析

nextTick实现完全基于语言执行机制实现,直接创建一个异步任务,那么nextTick自然就达到在同步任务后执行的目的

源码版本:3.2.22,源码地址:packages/runtime-core/src/sheduler.ts

const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null

export function nextTick<T = void>(this: T, fn?: (this: T) => void): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

可以看出 nextTick 接受一个函数为参数,同时会创建一个Promise微任务。所以,页面调用 nextTick 的时候,会把的参数 fn 赋值给 p.then(fn),在队列currentFlushPromise || resolvedPromise的任务完成后,执行fn

vue3nextTick的队列由几个方法维护,基本执行顺序是这样的:queueJob -> queueFlush -> flushJobs -> nextTick参数的 fn

先有个印象即可,后面按照执行顺序依次分析。

nextTick调用位置

入口函数 queueJob 是在renderer函数中调用:

// packages/runtime-core/src/renderer.ts - 1555行
function baseCreateRenderer(){
  const setupRenderEffect: SetupRenderEffectFn = (...) => {
    const effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(instance.update), // 当作参数传入
      instance.scope
    )
  }
}

这里先看一下ReactiveEffect类的构造函数:

// packages/reactivity/src/effect.ts - 53行
export class ReactiveEffect<T = any> {
  // ...
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    recordEffectScope(this, scope)
  }
 // ...
}

ReactiveEffect 这边接收过来的第二个形参就是 scheduler,最终被用到响应式源码的派发更新。

当响应式对象发生改变后,如果执行 effectscheduler 这个参数,会执行这个 scheduler 函数,并且把 effect 当做参数传入

// packages/reactivity/src/effect.ts - 330行
export function triggerEffects(
  // ...
  if (effect.scheduler) {
    effect.scheduler()
  } else {
    effect.run()
  }
}

然后看 queueJob具体做了什么。

queueJob()

该方法负责维护主任务队列,接受一个函数作为参数,为待入队任务,会将参数 pushqueue 队列中,有唯一性判断。会在当前宏任务执行结束后,清空队列

const queue: SchedulerJob[] = []
let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行

export function queueJob(job: SchedulerJob) {
  // 判断条件:主任务队列为空 或者 有正在执行的任务且没有在主任务队列中  && job 不能和当前正在执行任务及后面待执行任务相同
  // 重复数据删除:
  // - 使用Array.includes(Obj, startIndex) 的 起始索引参数:startIndex
  // - startIndex默认为包含当前正在运行job的index,此时,它不能再次递归触发自身
  // - 如果job是一个watch()回调函数或者当前job允许递归触发,则搜索索引将+1,以允许他递归触发自身-用户需要确保回调函数不会死循环
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    // 判断当前job id 是否存在 不存在则添加到主任务队列
    if (job.id == null) {
      queue.push(job)
    } else {
      // 存在则从当前任务队列中查到位置并删除替换
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    // 创建微任务
    queueFlush()
  }
}

queueFlush()

该方法负责尝试创建微任务,等待任务队列执行

let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行
const resolvedPromise: Promise<any> = Promise.resolve() // 微任务创建器
let currentFlushPromise: Promise<void> | null = null // 当前任务

function queueFlush() {
  // 当前没有微任务
  if (!isFlushing && !isFlushPending) {
    // 避免在事件循环周期内多次创建新的微任务
    isFlushPending = true
    // 创建微任务,把 flushJobs 推入任务队列等待执行
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

flushJobs()

该方法负责处理队列任务,主要逻辑如下:

  • 先处理前置任务队列
  • 根据 Id 进行队列排序,并遍历执行队列任务,执行完毕后清空并重置队列
  • 执行后置队列任务
  • 如果队列没有被清空会递归调用flushJobs清空队列
function flushJobs(seen?: CountMap) {
  isFlushPending = false // 是否正在等待执行
  isFlushing = true // 正在执行
  if (__DEV__) {
    seen = seen || new Map() // 开发环境下
  }

  flushPreFlushCbs(seen) // 执行前置任务队列

  // 根据 id 排序队列,这是为了一下两点:
  // 1. 组件更新顺序为:父到子,因为父级总是在子级前面先创建,它的渲染效果具有较小的优先级数
  // 2. 如果父组件更新期间卸载了子组件,则改子组件更新将跳过
  queue.sort((a, b) => getId(a) - getId(b))


  // checkRecursiveUpdate 的条件使用必须在 try ... catch 块外中确定,因为 Rollup 默认会在 try-catch 中取消优化 treeshaking。
  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    // 遍历主任务队列,批量执行更新任务
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && check(job)) {
          continue
        }
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0 // 队列任务执行完,重置队列索引
    queue.length = 0 // 清空队列

    flushPostFlushCbs(seen) // 执行后置队列任务

    isFlushing = false  // 重置队列执行状态
    currentFlushPromise = null // 重置当前微任务为 Null
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
    // 如果主任务队列、前置和后置任务队列还有没被清空,就继续递归执行
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs(seen)
    }
  }
}

flushPreFlushCbs()

该方法负责执行前置任务队列,说明都写在注释里了

  • 待处理前置任务队列不为空是,备份并清空前置任务队列
  • 并遍历执行待处理前置队列任务,执行完毕后当前任务队列
  • 如果队列没有被清空会递归调用flushJobs清空队列
const pendingPreFlushCbs: SchedulerJob[] = []
let activePreFlushCbs: SchedulerJob[] | null = null
let preFlushIndex = 0

export function flushPreFlushCbs(
  seen?: CountMap,
  parentJob: SchedulerJob | null = null
) {
  // 待处理前置任务队列不为空
  if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 待处理前置任务队列去重备份为activePreFlushCbs
    pendingPreFlushCbs.length = 0 // 待处理前置任务队列重置
    if (__DEV__) {
      seen = seen || new Map()
    }
    // 遍历执行队列里的任务
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
      ) {
        continue
      }
      // 任务执行
      activePreFlushCbs[preFlushIndex]()
    }
    // 清空当前活动的任务队列
    activePreFlushCbs = null 
    preFlushIndex = 0
    currentPreFlushParentJob = null
    // 递归执行,直到清空前置任务队列
    flushPreFlushCbs(seen, parentJob)
  }
}

flushPostFlushCbs()

该方法负责执行后置任务队列,说明都写在注释里了

const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0

export function flushPostFlushCbs(seen?: CountMap) {
  // 待处理后置任务队列队列不为空
  if (pendingPostFlushCbs.length) {
    // 待处理后置任务队列去重备份为deduped
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0 // 待处理后置任务队列重置

    // #1947 already has active queue, nested flushPostFlushCbs call
    // 如果当前已经有活动的队列,就添加到执行队列的末尾,并返回
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped)
      return
    }
    // 赋值为当前活动队列
    activePostFlushCbs = deduped
    if (__DEV__) {
      seen = seen || new Map()
    }

    // 队列排序
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
    // 遍历执行队列里的任务
    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
      ) {
        continue
      }
      activePostFlushCbs[postFlushIndex]()
    }
    // 清空当前活动的任务队列
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}

整个 nextTick 的源码到这就解析完啦

为什么要nextTick

一个例子让大家明白,如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新,有了nextTick机制,只需要更新一次。

{{num}}
for(let i=0; i<100000; i++){
	num = i
}

总结

nextTickvue 中的更新策略,也是性能优化手段,基于JS执行机制实现

vue 中我们改变数据时不会立即触发视图,如果需要实时获取到最新的DOM,可以手动调用 nextTick

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【VUE3源码学习】nextTick 实现原理 的相关文章

  • Javascript 函数查找数字的倍数

    创建一个名为的函数multiplesOf 它将接受两个参数 第一个参数是数字数组 第二个参数是数字 该函数应返回一个新数组 该数组由参数数组中的每个数字组成 该数字是参数数字的倍数 So multiplesOf 5 6 7 8 9 10 3
  • 渲染货币和符号并与来自不同单元格的数据相结合

    我正在使用最新的 jQuery DataTables v1 10 7 我正在尝试将数字解析为以下格式 239 90 USD 我可以使用此命令使货币正常工作 columns data Price render fn dataTable ren
  • 为什么我的淘汰单选按钮在另一个具有点击绑定的元素内时会失败?

    我有一个单选按钮列表 我想要点击 li 他们还检查单选按钮 这一切都有效 直到我放了一个name单选元素上的属性 然后我的代码停止工作 我的代码如下所示 ul li li ul li
  • IE从哪个版本开始支持Object.create(null)?

    您可以通过多种方式在 JavaScript 中创建对象 creates an object which makes the Object prototype of data var data1 new Object Object liter
  • 如何将内联 JavaScript 与 Express/Node.js 中动态生成的内容分开?

    对于具有几年 Web 开发经验但没有找到答案的人来说 这是一个有点菜鸟的问题程序员堆栈交换 or Google 我决定在这里问一下 我在用Express网络框架Node js 但这个问题并不特定于任何 Web 框架或编程语言 以下是从数据库
  • 如何针对 Node.js 中发生的每个错误发送电子邮件?

    假设我的 node js 应用程序正在运行 如果出现错误 我的意思是所有错误 不仅仅是网络错误 如果出现错误 则很重要 我如何调用函数向我发送电子邮件 基本上 在我希望它写入 err out 之前 我希望向我发送一封电子邮件 我正在使用no
  • 如何将函数附加到弹出窗口关闭事件(Twitter Bootstrap)

    我做了一些搜索 但我只能认为我可以将事件附加到导致其关闭的按钮 https stackoverflow com questions 13205103 attach event handler to button in twitter boo
  • 将 GMT 时间转换为当地时间

    我以这种格式从我的服务器获取 GMT 时间 Fri 18 Oct 2013 11 38 23 GMT 我的要求是使用Javascript将此时间转换为本地时间 例如 如果用户来自印度 首先我需要采用时区 5 30并将其添加到我的服务器时间并
  • 在 HTML5 画布中,如何用我选择的背景遮盖图像?

    我试图用画布来实现这一点 globalCompositeOperation 但没有运气 所以我在这里问 这里有类似的问题 但我没有在其中找到我的案例 我的画布区域中有图层 从下到上的绘制顺序 画布底座填充纯白色 fff 用fillRect
  • 为什么我们在打字稿中使用 HTMLInputElement ?

    我们为什么使用 document getElementById ipv as HTMLInputElement value 代替 document getElementById ipv value 功能getElementById返回具有类
  • 将 UMD Javascript 模块导入浏览器

    你好 我正在对 RxJS 进行一些研究 我可以通过在浏览器中引用它来使用该库 如下所示 它使用全局对象命名空间变量 Rx 导入 我可以制作可观察的东西并做所有有趣的事情 当我将 src 更改为指向最新的 UMD 文件时 一切都会崩溃 如下所
  • 页面上使用 HTML Editor Extender 进行回发会导致 IE11 中出现 JavaScript 错误

    我已将 HTML 编辑器扩展程序添加到我正在处理的页面中 现在每当我在页面上发回帖子时 都会收到以下 Javascript 错误 JavaScript 运行时错误 参数无效 之后什么也没有发生 这在 IE10 或更低版本以及我所知道的所有其
  • Vuejs 2:去抖动不适用于手表选项

    当我在 VueJs 中反跳此函数时 如果我提供毫秒数作为原语 它就可以正常工作 但是 如果我将其提供为对 prop 的引用 它会忽略它 这是道具的缩写版本 props debounce type Number default 500 这是不
  • 从数据库检查数据的异步解决方案各种循环子句

    我想要做的是异步检查数据库并从中获取结果 在我的应用程序中我试图实现Asynchronously将此步骤解决为 从数据库中检查手机号码JsonArray循环子句的种类 Create JsonArray从结果 打印创建的数组 我学到了足够多的
  • 如何在生产模式下为 Chrome 扩展启用 Vue 开发工具?

    我正在构建一个 chrome 扩展 并使用 vue cli webpack 配置 我希望能够在运行后使用 vue devtoolsnpm 运行构建命令 我尝试添加Vue config devtools true 在 main js 中 或者
  • 用于交互式图形绘制的轻量级 JavaScript 库? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我有兴趣了解用于绘制交互式图表的最轻量级 javascript 库 我掌握的数据主要是与海洋研究相关的科学数据 我知道一些 jquery
  • JavaScript 相对路径

    在第一个 html 文件中 我使用了一个变量类别链接 var categoryLinks Career prospects http localhost Landa DirectManagers 511 HelenaChechik Dim0
  • 分页在服务器端好还是前端好? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我正在构建 Laravel Vue 应用程序 我想知道在后端使用分页还是在前端使用分页更好 我认为最好在每页发送尽可能少的数据的请求 但我想听听
  • 如何在执行新操作时取消先前操作的执行?

    我有一个动作创建器 它会进行昂贵的计算 并在每次用户输入内容时调度一个动作 基本上是实时更新 但是 如果用户输入多个内容 我不希望之前昂贵的计算完全运行 理想情况下 我希望能够取消执行先前的计算并只执行当前的计算 没有内置功能可以取消Pro
  • 如何使用asm.js进行测试和开发?

    最近我读到asm js规范 看起来很酷 但是是否有任何环境 工具来开发和测试这个工具 这还只是处于规范阶段吗 您可以尝试使用 emscripten 和 ASM JS 1 并从侧分支在 firefox 构建中运行它 有关 asm js 的链接

随机推荐

  • MySQL 可重复读隔离级别,完全解决幻读了吗?

    我在上一篇文章中提到 MySQL InnoDB 引擎的默认隔离级别虽然是 可重复读 但是它很大程度上避免幻读现象 并不是完全解决了 解决的方案有两种 针对快照读 普通 select 语句 是通过 MVCC 方式解决了幻读 因为可重复读隔离级
  • 通过URL自动触发Jenkins构建任务

    方法一 进入jenkins安全设置 开启安全域及授权策略 2 在用户设置处生成api token 复制生成的token 3 选择测试项目 配置 构建触发器 选择触发远程构建 将token粘贴在身份验证令牌处 保存 4 在浏览器中输入 htt
  • 联想ideapad700-15isk(小新线下版)黑苹果完美驱动附详细安装过程

    直接上安装的流程 欢迎大家关注我的个人博客 联想ideapad700 15isk 我的配置如下 前期准备 制作安装盘 盘符分配 设置U盘启动 开始安装 添加本地引导 安装完成进入设置 关于clover 划重点 2020年8月30日更新 20
  • ssm框架+Layui整合案例

    业余写得整合案例 想学习的可以来参考 初来乍到 准备工作 Layui tomcat mysql 目录 1 实现的效果图 2 实现代码 2 0 前端代码 2 1 登录页面login jsp 2 2 登录后跳转的主页面 main jap web
  • SQLi LABS Less-22

    第22关使用POST请求提交参数 对账号和密码中的特殊字符执行了转译的操作 难度较大 这一关的重点在于Cookie 用户登录成功后 将base64编码后的用户名保存到Cookie中 点击提交按钮时 会从Cookie中获取用户名 使用base
  • Yum update和upgrade的区别

    Yum update和upgrade的区别 Linux yum中package升级命令有两个分别是 yum upgrade 和 yum update 1 区别 默认情况下 yum update和yum upgrade的功能是完全一样的 都是
  • 基于.NET CORE 3.1的WEB API通过EF CORE连接MySQL

    基于 NET CORE 3 1的WEB API通过EF CORE连接MySQL 注 本文不采用CodeFirst 不使用迁移 1 准备好一个WEB API项目 可以看我之前的文章 2 准备好一个MySQL数据库并创建表 3 引用nuget包
  • 神经网络学习小记录44——训练资源汇总贴

    神经网络学习小记录44 训练资源汇总贴 前言 权值文件 1 迁移学习 传统神经网络 2 目标检测 a keras权重 b pytorch权重 c tensorflow2权重 3 实例分割 4 语义分割 旧版 5 语义分割 新版 a kera
  • nginx请求返回html文件,nginx返回json或者文本格式的方法

    用nginx怎么返回json格式或者文本格式的数据 其实很简单 如下代码 1 返回文本格式 location get text default type text html return 200 hello world 2 返回json格式
  • sublime配置go环境_Win10下sublime text3搭建go语言开发环境--工具篇

    进行 go 语言开发环境的搭建 最近进行了大量的搜索 因为在搭建的过程中遇到了挺多的问题 先介绍搭建的环境 系统 Win10 IDE sublime text3 相关插件 GoSublime 这篇文会介绍如下几个部分 1 下载Golang
  • 知道 Redis RDB 这些细节,可以少踩很多坑

    在使用 Redis 的过程中 你是否遇到过下面这些问题 开启 RDB 落盘 业务频繁出现请求超时 除了 save 和 bgsave 命令 还有哪些操作会触发 RDB 落盘 执行了 flushall 发现 flushall 之前写的数据又冒出
  • 根据传入的年份和月份获取该月属于本年的第几周和每周的开始和结束日期

    function getInfo year month var getInfo function year month var d new Date d setFullYear year month 1 1 var w1 d getDay
  • unity学习笔记-unity(2019)实现与as相互跳转

    Unity学习笔记 Unity 2019 嵌入安卓开发 实现相互跳转 思路 流程 先在unity中添加跳转到安卓的方法 AS配置unity的信息 2021 5 27更新一下 as添加跳转至unity的方法 as添加unity跳转到app的方
  • 8086CPU只有16位寄存器,却可以访问20位的物理地址

    一 背景介绍 Intel 8086是一个由Intel于1978年所设计的16位微处理器芯片 是x86架构的鼻祖 它是以8080和8085的设计为基础 拥有类似的寄存器组 但是数据总线扩充为16位 总线界面单元 Bus Interface U
  • javafx实现登录注册界面

    package sample import JavaBigJob BaseScene import javafx scene Parent import javafx scene layout Pane import javafx appl
  • 前端学习总结:5、Bootstrap

    前端学习总结 5 Bootstrap 文章目录 前端学习总结 5 Bootstrap 1 前言 2 资料 3 下载安装 Bootstrap 4 Bootstrap css 按钮 5 Bootstrap css 表格 6 Bootstrap
  • ZYNQ PL开发流程

    2 ZYNQ PL开发 开发流程 开发使用vivado 流程如下 1 新建工程 工程项目含义 这里简单介绍下各个工程类型的含义 RTL Project 是指按照正常设计流程所选择的类型 这也是常用的一种类型 RTL Project 下的 D
  • 《大话数据结构》-程杰 读书笔记

    认为程序设计的实质是对确定的问题选择一种好的结构 加上设计一种好的算法 可见 数据结构在程序设计当中占据了重要的地位 程序设计 数据结构 算法 要你相信自己一定可以学得会 学得好 既然无数人已经掌握了 你凭什么不行 于每个链表来说 它所占用
  • CentOS中安装docker-compose

    下载安装包 wget https github com docker compose releases download v2 2 3 docker compose linux x86 64 移动到 usr local bin 目录 mv
  • 【VUE3源码学习】nextTick 实现原理

    什么是nextTick 定义 在下次 DOM 更新循环结束之后执行延迟回调 在修改数据之后立即使用这个方法 获取更新后的 DOM 看完这个定义不免心生疑问 下次 DOM 更新循环结束之后是什么时候 执行延迟回调 更新后的 DOM 基于以上问