关于异步任务的一点疑问,有没有老哥帮忙解答下

2023-01-11 10:11:21 +08:00
 yezheyu

假设我需要使用 XMLHttpRequest 下载一张图片,然后展示在网页上。

因为下载图片比较耗时,会阻塞主线程,因此会被放到网络线程中执行。

当网络线程下载完图片后,再把图片对象 img 以参数的形式传到回调函数 showPic(img)中,再把回调函数 showPic 封装为任务放到任务队列中,等待主线程空闲后执行,把图片加载到网页上

我想问下,在异步编程中,对于这种单独起一个线程执行耗时任务,为啥耗时任务执行完的后续收尾代码( showPic )非要放到任务队列中让主线程执行,为啥不干脆在自己的线程中执行。

反正对耗时任务的结果产生依赖的代码都在回调函数中,主线程中代码的执行又不依赖耗时任务的结果

而且万一主线程执行很慢,等图片下载完还没执行结束,那加载图片( showPic )不就被动延后。 而把 showPic 让网络进程执行,不就能在图片下载完后立即被加载出来,也不用管主线程是否执行完

3931 次点击
所在节点    程序员
43 条回复
yoozng
2023-01-11 15:36:27 +08:00
精彩的问答,持续关注~
baoyexi
2023-01-11 15:44:56 +08:00
io 阻塞可以考虑使用协程。
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>正常工作)
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 渲染等任务
yezheyu
2023-01-11 16:15:57 +08:00
@biguokang
@h0099

多谢老哥的详细解答,那再请教一个类似的问题。

对于 JavaScript 中事件绑定和触发,同样涉及到事件循环,我这样理解对吗?



当你为一个事件绑定一个回调函数 ( 事件触发后执行的一段代码,可以是绑定多个回调函数 )时,本质是使用一个容器 ( 如对象 ) 记录事件名和回调函数的映射

当浏览器监控到特定的事件被触发时,就会从容器中取出事件对应的回调函数,把事件对象作为参数传递进回调函数中,再封装成一个任务放入任务队列中等待主线程执行
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 也一样。
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 内部的
autoxbc
2023-01-11 19:48:48 +08:00
@h0099 我觉得 OP 的问题可以在一个浅层最小概念集中解释清楚。系统级语言程序员在思考 JS 问题时,经常会无意识的把问题复杂化,重新引入一些 JS 设计时有意屏蔽的概念
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
biguokang
2023-01-11 21:15:33 +08:00
@h0099 老哥你好,看你回复的语言格式,好奇问一下你是把 chatgpt 对接到了 V2EX 吗,然后用 V2EX api 进行自动回帖,因为你文风看起来挺像 chatgpt 的,你的回复里出现了大量的“您”,感觉一般网友不会这么说话。
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
randomstream
2023-01-11 21:59:17 +08:00
来了,两个经典视频: <amp-youtube data-videoid="8aGhZQkoFbQ" layout="responsive" width="480" height="270"></amp-youtube> 和 <amp-youtube data-videoid="cCOL7MC4Pl0" layout="responsive" width="480" height="270"></amp-youtube>
biguokang
2023-01-11 22:16:34 +08:00
@h0099 老哥,你真的吓到我了,因为我最近真的在玩 minecraft 的格雷科技 mod 。
yezheyu
2023-01-11 23:29:05 +08:00
@biguokang #30
老哥你这一说,我也觉得像,哈哈
Al0rid4l
2023-01-12 05:36:08 +08:00
说实话没看懂这个问题, 首先为什么要用 XHR 下载图片? 通常直接 Image 添加 src 就会下载图片而且是异步的
就算 XHR 下载图片二进制数据再自行处理, XHR 下载默认也是异步的, 说 XHR 下载图片会阻塞主线程, 难道手动修改了参数用了同步 XHR?
从回复里看, 似乎 OP 没搞清楚解释器和浏览器的区别, JS 执行是单线程, 但浏览器不是单线程, 下载这些事情本来就是其他线程完成的
yezheyu
2023-01-12 16:41:13 +08:00
@biguokang
@autoxbc
@Al0rid4l
@h0099

还有个问题想请教大家

---------------------------------------------------------
setTimeout( function(){ console.log(this), 0 } )
// 打印 window
--------------------------------------------------------

异步任务的回调函数中的 this 大多数是指向 window ,因为回调函数作为任务执行时,主线程的执行栈已清空,函数是单独调用,其默认绑定的就是 window

我这样理解对吗?

那为啥 button 点击事件的回调函数中 this 会指向事件的触发对象 button 呢?而不是 window ?
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 值
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
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 上下文作用域捕获进闭包罪恶行径了
Al0rid4l
2023-01-12 17:57:42 +08:00
@yezheyu this 这个和线程什么的完全没有关系, 这个纯粹是 JS 语法特性了, 去了解下 this 指向和 apply call bind 这些吧, 这些属于基础了, 虽然这玩意是糟粕了点, 相比其他语言来说反直觉了点, 不过也不是什么很难的东西, 规则就一句话的事

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/908083

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX