重读setTimeout()全局函数,以及在微服务下的应用场景

全局的 setTimeout() 方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段。

语法

js 复制代码
setTimeout(code)
setTimeout(code, delay)
setTimeout(functionRef)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay, param1)
setTimeout(functionRef, delay, param1, param2)
setTimeout(functionRef, delay, param1, param2, /* ... ,*/ paramN)

参数

  • functionRef

    当定时器到期后,将要执行的 function

  • code

    这是一个可选语法,允许你包含在定时器到期后编译和执行的字符串而非函数。使用该语法是不推荐的 ,原因和使用 eval() 一样,有安全风险。

  • delay 可选

    定时器在执行指定的函数或代码之前应该等待的时间,单位是毫秒。如果省略该参数,则使用值 0,意味着"立即"执行,或者更准确地说,在下一个事件循环执行。

    注意,无论是哪种情况,实际延迟可能会比预期长一些,参见下方延时比指定值更长的原因一节的叙述。

    还要注意的是,如果值不是数字,隐含的类型强制转换会静默地对该值进行转换,使其成为一个数字------这可能导致意想不到的、令人惊讶的结果;见非数字延迟值被静默地强制转化为数字以了解一个示例。

  • param1, ..., paramN 可选

    附加参数,一旦定时器到期,它们会作为参数传递给 functionRef 指定的函数。

返回值

返回值 timeoutID 是一个正整数,表示由 setTimeout() 调用创建的定时器的编号。这个值可以传递给 clearTimeout() 来取消该定时器。

保证 timeoutID 值不会被同一对象(window 或 worker)的后续调用 setTimeout()setInterval() 重复使用。然而,不同的对象使用不同的 ID 池。

用法

setTimeout()是用来在指定时间后,执行一个函数或者一段代码。是添加在宏任务队列中,意思是在指定最小时间插入宏任务队列,具体的执行时间看任务队列的情况,其执行时间大于指定时间的,因为在前面有任务需要执行。

  • 如果需要在微任务异步触发函数,其中一个函数只有在另一个函数完成后才会触发,请参见关于 Promise 的文档。
  • 如果要重复调用某个函数(如每 N 毫秒调用一次),考虑使用 setInterval()

this指向问题

当你向 setTimeout() 传递一个函数时,该函数中的 this 指向跟你的期望可能不同,这个问题在 JavaScript 参考中进行了详细解释。

setTimeout() 执行的代码是从一个独立于调用 setTimeout 的函数的执行环境中调用的。为被调用的函数设置 this 关键字的通常规则适用,如果你没有在调用中或用 bind 设置 this,它将默认为 window(或 global)对象。它将与调用 setTimeout 的函数的 this 值不一样。

ini 复制代码
const myArray = ["zero", "one", "two"];
myArray.myMethod = function (sProperty) {
  console.log(arguments.length > 0 ? this[sProperty] : this);
};

myArray.myMethod(); // 输出 "zero,one,two"
myArray.myMethod(1); // 输出 "one"

上面这段代码正常工作,当调用 myArray 时,它的 this 设定为 myArray,故在函数中 this[sProperty]myArray[sProperty] 等价。然而,在以下示例中:

scss 复制代码
setTimeout(myArray.myMethod, 1.0 * 1000); // 在 1 秒后输出 "[object Window]"
setTimeout(myArray.myMethod, 1.5 * 1000, "1"); // 在 1.5 秒后输出 "undefined"

传递给 setTimeout 的是 myArray.myMethod 函数,当调用它的时候,this 没有指向,故其默认指向 window 对象。

setTimeout 中也没有传递 thisArg 的选项,就像在 forEach()reduce() 等数组方法中一样。如下方示例所示,使用 call 来设置 this 也不起作用。

arduino 复制代码
setTimeout.call(myArray, myArray.myMethod, 2.0 * 1000); // 出错
setTimeout.call(myArray, myArray.myMethod, 2.5 * 1000, 2); // 同样会出错

解决this问题

一、使用包装函数

一个通用的方法是用包装函数来将 this 设置为所需要的值:

js 复制代码
setTimeout(function () {
  myArray.myMethod();
}, 2.0 * 1000); // 在 2 秒后输出 "zero,one,two"
setTimeout(function () {
  myArray.myMethod("1");
}, 2.5 * 1000); // 在 2.5 秒后输出 "one"

包装函数也可以是箭头函数:

js 复制代码
setTimeout(() => {
  myArray.myMethod();
}, 2.0 * 1000); // 在 2 秒后输出 "zero,one,two"
setTimeout(() => {
  myArray.myMethod("1");
}, 2.5 * 1000); // 在 2.5 秒后输出 "one"

