假设我需要使用 XMLHttpRequest 下载一张图片,然后展示在网页上。
因为下载图片比较耗时,会阻塞主线程,因此会被放到网络线程中执行。
当网络线程下载完图片后,再把图片对象 img 以参数的形式传到回调函数 showPic(img)中,再把回调函数 showPic 封装为任务放到任务队列中,等待主线程空闲后执行,把图片加载到网页上
我想问下,在异步编程中,对于这种单独起一个线程执行耗时任务,为啥耗时任务执行完的后续收尾代码( showPic )非要放到任务队列中让主线程执行,为啥不干脆在自己的线程中执行。
反正对耗时任务的结果产生依赖的代码都在回调函数中,主线程中代码的执行又不依赖耗时任务的结果
而且万一主线程执行很慢,等图片下载完还没执行结束,那加载图片( showPic )不就被动延后。 而把 showPic 让网络进程执行,不就能在图片下载完后立即被加载出来,也不用管主线程是否执行完
1
elonmask 2023-01-11 10:16:32 +08:00
因为主线程才能 UI 吧,你就算在别的线程中还得切换到主线程来执行图片加载,干脆搞个队列让主线程去取。
|
2
yezheyu OP @elonmask 有道理,但如果不是 UI 相关,收尾代码纯粹 console.log 或者保存图片等等,另一个线程也能做到,是不是就没必要放到任务队列中等主线程执行呢?
|
3
Mutoo 2023-01-11 10:28:19 +08:00
主线程的任务顺序严格可控,只取决于任务加入队列的先后。但如果在别的线程中执行,顺序就不受控了,会出现更多的 race condition ,代码非常难维护。
|
4
bruce0 2023-01-11 10:30:06 +08:00 1
在 Android 里,只有主线程才能更新 ui (surface 除外), 如果在子线程更新 ui,会直接抛出异常. 如果多线程更新 ui 可能会导致一些错乱和不可预知的问题, 比如 两个线程同时更新一张图片, 如果加锁,性能上有损失, 不如强制在主线程更新 ui 了
|
5
DingJZ 2023-01-11 10:40:00 +08:00
主要就是因为 UI 相关,保存完图片要不要在 UI 上提升用户保存成功,如果单纯的后台任务确实不需要回到主队列
|
6
MakHoCheung 2023-01-11 10:40:08 +08:00
@yezheyu #2 是的
|
7
unco020511 2023-01-11 10:55:56 +08:00
关键词:「主线程更新 UI 」
|
9
peterlitszo 2023-01-11 11:20:04 +08:00
请问上下文应该是前端吧?如果是前端的话,我很好奇 network thread 到底是什么?
按照我所学的,我的一点粗浅的理解是: - 浏览器不支持多 thread ,除非上 worker: https://developer.mozilla.org/zh-CN/docs/Web/API/Worker - 浏览器的异步支持事实上是单 thread ,加上事件循环,而多个协程共同协作,没有发生抢占。堵塞协程会及时让出。 |
10
okakuyang 2023-01-11 11:25:52 +08:00
UI 绘制本质上是一个线程负责的循环,当有 UI 操作的时候都要将操作加到这个循环之中。
|
11
cyndihuifei 2023-01-11 11:26:55 +08:00
我也表示疑问,上下文应该是前端吧?
|
12
yezheyu OP @cyndihuifei @peterlitszo
个人理解,可能有错,欢迎指出 js 之所以是单线程,主要为了避免资源竞争问题。 但实际上 js 不可能是单线程,就比如 xhr 图片加载,xhr 在下载时没有阻塞主线程,那图片的下载必定不是在主线程执行,那必定有一个线程帮你执行下载工作,就是网络线程。 所以我认为 js 单线程是指你在 js 代码中无法像其它语言那样使用多线程模块创建一个线程执行一个代码块。只能使用预留的 API 调用现有的线程,如浏览器的 xhr Web Api ,node 的 fs 模块 那像 xhr 中在网络线程中要渲染图片,涉及到操作 dom ,就会出现资源竞争问题,所以必须放到任务队列中让主线程来进行 那你在代码中的耗时任务该怎么办呢? js 又没有多线程让你开辟一个线程去处理,而浏览器也没有一个特定的 WEB API 帮你执行。 所以为了不阻塞后续代码只能延期执行,让其它代码先执行,执行完后再执行耗时任务和对其结果的依赖代码,即微任务。 每个宏任务都有一个微任务队列,里面放的就是这些延期代码。微任务的执行时机就是当前宏任务执行完,下一个宏任务执行前。这样就可以完美解决。 而 Promise 就是这样的实现 |
13
jybox 2023-01-11 13:10:17 +08:00 1
@yezheyu JS 就是单线程的,你说的的其他线程是引擎的实现细节,用来提供「事件循环」的语义,但你无法接触到这些线程(无法在其中执行你的 JS 代码)。
>那你在代码中的耗时任务该怎么办呢? 浏览器和 Node.js 分别有 Web Workers 和 Worker threads ,但这种线程其实相当于是一个独立的 JS 运行环境,不能直接操作主线程的内存,和主程序的通讯非常受限(其实和单独的进程差别并不是很大)。 |
14
MozzieW 2023-01-11 13:49:31 +08:00
1. 要先知道为什么有主线程( UI 线程),这个前提下再讨论其他问题
2. “对于这种单独起一个线程执行耗时任务,为啥耗时任务执行完的后续收尾代码( showPic )非要放到任务队列中让主线程执行,为啥不干脆在自己的线程中执行” --》把图片下载放到单独线程,就是把耗时任务从主线程摘除来。但是因为只能主线程更新 UI ,所以最后还要回到主线程刷新界面。这里经常遇到的一个问题是:下载后图片解析成 Bitmap 也是耗时的,经常遇到在主线程再解析,更好的方式是在下载线程解析。 3. “万一主线程执行很慢”--》这个万一出现了,就是要解决的问题。主线程要定时刷新页面,60 帧下刷新间隔是 16 毫秒,而且还有 90 帧、120 帧,时间要求更短,执行慢一点就掉帧。如果主线程卡一下,可能不是问题;一直卡,就是要解决的问题。正确情况下,图片的显示间隔不差这么几毫秒,要优化也是下载时间、解析时间。 |
15
h0099 2023-01-11 14:03:41 +08:00
> js 之所以是单线程,主要为了避免资源竞争问题。
js 是单线程+无栈异步( promise )+回调异步(传统的 callback hell 就像 IndexedDB ) > 但实际上 js 不可能是单线程,就比如 xhr 图片加载,xhr 在下载时没有阻塞主线程,那图片的下载必定不是在主线程执行,那必定有一个线程帮你执行下载工作,就是网络线程。 负责下载图片等网络请求的是 chrome 的 network thread ,其跟 js 无关 > 所以我认为 js 单线程是指你在 js 代码中无法像其它语言那样使用多线程模块创建一个线程执行一个代码块。 您可以用 worker 创建新的 js thread > 只能使用预留的 API 调用现有的线程,如浏览器的 xhr Web Api ,node 的 fs 模块 因此 xhr/fetch 本质上是浏览器暴露的一套 api ,其标准化于 https://xhr.spec.whatwg.org https://fetch.spec.whatwg.org 他们存在的目的是允许您在 js thread 中与 network thread 通信,以派发您的下载图片的任务 > 那像 xhr 中在网络线程中要渲染图片,涉及到操作 dom ,就会出现资源竞争问题,所以必须放到任务队列中让主线程来进行 xhr/fetch 没有渲染图片,他们只负责网络请求相关的 payload (实际上跟 os socket 打交道处理网络栈已经够复杂了) xhr/fetch 最终会给您一坨 byte[]也就是您的 http request 的 http response body ,以及相关的 response 元数据如 http header ( fetch 标准是都放在 https://developer.mozilla.org/en-US/docs/Web/API/Response 里) 要么您的 js thread 去取回这些 byte[](例如通过 https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer ) 要么是其他浏览器 thread 消费,比如有一个`<img src="url">`,那么 network thread 下载完了`url`后就会把 byte[]传给负责图片渲染和 css layout 的 thread ,让他们绘制这个<img>,这里同样没有 js 的事(即便您禁用了浏览器 js 也不会妨碍<img src>正常工作) 要么是没有任何人消费这些 byte[],那就直接丢弃 |
16
h0099 2023-01-11 14:09:34 +08:00
> 那你在代码中的耗时任务该怎么办呢? js 又没有多线程让你开辟一个线程去处理,而浏览器也没有一个特定的 WEB API 帮你执行。
现在您可以使用 web worker 标准的 api https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers 来创建新的 js/wasm thread 来跑您的 cpu 密集耗时任务 > 所以为了不阻塞后续代码只能延期执行,让其它代码先执行,执行完后再执行耗时任务和对其结果的依赖代码,即微任务。 这就是异步 > 每个宏任务都有一个微任务队列,里面放的就是这些延期代码。微任务的执行时机就是当前宏任务执行完,下一个宏任务执行前。这样就可以完美解决。 mirco/macrotask 是 html spec 中标准化了的对 js thread 上的 event loop 和异步调度的优先级实现: https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context > 而 Promise 就是这样的实现 promise 是无栈协程,也就是说他本质上跟回调地狱异步没有区别(这也是为什么 es6 spec 标准化 promise 之前有 jq promise https://api.jquery.com/category/deferred-object/ 、promises/a+ https://promisesaplus.com/ 等各种民间标准,但他们的本质都是对回调地狱的封装使其更好读) |
17
wangritian 2023-01-11 14:14:59 +08:00
可能用操作系统的异步 IO 接口吧,单线程应用也不会阻塞
|
18
yezheyu OP @h0099
多谢指正 所以 xhr 只是负责和浏览器的网络线程通信,下发任务 而网络线程就只管从服务器下载资源,对于下载好的资源,你怎么处理,就算只是打印一下,也必须让主线程处理。网络线程只负责 socket 通信。 所以我上面的例子,对于结果不管是更新 UI ,还是打印结果,都只能是主线程处理,对吗? |
19
biguokang 2023-01-11 14:30:07 +08:00
@yezheyu js 是单线程的,但是 chrome 是 c++写的,c++是多线程的。
所以本质上,是 js 代码通过调用浏览器 web api 从而调用 c++的过程,表面上你写的是 js 代码,但最后实际上干活的是 c++,这个道理放在 nodejs 也同理。 你看到的网络线程,不是 js 的网络线程,而是 chrome 的 c++网络线程。 包括图片渲染、dom 渲染,到了最后还是 c++干的活,js 只是指挥 c++干活的。 所以 js 回调函数,其实就是浏览器把活干好了(比如网络加载、计时等等),踢了一下 js 引擎,说我活干完了,你可以执行任务等待队列里的对应回调函数了。 |
20
autoxbc 2023-01-11 14:35:02 +08:00
OP 混淆了 JS 线程和 JS 引擎线程,JS 是单线程,引擎执行完下载,后续代码自然需要回到 JS 线程继续。JS 编码应该保持 JS 视角,只观察引擎提供的接口,不关心引擎实现的细节
|
21
yoozng 2023-01-11 15:36:27 +08:00
精彩的问答,持续关注~
|
22
baoyexi 2023-01-11 15:44:56 +08:00
io 阻塞可以考虑使用协程。
|
23
h0099 2023-01-11 15:50:07 +08:00
#18 @yezheyu
> 所以 xhr 只是负责和浏览器的网络线程通信,下发任务 xhr/fetch 是浏览器暴露的一套 api ,以允许您去指挥网络线程发起 http request 然而很明显在一个单纯的`<img src="url">`中不存在任何`<script>`中的 js 代码去通过 xhr/fetch api 请求这个 url 因此在这里实际上只有 html parser 在请求图片 url ,而后 network thread 再把 url 的 response body 传输给渲染图片的 thread ,还有可能导致 css reflow 建议复习 https://www.chromium.org/developers/design-documents/displaying-a-web-page-in-chrome/ https://www.chromium.org/developers/design-documents/multi-process-architecture/ https://www.chromium.org/developers/design-documents/multi-process-resource-loading/ https://chromium.googlesource.com/chromium/src/+/master/docs/how_cc_works.md > 对于下载好的资源,你怎么处理,就算只是打印一下,也必须让主线程处理。 如果阁下所说的`打印一下`是指`console.log(await(await fetch('https://www.v2ex.com')).text())`这样的 js 代码,那么 network thread 当然会把 response body 传回给 js thread 从而作为您所调用的的`Response.text()`返回的 promise 的 reslove 值 > 对于结果不管是更新 UI ,还是打印结果,都只能是主线程处理 但如果您没有在 js 中消费这个 Response 对象那不会有任何 byte[]被从 network thread 传输到您的 js thread 上,例如`console.log(await fetch('https://www.v2ex.com'))` 而对于最开始的例子 一个单纯的`<img src="url">` 这里同样没有任何 js 的存在,即上文所说的: > 要么是其他浏览器 thread 消费,比如有一个`<img src="url">`,那么 network thread 下载完了`url`后就会把 byte[]传给负责图片渲染和 css layout 的 thread ,让他们绘制这个<img>,这里同样没有 js 的事(即便您禁用了浏览器 js 也不会妨碍<img src>正常工作) |
24
h0099 2023-01-11 15:55:40 +08:00
#19 @biguokang
> 是 js 代码通过调用浏览器 web api 从而调用 c++的过程,表面上你写的是 js 代码,但最后实际上干活的是 c++,这个道理放在 nodejs 也同理。 实际上`干活的`涉及许多层级,chromium 的 cpp 代码实际上也是调包特定 os 提供的各种异步 io api 和 syscall ,渲染同理(如果涉及硬件加速还需要通过 ogl/dx 间接的让显卡驱动去跟更多的硬件通信) > 踢了一下 js 引擎,说我活干完了,你可以执行任务等待队列里的对应回调函数了。 这就是 event loop/message queue #20 @autoxbc > OP 混淆了 JS 线程和 JS 引擎线程,JS 是单线程,引擎执行完下载,后续代码自然需要回到 JS 线程继续。JS 编码应该保持 JS 视角,只观察引擎提供的接口,不关心引擎实现的细节 js 目前的主流解释器 v8 引擎只是一个 js 解释器,他同样不负责浏览器层面的那些网络请求 ui 渲染等任务 |
25
yezheyu OP |
26
biguokang 2023-01-11 16:40:48 +08:00
@yezheyu 其实可以理解为,只要存在回调函数(比如按钮、事件、计时、网络请求等等)的操作,都涉及到事件循环。
当 js 代码执行的时候,他并不会执行所有的异步回调函数,而是把所有的回调函数全都丢到等待队列里,等到 js 的所有同步代码都执行完成的时候,才会去等待浏览器发信号触发队列里的回调函数。 比如这样的代码 ```js console.log(1) //同步代码 setTimeout(()=>console.log(2), 0) // console.log(2)被存到了队列里了,等待浏览器跑完计时任务踢一脚 console.log(3) //同步代码 ``` 输出结果是: 1 3 2 哪怕 setTimeout 为 0 ,也要等 js 所有同步代码执行完后,才开始监听浏览器的回复。 要把 js 引擎和浏览器分开来,js 引擎负责解析 js 代码,浏览器才是最终真正干活的东西,这两者不是一体的。 当 js 引擎执行完所有的同步代码之后,你可以理解为 js 引擎唯一的工作就是,等浏览器踢一脚去执行队列里对应的回调函数。 js 引擎是单线程的,本身也没有计时、网络请求之类的功能,他的任务也只是把活交给浏览器去干。相对于的 nodejs 也一样。 |
27
h0099 2023-01-11 16:51:08 +08:00
> 要把 js 引擎和浏览器分开来,js 引擎负责解析 js 代码,浏览器才是最终真正干活的东西,这两者不是一体的。
分开理解 v8 和 chromium 最简单的方式就是去用 njs ,njs 环境下有浏览器 api 吗?您能在 njs 里去操作根本不存在的 dom 吗? > 当 js 引擎执行完所有的同步代码之后,你可以理解为 js 引擎唯一的工作就是,等浏览器踢一脚去执行队列里对应的回调函数。 setTimeout 是在往 loop 追加了一个应该在未来执行的 task ,而`delay: 0`参数只是意味着 loop 应该挂起这个 task 等到 0ms 后再执行( 0ms 也就是不等待) 但不论是传入 setTimeout 还是 Promise.reslove 的回调都是在未来才会发生的,所以才必须等到 console.log(1 和 3)两个同步调用都执行完(也就是整个`console.log(1) setTimeout console.log(3)`)后才会再去执行 loop 中已有的应该在未来执行的 task (在这里就是 setTimeout 的回调 console.log(2)) > js 引擎是单线程的,本身也没有计时、网络请求之类的功能,他的任务也只是把活交给浏览器去干。相对于的 nodejs 也一样。 准确地说是 v8 解释器解释执行 js 时是跑在一个 os thread 内部的 |
28
autoxbc 2023-01-11 19:48:48 +08:00
@h0099 我觉得 OP 的问题可以在一个浅层最小概念集中解释清楚。系统级语言程序员在思考 JS 问题时,经常会无意识的把问题复杂化,重新引入一些 JS 设计时有意屏蔽的概念
|
29
h0099 2023-01-11 20:11:50 +08:00
那就直接把 event loop 视作 FIFO stack
您每次 setTimeout/Interval 或创建 Promise (不论 new 还是 Promise.reslove/reject())都是 push 了一个回调进 stack 等您导致 push tsack 的这些同步 js 代码都执行完了之后 js 主线程空闲时就会去 pop stack 取出回调来逐个执行 这也不需要去阅读理解有关 spec html.spec.whatwg.org/multipage/webappapis.html#task-queue 中的 micro/marcotask 概念 stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context 请不要在每一个回复中都包括外链,这看起来像是在 spamming |
30
biguokang 2023-01-11 21:15:33 +08:00
@h0099 老哥你好,看你回复的语言格式,好奇问一下你是把 chatgpt 对接到了 V2EX 吗,然后用 V2EX api 进行自动回帖,因为你文风看起来挺像 chatgpt 的,你的回复里出现了大量的“您”,感觉一般网友不会这么说话。
|
31
h0099 2023-01-11 21:29:37 +08:00
@biguokang 建议深入学习贯彻泛银河系格雷科技分部邪恶组织四叶重工炼铜本部叶独头子叶独群组联合体陈意志第三帝国元首炼铜傻狗橙猫领导下的四叶 TG 本部( https://t.me/n0099_tg https://t.me/n0099official )话语体系文风:
https://sora.ink/archives/1574 https://github.com/n0099/TiebaMonitor/issues/24 https://github.com/Starry-OvO/aiotieba/issues/64 |
32
randomstream 2023-01-11 21:59:17 +08:00
来了,两个经典视频: 和
|
35
Al0rid4l 2023-01-12 05:36:08 +08:00
说实话没看懂这个问题, 首先为什么要用 XHR 下载图片? 通常直接 Image 添加 src 就会下载图片而且是异步的
就算 XHR 下载图片二进制数据再自行处理, XHR 下载默认也是异步的, 说 XHR 下载图片会阻塞主线程, 难道手动修改了参数用了同步 XHR? 从回复里看, 似乎 OP 没搞清楚解释器和浏览器的区别, JS 执行是单线程, 但浏览器不是单线程, 下载这些事情本来就是其他线程完成的 |
36
yezheyu OP @biguokang
@autoxbc @Al0rid4l @h0099 还有个问题想请教大家 --------------------------------------------------------- setTimeout( function(){ console.log(this), 0 } ) // 打印 window -------------------------------------------------------- 异步任务的回调函数中的 this 大多数是指向 window ,因为回调函数作为任务执行时,主线程的执行栈已清空,函数是单独调用,其默认绑定的就是 window 我这样理解对吗? 那为啥 button 点击事件的回调函数中 this 会指向事件的触发对象 button 呢?而不是 window ? |
37
h0099 2023-01-12 17:14:15 +08:00
#35 @Al0rid4l 估计他感觉到的`阻塞主线程`是发生在`XHR 下载图片二进制数据`之后的`再自行处理`阶段
#23 @h0099 对此早有预言: > 对于下载好的资源,你怎么处理,就算只是打印一下,也必须让主线程处理。 如果阁下所说的`打印一下`是指`console.log(await(await fetch('https://www.v2ex.com')).text())`这样的 js 代码,那么 network thread 当然会把 response body 传回给 js thread 从而作为您所调用的的`Response.text()`返回的 promise 的 reslove 值 |
38
h0099 2023-01-12 17:28:26 +08:00
#36 @yezheyu this 到底指向什么恶俗玩意跟异步同步毫无关系,他完完全全是由显式的.bind/apply/call 或隐式的闭包上下文捕获决定的
- 显式的.bind/apply/call: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call 的第一个参数 thisArg 就可以改变任何函数的 this 指向 - 隐式的闭包上下文捕获: 完整的 function ()语法`function () {}`(不论匿名还是具名)会从他的声明处寻找 this 指向,然后把自身的 this 指向声明处上级(本级是正在声明的 fun 自身)的 this 从声明处而不是执行处上下文获得 this 捕获进闭包的证明: `b.call({a:1})`将 arrow fun`b`的 this 修改为了一个`{a:1}` object ,但其 return 的会从上下文捕获 this 的 function()语法的回调函数的 this 并没有跟着变成{a:1},而仍然是最初声明 b 时从 b 那捕获的 this=window ( b 又从 global scope 那捕获了 this=window ) 即便把 b 改成 function()语法也不影响: 而 arrow fun 语法`() => {}`不会进行任何从上下文中捕获 this 的罪恶行径,也无法通过.bind/apply/call 来在声明后再次显式修改他内部的 this 指向,可以说 arrow fun 就根本没有 this > 那为啥 button 点击事件的回调函数中 this 会指向事件的触发对象 button 呢?而不是 window ? 您可以理解为有一个`buttonClickCallback.call(buttomElement, clickEvent)`的 js 被执行 您也可以自己执行这个`.call(buttomElement)`,同样会改变回调(只要不是用 arrow fun 语法声明的)的 this 为 button |
39
h0099 2023-01-12 17:35:56 +08:00
https://stackoverflow.com/questions/33308121/can-you-bind-this-in-an-arrow-function 的第一个回答进一步指出:
> You cannot rebind this in an arrow function. It will always be defined as the context in which it was defined. If you require this to be meaningful you should use a normal function. > From the ECMAScript 2015 Spec: http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions-runtime-semantics-evaluation > > Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function. 如果您能解释下图中的所有行为,那您就已经理解 js 的两种函数声明语法所带来的截然不同的 this 上下文作用域捕获进闭包罪恶行径了 |
40
Al0rid4l 2023-01-12 17:57:42 +08:00
@yezheyu this 这个和线程什么的完全没有关系, 这个纯粹是 JS 语法特性了, 去了解下 this 指向和 apply call bind 这些吧, 这些属于基础了, 虽然这玩意是糟粕了点, 相比其他语言来说反直觉了点, 不过也不是什么很难的东西, 规则就一句话的事
|
41
yezheyu OP |
42
h0099 2023-01-12 19:59:54 +08:00
#40 @Al0rid4l 不过也不是什么很难的东西, 规则就一句话的事
一句话解释 js 的奇妙深刻 this: 函数的闭包作用域是在声明时从声明处的词法作用域向上捕获所有会被函数引用到以及不论是否用到都捕获的 this 的上下文符号集合。对于使用 function()语法声明的函数,允许使用.bind/apply/call(thisArg)来重新定义该函数闭包作用域中的 this 指向,但对于 arrow fun 语法() => {}则不允许(因此 arrow fun 的 this 指向是 immutable 的) 这就像一句话解释 monad: 单子是自函子范畴上的幺半群 一样的正确但又令萌新完全听不懂 如同 https://www.v2ex.com/t/900380 |
43
h0099 2023-01-12 20:01:59 +08:00
#41 @yezheyu 阁下又凭什么假定浏览器内部实现这些 dom event 时写的是 js 代码用的是 js object ?
什么又叫“区别于一般的任务”?什么是您所说任务? 我已于#38 解释 > 那为啥 button 点击事件的回调函数中 this 会指向事件的触发对象 button 呢?而不是 window ? 您可以理解为有一个`buttonClickCallback.call(buttomElement, clickEvent)`的 js 被执行 您也可以自己执行这个`.call(buttomElement)`,同样会改变回调(只要不是用 arrow fun 语法声明的)的 this 为 button |