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

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

语法

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 值不一样。

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] 等价。然而,在以下示例中:

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 也不起作用。

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

解决this问题

一、使用包装函数

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


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

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

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

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

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

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() 具有相同的问题。

// 不要这样做
setTimeout("console.log('Hello World!');", 500);
// 这样做
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 }} 才会使用。

具体应用分析

下面先看一段代码:

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,不会出现问题,但是多应用变量互相覆盖也是一个问题。
import { registerMicroApps, start } from 'qiankun'

start({sandbox:{loose:true}})
  1. 直接传入window对象
;((window) => {
    setTimeout(window.__testOne, 500)
})(window)
  1. 使用上面介绍解决this问题的方案
;(() => {
    setTimeout(()=>{
        testOne()
    }, 500)
})()

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYVYmGn5' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片