全局的 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对象处理
- ProxySandboxProxy沙盒
qiankun 在实现 sandbox 时,先构建一个空对象 - fakeWindow 作为一个假的 window 对象,然后在 fakeWindow 的基础上通过原生的 Proxy 创建一个 proxy 对象,这个 proxy 最后会作为子应用 js 代码执行时的全局变量 。有了这个 proxy ,我们就可以很方便的劫持 js 代码中对全局变量的读写操作 。当子应用中需要添加(修改)全局变量时,直接在 fakeWindow 中添加(修改);当子应用需要从全局变量中读取某个属性(方法)时,先从 fakeWindow 中获取,如果 fakeWindow 中没有,再从原生 window 中获取。
- SnapshotSandbox快照沙盒
qiankun 在实现 SnapshotSandbox 时,也是先创建一个 fakeWindow 作为假的 window 对象 ,这个 fakeWindow 最后会作为子应用 js 代码执行时的全局变量 。由于不支持 proxy (也不支持 setter/getter ),所以 qiankun 将原生 window 上的属性、方法 全部拷贝了一份到 fakeWindow ,以便子应用在读取全局变量时,可以在 fakeWindow 中全部获取到。
- SingularProxySandbox单例沙盒
是 qiankun 在启用 单例模式(父应用只有一个子应用挂载) 时,会自动创建。SingularProxySandbox 也是基于 proxy 实现的。但是和 ProxySandbox 不同,SingularProxySandbox 是在原生 window 对象上直接修改属性的,这会导致父子应用之间全局变量的互相影响。目前,不管是单子应用 还是多子应用 ,qiankun 默认都使用 ProxySandbox 。SingularProxySandbox 只有我们我们在 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全局变量中查询引用。适配服务会有问题
解决方案:
- 可以使用 SingularProxySandbox ,这样只有一个window,不会出现问题,但是多应用变量互相覆盖也是一个问题。
js
import { registerMicroApps, start } from 'qiankun'
start({sandbox:{loose:true}})
- 直接传入window对象
js
;((window) => {
setTimeout(window.__testOne, 500)
})(window)
- 使用上面介绍解决this问题的方案
js
;(() => {
setTimeout(()=>{
testOne()
}, 500)
})()