文件流导出函数示例

  1. // 前端代码
  2. const exportExcel = (
  3. body: Record<
  4. string,
  5. {
  6. key_name: string;
  7. value: string;
  8. }[]
  9. >,
  10. setExportLoading: (bool: boolean) => void,
  11. ) => {
  12. const xhr = new XMLHttpRequest();
  13. const url = `/api/i18n/exportErrorI18nByJsonZipFile`;
  14. xhr.open('POST', url, true); // 也可用POST方式
  15. xhr.setRequestHeader('Content-Type', 'application/json');
  16. xhr.responseType = 'blob';
  17. xhr.onload = function () {
  18. if (this.status === 200) {
  19. setExportLoading(false);
  20. const blob = this.response;
  21. const a = document.createElement('a');
  22. a.download = 'errorLang.zip';
  23. a.href = URL.createObjectURL(blob);
  24. a.click();
  25. }
  26. setTimeout(() => {
  27. setExportLoading(false);
  28. }, 5000);
  29. };
  30. try {
  31. setExportLoading(true);
  32. xhr.send(JSON.stringify({ data: body }));
  33. } catch (err) {
  34. setExportLoading(false);
  35. }
  36. };
  37. // 后台代码
  38. const archiver = require('archiver');
  39. async exportErrorI18nByJsonZipFile(body) {
  40. const { data: __data } = body;
  41. const types = await this.app.mysql.query('select distinct type from i18n_config');
  42. const dir = path.join(__dirname, '../../tmp/exportLang');
  43. const newDir = path.join(dir, './temp');
  44. try {
  45. fs.mkdirSync(path.join(__dirname, '../../tmp'));
  46. } catch (e) {
  47. // 如果目录存在,则不能创建成功,但是这里不做处理
  48. }
  49. try {
  50. // 创建目录
  51. fs.mkdirSync(dir);
  52. } catch (e) {
  53. // 如果目录存在,则不能创建成功,但是这里不做处理
  54. }
  55. try {
  56. // 删除目录
  57. deleteFolder(newDir);
  58. fs.mkdirSync(newDir);
  59. fs.mkdirSync(path.resolve(newDir, './lang'));
  60. } catch (e) {
  61. // 如果目录存在,则不能创建成功,但是这里不做处理
  62. }
  63. for (let i = 0; i < types.length; i += 1) {
  64. const type = types[i].type;
  65. const data = __data[type] || [];
  66. try {
  67. fs.mkdirSync(path.resolve(newDir, `./lang/${type}`));
  68. } catch (e) {
  69. //
  70. }
  71. const writable = fs.createWriteStream(path.resolve(newDir, `./lang/${type}/index.json`));
  72. const obj = {};
  73. data.forEach(item => {
  74. obj[item.key_name] = item.value;
  75. });
  76. writable.write(JSON.stringify(obj));
  77. writable.end();
  78. }
  79. const targetDir = path.resolve(newDir, './lang.zip');
  80. const output = fs.createWriteStream(targetDir);
  81. const archive = archiver('zip', {
  82. zlib: { level: 9 }, // Sets the compression level.
  83. });
  84. archive.pipe(output);
  85. archive.directory(path.resolve(newDir, './lang'), 'lang');
  86. await archive.finalize();
  87. output.close();
  88. this.ctx.attachment('errorLang.zip');
  89. this.ctx.set('Content-Type', 'application/octet-stream');
  90. this.ctx.body = fs.createReadStream(targetDir);
  91. }

Google Chrome禁用触摸板滑动前进后退

window
浏览器中输入 chrome://flags/#overscroll-history-navigation
接下来就是将选项值设置为disable
mac环境
defaults write com.google.Chrome AppleEnableSwipeNavigateWithScrolls -bool FALSE

版本号比较

  1. const versionCompare = (source, target) => {
  2. // /^\d+.\d+.\d+$/.test(source)
  3. // /^\d+.\d+.\d+$/.test(target)
  4. const newVersion = target.split('.')
  5. const oldVersion = source.split('.')
  6. if (target === source) {
  7. return false
  8. }
  9. if (Number(newVersion[0]) > Number(oldVersion[0])) {
  10. return true
  11. }
  12. if (Number(newVersion[1]) > Number(oldVersion[1])) {
  13. return true
  14. }
  15. if (Number(newVersion[2]) > Number(oldVersion[2])) {
  16. return true
  17. }
  18. return false
  19. }

