强类型语言和弱类型语言的区别
强类型语言:强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Java 和 C++等语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串。
弱类型语言:弱类型语言也称为弱类型定义语言,与强类型定义相反。JavaScript 语言就属于弱类型语言。简单理解就是一种变量类型可以被忽略的语言。比如 JavaScript 是弱类型定义的,在 JavaScript 中就可以将字符串'12'和整数 3 进行连接得到字符串'123',在相加的时候会进行强制类型转换。
两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
说一下 HTTP 和 HTTPS 协议的区别?
1、HTTPS协议需要CA证书,费用较高;而HTTP协议不需要
2、HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;
3、使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443;
4、HTTP协议连接很简单,是无状态的;HTTPS协议是具有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全
复制代码
说一下购物车的逻辑?
//vue中购物车逻辑的实现
1. 购物车信息用一个数组来存储,数组中保存对象,对象中有id和count属性
2. 在vuex中state中添加一个数据 cartList 用来保存这个数组
3. 由于商品详情页需要用到加入购物车功能,所以我们需要提供一个mutation, 用来将购物车信息加入 cartList中
4. 加入购物车信息的时候,遵照如下规则: 如果购物车中已经有了该商品信息,则数量累加,如果没有该商品信息,则新增一个对象
5. 在商品详情页,点击加入购物车按钮的时候,调用vuex提供的addToCart这个mutation将当前的商品信息 (id count)传给addTocart this.$store.commit("addToCart", {id: , count:})
// js中购物车逻辑的实现
1.商品页点击“加入购物车”按钮,触发事件
2.事件调用购物车“增加商品”的Js程序(函数、对象方法)
3.向Js程序传递传递“商品id”、“商品数量”等数据
4.存储“商品id”、“商品数量”到浏览器的localStorage中
**展示购物车中的商品******
1.打开购物车页面
2.从localStorage中取出“商品Id”、“商品数量”等信息。
3.调用服务器端“获得商品详情”的接口得到购物车中的商品信息(参数为商品Id)
4.将获得的商品信息显示在购物车页面。
**完成购物车中商品的购买******
1.用户对购物车中的商品完成购买流程,产生购物订单
2.清除localStorage中存储的已经购买的商品信息
备注1:购物车中商品存储的数据除了“商品id”、“商品数量”之外,根据产品要求还可以有其他的信息,例如完整的商品详情(这样就不用掉服务器接口获得详情了)、购物车商品的过期时间,超过时间的购物车商品在下次打开网站或者购物车页面时被清除。
备注2:购物车商品除了存储在localStorage中,根据产品的需求不同,也可以存储在sessionStorage、cookie、session中,或者直接向服务器接口发起请求存储在服务器上。何种情况使用哪种方式存储、有啥区别请自己分析。
复制代码
实现数组原型方法
forEach
语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
参数:
callback
:为数组中每个元素执行的函数,该函数接受 1-3 个参数currentValue
: 数组中正在处理的当前元素index
(可选): 数组中正在处理的当前元素的索引array
(可选): forEach()
方法正在操作的数组 thisArg
(可选): 当执行回调函数 callback
时,用作 this
的值。
返回值:undefined
Array.prototype.forEach1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
// 创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数 this(当前数组)。
const O = Object(this);
// O.length >>> 0 无符号右移 0 位
// 意义:为了保证转换后的值为正整数。
// 其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型
const len = O.length >>> 0;
let k = 0;
while(k < len) {
if(k in O) {
callback.call(thisArg, O[k], k, O);
}
k++;
}
}
复制代码
map
语法: arr.map(callback(currentValue [, index [, array]])[, thisArg])
参数:与 forEach()
方法一样
返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。
Array.prototype.map1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let newArr = []; // 返回的新数组
let k = 0;
while(k < len) {
if(k in O) {
newArr[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}
return newArr;
}
复制代码
filter
语法:arr.filter(callback(element [, index [, array]])[, thisArg])
参数:
callback
: 用来测试数组的每个元素的函数。返回 true
表示该元素通过测试,保留该元素,false
则不保留。它接受以下三个参数:element、index、array
,参数的意义与 forEach
一样。
thisArg
(可选): 执行 callback
时,用于 this
的值。
返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。
Array.prototype.filter1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let newArr = []; // 返回的新数组
let k = 0;
while(k < len) {
if(k in O) {
if(callback.call(thisArg, O[k], k, O)) {
newArr.push(O[k]);
}
}
k++;
}
return newArr;
}
复制代码
some
语法:arr.some(callback(element [, index [, array]])[, thisArg])
参数:
callback
: 用来测试数组的每个元素的函数。接受以下三个参数:element、index、array,参数的意义与 forEach 一样。
thisArg
(可选): 执行 callback
时,用于 this
的值。返回值:数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false。
Array.prototype.some1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let k = 0;
while(k < len) {
if(k in O) {
if(callback.call(thisArg, O[k], k, O)) {
return true
}
}
k++;
}
return false;
}
复制代码
reduce
语法:arr.reduce(callback(preVal, curVal[, curIndex [, array]])[, initialValue])
参数:
callback
: 一个 “reducer” 函数,包含四个参数:
preVal
:上一次调用 callback
时的返回值。在第一次调用时,若指定了初始值 initialValue
,其值则为 initialValue
,否则为数组索引为 0 的元素 array[0]
。
curVal
:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue
,其值则为数组索引为 0 的元素 array[0]
,否则为 array[1]
。
curIndex
(可选):数组中正在处理的元素的索引。若指定了初始值 initialValue
,则起始索引号为 0,否则从索引 1 起始。
array
(可选):用于遍历的数组。initialValue(可选): 作为第一次调用 callback
函数时参数 preVal
的值。若指定了初始值 initialValue
,则 curVal
则将使用数组第一个元素;否则 preVal
将使用数组第一个元素,而 curVal
将使用数组第二个元素。返回值:使用 “reducer” 回调函数遍历整个数组后的结果。
Array.prototype.reduce1 = function(callback, initialValue) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let k = 0;
let accumulator = initialValue;
// 如果第二个参数为undefined的情况下,则数组的第一个有效值(非empty)作为累加器的初始值
if(accumulator === undefined) {
while(k < len && !(k in O)) {
k++;
}
// 如果超出数组界限还没有找到累加器的初始值,则TypeError
if(k >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = O[k++];
}
while(k < len) {
if(k in O) {
accumulator = callback(accumulator, O[k], k, O);
}
k++;
}
return accumulator;
}
复制代码
参考:前端进阶面试题详细解答
Object.is()
描述:Object.is
不会转换被比较的两个值的类型,这点和===
更为相似,他们之间也存在一些区别。
NaN
在 ===
中是不相等的,而在 Object.is
中是相等的
+0
和-0
在 ===
中是相等的,而在 Object.is
中是不相等的
实现:利用 ===
Object.is = function(x, y) {
if(x === y) {
// 当前情况下,只有一种情况是特殊的,即 +0 -0
// 如果 x !== 0,则返回true
// 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
return x !== 0 || 1 / x === 1 / y;
}
// x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
// x和y同时为NaN时,返回true
return x !== x && y !== y;
}
复制代码
代码输出问题
window.number = 2;
var obj = {
number: 3,
db1: (function(){
console.log(this);
this.number *= 4;
return function(){
console.log(this);
this.number *= 5;
}
})()
}
var db1 = obj.db1;
db1();
obj.db1();
console.log(obj.number); // 15
console.log(window.number); // 40
复制代码
这道题目看清起来有点乱,但是实际上是考察 this 指向的:
执行 db1()时,this 指向全局作用域,所以 window.number * 4 = 8,然后执行匿名函数, 所以 window.number * 5 = 40;
执行 obj.db1();时,this 指向 obj 对象,执行匿名函数,所以 obj.numer * 5 = 15。
单例模式
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
实现:
var Singleton = (function() {
// 如果在内部声明 SingletonClass 对象,则无法在外部直接调用
var SingletonClass = function() { };
var instance;
return function() {
// 如果已存在,则返回 instance
if(instance) return instance;
// 如果不存在,则new 一个 SingletonClass 对象
instance = new SingletonClass();
return instance;
}
})();
// 测试
var a = new Singleton();
var b = new Singleton();
console.log(a === b); // true
复制代码
Promise 以及相关方法的实现
题目描述:手写 Promise 以及 Promise.all Promise.race 的实现
实现代码如下:
class Mypromise {
constructor(fn) {
// 表示状态
this.state = "pending";
// 表示then注册的成功函数
this.successFun = [];
// 表示then注册的失败函数
this.failFun = [];
let resolve = (val) => {
// 保持状态改变不可变(resolve和reject只准触发一种)
if (this.state !== "pending") return;
// 成功触发时机 改变状态 同时执行在then注册的回调事件
this.state = "success";
// 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里为模拟异步
setTimeout(() => {
// 执行当前事件里面所有的注册函数
this.successFun.forEach((item) => item.call(this, val));
});
};
let reject = (err) => {
if (this.state !== "pending") return;
// 失败触发时机 改变状态 同时执行在then注册的回调事件
this.state = "fail";
// 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里模拟异步
setTimeout(() => {
this.failFun.forEach((item) => item.call(this, err));
});
};
// 调用函数
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
// 实例方法 then
then(resolveCallback, rejectCallback) {
// 判断回调是否是函数
resolveCallback =
typeof resolveCallback !== "function" ? (v) => v : resolveCallback;
rejectCallback =
typeof rejectCallback !== "function"
? (err) => {
throw err;
}
: rejectCallback;
// 为了保持链式调用 继续返回promise
return new Mypromise((resolve, reject) => {
// 将回调注册到successFun事件集合里面去
this.successFun.push((val) => {
try {
// 执行回调函数
let x = resolveCallback(val);
//(最难的一点)
// 如果回调函数结果是普通值 那么就resolve出去给下一个then链式调用 如果是一个promise对象(代表又是一个异步) 那么调用x的then方法 将resolve和reject传进去 等到x内部的异步 执行完毕的时候(状态完成)就会自动执行传入的resolve 这样就控制了链式调用的顺序
x instanceof Mypromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
});
this.failFun.push((val) => {
try {
// 执行回调函数
let x = rejectCallback(val);
x instanceof Mypromise ? x.then(resolve, reject) : reject(x);
} catch (error) {
reject(error);
}
});
});
}
//静态方法
static all(promiseArr) {
let result = [];
//声明一个计数器 每一个promise返回就加一
let count = 0;
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
//这里用 Promise.resolve包装一下 防止不是Promise类型传进来
Promise.resolve(promiseArr[i]).then(
(res) => {
//这里不能直接push数组 因为要控制顺序一一对应(感谢评论区指正)
result[i] = res;
count++;
//只有全部的promise执行成功之后才resolve出去
if (count === promiseArr.length) {
resolve(result);
}
},
(err) => {
reject(err);
}
);
}
});
}
//静态方法
static race(promiseArr) {
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
Promise.resolve(promiseArr[i]).then(
(res) => {
//promise数组只要有任何一个promise 状态变更 就可以返回
resolve(res);
},
(err) => {
reject(err);
}
);
}
});
}
}
// 使用
// let promise1 = new Mypromise((resolve, reject) => {
// setTimeout(() => {
// resolve(123);
// }, 2000);
// });
// let promise2 = new Mypromise((resolve, reject) => {
// setTimeout(() => {
// resolve(1234);
// }, 1000);
// });
// Mypromise.all([promise1,promise2]).then(res=>{
// console.log(res);
// })
// Mypromise.race([promise1, promise2]).then(res => {
// console.log(res);
// });
// promise1
// .then(
// res => {
// console.log(res); //过两秒输出123
// return new Mypromise((resolve, reject) => {
// setTimeout(() => {
// resolve("success");
// }, 1000);
// });
// },
// err => {
// console.log(err);
// }
// )
// .then(
// res => {
// console.log(res); //再过一秒输出success
// },
// err => {
// console.log(err);
// }
// );
复制代码
扩展思考:如何取消 promise
Promise.race()方法可以用来竞争 Promise 可以借助这个特性 自己包装一个 空的 Promise 与要发起的 Promise 来实现
function wrap(pro) {
let obj = {};
// 构造一个新的promise用来竞争
let p1 = new Promise((resolve, reject) => {
obj.resolve = resolve;
obj.reject = reject;
});
obj.promise = Promise.race([p1, pro]);
return obj;
}
let testPro = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
let wrapPro = wrap(testPro);
wrapPro.promise.then((res) => {
console.log(res);
});
wrapPro.resolve("被拦截了");
复制代码
代码输出结果
setTimeout(function () {
console.log(1);
}, 100);
new Promise(function (resolve) {
console.log(2);
resolve();
console.log(3);
}).then(function () {
console.log(4);
new Promise((resove, reject) => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 10);
})
});
console.log(7);
console.log(8);
复制代码
输出结果为:
代码执行过程如下:
首先遇到定时器,将其加入到宏任务队列;
遇到 Promise,首先执行里面的同步代码,打印出 2,遇到 resolve,将其加入到微任务队列,执行后面同步代码,打印出 3;
继续执行 script 中的代码,打印出 7 和 8,至此第一轮代码执行完成;
执行微任务队列中的代码,首先打印出 4,如遇到 Promise,执行其中的同步代码,打印出 5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器;
执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为 100ms,第二个定时器的时间为 10ms,所以先执行第二个定时器,打印出 6;
此时微任务队列为空,继续执行宏任务队列,打印出 1。
做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为 0 哦。
说一下前端登录的流程?
初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个 token,和一个用户信息的值,前端拿到 token,将 token 储存到 Vuex 中,然后从 Vuex 中把 token 的值存入浏览器 Cookies 中。把用户信息存到 Vuex 然后再存储到 LocalStroage 中,然后跳转到下一个页面,根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断 Cookies 中是否有 token,没有就跳转到登录页,有就跳转到相应的页面,我们应该再每次发送 post/get 请求的时候应该加入 token,常用方法再项目 utils/service.js 中添加全局拦截器,将 token 的值放入请求头中 后端判断请求头中有无 token,有 token,就拿到 token 并验证 token 是否过期,在这里过期会返回无效的 token 然后有个跳回登录页面重新登录并且清除本地用户的信息
浏览器资源缓存的位置有哪些?
资源缓存的位置一共有 3 种,按优先级从高到低分别是:
Service Worker:Service Worker 运行在 JavaScript 主线程之外,虽然由于脱离了浏览器窗体无法直接访问 DOM,但是它可以完成离线缓存、消息推送、网络代理等功能。它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。当 Service Worker 没有命中缓存的时候,需要去调用 fetch
函数获取 数据。也就是说,如果没有在 Service Worker 命中缓存,会根据缓存查找优先级去查找数据。但是不管是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示是从 Service Worker 中获取的内容。
Memory Cache: Memory Cache 就是内存缓存,它的效率最快,但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
Disk Cache: Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
Disk Cache: Push Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。其具有以下特点:
所有的资源都能被推送,但是 Edge 和 Safari 浏览器兼容性不怎么好
可以推送 no-cache
和 no-store
的资源
一旦连接被关闭,Push Cache 就被释放
多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
Push Cache 中的缓存只能被使用一次
浏览器可以拒绝接受已经存在的资源推送
可以给其他域名推送资源
什么是执行栈
可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。 当开始执行 JS 代码时,根据先进后出的原则,后执行的函数会先弹出栈,可以看到,foo
函数后执行,当执行完毕后就从栈中弹出了。
平时在开发中,可以在报错中找到执行栈的痕迹:
function foo() {
throw new Error('error')
}
function bar() {
foo()
}
bar()
复制代码
可以看到报错在 foo
函数,foo
函数又是在 bar
函数中调用的。当使用递归时,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题
function bar() { bar()}bar()
复制代码
代码输出结果
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
复制代码
输出结果如下:
1
finally2
finally
finally2后面的then函数 2
复制代码
.finally()
一般用的很少,只要记住以下几点就可以了:
.finally()
方法不管 Promise 对象最后的状态如何都会执行
.finally()
方法的回调函数不接受任何的参数,也就是说你在.finally()
函数中是无法知道 Promise 最终的状态是resolved
还是rejected
的
它最终返回的默认会是一个上一次的 Promise 对象值,不过如果抛出的是一个异常则返回异常的 Promise 对象。
finally 本质上是 then 方法的特例
.finally()
的错误捕获:
Promise.resolve('1')
.finally(() => {
console.log('finally1')
throw new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
复制代码
输出结果为:
'finally1'
'捕获错误' Error: 我是finally中抛出的异常
复制代码
代码输出结果
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')
复制代码
输出结果如下:
async1 start
async2
start
async1 end
复制代码
代码的执行过程如下:
首先执行函数中的同步代码async1 start
,之后遇到了await
,它会阻塞async1
后面代码的执行,因此会先去执行async2
中的同步代码async2
,然后跳出async1
;
跳出async1
函数后,执行同步代码start
;
在一轮宏任务全部执行完之后,再来执行await
后面的内容async1 end
。
这里可以理解为 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中。
Promise.reject
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason));
}
复制代码
你在工作终于到那些问题,解决方法是什么
经常遇到的问题就是Cannot read property ‘prototype’ of undefined
解决办法通过浏览器报错提示代码定位问题,解决问题
Vue项目中遇到视图不更新,方法不执行,埋点不触发等问题
一般解决方案查看浏览器报错,查看代码运行到那个阶段未之行结束,阅读源码以及相关文档等
然后举出来最近开发的项目中遇到的算是两个比较大的问题。
复制代码
Promise.allSettled
描述:等到所有promise
都返回结果,就返回一个promise
实例。
实现:
Promise.allSettled = function(promises) {
return new Promise((resolve, reject) => {
if(Array.isArray(promises)) {
if(promises.length === 0) return resolve(promises);
let result = [];
let count = 0;
promises.forEach((item, index) => {
Promise.resolve(item).then(
value => {
count++;
result[index] = {
status: 'fulfilled',
value: value
};
if(count === promises.length) resolve(result);
},
reason => {
count++;
result[index] = {
status: 'rejected'.
reason: reason
};
if(count === promises.length) resolve(result);
}
);
});
}
else return reject(new TypeError("Argument is not iterable"));
});
}
复制代码
代码输出结果
(function(){
var x = y = 1;
})();
var z;
console.log(y); // 1
console.log(z); // undefined
console.log(x); // Uncaught ReferenceError: x is not defined
复制代码
这段代码的关键在于:var x = y = 1; 实际上这里是从右往左执行的,首先执行 y = 1, 因为 y 没有使用 var 声明,所以它是一个全局变量,然后第二步是将 y 赋值给 x,讲一个全局变量赋值给了一个局部变量,最终,x 是一个局部变量,y 是一个全局变量,所以打印 x 是报错。
详细说明 Event loop
众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。
JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('script end');
复制代码
以上代码虽然 setTimeout
延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout
还是会在 script end
之后打印。
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs
,macrotask 称为 task
。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
复制代码
以上代码虽然 setTimeout
写在 Promise
之前,但是因为 Promise
属于微任务而 setTimeout
属于宏任务,所以会有以上的打印。
微任务包括 process.nextTick
,promise
,Object.observe
,MutationObserver
宏任务包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script
,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。
所以正确的一次 Event loop 顺序是这样的
执行同步代码,这属于宏任务
执行栈为空,查询是否有微任务需要执行
执行所有微任务
必要的话渲染 UI
然后开始下一轮 Event loop,执行宏任务中的异步代码
通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。
Node 中的 Event loop
Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为 6 个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
复制代码
timer
timers 阶段会执行 setTimeout
和 setInterval
一个 timer
指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。
下限的时间有一个范围:[1, 2147483647]
,如果设定的时间不在这个范围,将被设置为 1。
I/O
I/O 阶段会执行除了 close 事件,定时器和 setImmediate
的回调
idle, prepare
idle, prepare 阶段内部实现
poll
poll 阶段很重要,这一阶段中,系统会做两件事情
执行到点的定时器
执行 poll 队列中的事件
并且当 poll 中没有定时器的情况下,会发现以下两件事情
如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
如果 poll 队列为空,会有两件事发生
如果有 setImmediate
需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
如果没有 setImmediate
需要执行,会等待回调被加入到队列中并立即执行回调
如果有别的定时器需要被执行,会回到 timer 阶段执行回调。
check
check 阶段执行 setImmediate
close callbacks
close callbacks 阶段执行 close 事件
并且在 Node 中,有些情况下的定时器执行顺序是随机的
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout
复制代码
当然在这种情况下,执行顺序是相同的
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输出一定是 setImmediate,setTimeout
复制代码
上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行。
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中打印 timer1, promise1, timer2, promise2
// node 中打印 timer1, timer2, promise1, promise2
复制代码
Node 中的 process.nextTick
会先于其他 microtask 执行。
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1
复制代码
对 HTML 语义化的理解
语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化)。通俗来讲就是用正确的标签做正确的事情。
语义化的优点如下:
常见的语义化标签:
<header></header> 头部
<nav></nav> 导航栏
<section></section> 区块(有语义化的div)
<main></main> 主要区域
<article></article> 主要内容
<aside></aside> 侧边栏
<footer></footer> 底部
复制代码
评论