JS事件循环之宏任务和微任务
文本属于长期更新技术文档
前言
JS是单线程的,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成这门语言的核心特征,将来也不会改变。
所谓单线程是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个。 事件循环 Event Loop
程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
一般而言,异步任务有以下三种类型:
- 普通事件,如click、resize等
- 资源加载,如load、error等
- 定时器,包括setInterval、setTimeout等
事件循环具体过程就是:
- 同步任务进入主线程,异步任务进入Event Table并注册函数。
- 当异步任务完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕执行栈为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
宏任务与微任务
除了广义的同步任务和异步任务,对任务更细致费划分:
- macro-task(宏任务):包括script全部代码,setTimeout,setInterval等;
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...) - micro-task(微任务):Promise的then的回调函数,process.nextTick,async 函数await下面的代码等;
async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把await表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行
宏任务与微任务执行顺序:
- 执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务队列是否为空,如果为空的话,就执行宏任务,否则就一次性执行完所有微任务。
- 每次单个宏任务执行完毕后,检查微任务队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务后,设置微任务队列为null,然后再执行宏任务,如此循环。 总结:同步—>微任务—>宏任务
最后我们来解析一段较为复杂的代码:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
第一轮事件循环流程分析如下:
- 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
- 遇到setTimeout,其回调函数被分发到宏任务队列中。我们暂且记为setTimeout1。
- 遇到process.nextTick(),其回调函数被分发到微任务队列中。我们记为process1。
- 遇到Promise,new Promise直接执行,输出7。then被分发到微任务队列中。我们记为then1。
- 又遇到了setTimeout,其回调函数被分发到宏任务队列中,我们记为setTimeout2。
- 下表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
对照上述的事件循环流程图 宏任务结束之后我们接下来就开始去查看微任务中是否有任务 如果有就执行所有的微任务 这里有两个微任务process1和then1
- 执行process1,输出6。
- 执行then1,输出8。
第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。
第二轮事件循环从setTimeout1宏任务开始:
- 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务队列中,记为process2。
- new Promise立即执行输出4,then也分发到微任务队列中,记为then2 | 宏任务Event Queue |微任务Event Queue | | --- | --- | |setTimeout2 | process2 | | | then2 |
第二轮事件循环宏任务执行结束,执行两个微任务process2和then2。
- 执行process2,输出3。
- 执行then2,输出5。 第二轮事件循环正式结束,这二轮的结果是输出2,4,3,5。
第三轮事件循环从setTimeout2宏任务开始:
- 首先输出9。接下来遇到了process.nextTick(),同样将其分发到微任务队列中,记为process3。
- new Promise立即执行输出11,then也分发到微任务队列中,记为then3 | 宏任务Event Queue |微任务Event Queue | | --- | --- | | | process3 | | | then3 |
第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
- 执行process3,输出10。
- 执行then3,输出12。
第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
总结:下一个宏任务执行前会去查看微任务队列中是否有任务 有就执行所有的微任务 微任务全部执行完 再去执行下一个宏任务
setTimeout 模拟 setInterval
setInterval是一个宏任务(JS事件循环之宏任务和微任务中有详解),使用多了就会发现它其实并不是准确无误的。原因有下:
- 推入任务队列后的时间不准确:在setInterval被推入任务队列时,如果前面有很多任务或者某个任务等待时间比较长(如网络请求的等),那么这个定时器的执行时间和我们预定的执行时间可能不一致。
- 函数中操作耗时较长导致的不准确:如果定时器里面的函数代码需要进行大量的计算或DOM操作时,消耗时间较长,是有可能前一次代码还没执行完,后一次代码就被添加到队列了,也到导致定时器变得不准确,甚至会出现同一时间执行两次的情况。
模拟方法:
/*
改方法主要解决setInterval的两个缺点:
- 在前一个定时器执行完前,不会向队列插入新的定时器
- 保证定时器间隔
*/
function mySettimeout(fn, t) {
let timer = null;
function interval() {
fn();
timer = setTimeout(interval, t);
}
interval();
return {
cancel:()=>{
clearTimeout(timer)
}
}
}
// let a = mySettimeout(()=>{
// console.log(1);
// },1000)
如果使用setinterval 模拟实现 settimeout呢?
const mySetTimeout = (fn, time) => {
const timer = setInterval(() => {
clearInterval(timer);
fn();
}, time);
};
// mySetTimeout(()=>{
// console.log(1);
// },1000)
JS数组reduce()方法
语法:
arr.reduce(callback,[initialValue])
reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调用 reduce 的数组。
callback (执行数组中每个值的函数,包含四个参数)
1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue (数组中当前被处理的元素)
3、index (当前元素在数组中的索引)
4、array (调用 reduce 的数组)
initialValue (作为第一次调用 callback 的第一个参数。)
例子:
var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(prev, cur, index, arr) {
console.log(prev, cur, index);
return prev + cur;
},0) //注意这里设置了初始值
console.log(arr, sum);
/*
打印结果
0 1 0
1 2 1
3 3 2
6 4 3
[1, 2, 3, 4] 10
*/
注意:
1. 如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。
2. 如果数组为空,则会报错。但是如果设置了初始值,则不会报错。
reduce用法
- 数组求和/乘积
var arr = [1, 2, 3, 4];
var sum = arr.reduce((x,y)=>x+y)
var mul = arr.reduce((x,y)=>x*y)
console.log( sum ); //求和,10
console.log( mul ); //求乘积,24
- 计算数组中每个元素出现的次数
let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];
let nameNum = names.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++
}else{
pre[cur] = 1
}
return pre
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}
- 数组去重
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
// includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
- 对象里的属性求和
var result = [
{
subject: 'math',
score: 10
},
{
subject: 'chinese',
score: 20
},
{
subject: 'english',
score: 30
}
];
var sum = result.reduce(function(prev, cur) {
return cur.score + prev;
}, 0);
console.log(sum) //60
- 将二维数组转化为一维
let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre,cur)=>{
return pre.concat(cur)
},[])
console.log(newArr); // [0, 1, 2, 3, 4, 5]
- 将多维数组转化为一维
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]
数组去重
方法一:ES6 Set 对象
function uniqueArr(arr) {
return [...new Set(arr)];
}
方法二:reduce()方法
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
// includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
深拷贝
// while来实现一个通用的forEach遍历 用来提升性能优化
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function clone(target, map = new Map()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone2(target[key], map);
});
return cloneTarget;
} else {
return target;
}
}
这种写法解决了对象的循环引用问题并且做了性能优化。通过开辟一个存储空间来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这种方法可以巧妙的解决循环应用的问题。使用while的循环遍历方式可以有效提高代码性能。如果对这块代码有疑惑可以仔细阅读大佬的文章:如何写出一个惊艳面试官的深拷贝?
响应式web设计——Media Queries
在css2中,media type属性,用于判断媒体类型。
在css3中,新增了media query属性用于增强media type属性。media query属性的是media type属性的增强功能,使media type可以进行条件判断输出对应的css。
Media Queries直译过来就是“媒体查询”,可以针对不同的媒体类型定义不同的样式。
@media 可以针对不同的屏幕尺寸设置不同的样式,特别是如果你需要设置设计响应式的页面,@media 是非常有用的。
当你重置浏览器大小的过程中,页面也会根据浏览器的宽度和高度重新渲染页面。
CSS 语法
@media screen and (max-width: 500px) {
// 如果浏览器窗口小于 500px, 背景将变为浅蓝色:
body {
background-color: lightblue;
}
}
或直接在link中判断设备的尺寸,然后引用不同的css文件:
// 意思是当屏幕的宽度大于等于600px小于等于900px的时候,应用styleA.css
<link rel="stylesheet" type="text/css" href="styleA.css" media="screen and (min-width:600px) and (max-width:900px)">
screen
是媒体类型里的一种,CSS2.1定义了10种媒体类型。
and
被称为关键字,其他关键字还包括 not(排除某种设备),only(限定某种设备)。
(min-width: 400px)
是媒体特性,其被放置在一对圆括号中。
媒体类型
值 | 描述 |
---|---|
all | 用于所有设备 |
aural | 已废弃。用于语音和声音合成器 |
braille | 已废弃。 应用于盲文触摸式反馈设备 |
embossed | 已废弃。 用于打印的盲人印刷设备 |
handheld | 已废弃。 用于掌上设备或更小的装置,如PDA和小型电话 |
用于打印机和打印预览 | |
projection | 已废弃。 用于投影设备 |
screen | 用于电脑屏幕,平板电脑,智能手机等。 |
speech | 应用于屏幕阅读器等发声设备 |
tty | 已废弃。 用于固定的字符网格,如电报、终端设备和对字符有限制的便携设备 |
tv | 已废弃。 用于电视和网络电视 |
媒体功能
值 | 描述 |
---|---|
aspect-ratio | 定义输出设备中的页面可见区域宽度与高度的比率 |
color | 定义输出设备每一组彩色原件的个数。如果不是彩色设备,则值等于0 |
color-index | 定义在输出设备的彩色查询表中的条目数。如果没有使用彩色查询表,则值等于0 |
device-aspect-ratio | 定义输出设备的屏幕可见宽度与高度的比率。 |
device-height | 定义输出设备的屏幕可见高度。 |
device-width | 定义输出设备的屏幕可见宽度。 |
grid | 用来查询输出设备是否使用栅格或点阵。 |
height | 定义输出设备中的页面可见区域高度。 |
max-aspect-ratio | 定义输出设备的屏幕可见宽度与高度的最大比率。 |
max-color | 定义输出设备每一组彩色原件的最大个数。 |
max-color-index | 定义在输出设备的彩色查询表中的最大条目数。 |
max-device-aspect-ratio | 定义输出设备的屏幕可见宽度与高度的最大比率。 |
max-device-height | 定义输出设备的屏幕可见的最大高度。 |
max-device-width | 定义输出设备的屏幕最大可见宽度。 |
max-height | 定义输出设备中的页面最大可见区域高度。 |
max-monochrome | 定义在一个单色框架缓冲区中每像素包含的最大单色原件个数。 |
max-resolution | 定义设备的最大分辨率。 |
max-width | 定义输出设备中的页面最大可见区域宽度。 |
min-aspect-ratio | 定义输出设备中的页面可见区域宽度与高度的最小比率。 |
min-color | 定义输出设备每一组彩色原件的最小个数。 |
min-color-index | 定义在输出设备的彩色查询表中的最小条目数。 |
min-device-aspect-ratio | 定义输出设备的屏幕可见宽度与高度的最小比率。 |
min-device-width | 定义输出设备的屏幕最小可见宽度。 |
min-device-height | 定义输出设备的屏幕的最小可见高度。 |
min-height | 定义输出设备中的页面最小可见区域高度。 |
min-monochrome | 定义在一个单色框架缓冲区中每像素包含的最小单色原件个数 |
min-resolution | 定义设备的最小分辨率。 |
min-width | 定义输出设备中的页面最小可见区域宽度。 |
monochrome | 定义在一个单色框架缓冲区中每像素包含的单色原件个数。如果不是单色设备,则值等于0 |
orientation | 定义输出设备中的页面可见区域高度是否大于或等于宽度。 |
resolution | 定义设备的分辨率。如:96dpi, 300dpi, 118dpcm |
scan | 定义电视类设备的扫描工序。 |
width | 定义输出设备中的页面可见区域宽度。 |
关键字
not: not是用来排除掉某些特定的设备的,比如 @media not print(非打印设备)
only: 用来定某种特别的媒体类型。对于支持Media Queries的移动设备来说,如果存在only关键字,移动设备的Web浏览器会忽略only关键字并直接根据后面的表达式应用样式文件。对于不支持 Media Queries的设备但能够读取Media Type类型的Web浏览器,遇到only关键字时会忽略这个样式文件。
al: 所有设备,这个应该经常看到
案例:
@media only screen and (min-width: 1024px){
// 如果浏览器窗口大于 1024px
.content{
background-color: green;
}
}
@media only screen and (min-width: 400px) and (max-width: 1024px){
// 如果浏览器窗口在400px和1024px之间
.content{
background-color: blue;
}
}
@media only screen and (max-width: 400px){
// 如果浏览器窗口小于 400px
.content{
background-color: yellow;
}
}
viewport
viewport 是用户网页的可视区域。翻译为中文可以叫做"视区"。
对于桌面浏览器,viewport就是除去所有工具栏、状态栏、滚动条等等之后用于看网页的区域,这是真正有效的区域。由于移动设备屏幕宽度不同于传统web,因此开发时需要改变viewport。
移动设备上的viewport就是设备的屏幕上能用来显示我们的网页的那一块区域,在具体一点,就是浏览器上(也可能是一个app中的webview)用来显示网页的那部分区域,但viewport又不局限于浏览器可视区域的大小,它可能比浏览器的可视区域要大,也可能比浏览器的可视区域要小。在默认情况下,一般来讲,移动设备上的viewport都是要大于浏览器可视区域的,这是因为考虑到移动设备的分辨率相对于桌面电脑来说都比较小,所以为了能在移动设备上正常显示那些传统的为桌面浏览器设计的网站,移动设备上的浏览器都会把自己默认的viewport设为980px或1024px(也可能是其它值,这个是由设备自己决定的),但带来的后果就是浏览器会出现横向滚动条,用户可以通过平移和缩放来看网页的不同部分,因为浏览器可视区域的宽度是比这个默认的viewport的宽度要小的。
一个常用的针对移动网页优化过的页面的 viewport meta 标签大致如下:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- width:控制 viewport 的大小,可以指定的一个值,如 600,或者特殊的值,如 device-width 为设备的宽度(单位为缩放为 100% 时的 CSS 的像素)。
- height:和 width 相对应,指定高度。
- initial-scale:初始缩放比例,也即是当页面第一次 load 的时候缩放比例。
- maximum-scale:允许用户缩放到的最大比例。
- minimum-scale:允许用户缩放到的最小比例。
- user-scalable:用户是否可以手动缩放。
注意:
关于viewport,还有一个很重要的概念是:iphone 的safari 浏览器完全没有滚动条,而且不是简单的”隐藏滚动条”,是根本没有这个功能。
iphone 的safari 浏览器实际上从一开始就完整显示了这个网页,然后用viewport 查看其中的一部分。当你用手指拖动时,其实拖的不是页面,而是viewport。浏览器行为的改变不止是滚动条,交互事件也跟普通桌面不一样。
v-cloak
- 用法:
这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。 - 示例:
[v-cloak] {
// 为了防止 v-cloak 的display属性被优先级别高的样式覆盖所以添加!important.
display:none !important;
}
<div v-cloak>
{{ context }}
</div>
不会显示,直到编译结束。
以上是vue官网的解释,哈哈。其实v-cloak是用来解决当网络比较慢,网页还在加载vue.js,而导致vue来不及渲染,页面会显示vue源代码,屏幕闪屏的问题。
效果:
使用 v-cloak 指令之后的效果(demo):
在简单项目中,使用 v-cloak 指令是解决屏幕闪动的好方法。但在大型、工程化的项目中(webpack、vue-router)只有一个空的 div 元素,元素中的内容是通过路由挂载来实现的,这时我们就不需要用到 v-cloak 指令了。
防抖和节流
区别:
- 函数防抖:当你频繁触发后,n秒内只执行一次
- 函数节流:在固定的时间内触发事件,每隔n秒触发一次 防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
防抖
触发高频函数事件后,n秒内函数只能执行一次,如果在n秒内这个事件再次被触发的话,那么会重新计算时间
实现: 可以借助setTimeout函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现。
// 实现防抖功能的函数
function debounce(fn,delay){
let timer = null //借助闭包
return function() {
if(timer){
//进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay) // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
}
}
// ts
export function debounce(fn: { apply: (arg0: any, arg1: any) => void }, t: number) {
let timer: any = null; //借助闭包
const delay = t || 500;
return function (this: any, ...args: any) {
if (timer) {
//进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay); // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
};
}
// 需要添加防抖功能的函数
function someFun() {
console.log('防抖成功');
}
let box = document.getElementById('box')
box.addEventListener('click', debounce(someFun, 1000))
节流
高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率 实现: 这里同样借助setTimeout函数,加上一个状态位valid来表示当前函数是否处于工作状态。
function throttle(fn,t) {
let valid = true; // 通过闭包保存一个标记
const delay = t || 500;
return function () {
if (!valid) return; // 在函数开头判断标记是否为true,不为true则return
valid = false; // 立即设置为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
valid = true;
}, delay);
};
}
// ts
function throttle(fn: { apply: (arg0: any, arg1: any[]) => void }, t: number) {
let flag = true;
const delay = t || 500;
return function (this: any, ...args: any) {
if (flag) {
fn.apply(this, args);
flag = false;
setTimeout(() => {
flag = true;
}, delay);
}
};
}
// 需要添加节流功能的函数
function someFun() {
console.log('节流成功');
}
let box = document.getElementById('box')
box.addEventListener('click', throttle(someFun, 1000))
应用场景
防抖
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- 频繁操作点赞和取消点赞,因此需要获取最后一次操作结果并发送给服务器
节流
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
new Date()兼容性问题
通过new Date()创建的时间对象时,在Chrome
浏览器中是可以正常工作,但在IE/firefox/safari
下却显示NaN。
原因是因为date的日期格式为 ‘yyyy-mm-dd’ ,将格式转换为 ‘yyyy/mm/dd’ 即可。
let date = '2021-01-01 00:00:00'
//let newDate = new Date(newDate); // NAN
let newDate = date.replace(/-/g,"/");
let time = new Date(newDate);
let timeStamp = time.getTime(); // 转换时间戳
js中?? 和 ?.
空值合并操作符( ??
)
空值合并操作符(??
)是一个逻辑操作符,当左侧的操作数为 [null
] 或者 [undefined
]时,返回其右侧操作数,否则返回左侧操作数。
let val1 = status ?? '字符串'
// 等价于
let val1 = (status === null || status === undefined) ? '字符串' : status
逻辑运算符( ||
)
**如果第一个操作数的求值结果是true
,就不会对第二个操作数求值。
空值合并操作符(||
)只要前面的值转为布尔值为false时,就取后面,否则取前面,如undefined、null、false、空字符串(' ')、数值0和NAN。
let val1 = status || '字符串'
// 等价于
let val1 = (status === null || status === undefined || status === false || status === ’‘ || status === 0 || status === NAN) ? '字符串' : status
可选链操作符( ?.
)
可选链操作符( ?.
)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?.
操作符的功能类似于 .
链式操作符,不同之处在于,在引用为空[null
]或者 [undefined
]的情况下不会引起错误,该表达式短路返回值
this.total = res?.data?.page?.count ?? 0
// res.data.page.count 中可以任何一个值为null或undefined,不会报错,如果其中一个为null或undefined,则最后值等于0
结论
:可以利用’??‘或'||'来避免为变量赋值null
或undefined
值
类数组转化为数组
原生js获取的DOM集合是一个类数组对象,所以不能直接利用数组的方法(例如:forEach,map等),需要转换为数组后,才能用数组的方法。
// 类数组转化为数组
const arrayLikeObj = {
0: '张三',
1: '李四',
2: '王五',
length: 3
}
// 1. ES6语法:Array.from
console.log(Array.from(arrayLikeObj))
// 2. Array.apply
console.log(Array.apply(null, arrayLikeObj))
// 3. [].slice
console.log([].slice.call(arrayLikeObj))
// 4. [].concat
console.log([].concat.apply([], arrayLikeObj))
DOM 节点输出 JSON 的格式
const dom2json = (rootDom) => {
if (!rootDom) {
return
}
let rootObj = {
tagName: rootDom.tagName,
children: []
}
const children = rootDom.children
// 读取子节点(元素节点)
if (children && children.length) {
Array.from(children).forEach((ele, i) => {
// 递归处理
rootObj.children[ i ] = dom2json(ele)
})
}
return rootObj
}
JS策略模式
策略模式指的是定义同一类功能不同种的算法,把他们一个个封装起来,目的就是将算法的使用与算法的实现分离开来。用户主动选择任意算法来使用。
策略模式比较常用的使用是避免过多的if else判断,也可以替代简单逻辑的switch。
// if else
function handle(obj){
if(obj.name == "obj1"){
doSomething1();
}else if(obj.name == "obj2"){
doSomething2();
}else if(obj.name == "obj3"){
doSomething3();
}else if(obj.name == "obj4"){
doSomething4();
}else if(obj.name == "obj5"){
doSomething5();
}
}
// switch case
function handle(obj){
switch(obj.name) {
case "obj1":
doSomething1();
break;
case "obj2":
doSomething2();
break;
case "obj3":
doSomething3();
break;
case "obj4":
doSomething4();
break;
case "obj5":
doSomething5();
break;
default:
return;
}
}
// 策略树
const handle = (val) => {
const obj = {
obj1:function(){
doSomething1();
},
obj2:function(){
doSomething2();
},
obj3:function(){
doSomething3();
},
obj4:function(){
doSomething4();
},
obj5:function(){
doSomething5();
}
}
return obj[val]
}
handle(obj.name)
推荐一个前端设计模式文章:设计模式介绍 · JavaScript设计模式与开发实践 (imyangyong.com)
toLocaleString 方法使用
最常见的使用场景
哪些类中包含这个API toLocaleString 方法最常见的使用场景是,将 Date 对象 转成字符串形式。
const date = new Date()
console.log(date.toLocaleString()) // 2022/1/12 上午11:47:42
除了这种用法之外,toLocaleString 还有很多种用法其中有不少用法可以满足一些常见得业务需求。
哪些类中包含这个API
Array.prototype.toLocaleString([locales[,options]])
Number.prototype.toLocaleString([locales[,options]])
Date.prototype.toLocaleString([locales[,options]])
语法
toLocaleString([locales[,options]]);
参数
locales:可选,带有BCP 47语言标记的字符串或字符串数组,用来表示要转为目标语言的类型,具体参考这个 Intl
options:可选,配置属性对象。
-
style:数字展示样式
style字段值 说明 decimal 用于纯数字格式(默认) currency 用于货币格式 percent 用于百分比格式 unit 用于单位格式 -
currency:当 options.style为currency 时,options.currency 用来表示货币单位的类型
currency字段值 说明 USD 使用美元格式(默认) EUR 使用欧元格式 CNY 使用人民币格式
示例
// 1、将数字进行千分位切割展示
var num = 1331231
console.log(num.toLocaleString()) // 1,331,231
// 2、将数字转为货币样式
var number = 123456.789;
console.log(number.toLocaleString('zh', { style: 'currency', currency: 'EUR' })); //€123,456.79
console.log(number.toLocaleString('zh', { style: 'currency', currency: 'CNY' })); //¥123,456.79
console.log(number.toLocaleString('zh', { style: 'currency', currency: 'CNY',currencyDisplay:'code' })); //CNY 123,456.79
console.log(number.toLocaleString('zh', { style: 'currency', currency: 'CNY',currencyDisplay:'name' })); //123,456.79人民币
// 3、将数字转为百分比形式
var num1 = 0.12
var num2 = 2.45
console.log(num1.toLocaleString('zh',{style:'percent'})) // 12%
console.log(num2.toLocaleString('zh',{style:'percent'})) // 245%
// 4、将纯数字/字符串数组所有元素用逗号拼接起来
var numArray = [12,564,'55',5,'8']
console.log(numArray.toLocaleString('zh')) // 12,564,55,5,8
此处仅展示一些日常需求可能会使用到的场景,更多用法大家可以去MDN官网 MDN官网-toLocaleString 去研究学习一下。
js判断数据类型
按钮添加禁止选中
对于一些可能频繁操作的按钮,可能出现如下尴尬的场景:
- 文本按钮的快速点击,触发了浏览器的双击快速选择,导致文本被选中:
- 翻页按钮的快速点击,触发了浏览器的双击快速选择:
对于这种场景,我们需要把不可被选中元素设置为不可被选中,利用 CSS 可以快速的实现这一点:
{
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
这样,无论点击的频率多快,都不会出现尴尬的内容选中:
table固定表头,横、纵向滚动
// html
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
<th>Header 3</th>
<th>Header 4</th>
<th>Header 5</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data 1</td>
<td>Data 2</td>
<td>Data 3</td>
<td>Data 4</td>
<td>Data 5</td>
</tr>
<tr>
<td>Data 6</td>
<td>Data 7</td>
<td>Data 8</td>
<td>Data 9</td>
<td>Data 10</td>
</tr>
<!-- More rows... -->
</tbody>
</table>
// css
<style scoped lang="scss">
.table-container {
overflow: auto;
height: 300px; /* set the height of the container */
table {
border-collapse: collapse;
width: 100%;
thead {
position: sticky;
top: 0;
background-color: #fff;
th {
background-color: #ddd;
padding: 10px;
text-align: left;
border: 1px solid #ccc;
}
}
tbody {
td {
padding: 10px;
border: 1px solid #ccc;
}
}
}
}
</style>
IntersectionObserver实现‘列表滚动获取分页数据’
<template>
<div>
<ul>
<li v-for="item in items" :key="item.id" class="list-item">{{ item.name }}</li>
</ul>
<div ref="bottomRef" />
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
const page = ref(1);
const items = ref([]);
const bottomRef = ref();
let observer = new IntersectionObserver(([entry]) => {
if (entry && entry.isIntersecting) {
intersected();
}
}, {});
onMounted(() => {
if (bottomRef.value) {
observer.observe(bottomRef.value);
}
});
// 获取最新分页数据
const intersected = () => {
const res = [];
for (let i = 0; i < 50; i++) {
res.push({
id: i,
name: `Item ${i}`,
});
}
page.value++;
items.value = [...items.value, ...res];
};
onUnmounted(() => {
observer.value.disconnect();
});
</script>
虚拟列表
<template>
<div
ref="listRef"
:style="{ height }"
class="infinite-list-container"
@scroll="scrollEvent($event)"
>
<div ref="phantomRef" class="infinite-list-phantom"></div>
<div ref="contentRef" class="infinite-list">
<div
v-for="item in visibleData"
:id="item._index"
ref="itemsRef"
:key="item._index"
class="infinite-list-item"
>
<!-- <slot ref="slot" :item="item.item"></slot>-->
<p>
<span style="color: red">{{ item.item.id }}</span
>{{ item.item.value }}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUpdated } from "vue";
interface ListItem {
_index: string;
item: any;
}
interface Positons {
index: number;
height: number;
top: number;
bottom: number;
}
interface Props {
//所有列表数据
listData: any[];
//预估列高度
estimatedItemSize?: number;
//缓冲区比例
bufferScale?: number;
//容器高度 100px or 50vh
height?: string;
}
const props = withDefaults(defineProps<Props>(), {
listData: () => [],
estimatedItemSize: 35,
bufferScale: 1,
height: "500px",
});
let listDataFc: any[] = [];
for (let id = 0; id < 1000; id++) {
listDataFc.push({
id,
value: `textfccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc---${id}`, // 长文本
});
}
const listRef = ref();
const phantomRef = ref();
const contentRef = ref();
const itemsRef = ref([]);
const positions = ref<Positons[]>([]);
//可视区域高度
const screenHeight = ref(0);
//起始索引
const start = ref(0);
//结束索引
const end = ref(0);
const _listData = computed<ListItem[]>(() =>
listDataFc.map((item: any, index: number) => ({
_index: `_${index}`,
item,
}))
);
// 可视数据数量
const visibleCount = computed<number>(() =>
Math.ceil(screenHeight.value / props.estimatedItemSize)
);
// 上方缓冲区数据的数量
const aboveCount = computed<number>(() =>
Math.min(start.value, props.bufferScale * visibleCount.value)
);
// 下方缓冲区数据的数量
const belowCount = computed<number>(() =>
Math.min(listDataFc.length - end.value, props.bufferScale * visibleCount.value)
);
const visibleData = computed<ListItem[]>(() => {
const startIndex = start.value - aboveCount.value;
const endIndex = end.value + belowCount.value;
return _listData.value.slice(startIndex, endIndex);
});
onMounted(() => {
screenHeight.value = listRef.value.clientHeight;
start.value = 0;
end.value = start.value + visibleCount.value;
});
onUpdated(() => {
if (!itemsRef.value.length) return;
// 获取真实元素大小,修改对应的尺寸缓存
updateItemsSize();
// 更新列表总高度
updatePhantomHeight();
// 更新真实偏移量
setStartOffset();
});
const initPositions = () => {
positions.value = listDataFc.map((d, index) => ({
index,
height: props.estimatedItemSize,
top: index * props.estimatedItemSize,
bottom: (index + 1) * props.estimatedItemSize,
}));
};
// 二分法查找
const binarySearch = (list: Positons[], value: number) => {
let start = 0;
let end = list.length - 1;
let tempIndex: number | null = null;
while (start <= end) {
const midIndex = Math.floor((start + end) / 2);
const midValue = list[midIndex].bottom;
if (midValue === value) {
return midIndex + 1;
} else if (midValue < value) {
start = midIndex + 1;
} else if (midValue > value) {
if (tempIndex === null || tempIndex > midIndex) {
tempIndex = midIndex;
}
end = end - 1;
}
}
return tempIndex as number;
};
// 获取列表起始索引
const getStartIndex = (scrollTop = 0) => {
return binarySearch(positions.value, scrollTop);
};
// 获取可视列表项的当前尺寸 修改对应的尺寸缓存
const updateItemsSize = () => {
itemsRef.value.forEach((node: HTMLElement) => {
const rect = node.getBoundingClientRect();
const height = rect.height;
const index = +node.id.slice(1);
const oldHeight = positions.value[index].height;
const dValue = oldHeight - height;
if (dValue) {
positions.value[index].bottom = positions.value[index].bottom - dValue;
positions.value[index].height = height;
for (let k = index + 1; k < positions.value.length; k++) {
positions.value[k].top = positions.value[k - 1].bottom;
positions.value[k].bottom = positions.value[k].bottom - dValue;
}
}
});
};
// 获取当前的偏移量
const setStartOffset = () => {
let startOffset;
if (start.value >= 1) {
const size =
positions.value[start.value].top -
(positions.value[start.value - aboveCount.value]
? positions.value[start.value - aboveCount.value].top
: 0);
startOffset = positions.value[start.value - 1].bottom - size;
} else {
startOffset = 0;
}
contentRef.value.style.transform = `translate3d(0,${startOffset}px,0)`;
};
// 更新列表总高度
const updatePhantomHeight = () => {
const height = positions.value[positions.value.length - 1].bottom;
phantomRef.value.style.height = `${height}px`;
};
// 滚动事件
const scrollEvent = () => {
const scrollTop = listRef.value.scrollTop;
// 此时的开始索引
start.value = getStartIndex(scrollTop);
// 此时的结束索引
end.value = start.value + visibleCount.value;
// 此时的偏移量
setStartOffset();
};
initPositions();
</script>
<style scoped>
.infinite-list-container {
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.infinite-list-item {
padding: 5px;
box-sizing: border-box;
border-bottom: 1px solid #999;
/* height:200px; */
}
</style>
图片压缩
npm i image-conversion --save
import * as imageConversion from "image-conversion";
const newBlob = await imageConversion.compressAccurately(uploadFile.raw, {
size: 200,
accuracy: 0.5,
});
// 将Blob类型转为File类型
newFile = new window.File([newBlob], uploadFile.name, { type: newBlob.type });
NC计算器
即取即用
function getData(arr, allData) {// arr:奶茶原价数组;allData:总付金额
let total = arr.reduce((x, y) => x + y);
let data = [];
arr.forEach(item => data.push((item / total) * allData));
return data
}
getData([17,20,14,19,22],62)