从 14 道面试题,彻底搞懂 Event Loop 与 Vue2 nextTick 底层原理

从 14 道执行顺序题,彻底搞懂 JavaScript Event Loop 与 Vue2 nextTick 原理

你真的理解 JavaScript 事件循环吗?为什么 Promise 比 setTimeout 先执行?为什么 Vue2 连续修改三次数据,DOM 却只更新一次?为什么 $nextTick 有时候比 Promise.then 先执行,有时候又排在后面?

这篇文章从基础执行顺序出发,逐步深入微任务队列、async/await、浏览器渲染、Vue2 Watcher 调度器以及 nextTick 的源码实现。

前言

作为前端开发者,我们经常会写这样的代码:

javascript 复制代码
this.message = 'hello';

console.log(this.$refs.title.textContent);

this.$nextTick(() => {
  console.log(this.$refs.title.textContent);
});

明明 message 已经修改了,为什么第一次读取 DOM 还是旧值,而 $nextTick 中却能拿到新值?

很多人会回答:

因为 Vue 的 DOM 更新是异步的。

这个答案没错,但还不够。

继续追问:

  • Vue2 的异步更新到底异步在哪里?
  • Promise.then 和 $nextTick 都是微任务吗?
  • Vue2 为什么要维护 Watcher 队列?
  • flushSchedulerQueue 和 flushCallbacks 有什么区别?
  • 嵌套 $nextTick 为什么会产生新一批回调?
  • DOM 更新完成是否意味着浏览器已经把画面绘制出来?

要回答这些问题,必须先理解 JavaScript 的 Event Loop。

本文主要以浏览器环境和 Vue 2.6 默认异步更新机制为背景。涉及 Vue 内部实现的代码均为便于理解的简化版本。


一、JavaScript 为什么需要事件循环?

JavaScript 在浏览器主线程上的执行具有一个重要特点:同一时刻只能执行一段 JavaScript 代码。

假设有以下代码:

javascript 复制代码
console.log('A');

setTimeout(() => {
  console.log('B');
}, 0);

console.log('C');

最终输出:

text 复制代码
A
C
B

为什么 setTimeout 的延迟设置为 0,B 还是最后执行?

因为 setTimeout(fn, 0) 的含义不是立即执行 fn,而是在满足计时条件后,将回调安排为一个等待执行的任务。

只有当前 JavaScript 执行结束,并且事件循环调度到这个任务时,它才有机会执行。

1.1 调用栈

调用栈(Call Stack)用于记录当前正在执行的函数。

例如:

javascript 复制代码
function a() {
  b();
}

function b() {
  console.log('hello');
}

a();

执行过程:

text 复制代码
进入 a()
  ↓
进入 b()
  ↓
执行 console.log()
  ↓
b() 返回
  ↓
a() 返回

JavaScript 不会在 b() 执行到一半时,突然跳过去执行一个普通的 setTimeout 回调。

1.2 任务与微任务

浏览器中的异步回调可以来自不同的调度机制。

常见的任务来源:

  • setTimeout
  • setInterval
  • 用户点击事件
  • 部分网络与消息事件

常见的微任务来源:

  • Promise.then / catch / finally
  • queueMicrotask
  • async/await 的后续执行
  • MutationObserver

这里需要注意:async/await 并不是一个独立的"更高级队列"。它的后续执行依赖 Promise 相关的微任务机制。

1.3 Event Loop 的简化模型

可以把浏览器的一次典型调度过程理解为:

text 复制代码
执行一个任务
     ↓
运行同步 JavaScript
     ↓
调用栈清空
     ↓
执行微任务检查点
     ↓
持续处理微任务,直到队列清空
     ↓
浏览器可能进行渲染更新
     ↓
选择下一个任务

这里有两个重点。

第一,微任务检查点会持续处理新加入的微任务,而不是只执行检查点开始时已经存在的那些。

第二,浏览器不是每执行完一个任务就一定绘制一帧。是否更新渲染取决于浏览器的渲染时机和调度策略。


二、Promise 为什么比 setTimeout 先执行?

来看一道经典面试题:

javascript 复制代码
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

输出:

text 复制代码
1
4
3
2

分析:

第一阶段:同步代码

text 复制代码
输出 1
注册 setTimeout
注册 Promise.then
输出 4

此时:

text 复制代码
调用栈:即将清空

微任务队列:
[Promise.then]

待执行的定时器任务:
[setTimeout]

第二阶段:微任务检查点

执行 Promise 回调:

text 复制代码
输出 3

第三阶段:执行定时器任务

text 复制代码
输出 2

因此结果为:

text 复制代码
1 → 4 → 3 → 2

记住一个原则:

不要只看代码出现的位置,还要看回调通过什么机制被调度。


三、嵌套 Promise:微任务遵循先进先出

下面这道题比上一题更容易出错:

javascript 复制代码
console.log('A');

Promise.resolve().then(() => {
  console.log('B');

  Promise.resolve().then(() => {
    console.log('C');
  });
});

Promise.resolve().then(() => {
  console.log('D');
});

console.log('E');

你可能会以为,B 执行之后紧接着就会执行它内部的 C。

实际上输出为:

text 复制代码
A
E
B
D
C

3.1 为什么不是 A E B C D?

同步代码执行结束时,微任务队列为:

text 复制代码
[B, D]

开始执行 B:

javascript 复制代码
console.log('B');

Promise.resolve().then(() => {
  console.log('C');
});

执行 B 的过程中,新注册的 C 会追加到微任务队列尾部。

此时队列变成:

text 复制代码
[D, C]

所以 D 先执行,C 后执行。

整个过程:

步骤 执行内容 微任务队列
1 同步输出 A 空
2 注册 B B
3 注册 D B、D
4 同步输出 E B、D
5 执行 B,注册 C D、C
6 执行 D C
7 执行 C 空

结论:微任务执行期间创建的新微任务,不会抢占已经在等待的微任务。


四、async/await 的执行顺序

async/await 让异步代码看起来像同步代码,但它不会改变 Promise 的基本调度机制。

例如:

javascript 复制代码
console.log('1');

async function test() {
  console.log('2');

  await Promise.resolve();

  console.log('3');
}

test();

Promise.resolve().then(() => {
  console.log('4');
});

console.log('5');

输出:

text 复制代码
1
2
5
3
4

4.1 await 前后有什么区别?

执行:

javascript 复制代码
async function test() {
  console.log('2');

  await Promise.resolve();

  console.log('3');
}

其中:

javascript 复制代码
console.log('2');

属于当前调用过程中的同步执行。

而:

javascript 复制代码
console.log('3');

需要等待 await 对应的 Promise 完成,并通过微任务继续执行。

即使等待的是一个已经 fulfilled 的 Promise,await 后面的代码也不会直接作为同一段同步代码继续执行。

4.2 一个常见误区

不要把:

javascript 复制代码
await somePromise;

简单理解成:

javascript 复制代码
setTimeout(..., 0);

两者使用的调度机制不同。

另外,await 后续执行与其他 Promise 回调的具体先后关系,还要结合它们实际入队的时间判断。


五、DOM 更新与浏览器绘制不是一回事

来看下面的代码:

javascript 复制代码
const box = document.getElementById('box');

box.style.backgroundColor = 'red';

Promise.resolve().then(() => {
  box.style.backgroundColor = 'blue';
});

setTimeout(() => {
  box.style.backgroundColor = 'green';
}, 0);

从 JavaScript 执行角度看,颜色依次被设置为:

text 复制代码
red → blue → green

但是用户是否一定能看到红色、蓝色和绿色三个画面?

不一定。

原因是:

  • JavaScript 修改 DOM 或样式,不代表浏览器立即完成绘制。
  • Promise 微任务通常会在浏览器获得下一次渲染机会前执行。
  • 浏览器可能合并多次样式变化。
  • 定时器执行前是否发生一次绘制,也不能仅凭 setTimeout(0) 保证。

所以,最终 DOM 样式是绿色,不代表屏幕一定经历了三次可见颜色变化。

5.1 requestAnimationFrame 与 setTimeout

requestAnimationFrame 用于安排与浏览器渲染更新相关的回调。

例如:

javascript 复制代码
console.log('A');

requestAnimationFrame(() => {
  console.log('B');
});

Promise.resolve().then(() => {
  console.log('C');
});

setTimeout(() => {
  console.log('D');
}, 0);

console.log('E');

可以确定同步输出:

text 复制代码
A
E

然后执行 Promise 微任务:

text 复制代码
C

但 requestAnimationFrame 回调 B 与定时器回调 D 的先后,不能简单地写死为一种顺序。

requestAnimationFrame 并不是普通的定时器任务,它与浏览器的渲染更新时机相关;后台标签页中还可能受到节流。