ssh错误

IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:eOUyGO7XUx+YBeJ8Bz9AekyGps46wC8y13FDCIeFCFo.
Please contact your system administrator.
Add correct host key in /Users/lijunyang/.ssh/known_hosts to get rid of this message.
Offending ED25519 key in /Users/lijunyang/.ssh/known_hosts:3
Host key for 1.117.51.164 has changed and you have requested strict checking.
Host key verification failed.

  1. rm -rf ~/.ssh/known_hosts

require的实现

stream的实现

stream的类型

git 查看代码提交量

  1. git log --since="2022-02-01" --before="2022-02-28" --author="$(git config --get user.name)" --pretty=tformat: --numstat | gawk '{ add += $1 ; subs += $2 ; loc += $1 - $2 } END { printf "added lines: %s removed lines : %s total lines: %s\n",add,subs,loc }'
  2. # gawk 不存在用下面的语句
  3. git log --since="2022-02-01" --before="2022-02-28" --author="$(git config --get user.name)" --pretty=tformat: --numstat | awk '{ add += $1 ; subs += $2 ; loc += $1 - $2 } END { printf "added lines: %s removed lines : %s total lines: %s\n",add,subs,loc }'

google 跨域问题解决方案

The request client is not a secure context and the resource is in more-private address space local.

  1. chrome://flags
  2. 搜索Block insecure private network requests
  3. 设置为Disabled,Relaunch就好了。

按下 F12,然后同时按下组合键 Ctrl+Shift+P ,接着在跳出的框框里输入入 Capture full size screenshot ,如下。
前端面试题 - 图1

https 生成证书

npm i -g mkcert
mkcert create-ca
mkcert create-cert —domains localhost,127.0.0.1,你的域名

mac 下 添加证书,打开下面界面,选择文件导入项目,然后找到你的证书,双击它,信任。 打开 Chrome 。 在右上角,依次点击“更多”图标 设置。 在“隐私设置和安全性”下,依次点击网站设置 不安全内容。 点击“不得使用”旁边的添加并插入网站网址。 如需更改所添加网站的具体默认设置,请点击网址旁边的“更多”图标 。 点击允许、修改或移除。

image.png

nginx

  1. server {
  2. listen 443 ssl http2;
  3. listen [::]:443 ssl http2;
  4. ssl_certificate /opt/homebrew/etc/nginx/https/cert.crt;
  5. ssl_certificate_key /opt/homebrew/etc/nginx/https/cert.key;
  6. server_name m-local.hibobi.com;
  7. add_header Strict-Transport-Security max-age=31536000;
  8. error_page 500 502 503 504 /50x.html;
  9. location = /50x.html {
  10. root html;
  11. }
  12. location / {
  13. proxy_redirect off;
  14. proxy_set_header Host $host;
  15. proxy_set_header X-Real-IP $remote_addr;
  16. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  17. proxy_pass https://webserver1;
  18. }
  19. }
  20. upstream webserver1 {
  21. server 127.0.0.1:8003 weight=1;
  22. # ip_hash;
  23. }

nuxt 使用 此证书

  1. // nuxt.config.js
  2. module.exports = {
  3. server: {
  4. https: {
  5. key: fs.readFileSync(path.join(__dirname, '/cert/cert.key')),
  6. cert: fs.readFileSync(path.join(__dirname, '/cert/cert.crt'))
  7. }
  8. }
  9. }

测试网速

https://tool.chinaz.com/speedworld/https://images.hibobi.com/products/000007/456d6049-e235-46ab-80c4-d1b7a8dd2f84.jpg

iphone 手机调试 使用 Safari 手机

一、首先打开Safari开发-偏好设置-高级-勾选“在菜单栏中显示“开发”菜单”
二、点击开发菜单-点击对应的iphone
image.png
image.png
image.png

获取滚动条位置

  1. const scrollTop = document.body.scrollTop || document.documentElement.scrollTop

选中与选取

https://developer.mozilla.org/zh-CN/docs/Web/API/Selection

  1. var selObj = window.getSelection() // 会返回
  2. // sel.setBaseAndExtent(anchorNode,anchorOffset,focusNode,focusOffset)
  3. selObj.setBaseAndExtent(dom1, 0, dom2, 0)
  4. var input = document.getElementById('Id');
  5. input.value = 'abc';
  6. input.foucs(); // 聚焦
  7. input.select(); // 全选
  8. input.setSelectionRange(0, 2); // 'ab' 会被选中