二、 提前使用bind()绑定this

或者,也可以使用 bind() 来为所有对特定函数的调用设置 this 的值:

js 复制代码
const myArray = ["zero", "one", "two"];
const myBoundMethod = function (sProperty) {
  console.log(arguments.length > 0 ? this[sProperty] : this);
}.bind(myArray);

myBoundMethod(); // 输出 "zero,one,two"。因为 'this' 在函数中绑定到了 myArray 
myBoundMethod(1); // 输出 "one"
setTimeout(myBoundMethod, 1.0 * 1000); // 由于绑定问题,还是在 1 秒后输出 "zero,one,two"
setTimeout(myBoundMethod, 1.5 * 1000, "1"); // 在 1.5 秒后输出 "one" 

传递字符串字面量的危害

将字符串而不是函数传递给 setTimeout() 与使用 eval() 具有相同的问题。

javascript 复制代码
// 不要这样做
setTimeout("console.log('Hello World!');", 500);
javascript 复制代码
// 这样做
setTimeout(() => {
  console.log("Hello World!");
}, 500);

传递给 setTimeout() 的字符串是在全局上下文中求值的,因此当字符串被求值为代码时,setTimeout() 被调用的上下文中的局部符号将不可用。

很多网站的示例代码都是使用字符串字面量,这是非常不可取,在微服务中很容易造成隐藏的bug。下面分析微服务对于Window全局对象的处理。

乾坤对window对象处理

  1. ProxySandboxProxy沙盒

qiankun 在实现 sandbox 时,先构建一个空对象 - fakeWindow 作为一个假的 window 对象,然后在 fakeWindow 的基础上通过原生的 Proxy 创建一个 proxy 对象,这个 proxy 最后会作为子应用 js 代码执行时的全局变量 。有了这个 proxy ,我们就可以很方便的劫持 js 代码中对全局变量的读写操作 。当子应用中需要添加(修改)全局变量时,直接在 fakeWindow 中添加(修改);当子应用需要从全局变量中读取某个属性(方法)时,先从 fakeWindow 中获取,如果 fakeWindow 中没有,再从原生 window 中获取。

  1. SnapshotSandbox快照沙盒

qiankun 在实现 SnapshotSandbox 时,也是先创建一个 fakeWindow 作为假的 window 对象 ,这个 fakeWindow 最后会作为子应用 js 代码执行时的全局变量 。由于不支持 proxy (也不支持 setter/getter ),所以 qiankun 将原生 window 上的属性、方法 全部拷贝了一份到 fakeWindow ,以便子应用在读取全局变量时,可以在 fakeWindow 中全部获取到。

  1. SingularProxySandbox单例沙盒

qiankun 在启用 单例模式(父应用只有一个子应用挂载) 时,会自动创建。SingularProxySandbox 也是基于 proxy 实现的。但是和 ProxySandbox 不同,SingularProxySandbox 是在原生 window 对象上直接修改属性的,这会导致父子应用之间全局变量的互相影响。目前,不管是单子应用 还是多子应用qiankun 默认都使用 ProxySandboxSingularProxySandbox 只有我们我们在 start 方法中显示配置 { sandbox: {loose: true }} 才会使用。

具体应用分析

下面先看一段代码:

js 复制代码
const arr = [1, 2, 3]
function testOne() {
    console.log('testOne app', arr)
}
window.__testOne = testOne

(function (){
    setTimeout("__testOne()", 500)
})()

在独立应用中,执行这段逻辑没有问题,在微服务环境下,采用 ProxySandboxProxy沙盒 SnapshotSandbox快照沙盒 的情况下,由于 setTimeout()函数是由当前Window下调用,当前Window对象只有主应用有,这时会报找不到函数的错误。这时就暴露出,在setTimeout()调用字符串字面量的危害:

  • evel 函数,需要动态编译执行,有安全性问题
  • 需要在window全局变量中查询引用。适配服务会有问题

解决方案:

  1. 可以使用 SingularProxySandbox ,这样只有一个window,不会出现问题,但是多应用变量互相覆盖也是一个问题。
js 复制代码
import { registerMicroApps, start } from 'qiankun'

start({sandbox:{loose:true}})
  1. 直接传入window对象
js 复制代码
;((window) => {
    setTimeout(window.__testOne, 500)
})(window)
  1. 使用上面介绍解决this问题的方案
js 复制代码
;(() => {
    setTimeout(()=>{
        testOne()
    }, 500)
})()
相关推荐
不收藏找不到我几秒前
浏览器交互事件汇总
前端·交互
YBN娜14 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=14 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck19 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!39 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。1 小时前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架