因此不能武断地说:

requestAnimationFrame 永远比 setTimeout 快。


六、进入 Vue2:为什么修改数据后 DOM 不会立即更新?

现在开始理解 Vue2 的异步更新机制。

假设组件中有:

javascript 复制代码
data() {
  return {
    message: 'old'
  };
}

模板:

html 复制代码
<div ref="title">{{ message }}</div>

然后执行:

javascript 复制代码
this.message = 'hello';

console.log(this.message);

console.log(this.$refs.title.textContent);

在组件已挂载、使用默认异步更新的情况下,可能输出:

text 复制代码
hello
old

为什么?

因为 Vue2 的响应式 setter 会立即更新数据,但组件 DOM 的更新通常被安排到异步刷新队列中。

可以简单理解为:

text 复制代码
this.message = 'hello'
          ↓
响应式 setter
          ↓
通知相关 Watcher
          ↓
Watcher 进入更新队列
          ↓
安排异步刷新
          ↓
执行渲染 Watcher
          ↓
更新真实 DOM

这里的 Watcher 是 Vue2 响应式系统的重要组成部分。

6.1 什么是 Watcher?

Watcher 可以理解为负责观察依赖并响应变化的对象。

Vue2 中常见的 Watcher 包括:

  • 组件渲染 Watcher:负责组件重新渲染。
  • 用户 Watcher:例如通过 watch 或 $watch 创建。
  • 计算属性 Watcher:配合计算属性的懒求值和缓存机制。

这些 Watcher 并不是完全相同的调度方式。

本文后面讨论的批量更新,主要针对默认异步模式下的组件渲染 Watcher。


七、Vue2 为什么连续修改三次数据,却只更新一次 DOM?

经典代码:

javascript 复制代码
this.count = 1;
this.count = 2;
this.count = 3;

this.$nextTick(() => {
  console.log(this.$refs.count.textContent);
});

假设模板:

html 复制代码
<div ref="count">{{ count }}</div>

最终输出:

text 复制代码
3

而且在典型情况下,同一个组件的渲染 Watcher 在这一批更新中只需要运行一次。

7.1 Vue2 的 Watcher 队列

Vue2 不会在每次响应式赋值时立即重新渲染整个组件。

它会通过调度器把需要更新的 Watcher 加入队列。

假设渲染 Watcher 的 ID 为 7。

执行:

javascript 复制代码
this.count = 1;

Watcher 队列:

text 复制代码
[Watcher #7]

继续执行:

javascript 复制代码
this.count = 2;

Watcher 队列仍然是:

text 复制代码
[Watcher #7]

继续执行:

javascript 复制代码
this.count = 3;

队列还是:

text 复制代码
[Watcher #7]

为什么不会重复入队?

因为 Vue2 使用 Watcher ID 进行去重。

7.2 queueWatcher 的核心思想

简化实现:

javascript 复制代码
const queue = [];
const has = {};
let waiting = false;

function queueWatcher(watcher) {
  const id = watcher.id;

  if (has[id] == null) {
    has[id] = true;

    queue.push(watcher);

    if (!waiting) {
      waiting = true;

      nextTick(flushSchedulerQueue);
    }
  }
}

其中:

queue 用于保存待更新的 Watcher。

has 用于判断某个 Watcher 是否已经入队。

waiting 用于避免重复安排同一轮刷新。

所以:

javascript 复制代码
this.count = 1;
this.count = 2;
this.count = 3;

虽然发生了三次赋值,但同一个渲染 Watcher 通常只进入当前队列一次。

7.3 flushSchedulerQueue 做什么?

flushSchedulerQueue 是 Vue2 调度器中负责执行 Watcher 队列的重要函数。

它的核心工作包括:

  1. 对 Watcher 队列排序。
  2. 依次执行 Watcher。
  3. 处理刷新相关的生命周期逻辑。
  4. 重置调度状态,为后续更新做准备。

当渲染 Watcher 真正运行时:

javascript 复制代码
count === 3

所以组件渲染读取到的是最新值 3。

这就是 Vue2 批量更新的核心优化。

注意:去重并不意味着 setter 只执行了一次,而是避免同一批次内重复安排相同的异步 Watcher。


八、nextTick 的原理到底是什么?

我们经常写:

javascript 复制代码
this.message = 'hello';

this.$nextTick(() => {
  console.log(this.$refs.title.textContent);
});

很多人认为 $nextTick 就等于:

javascript 复制代码
Promise.resolve().then(callback);

这种理解过于简单。

Vue2 的 $nextTick 本身还维护了一份内部回调列表。

8.1 nextTick 的简化实现

javascript 复制代码
const callbacks = [];

let pending = false;

function nextTick(cb) {
  callbacks.push(cb);

  if (!pending) {
    pending = true;

    Promise.resolve().then(flushCallbacks);
  }
}

function flushCallbacks() {
  pending = false;

  const copies = callbacks.slice();

  callbacks.length = 0;

  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

这里的代码是原理简化版,并非 Vue2 完整源码。

需要注意两个不同层次:

Vue 内部回调列表:

text 复制代码
[flushSchedulerQueue, 用户 nextTick 回调]

浏览器微任务队列:

text 复制代码
[Vue flushCallbacks]

它们不是同一个队列。

flushSchedulerQueue 是 Vue 内部回调,通常由 flushCallbacks 调用;并不是每一个 flushSchedulerQueue 都直接对应一个独立的浏览器微任务。

这也是理解 Vue2 和原生 Promise 注册顺序的关键。


九、Vue2 nextTick 与 Promise.then,到底谁先执行?

这是本文最值得反复理解的一节。

9.1 情况一:先修改响应式数据,再注册 Promise

javascript 复制代码
console.log('A');

this.message = 'hello';

this.$nextTick(() => {
  console.log('B');
});

Promise.resolve().then(() => {
  console.log('C');
});

console.log('D');

在 Vue 2.6 的典型现代浏览器环境中,输出:

text 复制代码
A
D
B
C

原因:

执行:

javascript 复制代码
this.message = 'hello';

Vue 会安排自己的刷新微任务。

然后:

javascript 复制代码
this.$nextTick(() => {
  console.log('B');
});

B 加入 Vue 内部回调列表。

之后才注册原生 Promise C。

所以浏览器微任务队列为:

text 复制代码
[Vue flushCallbacks, Promise C]

Vue 先更新 DOM,再执行 B。

然后才执行 C。

9.2 情况二:先注册 Promise,再修改响应式数据

把顺序改一下:

javascript 复制代码
console.log('A');

Promise.resolve().then(() => {
  console.log('C');
});

this.message = 'hello';

this.$nextTick(() => {
  console.log('B');
});

console.log('D');

这次输出:

text 复制代码
A
D
C
B

为什么?

因为原生 Promise C 比 Vue 的刷新微任务更早入队。

微任务队列:

text 复制代码
[Promise C, Vue flushCallbacks]

所以 C 先执行,B 后执行。

9.3 结论

$nextTick 不具有天然高于 Promise 的执行优先级。

在 Vue2 使用 Promise 微任务调度时,执行顺序取决于:

  • Vue 的刷新微任务何时被安排。
  • 原生 Promise 回调何时入队。
  • 用户的 $nextTick 回调位于 Vue 内部回调列表的什么位置。

因此,在 Vue2 项目中需要读取更新后的 DOM 时,应该明确使用:

javascript 复制代码
this.$nextTick(() => {
  // 读取更新后的 DOM
});

而不是依赖某个 Promise 回调恰好在 Vue 更新之后执行。


十、嵌套 nextTick:为什么会出现两批 DOM 更新?

看下面的代码:

javascript 复制代码
this.count = 1;
this.count = 2;

this.$nextTick(() => {
  this.count = 3;

  this.$nextTick(() => {
    console.log(this.$refs.count.textContent);
  });
});

最终输出:

text 复制代码
3

在典型情况下,组件渲染 Watcher 会运行两次。

10.1 第一批更新

同步执行:

javascript 复制代码
this.count = 1;
this.count = 2;

由于 Watcher 去重,只安排一次组件渲染。

第一批更新后:

text 复制代码
count = 2
DOM = 2

然后执行外层 $nextTick。

10.2 第二批更新

外层回调执行:

javascript 复制代码
this.count = 3;

此时第一批 Watcher 调度已经完成,相关去重状态已经重置。

所以渲染 Watcher 可以再次入队。

第二批更新后:

text 复制代码
count = 3
DOM = 3

随后执行内层 $nextTick,输出 3。

10.3 去重范围不是永久的

Vue2 的 Watcher 去重主要服务于当前调度批次。

它不会因为某个 Watcher 曾经执行过,就永远不允许它再次更新。

另外,两批 Vue 更新不等于浏览器一定绘制了两帧。

两批刷新完全可能在浏览器下一次绘制之前连续完成。


十一、nextTick 中注册 Promise 和新的 nextTick

继续看一道稍复杂的题:

javascript 复制代码
console.log('A');

this.count = 1;

this.$nextTick(() => {
  console.log('B');

  Promise.resolve().then(() => {
    console.log('C');
  });

  this.$nextTick(() => {
    console.log('D');
  });
});

console.log('E');

输出:

text 复制代码
A
E
B
C
D

11.1 为什么 C 在 D 前面?

同步阶段:

text 复制代码
A
E

Vue 执行第一次 flushCallbacks:

text 复制代码
更新 DOM
执行外层 nextTick
输出 B

在 B 所在的回调中:

javascript 复制代码
Promise.resolve().then(() => {
  console.log('C');
});

先安排原生 Promise C 微任务。

然后:

javascript 复制代码
this.$nextTick(() => {
  console.log('D');
});

Vue 将 D 加入新的内部回调列表,并安排第二次 flushCallbacks。

因此微任务队列变成:

text 复制代码
[Promise C, Vue flushCallbacks #2]

所以先输出 C,再输出 D。

11.2 如果交换注册顺序?

javascript 复制代码
this.$nextTick(() => {
  console.log('B');

  this.$nextTick(() => {
    console.log('D');
  });

  Promise.resolve().then(() => {
    console.log('C');
  });
});

在相同前提下,这部分会变成:

text 复制代码
B
D
C

原因很简单:Vue 的第二次刷新微任务先于 Promise C 入队。

这再次证明:

不要死记 API 的优先级,要追踪微任务实际入队的时间。


十二、微任务饥饿:为什么 Promise 也可能卡死页面?

微任务虽然很方便,但如果使用不当,也可能造成严重的性能问题。

例如:

javascript 复制代码
function loop() {
  Promise.resolve().then(loop);
}

loop();

setTimeout(() => {
  console.log('定时器执行');
}, 0);

这里每次执行 loop,都会再次创建一个 Promise 微任务。

结果就是:

text 复制代码
执行微任务
   ↓
创建下一个微任务
   ↓
执行下一个微任务
   ↓
继续创建微任务
   ↓
......

微任务队列始终无法清空。

这样会导致:

  • 定时器迟迟无法执行。
  • 浏览器无法正常处理后续任务。
  • 页面可能无法及时响应点击。
  • 浏览器可能长时间得不到绘制机会。

这就是微任务饥饿。

12.1 如何避免?

如果需要处理大量数据,可以分批执行。

例如:

javascript 复制代码
function processInChunks(items) {
  let index = 0;
  const chunkSize = 100;

  function runChunk() {
    let processed = 0;

    while (
      index < items.length &&
      processed < chunkSize
    ) {
      processItem(items[index]);

      index++;
      processed++;
    }

    if (index < items.length) {
      setTimeout(runChunk, 0);
    }
  }

  runChunk();
}

通过将后续工作安排到新的任务中,浏览器有机会在任务之间处理其他工作。

但 setTimeout 不保证精确的绘制时机。

如果涉及动画,可以考虑 requestAnimationFrame;如果是大量纯计算任务,可以考虑 Web Worker。


十三、Vue2 实际开发中的几个建议

13.1 修改数据后读取 DOM,使用 nextTick

推荐:

javascript 复制代码
this.list.push(newItem);

this.$nextTick(() => {
  const height = this.$refs.list.offsetHeight;

  console.log(height);
});

不要为了等待 DOM 更新而随意写:

javascript 复制代码
setTimeout(() => {
  // 读取 DOM
}, 0);

定时器不是 Vue DOM 更新完成的语义保证。

13.2 不要滥用嵌套 nextTick

一般情况下,一次 $nextTick 就够了。

只有当你在第一个回调中又修改了响应式数据,并且确实需要等待这次修改对应的 DOM 更新,才需要再次等待。

13.3 不要误以为 nextTick 等于浏览器完成绘制

$nextTick 保证的是 Vue 对已安排更新的 DOM 刷新时机,而不是保证屏幕已经完成绘制。

如果你的逻辑依赖浏览器渲染帧,例如动画、过渡或某些布局时序,需要结合 requestAnimationFrame 等机制具体分析。

13.4 不要把 Watcher 去重理解成所有更新都只执行一次

默认异步渲染 Watcher 的批量去重,是 Vue2 的重要优化。

但同步 Watcher、计算属性 Watcher、刷新过程中再次触发更新等情况,都有各自的行为。

不能把"一个 tick 只更新一次"当成没有例外的绝对规则。


十四、一张表总结 Event Loop 与 Vue2 更新机制

知识点 核心结论
同步代码 当前调用栈内直接执行
setTimeout 安排后续任务,不保证立即执行
Promise.then 回调通过微任务调度
async/await await 后续执行与 Promise 微任务机制有关
嵌套 Promise 新微任务追加到队列尾部
requestAnimationFrame 与浏览器渲染更新时机相关
Vue2 响应式 setter 数据值立即改变
Vue2 Watcher 队列 负责批量调度响应式更新
Watcher 去重 避免当前批次内重复安排相同 Watcher
flushSchedulerQueue 执行 Watcher 更新队列
flushCallbacks 执行 Vue nextTick 内部回调列表
$nextTick 等待 Vue 已安排的 DOM 更新
嵌套 $nextTick 可能产生新的回调批次
微任务饥饿 微任务不断追加,可能阻塞其他任务和渲染

十五、最后留一道综合面试题

如果你已经理解前面的内容,可以试着独立分析下面的代码。

假设 Vue2 组件已挂载,count 是响应式属性,模板直接渲染 count。

javascript 复制代码
console.log('A');

this.count = 1;

Promise.resolve().then(() => {
  console.log('B');

  this.count = 2;

  this.$nextTick(() => {
    console.log(
      'D',
      this.$refs.count.textContent
    );
  });
});

this.$nextTick(() => {
  console.log(
    'C',
    this.$refs.count.textContent
  );
});

console.log('E');

思考三个问题:

  1. Console 的完整输出顺序是什么?
  2. 组件渲染 Watcher 通常会执行几次?
  3. 为什么 C 和 D 读取到的 DOM 内容可能不同?

这道题同时涉及原生 Promise、Vue2 Watcher、两批响应式更新和 $nextTick 的回调顺序。

如果能独立分析清楚,说明你已经不只是会背 Event Loop 的执行顺序,而是真正理解了 Vue2 异步更新机制。


总结

理解 JavaScript Event Loop,不能只记住一句"同步代码先执行,微任务再执行,最后执行宏任务"。

真正重要的是理解:

第一,异步回调是什么时候被安排的。

第二,回调进入了哪个调度队列。

第三,执行过程中是否又创建了新的微任务。

第四,Vue2 如何在浏览器微任务机制之上,实现自己的 Watcher 批量更新和 nextTick 回调调度。

把这些机制串起来,很多过去看似随机的执行顺序就有迹可循了。

一句话概括:Event Loop 决定 JavaScript 回调何时执行,Vue2 Scheduler 决定响应式更新如何合并,nextTick 则提供了等待 Vue DOM 更新完成的接口。

理解这三者的关系,才算真正掌握 Vue2 的异步更新原理。


文章适用范围:浏览器 JavaScript、Vue 2.6 默认异步更新机制。不同浏览器环境、Vue 版本和特殊调度配置可能存在实现差异。

相关推荐
qq_340040882 小时前
idea2021版,vue项目的package.json不展示scripts的绿色箭头
javascript·vue.js·json
2601_968900772 小时前
玻璃深加工的工艺窗口:4 道工序的温度与时间参数边界
android·java·javascript
程序员大阳3 小时前
副队长JavaScript教程(53)-DOM操作元素的属性
javascript·dom·属性·setattribute·getattribuite
Non-existent9873 小时前
在 WPS/Excel 里做地理数据处理:功能市场 + 奥维 / Google Earth / QGIS 三种跳转实战
开发语言·javascript·ecmascript
zhougl9963 小时前
什么是 URL
javascript
螺丝钉tina4 小时前
[Playwright][01] 从 Puppeteer 到 Playwright:为什么选择 Playwright + TypeScript?
前端·javascript·typescript
大团米线4 小时前
vite多环境合并打包config
javascript·vue.js·ecmascript
程序员大阳4 小时前
副队长JavaScript教程(57)-DOM事件机制
javascript·键盘事件·dom·鼠标·事件·绑定·冒泡
唔665 小时前
在透镜圆正中加了一个描边空心字
前端·javascript