验证码组件

  1. <template>
  2. <div class="verify-short-message">
  3. <span
  4. v-for="(item, index) in value"
  5. :key="index"
  6. :class="index === activeIndex ? 'active' : ''"
  7. @click="clickFn(index)"
  8. >
  9. {{ item || '' }}
  10. </span>
  11. <input
  12. id="verify"
  13. ref="verify"
  14. autocomplete="off"
  15. maxlength="5"
  16. class="short-message-input"
  17. @input="inputChange"
  18. @blur="blur"
  19. type="tel"
  20. />
  21. </div>
  22. <script>
  23. export default {
  24. data () {
  25. return {
  26. value: ['', '', '', '', ''],
  27. activeIndex: -1,
  28. }
  29. }
  30. methods: {
  31. getHeight () {
  32. return (window.innerHeight ||
  33. document.documentElement.clientHeight ||
  34. document.body.clientHeight)
  35. },
  36. clickFn (index) {
  37. if (!(this.$refs && this.$refs.verify)) {
  38. return
  39. }
  40. this.$refs.verify.focus()
  41. if (this.getHeight() < 480) {
  42. // 兼容ios手机,小于这个屏幕会被挡住输入框的问题
  43. setTimeout(() => {
  44. document.documentElement.scrollTop = 30
  45. document.body.scrollTop = 30
  46. }, 200)
  47. }
  48. const value = this.$refs.verify.value
  49. if (value.length) {
  50. try {
  51. if (value.length === 5) {
  52. this.activeIndex = index <= value.length ? index : value.length
  53. this.$refs.verify.setSelectionRange(index, index + 1)
  54. return
  55. }
  56. this.activeIndex = value.length
  57. } catch (e) {
  58. console.error(e)
  59. }
  60. } else {
  61. this.activeIndex = 0
  62. }
  63. },
  64. blur () {
  65. // 离开焦点
  66. this.activeIndex = -1
  67. },
  68. valueFormat (value) {
  69. if (!value) {
  70. return ''
  71. }
  72. return value.replace(/[^0-9]/g, '')
  73. },
  74. inputChange (e) {
  75. if (e.data && /[^0-9]/g.test(e.data)) {
  76. e.target.value = this.value.join('')
  77. if (this.activeIndex !== e.target.value.length) {
  78. this.$refs.verify.setSelectionRange(this.activeIndex, this.activeIndex + 1)
  79. }
  80. e.preventDefault()
  81. return false
  82. }
  83. let value = e.target.value
  84. this.value = [
  85. this.valueFormat(value[0]),
  86. this.valueFormat(value[1]),
  87. this.valueFormat(value[2]),
  88. this.valueFormat(value[3]),
  89. this.valueFormat(value[4])
  90. ]
  91. if (value && value.length > 5) {
  92. e.target.value = this.value.join('')
  93. }
  94. if (value.length === 5) {
  95. this.verify()
  96. return
  97. }
  98. if (e.data) {
  99. this.activeIndex++
  100. this.$refs.verify.setSelectionRange(this.activeIndex, this.activeIndex + 1)
  101. } else {
  102. this.activeIndex--
  103. if (this.activeIndex < 0) {
  104. this.activeIndex = 0
  105. }
  106. this.$refs.verify.setSelectionRange(this.activeIndex, this.activeIndex + 1)
  107. }
  108. }
  109. }
  110. }
  111. </script>
  112. <style>
  113. .verify-short-message {
  114. width: 396/@rem;
  115. height: 100%;
  116. margin: 0 auto;
  117. font-size: 0;
  118. position: relative;
  119. margin-bottom: 0.5rem;
  120. .short-message-input {
  121. position: fixed;
  122. height: 100%;
  123. width: 280px;
  124. z-index: 2;
  125. bottom: -100%;
  126. // right: 30px;
  127. // top: 50px;
  128. border: 1px solid;
  129. background-color: rgba(0, 0, 0, 0);
  130. color: rgba(0,0,0,0);
  131. height: 60/@rem;
  132. font-size: 0.24rem;
  133. }
  134. /deep/span {
  135. position: relative;
  136. border: 1px solid;
  137. width: 60/@rem;
  138. height: 60/@rem;
  139. line-height: 0.6rem;
  140. display: inline-block;
  141. text-align: center;
  142. margin-right: 0.24rem;
  143. font-size: 0.24rem;
  144. vertical-align: text-top;
  145. &.active:after {
  146. content: ' ';
  147. width: 60%;
  148. margin: 0 auto;
  149. border-bottom: 1px solid #000;
  150. display: block;
  151. position: absolute;
  152. left: 50%;
  153. transform: translateX(-50%);
  154. bottom: 0.08rem;
  155. animation-name: TopTransition;
  156. animation-duration: 1.5s; // 动画完成一个周期所需要的时间
  157. animation-timing-function: linear; // 动画的曲速
  158. animation-iteration-count: infinite;
  159. }
  160. &:nth-child(5) {
  161. margin-right: 0;
  162. }
  163. }
  164. }
  165. </style>
  166. </template>

