♥-v8引擎及js代码的执行
v8引擎工作原理
// 假如有这样一段JavaScript源代码
console.log("hello world");
function sum(num1, num2) {
return num1 + num2;
}
Parse解析器,会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;- 如果函数没有被调用,那么是不会被转换成
AST的; Parse的V8官方文档:https://v8.dev/blog/scanner
- 如果函数没有被调用,那么是不会被转换成
Ignition解释器,会将AST转换成ByteCode(字节码);- 同时会收集
TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算); - 如果函数只调用一次,
Ignition直接解释为ByteCode; Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter
- 同时会收集
TurboFan优化编译器,可以将字节码编译为CPU可以直接执行的机器码;- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过
TurboFan转换成优化的机器码,提高代码的执行性能; - 但是,机器码实际上也会被还原为
ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码; TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过
V8的内存回收
Orinoco模块,负责垃圾回收,将程序中不需要的内存回收;Orinoco的V8官方文档:https://v8.dev/blog/trash-talk
V8引擎
- V8引擎是谷歌开源的高性能
JavaScript引擎,主要工作是: - 编译和执行
JavaScript - 处理调用栈
- 内存分配
- 垃圾回收
js代码的执行
在执行一段代码时,
JS引擎会首先创建一个执行栈然后
JS引擎会创建一个全局执行上下文,并push到执行栈中, 这个过程JS引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,
JS引擎会进入执行阶段,这个过程JS引擎会逐行的执行代码,即 为之前分配好内存的变量逐个赋值(真实值)。如果这段代码中存在
function的声明和调用,那么JS引擎会创建一个函数执行上下文,并push到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,
JS引擎会在父函数执行的过程中,将子函数的全局执行上下文push到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。还有一种特殊情况是,在子函数执行的过程中,父函数已经
return了,这种情况下,JS引擎会将父函数的上下文从执行栈中移除,与此同时,
JS引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,
JS引擎才会将子函数的上下文及闭包一并从执行栈中移除。
js 如何处理高并发
最后,JS引擎是单线程的,那么它是如何处理高并发的呢?即当代码中存在异步调用时JS是如何执行的。
- 比如
setTimeout或fetch请求都是非阻塞(non-blocking)的, - 当异步调用代码触发时,
JS引擎会将需要异步执行的代码移出调用栈,直到等待到返回结果, JS引擎会立即将与之对应的回调函数push进任务队列中等待被调用,- 当调用(执行)栈中已经没有需要被执行的代码时,
JS引擎会立刻将任务队列中的回调函数逐个push进调用栈并执行。这个过程我们也称之为事件循环。
附言
需要更深入的了解JS引擎,必须理解几个概念,
- 执行上下文,
- 闭包,
- 作用域,
- 作用域链,
- 事件循环。
建议去网上多看看相关文章,这里推荐一篇非常精彩的博客,对于JS引擎的执行做了图形化的说明,更加便于理解。
☆-原型设计模式
原型模式可用于创建新对象,但它创建的不是非初始化的对象,而是使用原型对象(或样本对 象)的值进行初始化的对象。原型模式也称为属性模式。
原型模式在初始化业务对象时非常有用,业务对象的值与数据库中的默认值相匹配。原型对象中 的默认值被复制到新创建的业务对象中。
经典的编程语言很少使用原型模式,但作为原型语言的
JavaScript在构造新对象及其原型时使用 了这个模式。
☆-基础考察
下面代码的输出结果是什么?
// 按顺序执行原则
sayHi()
function sayHi() {
console.log(name);
console.log(age);
var name = 'Lydia';
let age = 21;
}
// A. undefined 和 undefined;
// B. Lydia 和 ReferenceError;
// C. ReferenceError 和 21;
// D. undefined 和 ReferenceError;
// 本题的考点主要是var与let的区别以及var的预解析问题。
// var所声明的变量会被预解析,var name;提升到作用域最顶部,所以在开始的console.log(name)时,
// name已经存在,但是由于没有赋值,所以是undefined;
// 而let会有暂时性死区,也就是在let声明变量之前,你都无法使用这个变量,会抛出一个错误,故选D。
下面代码的输出结果是什么
const arr = [1, 2, [3, 4, [5]]];
console.log(arr.flat(1)); // es6
// A. [1, 2, [3, 4, [5]]];
// B. [1, 2, 3, 4, [5]];
// C. [1, 2, [3, 4, 5]];
// D. [1, 2, 3, 4, 5];
// 这里主要是考察Array.prototype.flat方法的使用,扁平化会创建一个新的,被扁平化的数组,扁
// 平化的深度取决于传入的值;这里传入的是1也就是默认值,所以数组只会被扁平化一层,相当于
// [].concat([1, 2], [3, 4, [5]]),故选B。
const name = 'Lydia Hallie';
const age = 21;
console.log(Number.isNaN(name));
console.log(Number.isNaN(age));
console.log(isNaN(name));
console.log(isNaN(age));
// A. true false true false
// B. true false false false
// C. false false true false
// D. false true false true
// 本题主要考察isNaN(es5)和Number.isNaN(es6)的区别;
// 首先isNaN在调用的时候,会先将传入的参数转换为数字类型,所以非数字值传入也有可能返回true,
// 所以第三个和第四个打印分别是true false;
// Number.isNaN不同的地方是,他会首先判断传入的值是否为数字类型,如果不是,直接返回
// false,本题中传入的是字符串类型,所以第一个和第二个打印均为false,故选C。
以下代码运行输出为
var a = [1, 2, 3],
b = [1, 2, 3],
c = [1, 2, 4];
console.log(a == b);
console.log(a === b);
console.log(a > c);
console.log(a < c);
// A: false, false, false, true
// B: false, false, false, false
// C: true, true, false, true
// D: other
// JavaScript中Array的本质也是对象,所以前两个的结果都是false, 而JavaScript中Array的'>'运算
// 符和'<'运算符的比较方式类似于字符串比较字典序,会从第一个元素开始进行比较,如果一样比
// 较第二个,还一样就比较第三个,如此类推,所以第三个结果为false,第四个为true。 综上所
// 述,结果为false, false, false, true,选A
以下代码运行结果为
var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');
// A: Value is Something
// B: Value is Nothing
// C: NaN
// D: other
// 这题考的javascript中的运算符优先级,这里'+'运算符的优先级要高于'?'所以运算符,实际上是
// 'Value is true'?'Something' : 'Nothing',当字符串不为空时,转换为bool为true,所以结果
// 为'Something',选D
以下代码运行结果为
[,,,].join(", ")
// A: ", , , "
// B: "undefined, undefined, undefined, undefined"
// C: ", , "
// D: ""
// JavaScript中使用字面量创建数组时,如果最末尾有一个逗号’,’,会被省略,所以实际上这个数组
// 只有三个元素(都是undefined): console.log([,].length);//输出结果://3 而三个元素,使用
// join方法,只需要添加两次,所以结果为", , ",选C
以下代码运行结果为
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1)
// A: 3
// B: 12
// C: error
// D: other
// 这题考的是JS的函数arguments的概念: 在调用函数时,函数内部的arguments维护着传递到这
// 个函数的参数列表。它看起来是一个数组,但实际上它只是一个有length属性的Object,不从
// Array.prototype继承。所以无法使用一些Array.prototype的方法。 arguments对象其内部属性
// 以及函数形参创建getter和setter方法,因此改变形参的值会影响到arguments对象的值,反过来
// 也是一样 具体例子可以参见Javascript秘密花园#arguments 所以,这里所有的更改都将生效,a
// 和c的值都为10,a+b+c的值将为21,选D
以下代码运行结果为
var name = 'World!';
// 立即执行函数
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
// 等价于
var name = ‘World!’;
(function () {
// 作用域中的变量提升
var name;//现在还是undefined
if (typeof name === 'undefined'){
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
// A: Goodbye Jack
// B: Hello Jack
// C: Hello undefined
// D: Hello World
// 这题考的是javascript作用域中的变量提升,javascript的作用于中使用var定义的变量都会被提升
// 到所有代码的最前面,这样就很好理解了,typeof name ===
// ‘undefined’的结果为true,所以最后会输出’Goodbye Jack’,选A
以下代码运行结果为
var a = [0];
if ([0]) {
// 隐式类型转换
console.log(a == true);
} else {
console.log("wut");
}
// A: true
// B: false
// C: "wut"
// D: other
// 同样是一道隐式类型转换的题,不过这次考虑的是’'运算符,a本身是一个长度为1的数组,而当数
// 组不为空时,其转换成bool值为true。 而左右的转换,会使用如果一个操作值为布尔值,则在比
// 较之前先将其转换为数值的规则来转换,Number([0]),也就是0,于是变成了0 == true,结果自
// 然是false,所以最终结果为B
以下代码运行结果为
var ary = Array(3);
ary[0]=2
ary.map(function(elem) { return '1'; });
// A: [2, 1, 1]
// B: ["1", "1", "1"]
// C: [2, "1", "1"]
// D: other
// 又是考的Array.prototype.map的用法,map在使用的时候,只有数组中被初始化过元素才会被
// 触发,其他都是undefined,所以结果为[“1”, undefined ,undefined],选D
以下代码运行结果为
var a = 111111111111111110000,
b = 1111;
console.log(a + b);
// A: 111111111111111111111
// B: 111111111111111110000
// C: NaN
// D: Infinity
// 又是一道考查JavaScript数字的题,由于JavaScript实际上只有一种数字形式IEEE 754标准的64位
// 双精度浮点数,其所能表示的整数范围为-253~253(包括边界值)。这里的111111111111111110000已
// 经超过了2^53次方,所以会发生精度丢失的情况。综上选B
以下代码运行结果为
(function(){
var x = y = 1;
})();
console.log(y);
console.log(x);
// A: 1, 1
// B: error, error
// C: 1, error
// D: other
// 变量提升和隐式定义全局变量的题,也是一个JavaScript经典的坑… 还是那句话,在作用域内,变
// 量定义和函数定义会先行提升,所以里面就变成了: (function(){ var x; y = 1; x = 1; })(); 这点会问
// 了,为什么不是var x, y;,这就是坑的地方…这里只会定义第一个变量x,而y则会通过不使用var
// 的方式直接使用,于是乎就隐式定义了一个全局变量y 所以,y是全局作用域下,而x则是在函数
// 内部,结果就为1, error,选C
☆-其他类型值转换为字符串时的规则
ToString负责处理非字符串到字符串的强制类型转换。
- (1)
Null和Undefined类型 ,null转换为"null",undefined转换为"undefined", - (2)
Boolean类型,true转换为"true",false转换为"false"。 - (3)
Number类型的值直接转换,不过那些极小和极大的数字会使用指数形式。 - (4)
Symbol类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。 - (5)对普通对象来说,除非自行定义
toString()方法,否则会调用toString((Object.prototype.toString())来返回内部属性[[Class]]的值,如"[object Object]"。如果对象有自己的toString()方法,字符串化时就会调用该方法并使用其返回值。
☆-其他类型值转换为数字时的规则
将非数字值当作数字来使用,比如数学运算抽象操作 ToNumber。
- (1)
Undefined类型的值转换为NaN。 - (2)
Null类型的值转换为0。 - (3)
Boolean类型的值,true转换为1,false转换为0。 - (4)
String类型的值转换如同使用Number()函数进行转换,如果包含非数字值则转换为NaN,空字符串为0。 - (5)
Symbol类型的值不能转换为数字,会报错。 - (6)对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值, 则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作
DefaultValue)检查该值是否有valueOf() 方法。
如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString() 的返回值
(如果存在)来进行强制类型转换.如果 valueOf() 和 toString() 均不返回基本类型值,会产生
TypeError 错误。
☆-Object.is()和"==="、"=="的区别
- 使用双等号
"=="进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。 - 使用三等号
"==="进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回false。 - 使用
Object.is来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,- 比如
-0和+0不再相等, - 两 个
NaN认定为是相等的。
- 比如
☆-CommonJS 模块
Node应用由模块组成,采用CommonJS模块规范。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
- 模块输出方式:
exports和module.exports - 模块输出方式:
require
var x = 5;
var addX = function (value) {
return value + x;
};
// 通过module.exports输出变量x和函数addX。
module.exports.x = x;
module.exports.addX = addX;
require方法用于加载模块,其实就是加载模块的module.exports属性。
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
// 可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样
var exports = module.exports;
注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
导出基础数据类型
对于原始数据类型,属于值的拷贝:一旦输出一个值,模块内部的变化就影响不到这个值。原始数据类型的值,会被缓存。
// a.js
let count = 1
let incCount = () => {
count++
}
setTimeout(()=>{
console.log('a, 1s后',count);
},1000)
module.exports = {
count, // 对count的拷贝:模块内部的变化不影响这个值
incCount
}
// b.js
const mod = require('./a.js')
console.log('main', mod.count); // 1
mod.incCount()
console.log('main,incCount后', mod.count); // 1
setTimeout(()=>{
mod.count=3
console.log('main, 2s后', mod.count); //3
},2000)
// 打印结果
// main 1
// main,incCount后 1
// a, 1s后 2
// main, 2s后 3
main.js获得内部变动后的值,a.js的模块输出如下:
// a.js
let count = 1
let incCount = () => {
count++
}
setTimeout(()=>{
console.log('a, 1s后',count);
},1000)
// 取消对原始数据类型的缓存
module.exports = {
get count() {
// 取值器函数,在main.js中可正确读取内部的变量的变动
return count
},
incCount
}
// main.js的输出
// main 1
// main,incCount后 2
// a, 1s后 2
// main, 2s后 2
导出复杂数据类型
对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
// b.js
let obj = {
count: 1
}
let incCount = () => {
obj.count++
}
setTimeout(()=>{
console.log('b,1s后',obj.count);
},1000)
setTimeout(()=>{
console.log('b,3s后',obj.count);
},3000)
module.exports = {
obj,
incCount
}
// main.js
const {obj,incCount} = require('./b.js')
console.log('main', obj.count);
incCount()
console.log('main,incCount后', obj.count);
setTimeout(()=>{
obj.count=3
console.log('main, 2s后', obj.count);
},2000)
// main 1
// main,incCount后 2
// b,1s后 2
// main, 2s后 3
// b,3s后 3
require
- 当使用
require命令加载某个模块时,就会运行整个模块的代码。 - 当使用
require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
// a.js
let count = 1
let incCount = () => {
count++
}
setTimeout(()=>{
console.log('a, 1s后',count);
},1000)
module.exports = {
count,
incCount
}
const mod = require('./a.js')
console.log(111, mod.count);
mod.incCount()
const xxx = require('./a.js')
console.log(222,mod.count)
console.log(333, xxx.count);
// 111 1
// 222 1
// 333 1
// a, 1s后 2
- 循环加载时,属于加载时执行。即脚本代码在
require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
// x.js
exports.done = false
let y = require('./y.js')
console.log('x', y.done);
exports.done = true
console.log('x 执行完毕');
// y.js
exports.done = false
let x = require('./x.js')
console.log('y.js', x.done);
exports.done = true
console.log('y.js 执行完毕');
// z.js
let x = require('./x')
let y = require('./y')
console.log('z 执行完毕',x.done,y.done);
☆-ES6 模块
- ES6模块输出方式:
export和export default - ES6模块输入方式:
import ... from ...
ES6 模块中的值
- 1、
ES6模块中的值属于【动态只读引用】 - 2、对于只读来说,即不允许修改引入变量的值,
import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。 - 3、对于动态来说,原始值发生变化,
import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
// a.js
import {obj} from './b.js'
// obj = {} 报错
console.log('a.js',obj);
setTimeout(()=>{
console.log('a,2s后',obj);
},2000)
// b.js
export let obj = {
count: 1
}
setTimeout(()=>{
console.log('b', obj.count);
obj.count++
},1000)
- 4、循环加载时,
ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。
// x.js
import {bar} from './y.js'
export function foo() {
console.log('');
bar();foo
console.log('执行完毕');
}
foo()
// y.js
import {foo} from './x.js'
export function bar() {
console.log('bar');
if(Math.random()>0.5) {
foo()
}
}
// 执行结果有多重可能
// x.js:3 foo
// y.js:3 bar
// x.js:3 foo
// y.js:3 bar
// x.js:5 执行完毕
// x.js:5 执行完毕
♥-CommonJS模块与ES6模块的区别
- 语法不同,
ES6 module导出 是export,import导入;commonjs导出是module.exports,导入是require; ES6 module针对 前端,commonjs针对后端;- 加载时机不同,
commonjs运行时加载模块,ES6 module编译时加载模块; commonjs导出基础类型属于值的拷贝,通过getter可以修改,导出复杂数据类型属于值的引用;ES6 module导出一个值的引用,且只读,不能修改;commonjs一个文件只能导出一个值,即模块中的module.exports属性;ES6 module可以导出多个。
☆-Commonjs中module.exports和exports的区别
Commonjs规范中module.exports和exports的区别
require只能引入module.exports导出的值,不能引入exports导出的值module.exports是一个对象,exports默认则是指向这个对象的引用module.exports和exports是等价的- 每个导出模块
node.js默认会返回return module.exports
// 写法是一致的,给最初的空对象{}添加了一个属性
exports.age = 18;
module.exports.age = 18;
// 通过require得到的就是{age: 18}
☆-请解释原型设计模式
原型模式可用于创建新对象,但它创建的不是非初始化的对象,而是使用原型对象(或样本对 象)的值进行初始化的对象。原型模式也称为属性模式。
原型模式在初始化业务对象时非常有用,业务对象的值与数据库中的默认值相匹配。原型对象中 的默认值被复制到新创建的业务对象中。
经典的编程语言很少使用原型模式,但作为原型语言的 JavaScript 在构造新对象及其原型时使用
了这个模式。
☆-JS判断变量的类型
- (1) 使用 typeof 检测 当需要变量是否是
number,string,boolean,function,undefined类型 时,可以使用typeof进行判断。arr, json, nul, date, reg, error全部被检测为object类型,其他 的变量能够被正确检测出来。 - (2) 使用 instanceof 检测
instanceof运算符与typeof运算符相似,用于识别正在处理的对象的类 型。与typeof方法不同的是,instanceof方法要求开发者明确地确认对象为某特定类型。 - (3) 使用 constructor 检测
constructor本来是原型对象上的属性,指向构造函数。但是根据实例 对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的。
♥-防抖与节流
防抖和节流严格算起来应该属于性能优化的知识。实际上遇到的频率相当高,处理不当或者放任不管就容易引起浏览器卡死。
滚动条案例
很多网站会提供这么一个按钮:用于返回顶部。这个按钮只会在滚动到距离顶部一定位置之后才出现,那么我们现在抽象出这个功能需求-- 监听浏览器滚动事件,返回当前滚条与顶部的距离
function showTop() {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop
console.log('滚动位置:', scrollTop);
}
window.onscroll = showTop
// scroll事件的默认执行频率,太高了:点击一次键盘的【向下方向键】,会发现函数执行了8-9次!
let displayEle = document.querySelector('.display')
window.onscroll = function() {
window.requestAnimationFrame(function(){
displayEle.style.display = window.scrollY>100?'block':'none'
document.querySelector('#totop').onclick = function() {
window.scrollTo(0,0)
}
})
}
防抖(debounce)
对于短时间内连续触发的事件(上面的滚动事件),防抖的含义就是让某个时间期限(如上面的1000毫秒)内,事件处理函数只执行一次
在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后:
- 如果在
200ms内没有再次触发滚动事件,那么就执行函数 - 如果在
200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时
优化效果:如果短时间内大量触发同一事件,只会执行一次函数。
实现:既然前面都提到了计时,那实现的关键就在于setTimeout这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现:
/*
* fn[function] 需要防抖的函数
* delay[number] 毫秒,防抖期限值
*/
function debounce(fn, delay) {
let timer = null // 闭包
return function() {
if(timer){
clearTimeout(timer) // delay内多次触发,取消计时,重新计算
}
timer = setTimeout(fn, delay)
}
}
滚动条防抖实现:
function showTop() {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop
console.log('滚动位置:', scrollTop);
}
function debounce(fn, delay) {
let timer = null // 闭包
return function() {
if(timer){
clearTimeout(timer) // delay内多次触发,取消计时,重新计算
}
timer = setTimeout(fn, delay)
}
}
window.onscroll = debounce(showTop,1000)
节流(throttle)
使用上面的防抖方案来处理问题的结果是:
- 如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离。
但是如果产品同学的期望处理方案是:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈呢?
可以设计一种类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活(类似于技能冷却时间)
效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。
实现:这里借助setTimeout来做一个简单的实现,加上一个状态位valid来表示当前函数是否处于工作状态:
function thottle(fn, delay) {
let valid = true
return function() {
if(!valide) {
return false
}
valid = false
setTimeout(() => {
fn()
valid = true
}, delay)
}
}
window.onscroll = thottle(fn, delay)
其他应用场景举例
- 搜索框
input事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求。 - 页面
resize事件,常见于需要做页面适配的时候。需要根据最终呈现的页面情况进行dom渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)
☆-Window.onload和DOMContentLoaded区别是什么
Window.onload网页资源加载完毕后触发,包括html、js、css、图片等DOMContentLoaded只要DOM结构加载完成后就触发