一、垃圾回收机制和内存泄漏
不想C语言那样,拥有原始底层的内存操作方法如 malloc free。js使用的是自动垃圾回收机制,也就是说js引擎会自动去判别变量的使用情况来自动回收那些不使用的内存块。
即使是使用高级语言,开发者对内存管理也应该有所了解(至少要有基础的了解)。有时,开发者必须理解自动内存管理会遇到问题(例如:垃圾回收中的错误或者性能问题等),以便能够正确处理它们。(或者是找到适当的解决方法,用最小的代价去解决。)
如果一个值不再需要了,但是垃圾回收机制确无法回收,这时候就是内存泄漏了。
const arr = [1, 2, 3, 4];
console.log('hello world');
上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。
如果增加一行代码,解除arr对[1, 2, 3, 4]引用,这块内存就可以被垃圾回收机制释放了。
const arr = [1, 2, 3, 4];
console.log('hello world');
arr = null;
以上例子是在全局下的,arr为全局变量,它属于全局变量对象,全局变量对象只有在浏览器窗口关闭的时候才会被销毁,因此我们才会不推荐使用过多的全局变量。
因此,并不是说有了垃圾回收机制,程序员就轻松了。你还是需要关注内存占用:那些很占空间的值,一旦不再用到,你必须检查是否还存在对它们的引用。如果是的话,就必须手动解除引用。
1、内存的生命周期
2、垃圾回收机制
(1)标记清除
该算法由以下步骤组成:
- 垃圾回收器构建“roots”列表。Roots 通常是代码中保留引用的全局变量。在 JavaScript 中,“window” 对象可以作为 root 全局变量示例。
- 所有的 roots 被检查并标记为 active(即不是垃圾)。所有的 children 也被递归检查。从 root 能够到达的一切都不被认为是垃圾。
- 所有未被标记为 active 的内存可以被认为是垃圾了。收集器限制可以释放这些内存并将其返回到操作系统
如果是该算法,循环引用就不会出现。在函数调用后,两个对象不再被从全局对象可访问的东西所引用。因此,垃圾回收器将发现它们是不可达的。
(2)引用计数
如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 references o2
o2.p = o1; // o2 references o1. This creates a cycle.
}
f();
在函数调用之后,它们离开了作用域,因此它们实际上已经无用了,可以被释放了。然而,引用计数算法认为,由于两个对象中的每一个至少被引用了一次,所以也不能被垃圾回收。
3、什么是内存泄漏
实质上,内存泄漏可以被定义为应用程序不再需要的内存,但由于某种原因,内存不会返回到操作系统或可用内存池中。
4、内存泄漏的例子
(1)意外的全局变量
function foo(arg) {
bar = "this is a hidden global variable";
//等同于window.bar="this is a hidden global variable"
this.bar2= "potential accidental global";
//这里的this 指向了全局对象(window),等同于window.bar2="potential accidental global"
}
如果是在函数中未使用var声明的变量,那么会将其放到全局window上,会产生一个意外的全局变量。全局变量会一直驻留内存,一次我们要坚决避免这种意外发生。
解决办法就是使用'use strict'开启严格模式。
(2)循环引用
let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1
let obj2 = obj1; // A 的引用个数变为 2
obj1 = null; // A 的引用个数变为 1
obj2 = null; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了
但是引用计数有个最大的问题: 循环引用。
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
函数执行完毕之后,按道理是可以被销毁的。内部的变量也会被销毁。但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
obj1 = null;
obj2 = null;
(3)被遗忘的计时器和回调函数
let someResource = getData();
setInterval(() => {
const node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
每隔一秒执行一次匿名回调函数,该函数由于会被长期调用,因此其内部的变量都不会被回收,引用外部的someResource也不会被回收。那什么才叫结束呢?就是调用了 clearInterval。
比如开发SPA页面,当我们的某一个页面中存在这类定时器,跳转到另一个页面的时候,其实这里的定时器已经暂时没用了,但是我们在另一个页面的时候,内存中还是回你保留上一个页面的定时器资源,因此这就会导致内存泄漏。解决办法就是即使的使用clearInterval来清除定时器。
(4)闭包
JavaScript 开发的一个关键方面就是闭包:一个可以访问外部(封闭)函数变量的内部函数。
值得注意的是闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏。闭包会造成对象引用的生命周期脱离当前函数的上下文,如果闭包如果使用不当,可以导致环形引用(circular reference),类似于死锁,只能避免,无法发生之后解决,即使有垃圾回收也还是会内存泄露。
(5)console
console.log:向web开发控制台打印一条消息,常用来在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉console.log语句,这可能造成内存泄露。
在传递给console.log的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。
(6)DOM泄漏
在Js中对DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。
假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。
为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。
var refA = document.getElementById('refA');
document.body.removeChild(refA);
// #refA不能回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。
// 使用refA = null; 来释放内存
var MyObject = {};
document.getElementById('myDiv').myProp = MyObject;
解决方法:
在window.onunload事件中写上: document.getElementById('myDiv').myProp = null;
给DOM对象用attachEvent绑定事件:
function doClick() {}
element.attachEvent("onclick", doClick);
解决方法:
在onunload事件中写上: element.detachEvent('onclick', doClick);
从外到内执行appendChild。这时即使调用removeChild也无法释放。范例:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
document.body.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
解决方法:
从内到外执行appendChild:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
parentDiv.appendChild(childDiv);
document.body.appendChild(parentDiv);
反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。范例:
for(i = 0; i < 5000; i++) {
hostElement.text = "asdfasdfasdf";
}
这种方式相当于定义了5000个属性!
解决方法:
其实没什么解决方法:P~~~就是编程的时候尽量避免出现这种情况咯~~
5、WeakMap 你了解吗?
前面说过,及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。
最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就可以了。
ES6 考虑到了这一点,推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,是一种弱引用,所以名字里面才会有一个"Weak",表示这是弱引用。
const wm = new WeakMap();
const element = document.getElementById('example'); // 引用计数1
wm.set(element, 'some information'); // 此处是弱引用,不计数
wm.get(element) // "some information"
WeakMap里面对element的引用就是弱引用,不会被计入垃圾回收机制。
也就是说,DOM节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap保存的这个键值对,也会自动消失。
总结
虽然当下的浏览器已经对垃圾回收机制做出了一定的改进和提升,但是内存泄漏的问题我们还是需要关注的。
二、AJAX相关面试问题
1、什么是Ajax
Ajax是全称是asynchronous JavaScript andXML,简答来记就是异步的js和XML。它是一种异步加载数据的机制,可以使得我们在不刷新整个页面的情况下去请求数据内容,实现局部刷新。
优点就是:实现异步通信,速度快,页面局部刷新,用户体验好。AJAX出现之前一直是服务端渲染的天下,服务器去处理页面的数据填充,然后响应页面给我们。
当需要修改页面的时候,需要表单进行提交,然后服务器接收到请求后去查询和处理数据,重新填充到页面上,返回新的html页面给我们,这种交互的的缺陷是显而易见的,任何和服务器的交互都需要刷新页面,用户体验非常差,Ajax的出现解决了这个问题。
ajax可以实现,我们发送请求,获取相应的数据,然后通过js去动态渲染页面,而不需要服务器拼接HTML,页面的刷新也只是局部的刷新,不再是整个页面的刷新了。
2、原生ajax怎么写?
// [1]
var xhr = new XMLHttpRequest();
// [2]
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && (xhr.status > 200 && xhr.status < 300 || xhr.status == 304)) {
console.log(xhr.responseText);
}
}
// [3]
xhr.open('POST', 'http://', true);
// [4]
xhr.setRequestHeader("Content-type",
"application/x-www-form-urlencoded");
// [5]
xhr.send("name=zjj&pwd=123456");
// [6]
xhr.onerror = function() {
console.log('err');
}
IE中通过new ActiveXObject()得到,其他主流浏览器中通过newXMLHttpRequest()得到.使用jquery封装好的ajax,会避免这些问题.
3、XMLHttpRequest对象
XMLHttpRequest是ajax的核心。我们的ajax请求就是通过该对象来完成的,他有一些属性和方法。
var xhr = new XMLHttpRequest();
比较重要的两个方法open、send。
xhr.open(method, url, async)
// open 方法用于初始化一个请求,提供请求方式 、请求url、以及是否执行异步。
xhr.send(data)
// send方法用于发起请求,我们可以将需要传递的数据作为参数传入。
// 当请求方式为 post 时,可以将请求体的参数传入
// 当请求方式为 get 时,可以不传或传入 null
// 不管是 get 还是 post,参数都需要通过 encodeURIComponent 编码后拼接
- responseXML 接收响应的字符串类型数据
- responseText 接收"text/xml""application/xml"格式的响应
- status 响应的HTTP状态码
- timeout 超时时间
- readyState 请求和响应的当前阶段
如何确定我们的请求到了哪一阶段了呢,我们需要借助 readyState来识别。
xhr.readyStatus==0 尚未调用 open 方法
xhr.readyStatus==1 已调用 open 但还未发送请求(未调用 send)
xhr.readyStatus==2 已发送请求(已调用 send)
xhr.readyStatus==3 已接收到请求返回的数据
xhr.readyStatus==4 请求已完成
当readyStatus的状态发生改变时,会触发 xhr 的事件onreadystatechange,于是我们就可以在这个方法中,对接收到的数据进行处理.
xhr.onreadystatechange = () => {
if (xhr.readyStatus === 4) {
// HTTP 状态在 200-300 之间表示请求成功
// HTTP 状态为 304 表示请求内容未发生改变,可直接从缓存中读取
if (xhr.status >= 200 &&
xhr.status < 300 ||
xhr.status == 304) {
console.log('请求成功', xhr.responseText)
}
}
}
当网络不佳时,我们需要给请求设置一个超时时间
// 超时时间单位为毫秒
xhr.timeout = 1000
// 当请求超时时,会触发 ontimeout 方法
xhr.ontimeout = () => console.log('请求超时')
- onprogress
xhr.onprogress = function(event){
console.log(event.loaded / event.total);
}
回调函数可以获取资源总大小total,已经加载的资源大小loaded,用这两个值可以计算加载进度
- 该 ajax 方法通过 Promise 方式实现回调
function ajax (options) {
let url = options.url
const method = options.method.toLocaleLowerCase() || 'get'
const async = options.async != false // default is true
const data = options.data
const xhr = new XMLHttpRequest()
if (options.timeout && options.timeout > 0) {
xhr.timeout = options.timeout
}
return new Promise ( (resolve, reject) => {
xhr.ontimeout = () => reject && reject('请求超时')
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
resolve && resolve(xhr.responseText)
} else {
reject && reject()
}
}
}
xhr.onerror = err => reject && reject(err)
let paramArr = []
let encodeData
if (data instanceof Object) {
for (let key in data) {
// 参数拼接需要通过 encodeURIComponent 进行编码
paramArr.push( encodeURIComponent(key) + '=' + encodeURIComponent(data[key]) )
}
encodeData = paramArr.join('&')
}
if (method === 'get') {
// 检测 url 中是否已存在 ? 及其位置
const index = url.indexOf('?')
if (index === -1) url += '?'
else if (index !== url.length -1) url += '&'
// 拼接 url
url += encodeData
}
xhr.open(method, url, async)
if (method === 'get') xhr.send(null)
else {
// post 方式需要设置请求头
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8')
xhr.send(encodeData)
}
} )
}
ajax({
url: 'your request url',
method: 'get',
async: true,
timeout: 1000,
data: {
test: 1,
aaa: 2
}
}).then(
res => console.log('请求成功: ' + res),
err => console.log('请求失败: ' + err)
)
4、jquery中的ajax
$.ajax({
url:发送请求的地址,
data:数据的拼接,//发送到服务器的数据
type:'get',//请求方式,默认get请求
contentType: 'application/json', // 设置参数类型
headers: {'Content-Type','application/json'},// 设置请求头
dataType:'json',//服务器返回的数据类型
async:true,//是否异步,默认true
cache:false,//设置为 false 将不会从浏览器缓存中加载请求信息
success:function(){},//请求成功后的回调函数
error: function(){}//请求失败时调用此函数
})
5、说以下异步与同步的区别
同步会阻塞,异步不会阻塞
同步:程序运行从上而下,浏览器必须把这个任务执行完毕,才能继续执行下一个任务
异步:程序运行从上而下,浏览器任务没有执行完,但是可以继续执行下一行代码
6、AJAX的底层实现?
面试官曾考过我一次这样的问题,当时一脸懵,没有猜到面试管想问的底层原理是什么?结果没答出来~~回来后网上找了好多文章,说的都是XMLHttpRequest。哦我的天~~
后来自己斟酌觉得其实面试管想考的并不是太底层的东西,不会问你C++源码的实现原理。只是想考你几个关键词:异步、线程、回调。
AJAX告诉浏览器,我要准备发送一个HTTP请求了,你帮我重开一个线程(网络线程),这时候我们的请求就前往了网络线程去执行,主线程继续执行我们的代码(这就是异步和线程)。同时回设置一个事件监听,去监听我们请求的状态,如果请求完毕,就回去执行我们回调队列中的回调函数,将其调入主线程去执行 (回调)。
其实完整的一次AJAX过程就是一次HTTP请求过程。
三、浏览器事件流
针对事件,面试官可能问:
1、了解事件流的顺序,对日常的工作有什么帮助么?
2、在 vue 的文档中,有一个修饰符 native ,把它用 . 的形式 连结在事件之后,就可以监听原生事件了。它的背后有什么原理?
3、事件的 event 对象中,有好多的属性和方法,该如何使用?
1、事件流的概念
事件流分为三个阶段:捕获阶段、目标阶段、冒泡阶段。 先调用捕获阶段的处理函数,其次调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。
最初网景公司提出了捕获事件,微软公司提出了冒泡事件。
低版本IE(IE8及以下版本)不支持捕获阶段
捕获事件流:Netscape提出的事件流,即事件由页面元素接收,逐级向下,传播到最具体的元素。(顶层元素先收到事件,然后往下传递,直到目标元素)
冒泡事件流:IE提出的事件流,即事件由最具体的元素接收,逐级向上,传播到页面。(目标元素先收到事件,然后往上,直到最顶层)
w3c 为了制定统一的标准,采取了折中的方式:先捕获在冒泡。
W3C
同一个 DOM 元素可以注册多个同类型的事件,通过 addEventListener和 removeEventListener进行管理。addEventListener 的第三个参数,就是为了捕获和冒泡准备的。
- 注册事件
target.addEventListener(type, listener[, useCapture]);
// 第三个事件来区分,true为事件捕获,false为事件冒泡
- 移除事件
target.removeEventListener(type, listener[, useCapture]);
const btn = document.getElementById("test");
//将回调存储在变量中
const fn = function(e){
alert("ok");
};
//绑定
btn.addEventListener("click", fn, false);
//解除
btn.removeEventListener("click", fn, false);
兼容IE
- 注册事件
target.attacEvent(type,listener);
btn.attachEvent('onclick',function(){
//do something...
})
- 移除事件
detachEvent(event,function);
目前支持以addEventListener绑定事件的浏览器:
FF、Chrome、Safari、Opera、IE9-11
目前支持以attachEvent绑定事件的浏览器:IE6-10
通过
stopPropagation()或cancelBubble来阻止事件进一步传播。cancelBubble是IE标准下阻止事件传递的属性,设置cancelBubble=true表示阻止冒泡
一般来说,我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件冒泡,但是还能阻止该事件目标执行别的注册事件。
node.addEventListener(
'click',
event => {
event.stopPropagation();
console.log('只在目标阶段触发,不冒泡');
},
false
)
node.addEventListener(
'click',
event => {
event.stopImmediatePropagation()
console.log('冒泡')
},
false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
'click',
event => {
console.log('捕获 ')
},
true
)
本来当一个DOM绑定了两个事件,一个冒泡、一个捕获,那么会按照哦顺序执行,但是使用了event.stopImmediatePropagation()之后,就只执行一个。
2、事件代理
我们经常会遇到,要监听列表中多项 li 的情况,假设我们有一个列表如下:
<ul id="list">
<li id="item1">item1</li>
<li id="item2">item2</li>
<li id="item3">item3</li>
<li id="item4">item4</li>
</ul>
如果我们要实现以下功能:当鼠标点击某一 li 时,输出该 li 的内容,我们通常的写法是这样的:
window.onload=function(){
const ulNode = document.getElementById("list");
const liNodes = ulNode.children;
for(var i=0; i<liNodes.length; i++){
liNodes[i].addEventListener('click',function(e){
console.log(e.target.innerHTML);
}, false);
}
}
在传统的事件处理中,我们可能会按照需要,为每一个元素添加或者删除事件处理器。然而,事件处理器将有可能导致内存泄露,或者性能下降,用得越多这种风险就越大。JavaScript 的事件代理,则是一种简单的技巧。
事件代理: 通过监听子元素从哪里冒泡上来,实现事件的代理。
window.onload=function(){
const ulNode=document.getElementById("list");
ulNode.addEventListener('click', function(e) {
/*判断目标事件是否为li*/
if(e.target && e.target.nodeName.toUpperCase()=="LI"){
console.log(e.target.innerHTML);
}
}, false);
};
3、vue 中的 native 修饰符
4、 react 中的合成事件
5、事件对象 event
event.target:指的是触发事件的那个节点,也就是事件最初发生的节点。
event.target.matches:可以对关键节点进行匹配,来执行相应操作。
event.currentTarget:指的是正在执行的监听函数的那个节点。
event.isTrusted:表示事件是否是真实用户触发。
event.preventDefault():取消事件的默认行为。
event.stopPropagation():阻止事件的派发(包括了捕获和冒泡)。
event.stopImmediatePropagation():阻止同一个事件的其他监听函数被调用。
6、测试题
题目二
<div class="test1">
<div class="test2"></div>
</div>
<script>
document.querySelector('.test1').addEventListener('click',function () {
console.log(1)
})
document.querySelector('.test2').addEventListener('click',function () {
console.log(2)
})
</script>
点击test1,只打印1。如果点击test2,打印2,1;
题目三
<div class="test1">
<div class="test2"></div>
</div>
<script>
document.querySelector('.test1').addEventListener('click', function () {
console.log(1)
}, true)
document.querySelector('.test2').addEventListener('click', function () {
console.log(2)
}, true)
</script>
点击test1,只打印1。如果点击test2,打印1,2;
题目四
<div class="test1">
<div class="test2"></div>
</div>
<script>
document.querySelector('.test1').addEventListener('click', function () {
console.log(1)
}, false)
document.querySelector('.test2').addEventListener('click', function () {
console.log(2)
}, true)
</script>
点击test1,只打印1。如果点击test2,打印2,1;
题目五
<div class="test1">
<div class="test2"></div>
</div>
<script>
document.querySelector('.test1').addEventListener('click', function () {
console.log(1)
}, true)
document.querySelector('.test2').addEventListener('click', function () {
console.log(2)
}, false)
</script>
点击test1,只打印1。如果点击test2,打印1,2;
四、事件循环EventLoop
1、js单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
2、任务队列
单线程就意味这我们必须等待一个任务结束之后再去执行下一个任务,如果耗时很长,那么需等待很久,如果是计算任务也就算了,但是往往很多都是一个IO操作、网络请求、事件监听触发等,在等待期间CPU空闲,是的CPU资源浪费。
js设计者也认识到了,因此遇到的异步任务,将会放入到主线程之外的一个任务队列中。主线程的同步任务继续执行。具体来说,事件循环的机制是这样的:
(1)所有的同步任务都在主线程上执行,形成了一个执行栈。
(2)遇到异步任务,就将去放入到任务队列中,只要异步任务有了结果,那么就放置一个事件(同时绑定相应的回调函数)。
(3)一旦执行栈中清空了,那么系统会自动读取任务队列,看看有哪些事件,那么就让其结束等待状态,将其调入主线程的执行栈,开始执行。
(4)之后会一直循环第三步。
3、事件和回调函数
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
回调函数就是那些被主线程挂起的代码。当拿到任务队列中的事件之后,放入主线程执行其实就是去调用相对应的回调函数去了~
4、事件循环
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
5、不同的任务队列
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
微任务包括
process.nextTick,promise,Object.observe,MutationObserver
宏任务包括
script,setTimeout,setInterval,setImmediate,I/O,UI rendering
先来个例子:
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
首先执行同步代码,遇到promise的话,会首先执行内部的同步代码,然后再继续执行同步代码。途中遇到的settimeout和promise放入不同的任务队列中,这时候由于执行栈已经为空,所以需要开始执行异步任务,首先查看微任务队列,发现又promise已经可以了,那么就执行promise的then,把所有可以执行的微任务都执行完成之后才会去宏任务队列找,发现又setTimeout可以执行了,就执行内部的代码。
所以正确的一次 Event loop 顺序是这样的
先执行同步代码,(script)相当于是宏任务。
执行栈为空后,去查询微任务队列,如果有则调用执行所有的微任务(promise...)
必要的时候渲染UI
然后执行栈清空,开始下一轮循环,去执行宏任务队列中的任务(定时器...)
6、测试题
题目一
console.log(1);
setInterval(()=>{
console.log('setInterval');
},0);
setTimeout(()=>{
console.log(2);
},0);
setTimeout(()=>{
console.log(3);
},0);
new Promise((resolve)=>{
console.log(4);
for(let i=0;i<10000;i++){
i===9999&&resolve();
}
console.log(5);
}).then(()=>{
console.log(6);
});
new Promise((resolve)=>{
resolve();
console.log(10);
}).then(()=>{
console.log(11);
});
console.log(7);
console.log(8);
// 1、4、5、10、7、8、6、11、'setInterval'、2、3
题目二
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve().then(() => {
console.log('promise 3')
}).then(() => {
console.log('promise 4')
}).then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve().then(() => {
console.log('promise 5')
}).then(() => {
console.log('promise 6')
}).then(() => {
clearInterval(interval)
})
}, 0)
})
}, 0)
Promise.resolve().then(() => {
console.log('promise 1')
}).then(() => {
console.log('promise 2')
})
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval // 大部分情况下2次, 少数情况下一次
setTimeout 2
promise 5
promise 6
promise 4后面大部分情况下出现2次setInterval、少数情况出现一次的原因就是浏览器在执行setInterval回调函数后、执行setTimeout回调函数前, 时间间隔大部分情况超过了这个最短时间.
上诉题目还没有涉及async/await。所以推荐几篇文章,大佬写非常好~~
-
初步认识js的异步执行和事件循环推荐:10分钟理解JS引擎的执行机制
-
认识async/await推荐读: 理解 JavaScript 的 async/await
深入理解async/await -
深入理解js执行机制(附带async/await)推荐读:8张图帮你一步步看清 async/await 和 promise 的执行顺序
github博客中的一些例题
看完上诉几篇文章,相信我们就能应付大部分的异步执行面试考题了,加油!!
五、跨域解决方案
跨域问题是经常出现的一种情况,我们需要对其有一定的认识~~
1、同源和同源策略
域名、端口、协议都相同的情况下才是同源。有一处不同都称为是非同源。
2、跨域
非同源下的访问和交互就属于跨域行为。
script、img、link、iframe这几个标签允许跨域访问资源。
cookies不能在不同域名下使用、ajax跨域不允许都是同源策略的限制。
- 注意:协议和端口造成的跨域,前端无法处理。
如果发生跨域了,那么请求到底发送过去了吗?
跨域并不是没有发请求也不是没有发过去,服务端能够接受到发来的请求,只是浏览器觉得它不安全,所以拦截掉了。你可能会疑问为什么表单能够发送跨域请求,为什么ajax不会?因为归根揭底跨域就是浏览器为了阻止用户读取非同源下的目标。ajax可以响应,但是浏览器说它不安全,所以必须拦截掉,但是表单并不会获取新的内容,只是提交就行了,因此可以跨域请求。同时也说明了跨域不能完全解决CSRF,因为毕竟请求浏览器还是收到了。
3、跨域解决方案
JSONP跨域
jsonp跨域就是利用script标签可以跨域的特点,jsonp有一个缺点就是只支持get方法。
第一步
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="./jsonp.js" charset="utf-8"></script>
<script type="text/javascript">
JSONP({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
</script>
</body>
</html>
// jsonp.js
var JSONP = (function(window) {
var jsonp = function({
url,
params,
callback
}) {
return new Promise((resolve, reject) => {
// 【1】动态创建script
let script = document.createElement('script');
// 【2】全局设置一个回调函数,服务器返回后会调用执行
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
// 【3】将回调函数作为参数传递
params = {
...params,
callback
}
// 数据拼接处理
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
// wd=b&callback=show
script.src = `${url}?${arrs.join('&')}`
// 【4】开始请求数据
document.body.appendChild(script)
})
}
return jsonp;
})(window);
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let { wd, callback } = req.query
console.log(wd) // Iloveyou
console.log(callback) // show
res.end(`${callback}('我不爱你')`)
})
app.listen(3000)
jsonp跨域原理:
1、客户端使用script标签发送get请求,需要传递的参数拼接到后面,外加一个callback函数。
2、客户端回调函数需要在全局添加一个,参数为服务端返回的数据字符串。
3、服务器收到get请求之后,会解析参数,查询数据,将数据以字符串的形式与callback函数名拼接。返回出去。
4、客户端接收到之后就会调用全局的callback函数,然后通过参数可以接收到服务器返回的数据。
CORS跨域资源共享
CORS被称为是跨域资源共享,需要客户端和服务端都支持。浏览器是默认开启的,关键就是服务器。通常项目中使用该方式来实现跨域访问。
原理是在请求头中设置Access-control-Allow-Origin来开启CORS。该属性可以指定哪些域名可以访问资源,如果是*则表示所有都可以。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。【这样其实就是拦截掉了】
const express = require('express');
const app = express();
let whitList = ['http://localhost:3000', 'http://127.0.0.1:62997'] //设置白名单
app.use((req, res, next) => {
let origin = req.headers.origin // 获取来源
if(whitList.includes(origin)) {
// 设置那个源头访问的我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, HEAD')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end('我爱小宝贝')
})
app.get('/getData1', function(req, res) {
res.end('get支持')
})
app.post('/getData2', function(req, res) {
res.end('post支持')
})
app.listen(4000)
上面是一个简单的例子,我们可以设置白名单,当请求的Origin匹配的时候,就通过设置 Access-Control-Allow-Origin来告诉浏览器我服务器允许,然后浏览器就不会拦截了,这样就能看到服务器的响应了~~
代理服务器代理
同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。代理服务器,需要做以下几个步骤:
// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$.ajax({
url: 'http://localhost:3000',
type: 'post',
data: { name: 'xiamen', password: '123456' },
contentType: 'application/json;charset=utf-8',
success: function(result) {
console.log(result) // {"title":"fontend","password":"123456"}
},
error: function(msg) {
console.log(msg)
}
})
</script>
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
// 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
response.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type'
})
// 第二步:将请求转发给服务器
const proxyRequest = http
.request(
{
host: '127.0.0.1',
port: 4000,
url: '/',
method: request.method,
headers: request.headers
},
serverResponse => {
// 第三步:收到服务器的响应
var body = ''
serverResponse.on('data', chunk => {
body += chunk
})
serverResponse.on('end', () => {
console.log('The data is ' + body)
// 第四步:将响应结果转发给浏览器
response.end(body)
})
}
)
.end()
})
server.listen(3000, () => {
console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
if (request.url === '/') {
response.end(JSON.stringify(data))
}
})
server.listen(4000, () => {
console.log('The server is running at http://localhost:4000')
})
postMessage
postMessage是H5的新API。可以实现多窗口之间的信息传递,可以是页面和iframe之间、页面与新打开的窗口之间、多个窗口之间的消息传递。
<body>
<iframe src="http://127.0.0.1:53402/b.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('a发过去的', 'http://127.0.0.1:53402') //发送数据
// 监听消息传来的事件
window.onmessage = function (e) { //接受返回数据
console.log(e.data) //我不爱你
}
}
</script>
</body>
<body>
<script>
// 监听消息传来的事件
window.onmessage = function (e) {
console.log(e.data) // a发来的
// 监听到a发来的时候,再去发送出去
e.source.postMessage('b发来的', e.origin)
}
</script>
</body>
nginx跨域
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
总结
CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
日常工作中,用得比较多的跨域方案是cors和nginx反向代理.
六、函数柯里化
1、什么是函数柯里化
柯里化,是函数式编程的一个重要概念。它既能减少代码冗余,也能增加可读性.
如下案例:
// 写一个 sum 方法,当使用下面的语法调用时,能正常工作
console.log(sum(2, 3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5
实现可以是:
function sum(x) {
if(arguments.length == 2) {
return arguments[0] + arguments[1];
}
return function(y) {
return x + y;
}
}
2、函数柯里化的实现
如果是这样:
function sum (a, b, c) {
console.log(a + b + c);
}
sum(1, 2, 3); // 6
调用的写法可以是这样: sum(1, 2)(3); 或这样 sum(1, 2)(10); 。就是,先把前2个参数的运算结果拿到后,再与第3个参数相加。
比如:sum(1, 2)(3),sum(1,2)执行之后应该还是一个函数。
实现一个通用的函数柯里化封装:
function curry (fn, currArgs) {
return function() {
let args = [].slice.call(arguments);
// 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
if (currArgs !== undefined) {
args = args.concat(currArgs);
}
// 递归调用
if (args.length < fn.length) {
return curry(fn, args);// curry执行后还是会返回一个新的函数f
}
// 递归出口
return fn.apply(null, args);
}
}
function sum(a, b, c) {
console.log(a + b + c);
}
const fn = curry(sum);
// fn(1, 2, 3); // 6
fn(1, 2)(3); // 6
// fn(1)(2, 3); // 6
// fn(1)(2)(3); // 6