input [type=”number”] 隐藏步长计步器

  1. input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; }
  2. input[type="number"]{
  3. appearance: textfield;
  4. -moz-appearance: textfield;
  5. }

编辑器的实现

https://developer.mozilla.org/zh-CN/docs/Web/API/Selection
image.png

  1. <div
  2. class="verify-short-message"
  3. contenteditable="true"
  4. >
  5. </div>

键盘监听按下键

  1. function keyUp(e) {
  2. var currKey=0,e=e||event;
  3. currKey=e.keyCode||e.which||e.charCode;
  4. var keyName = String.fromCharCode(currKey);
  5. alert("按键码: " + currKey + " 字符: " + keyName);
  6. }
  7. document.onkeyup = keyUp;

node eventloop

Micro-Task 与 Macro-Task

  1. 浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。**宏任务队列可以有多个,微任务队列只有一个**。
  • 常见的 macro-task 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。

1.Node Task分类

前端面试题 - 图7

2. Node Task Queue说明

  • 定时器(timer):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。(也就是那些到时间应该执行的定时器回调函数。)
  • 待定回调(pending callbacks):执行延迟到下一个循环迭代的 I/O 回调。(这个阶段参考poll阶段的说明2)
  • idle, prepare:仅系统内部使用。idle 闲置的 [ˈaɪdl] ,prepare 准备,预备 [prɪˈpeə(r)]
  • 轮询(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。(说明:

1. 适当的时候:已经没有其它可执行的任务的时候,此时还有异步的I/O没有返回;此时程序会在此阻塞,等待I/O的回调;
2. 当队列用尽或达到回调限制,事件循环将移动到下一阶段执行,也就是到待定回调pending callbacks阶段执行 )

  • 检测(check):setImmediate() 回调函数在这里执行。
  • 关闭的回调函数(close callbacks):一些关闭的回调函数,如:socket.on(‘close’, …)。

其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
前端面试题 - 图8
image.png
大致看出node中的事件循环的顺序:

外部输入数据—>轮询阶段(poll)—>检查阶段(check)—>关闭事件回调阶段(close callback)—>定时器检测阶段(timer)—>I/O事件回调阶段(I/O callbacks)—>闲置阶段(idle, prepare)—>轮询阶段(按照该顺序反复运行)…

  • timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调
  • close callbacks 阶段:执行 socket 的 close 事件回调

注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)

接下去我们详细介绍timers、poll、check这3个阶段,因为日常开发中的绝大部分异步任务都是在这3个阶段处理的。

(1) timer

timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。 同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行

(2) poll

poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情
1.回到 timer 阶段执行回调
2.执行 I/O 回调
并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
  • 如果 poll 队列为空时,会有两件事发生
    • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

