浏览器缓存的全过程:
很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS 或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新。
我们经常需要对业务中的一些数据进行存储,通常可以分为 短暂性存储 和 持久性储存。
cookie
: 通常用于存储用户身份,登录状态等http
中自动携带, 体积上限为 4K
, 可自行设置过期时间localStorage / sessionStorage
: 长久储存/窗口关闭删除, 体积限制为 4~5M
indexDB
redis
cookie和localSrorage、session、indexDB 的区别
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
数据生命周期 | 一般由服务器生成,可以设置过期时间 | 除非被清理,否则一直存在 | 页面关闭就清理 | 除非被清理,否则一直存在 |
数据存储大小 |
|
|
| 无限 |
与服务端通信 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
从上表可以看到,
cookie
已经不建议用于存储。如果没有大量数据存储需求的话,可以使用localStorage
和sessionStorage
。对于不怎么改变的数据尽量使用localStorage
存储,否则可以用sessionStorage
存储。
对于 cookie
,我们还需要注意安全性
属性 | 作用 |
---|---|
| 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| 不能通过 |
| 只能在协议为 |
| 规定浏览器不能在跨域请求中携带 |
Name
,即该 Cookie
的名称。Cookie
一旦创建,名称便不可更改。Value
,即该 Cookie
的值。如果值为 Unicode
字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64
编码。Max Age
,即该 Cookie
失效的时间,单位秒,也常和 Expires
一起使用,通过它可以计算出其有效时间。Max Age
如果为正数,则该 Cookie
在 Max Age
秒之后失效。如果为负数,则关闭浏览器时 Cookie
即失效,浏览器也不会以任何形式保存该 Cookie
。Path
,即该 Cookie
的使用路径。如果设置为 /path/
,则只有路径为 /path/
的页面可以访问该 Cookie
。如果设置为 /
,则本域名下的所有页面都可以访问该 Cookie
。Domain
,即可以访问该 Cookie
的域名。例如如果设置为 .zhihu.com
,则所有以 zhihu.com
,结尾的域名都可以访问该 Cookie
。 Size
字段,即此 Cookie
的大小。Http
字段,即 Cookie
的 httponly
属性。若此属性为 true
,则只有在 HTTP Headers
中会带有此 Cookie 的信息,而不能通过 document.cookie
来访问此 Cookie。Secure
,即该 Cookie
是否仅被使用安全协议传输。安全协议。安全协议有 HTTPS、SSL
等,在网络上传输数据之前先将数据加密。默认为 false
。UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输(数据顺序和正确性),使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用,例如视频会议、直播 | 适用于要求可靠传输的应用,例如文件传输 |
TCP 和 UDP都是传输层协议,他们都属于TCP/IP协议族:
(1)UDP
UDP的全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
它的特点如下:
1)面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
具体来说就是:
2)有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
3)面向报文
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
4)不可靠性
首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
5)头部开销小,传输数据报文时是很高效的。
UDP 头部包含了以下几个数据:
因此 UDP 的头部开销小,只有8字节,相比 TCP 的至少20字节要少得多,在传输数据报文时是很高效的。
(2)TCP TCP的全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是面向连接的、可靠的流协议(流就是指不间断的数据结构)。
它有以下几个特点:
1)面向连接
面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
2)仅支持单播传输
每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。
3)面向字节流
TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
4)可靠传输
对于可靠传输,判断丢包、误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
5)提供拥塞控制
当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞。
6)提供全双工通信
TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)
script
来执行(宏任务)promise
click
事件...不会立即执行,需要等待当前代码全部执行完毕script
执行完毕后,会清空所有的微任务例子
自动执行的情况 会输出 listener1 listener2 task1 task2
如果手动点击click 会一个宏任务取出来一个个执行,先执行click的宏任务,取出微任务去执行。会输出 listener1 task1 listener2 task2
console.log(1)
async function asyncFunc(){
console.log(2)
// await xx ==> promise.resolve(()=>{console.log(3)}).then()
// console.log(3) 放到promise.resolve或立即执行
await console.log(3)
// 相当于把console.log(4)放到了then promise.resolve(()=>{console.log(3)}).then(()=>{
// console.log(4)
// })
// 微任务谁先注册谁先执行
console.log(4)
}
setTimeout(()=>{console.log(5)})
const promise = new Promise((resolve,reject)=>{
console.log(6)
resolve(7)
})
promise.then(d=>{console.log(d)})
asyncFunc()
console.log(8)
// 输出 1 6 2 3 8 7 4 5
1. 浏览器事件循环
涉及面试题:异步代码执行顺序?解释一下什么是
Event Loop
?JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变
js代码执行过程中会有很多任务,这些任务总的分成两类:
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明:
我们解释一下这张图:
那主线程执行栈何时为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数
以上就是js运行的整体流程
面试中该如何回答呢? 下面是我个人推荐的回答:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
setTimeout
,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise
立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick
,同样将回调函数扔到微任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick
两个微任务,先执行哪个呢?process.nextTick
指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。
JS
在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到Task
(有多种task
) 队列中。一旦执行栈为空,Event
Loop
就会从Task
队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说JS
中的异步还是同步行为
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('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 顺序是这样的
Event loop
,执行宏任务中的异步代码通过上述的
Event loop
顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作DOM
的话,为了更快的响应界面响应,我们可以把操作DOM
放入微任务中
总结起来就是:
一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务
。
2. Node 中的 Event loop
当 Node.js 开始启动时,会初始化一个 Eventloop,处理输入的代码脚本,这些脚本会进行 API 异步调用,
process.nextTick()
方法会开始处理事件循环。下面就是 Node.js 官网提供的Eventloop
事件循环参考流程
Node
中的 Event loop
和浏览器中的不相同。Node
的 Event loop
分为6
个阶段,它们会按照顺序反复运行process.nextTick
node中的微任务,当前执行栈的底部,优先级比promise
要高整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。我们来分别看下这六个阶段都做了哪些事情。
setTimeout
和 setInterval
的回调函数,简单理解就是由这两个函数启动的回调函数。poll
阶段是一个重要且复杂的阶段,几乎所有 I/O
相关的回调,都在这个阶段执行(除了setTimeout
、setInterval
、setImmediate
以及一些因为 exception
意外关闭产生的回调)。检索新的 I/O 事件,执行与 I/O 相关的回调
,其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段,所有的事件循环以及回调处理都在这个阶段执行。这个阶段的主要流程如下图所示。setImmediate()
回调函数在这里执行,setImmediate
并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分
,如下代码所示。const fs = require('fs');
setTimeout(() => { // 新的事件循环的起点
console.log('1');
}, 0);
setImmediate( () => {
console.log('setImmediate 1');
});
/// fs.readFile 将会在 poll 阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('poll callback');
});
// 首次事件循环执行
console.log('2');
在这一代码中有一个非常奇特的地方,就是 setImmediate
会在 setTimeout
之后输出。有以下几点原因:
setTimeout
如果不设置时间或者设置时间为0
,则会默认为1ms
主流程执行完成后,超过1ms
时,会将setTimeout
回调函数逻辑插入到待执行回调函数poll
队列中; 由于当前poll
队列中存在可执行回调函数,因此需要先执行完,待完全执行完成后,才会执行check:setImmediate
。因此这也验证了这句话,
先执行回调函数,再执行 setImmediate
socket.on('close', ...)
除了把 Eventloop 的宏任务细分到不同阶段外。node 还引入了一个新的任务队列
Process.nextTick()
可以认为,Process.nextTick()
会在上述各个阶段结束时,在进入下一个阶段之前立即执行
(优先级甚至超过 microtask
队列)
事件循环的主要包含微任务和宏任务。具体是怎么进行循环的呢
process.nextTick
和 Promise
。微任务在事件循环中优先级是最高的
,因此在同一个事件循环中有其他任务存在时,优先执行微任务队列。并且process.nextTick 和 Promise
也存在优先级,process.nextTick
高于 Promise
setTimeout
、setInterval
、setImmediate
和 I/O
。宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列我们可以看到有一个核心的主线程,它的执行阶段主要处理三个核心逻辑。
const fs = require('fs');
// 首次事件循环执行
console.log('start');
/// 将会在新的事件循环中的阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
setTimeout(() => { // 新的事件循环的起点
console.log('setTimeout');
}, 0);
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('Promise callback');
});
/// 执行 process.nextTick
process.nextTick(() => {
console.log('nextTick callback');
});
// 首次事件循环执行
console.log('end');
分析下上面代码的执行过程
Promise.resolve 和 process.nextTick
,宏任务队列包含:fs.readFile 和 setTimeout
;process.nextTick 再执行 Promise.resolve
,所以先输出 nextTick callback
再输出 Promise callback
;宏任务插入先后顺序执行 setTimeout 再执行 fs.readFile
,这里需要注意,先执行 setTimeout
由于其回调时间较短,因此回调也先执行,并非是 setTimeout
先执行所以才先执行回调函数,但是它执行需要时间肯定大于 1ms
,所以虽然 fs.readFile
先于setTimeout
执行,但是 setTimeout
执行更快,所以先输出 setTimeout
,最后输出 read file success
。// 输出结果
start
end
nextTick callback
Promise callback
setTimeout
read file success
当微任务和宏任务又产生新的微任务和宏任务时,又应该如何处理呢?如下代码所示:
const fs = require('fs');
setTimeout(() => { // 新的事件循环的起点
console.log('1');
fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file sync success');
});
}, 0);
/// 回调将会在新的事件循环之前
fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('poll callback');
});
// 首次事件循环执行
console.log('2');
在上面代码中,有 2 个宏任务和 1 个微任务,宏任务是 setTimeout 和 fs.readFile
,微任务是 Promise.resolve
。
poll callback
。fs.readFile 和 setTimeout
,由于 fs.readFile
优先级高,先执行 fs.readFile
。但是处理时间长于 1ms
,因此会先执行 setTimeout
的回调函数,输出 1
。这个阶段在执行过程中又会产生新的宏任务 fs.readFile
,因此又将该 fs.readFile 插入宏任务队列
fs.readFile
,因此执行该宏任务,并等待处理完成后的回调,输出 read file sync success
。// 结果
2
poll callback
1
read file success
read file sync success
Process.nextick() 和 Vue 的 nextick
Node.js
和浏览器端宏任务队列的另一个很重要的不同点是,浏览器端任务队列每轮事件循环仅出队一个回调函数接着去执行微任务队列;而Node.js
端只要轮到执行某个宏任务队列,则会执行完队列中所有的当前任务,但是当前轮次新添加到队尾的任务则会等到下一轮次才会执行。
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 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
// 也可能打印 timer1, promise1, timer2, promise2
Node
中的process.nextTick
会先于其他microtask
执行
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
// poll阶段执行
fs.readFile('./test',()=>{
// 在poll阶段里面 如果有setImmediate优先执行,setTimeout处于事件循环顶端 poll下面就是setImmediate
setTimeout(()=>console.log('setTimeout'),0)
setImmediate(()=>console.log('setImmediate'),0)
})
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1,setImmediate,setTimeout
对于
microtask
来说,它会在以上每个阶段完成前清空microtask
队列,下图中的Tick
就代表了microtask
谁来启动这个循环过程,循环条件是什么?
当 Node.js 启动后,会初始化事件循环,处理已提供的输入脚本,它可能会先调用一些异步的 API、调度定时器,或者
process.nextTick()
,然后再开始处理事件循环。因此可以这样理解,Node.js 进程启动后,就发起了一个新的事件循环,也就是事件循环的起点。
总结来说,Node.js 事件循环的发起点有 4 个:
Node.js
启动后;setTimeout
回调函数;setInterval
回调函数;I/O
后的回调函数。无限循环有没有终点
当所有的微任务和宏任务都清空的时候,虽然当前没有任务可执行了,但是也并不能代表循环结束了。因为可能存在当前还未回调的异步 I/O,所以这个循环是没有终点的,只要进程在,并且有新的任务存在,就会去执行
Node.js 是单线程的还是多线程的?
主线程是单线程执行的
,但是 Node.js存在多线程执行
,多线程包括setTimeout 和异步 I/O 事件
。其实 Node.js 还存在其他的线程,包括垃圾回收、内存优化
等
EventLoop 对渲染的影响
requestIdlecallback 和 requestAnimationFrame
,这两个函数在我们之前的内容中没有讲过,但是当你开始考虑它们在 Eventloop 的生命周期的哪一步触发,或者这两个方法的回调会在微任务队列还是宏任务队列执行的时候,才发现好像没有想象中那么简单。这两个方法其实也并不属于 JS 的原生方法,而是浏览器宿主环境提供的方法,因为它们牵扯到另一个问题:渲染。requestAnimationFrame
的出现却把这两件事情给关联起来requestAnimationFrame
我们可以在下次渲染之前执行回调函数。那下次渲染具体是哪个时间点呢?渲染和 Eventloop 有什么关系呢?Eventloop
的末尾,判断当前页面是否处于渲染时机,就是重新渲染
16.6ms
,因为就算渲染了屏幕上也看不到。当然浏览器也不能保证一定会每 16.6ms 会渲染一次,因为还会受到处理器的性能、JavaScript 执行效率等其他因素影响。requestAnimationFrame
,这个 API 保证在下次浏览器渲染之前一定会被调用,实际上我们完全可以把它看成是一个高级版的 setInterval
。它们都是在一段时间后执行回调,但是前者的间隔时间是由浏览器自己不断调整的,而后者只能由用户指定。这样的特性也决定了 requestAnimationFrame
更适合用来做针对每一帧来修改的动画效果requestAnimationFrame
不是 Eventloop
里的宏任务,或者说它并不在 Eventloop
的生命周期里,只是浏览器又开放的一个在渲染之前发生的新的 hook。另外需要注意的是微任务的认知概念也需要更新,在执行 animation callback 时也有可能产生微任务(比如 promise 的 callback),会放到 animation queue 处理完后再执行。所以微任务并不是像之前说的那样在每一轮 Eventloop 后处理,而是在 JS 的函数调用栈清空后处理但是 requestIdlecallback
却是一个更好理解的概念。当宏任务队列中没有任务可以处理时,浏览器可能存在“空闲状态”。这段空闲时间可以被 requestIdlecallback
利用起来执行一些优先级不高、不必立即执行的任务,如下图所示:
js 中现在比较成熟的有四种模块加载方案:
在有
Babel
的情况下,我们可以直接使用ES6
的模块化
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}
import {a, b} from './a.js'
import XXX from './b.js'
CommonJS
CommonJs
是Node
独有的规范,浏览器中使用就需要用到Browserify
解析了。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
在上述代码中,
module.exports
和exports
很容易混淆,让我们来看看大致内部实现
var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// 基本实现
var module = {
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
再来说说
module.exports
和exports
,用法其实是相似的,但是不能对exports
直接赋值,不会有任何效果。对于
CommonJS
和ES6
中的模块化的两者区别是:
require(${path}/xx.js)
,后者目前不支持,但是已有提案,前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。require/exports
来执行的AMD
AMD
是由RequireJS
提出的
AMD 和 CMD 规范的区别?
// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此处略去 100 行
var b = require("./b"); // 依赖可以就近书写
b.doSomething();
// ...
});
// AMD 默认推荐
define(["./a", "./b"], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
// ...
})
requirejs
在推广过程中对模块定义的规范化产出,提前执行,推崇依赖前置seajs
在推广过程中对模块定义的规范化产出,延迟执行,推崇依赖就近copy
,运行时加载,加载的是一个对象(module.exports
属性),该对象只有在脚本运行完才会生成ES6
模块不是对象,它对外接口只是一种静态定义,在代码静态解析阶段就会生成。谈谈对模块化开发的理解
当执行
JS
代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,
JS
解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为undefined
,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
var
会产生很多错误,所以在 ES6中引入了let
。let
不能在声明前使用,但是这并不是常说的let
不会提升,let
提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用
元素提升为一个比较特殊的图层,在三维空间中 (z轴) 高出普通元素一等。
触发条件
html
)position
css3
属性flex
transform
opacity
filter
will-change
webkit-overflow-scrolling
层叠等级:层叠上下文在z轴上的排序
z-index
的优先级最高它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
面试手写(原生):
//1:创建Ajax对象
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
//2:配置 Ajax请求地址
xhr.open('get','index.xml',true);
//3:发送请求
xhr.send(null); // 严谨写法
//4:监听请求,接受响应
xhr.onreadysatechange=function(){
if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )
console.log(xhr.responsetXML)
}
jQuery写法
$.ajax({
type:'post',
url:'',
async:ture,//async 异步 sync 同步
data:data,//针对post请求
dataType:'jsonp',
success:function (msg) {
},
error:function (error) {
}
})
promise 封装实现:
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}
打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。
浏览器进程
:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。渲染进程
:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。GPU 进程
:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。网络进程
:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。插件进程
:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。实际上,DNS解析是一个包含迭代查询和递归查询的过程。
一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我 们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次 查询的结果,下一级的查询由本地 DNS 服务器自己进行。
客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。
服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。
一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。
Iterator
(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator
接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator语法:
const obj = {
[Symbol.iterator]:function(){}
}
[Symbol.iterator]
属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。
next
都会返回一个对象,该对象有两个属性value
代表想要获取的数据done
布尔值,false表示当前指针指向的数据有值,true表示遍历已经结束Iterator 的作用有三个:
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
let arr = [{num:1},2,3]
let it = arr[Symbol.iterator]() // 获取数组中的迭代器
console.log(it.next()) // { value: Object { num: 1 }, done: false }
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }
对象没有布局Iterator接口,无法使用
for of
遍历。下面使得对象具备Iterator接口
为什么对象(Object)没有部署Iterator接口呢?
Iterator
接口并不是很必要,因为Map
弥补了它的缺陷,又正好有Iteraotr
接口let obj = {
id: '123',
name: '张三',
age: 18,
gender: '男',
hobbie: '睡觉'
}
obj[Symbol.iterator] = function () {
let keyArr = Object.keys(obj)
let index = 0
return {
next() {
return index < keyArr.length ? {
value: {
key: keyArr[index],
val: obj[keyArr[index++]]
}
} : {
done: true
}
}
}
}
for (let key of obj) {
console.log(key)
}
CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入的解析过程如下:
(1) 检查浏览器缓存
(2)检查操作系统缓存,常见的如hosts文件
(3)检查路由器缓存
(4)如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询
(5)如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
.com
,.cn
,.org
等的地址,该例子中会返回.com
的地址.test
的地址www.test.com
的地址CDN的工作原理: (1)用户未使用CDN缓存资源的过程:
(2)用户使用CDN缓存资源的过程:
如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。
CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。
不同情况的调用,
this
指向分别如何。顺带可以提一下es6
中箭头函数没有this
,arguments
,super
等,这些只依赖包含箭头函数最接近的函数我们先来看几个函数调用的场景
function foo() {
console.log(this.a)
}
var a = 1
foo()
const obj = {
a: 2,
foo: foo
}
obj.foo()
const c = new foo()
foo
来说,不管 foo
函数被放在了什么地方,this
一定是window
obj.foo()
来说,我们只需要记住,谁调用了函数,谁就是 this
,所以在这个场景下 foo
函数中的 this
就是 obj
对象new
的方式来说,this
被永远绑定在了 c
上面,不会被任何方式改变 this
说完了以上几种情况,其实很多代码中的
this
应该就没什么问题了,下面让我们看看箭头函数中的this
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
this
的,箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的 this
。在这个例子中,因为包裹箭头函数的第一个普通函数是 a
,所以此时的 this
是 window
。另外对箭头函数使用 bind
这类函数是无效的。bind
这些改变上下文的 API
了,对于这些函数来说,this
取决于第一个参数,如果第一个参数为空,那么就是 window
。bind
,不知道大家是否考虑过,如果对一个函数进行多次 bind
,那么上下文会是什么呢?let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?
如果你认为输出结果是
a
,那么你就错了,其实我们可以把上述代码转换成另一种形式
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
可以从上述代码中发现,不管我们给函数
bind
几次,fn
中的this
永远由第一次bind
决定,所以结果永远是window
let a = { name: 'poetries' }
function foo() {
console.log(this.name)
}
foo.bind(a)() // => 'poetries'
以上就是
this
的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定this
最终指向哪里。首先,
new
的方式优先级最高,接下来是bind
这些函数,然后是obj.foo()
这种调用方式,最后是foo
这种调用方式,同时,箭头函数的this
一旦被绑定,就不会再被任何方式所改变。
函数执行改变this
this
。因此要明白
this
指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如
obj.fn()
,便是 obj
调用了函数,既函数中的 this === obj
fn()
,这里可以看成 window.fn()
,因此 this === window
但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改
this
的指向:
call: fn.call(target, 1, 2)
apply: fn.apply(target, [1, 2])
bind: fn.bind(target)(1,2)
MDN中对documentFragment
的解释:
DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。
当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。
当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。
下面这些操作会导致回流:
在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:
当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。
下面这些操作会导致回流:
注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。
以下代码就是一个回调函数的例子:
ajax(url, () => {
// 处理逻辑
})
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,可能会有如下代码:
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
以上代码看起来不利于阅读和维护,当然,也可以把函数分开来写:
function firstAjax() {
ajax(url1, () => {
// 处理逻辑
secondAjax()
})
}
function secondAjax() {
ajax(url2, () => {
// 处理逻辑
})
}
ajax(url, () => {
// 处理逻辑
firstAjax()
})
以上的代码虽然看上去利于阅读了,但是还是没有解决根本问题。回调地狱的根本问题就是:
当然,回调函数还存在着别的几个缺点,比如不能使用 try catch
捕获错误,不能直接 return
。
图片的加载是由src
引起的,当对src
赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx
属性来储存图片的路径,在需要加载图片的时候,将data-xxx
中图片的路径赋值给src
,这样就实现了图片的按需加载,即懒加载。
注意:data-xxx
中的xxx
可以自定义,这里我们使用data-src
来定义。
懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。
使用原生JavaScript实现懒加载:
知识点:
(1)window.innerHeight
是浏览器可视区的高度
(2)document.body.scrollTop || document.documentElement.scrollTop
是浏览器滚动的过的距离
(3)imgs.offsetTop
是元素顶部距离文档顶部的高度(包括滚动条的距离)
(4)图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
图示: 代码实现:
<div class="container">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
</div>
<script>
var imgs = document.querySelectorAll('img');
function lozyLoad(){
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
var winHeight= window.innerHeight;
for(var i=0;i < imgs.length;i++){
if(imgs[i].offsetTop < scrollTop + winHeight ){
imgs[i].src = imgs[i].getAttribute('data-src');
}
}
}
window.onscroll = lozyLoad();
</script>
属性值 | 作用 |
---|---|
none | 元素不显示,并且会从文档流中移除。 |
block | 块类型。默认宽度为父元素宽度,可设置宽高,换行显示。 |
inline | 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。 |
inline-block | 默认宽度为内容宽度,可以设置宽高,同行显示。 |
list-item | 像块类型元素一样显示,并添加样式列表标记。 |
table | 此元素会作为块级表格来显示。 |
inherit | 规定应该从父元素继承display属性的值。 |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。