(3) check阶段

  1. setImmediate()的回调会被加入check队列中,从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。 我们先来看个例子:
  1. console.log('start')
  2. setTimeout(() => {
  3. console.log('timer1')
  4. Promise.resolve().then(function() {
  5. console.log('promise1')
  6. })
  7. }, 0)
  8. setTimeout(() => {
  9. console.log('timer2')
  10. Promise.resolve().then(function() {
  11. console.log('promise2')
  12. })
  13. }, 0)
  14. Promise.resolve().then(function() {
  15. console.log('promise3')
  16. })
  17. console.log('end')
  18. //start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
  • 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出start end,并将2个timer依次放入timer队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出promise3
  • 然后进入timers阶段,执行timer1的回调函数,打印timer1,并将promise.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;这点跟浏览器端相差比较大,timers阶段有几个setTimeout/setInterval都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于Node与浏览器的 Event Loop 差异,下文还会详细介绍)。

    3.Micro-Task 与 Macro-Task

    Node端事件循环中的异步队列也是这两种:macro(宏任务)队列和 micro(微任务)队列。

  • 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等。

  • 常见的 micro-task 比如: process.nextTick、new Promise().then(回调)等。

4.注意点

(1) setTimeout 和 setImmediate

二者非常相似,区别主要在于调用时机不同。

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行,但它在timer阶段执行

    1. setTimeout(function timeout () { console.log('timeout'); }, 0);
    2. setImmediate(function immediate () { console.log('immediate'); });
  • 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
  • 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

  1. const fs = require('fs')
  2. fs.readFile(__filename, () => {
  3. setTimeout(() => {
  4. console.log('timeout');
  5. }, 0)
  6. setImmediate(() => {
  7. console.log('immediate')
  8. })
  9. })
  10. // immediate
  11. // timeout

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

(2) process.nextTick

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

  1. setTimeout(() => {
  2. console.log('timer1')
  3. Promise.resolve().then(function() {
  4. console.log('promise1')
  5. })
  6. }, 0)
  7. process.nextTick(() => {
  8. console.log('nextTick')
  9. process.nextTick(() => {
  10. console.log('nextTick')
  11. process.nextTick(() => {
  12. console.log('nextTick')
  13. process.nextTick(() => {
  14. console.log('nextTick')
  15. })
  16. })
  17. })
  18. })
  19. // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

Node与浏览器的 Event Loop 差异

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务

  1. setTimeout(() => {
  2. console.log('timer1')
  3. Promise.resolve().then(function () {
  4. console.log('promise1')
  5. })
  6. })
  7. setTimeout(() => {
  8. console.log('timer2')
  9. Promise.resolve().then(function () {
  10. console.log('promise2')
  11. })
  12. })
  13. // node v10.16.3
  14. timer1
  15. timer2
  16. promise1
  17. promise2
  18. // node v16.5.0
  19. timer1
  20. promise1
  21. timer2
  22. promise2
  23. // 浏览器 google 94.x
  24. timer1
  25. promise1
  26. timer2
  27. promise2

例子

  1. const fs = require('fs');
  2. console.log('1.同步代码!'); // 同步代码一定比异步代码先执行
  3. process.nextTick(() => { // 整个函数我们理解为一个宏任务,当整个代码执行完,
  4. // 会执行此宏任务产生的微任务,process.nextTick 会立即执行 console.log('3.process.nextTick!');
  5. });
  6. setImmediate(() => { // check阶段执行
  7. console.log('5.setImmediate!');
  8. });
  9. Promise.resolve().then(() => { // 微任务,等待宏任务执行完毕后执行,执行顺序在process.nextTick 之后
  10. console.log('4.Promise.resolve then!');
  11. });
  12. setTimeout(() => { // 1s后会将此会将此setTimeout的回调推送到 timer阶段对应的task queue中
  13. console.log('7.setTimeout');
  14. process.nextTick(() => {
  15. console.log('8.setTimeout process.nextTick!');
  16. });
  17. Promise.resolve().then(() => {
  18. console.log('9.setTimeout Promise.resolve then');
  19. });
  20. }, 1000);
  21. fs.readFile('./event.js', () => { // I/O 回调在poll阶段执行,具体何时执行以来fs.readFile异步读取文件的所需时间
  22. console.log('6.fs.readFile');
  23. });
  24. console.log('2.同步代码!');
  25. // 以下是输出:// 1.同步代码!
  26. // 2.同步代码!
  27. // 3.process.nextTick!
  28. // 4.Promise.resolve then!
  29. // 5.setImmediate!
  30. // 6.fs.readFile
  31. // 7.setTimeout
  32. // 8.setTimeout process.nextTick!
  33. // 9.setTimeout Promise.resolve then

作者:浪里行舟
链接:https://juejin.cn/post/6844903761949753352
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:linge
链接:https://juejin.cn/post/6921692824535007246
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。