在地址栏里输入一个地址回车会发生哪些事情
1、解析URL:首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
2、缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
3、DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
4、获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
5、TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向客户端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
6、HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
7、返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
8、页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
9、TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
----问题知识点分割线----
手写题:Promise 原理
class MyPromise {
constructor(fn) {
this.callbacks = [];
this.state = "PENDING";
this.value = null;
fn(this._resolve.bind(this), this._reject.bind(this));
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) =>
this._handle({
onFulfilled: onFulfilled || null,
onRejected: onRejected || null,
resolve,
reject,
})
);
}
catch(onRejected) {
return this.then(null, onRejected);
}
_handle(callback) {
if (this.state === "PENDING") {
this.callbacks.push(callback);
return;
}
let cb =
this.state === "FULFILLED" ? callback.onFulfilled : callback.onRejected;
if (!cb) {
cb = this.state === "FULFILLED" ? callback.resolve : callback.reject;
cb(this.value);
return;
}
let ret;
try {
ret = cb(this.value);
cb = this.state === "FULFILLED" ? callback.resolve : callback.reject;
} catch (error) {
ret = error;
cb = callback.reject;
} finally {
cb(ret);
}
}
_resolve(value) {
if (value && (typeof value === "object" || typeof value === "function")) {
let then = value.then;
if (typeof then === "function") {
then.call(value, this._resolve.bind(this), this._reject.bind(this));
return;
}
}
this.state === "FULFILLED";
this.value = value;
this.callbacks.forEach((fn) => this._handle(fn));
}
_reject(error) {
this.state === "REJECTED";
this.value = error;
this.callbacks.forEach((fn) => this._handle(fn));
}
}
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error("fail")), 3000);
});
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000);
});
p2.then((result) => console.log(result)).catch((error) => console.log(error));
----问题知识点分割线----
一般如何产生闭包
- 返回函数
- 函数当做参数传递
----问题知识点分割线----
画一条0.5px的线
- 采用transform: scale()的方式,该方法用来定义元素的2D 缩放转换:
transform: scale(0.5,0.5);
- 采用meta viewport的方式
<meta name="viewport" content="width=device-width, initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5"/>
这样就能缩放到原来的0.5倍,如果是1px那么就会变成0.5px。viewport只针对于移动端,只在移动端上才能看到效果
----问题知识点分割线----
setState
在了解setState之前,我们先来简单了解下 React 一个包装结构: Transaction:
事务 (Transaction)
是 React 中的一个调用结构,用于包装一个方法,结构为: initialize - perform(method) - close。通过事务,可以统一管理一个方法的开始与结束;处于事务流中,表示进程正在执行一些操作
- setState: React 中用于修改状态,更新视图。它具有以下特点:
异步与同步: setState并不是单纯的异步或同步,这其实与调用时的环境相关:
- 在合成事件 和 生命周期钩子 (除 componentDidUpdate) 中,setState是"异步"的;
- 原因: 因为在setState的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents队列中等待执行;否则,开始执行batchedUpdates队列更新;
- 在生命周期钩子调用中,更新策略都处于更新之前,组件仍处于事务流中,而componentDidUpdate是在更新之后,此时组件已经不在事务流中了,因此则会同步执行;
- 在合成事件中,React 是基于 事务流完成的事件委托机制 实现,也是处于事务流中;
- 问题: 无法在setState后马上从this.state上获取更新后的值。
- 解决: 如果需要马上同步去获取新值,setState其实是可以传入第二个参数的。setState(updater, callback),在回调中即可获取最新值;
- 原因: 因为在setState的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents队列中等待执行;否则,开始执行batchedUpdates队列更新;
- 在 原生事件 和 setTimeout 中,setState是同步的,可以马上获取更新后的值;
- 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
- 批量更新 : 在 合成事件 和 生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新;
- 函数式 : 由于 Fiber 及 合并 的问题,官方推荐可以传入 函数 的形式。setState(fn),在fn中返回新的state对象即可,例如this.setState((state, props) => newState);
- 使用函数式,可以用于避免setState的批量更新的逻辑,传入的函数将会被 顺序调用;
注意事项:
- setState 合并,在 合成事件 和 生命周期钩子 中多次连续调用会被优化为一次;
- 当组件已被销毁,如果再次调用setState,React 会报错警告,通常有两种解决办法
- 将数据挂载到外部,通过 props 传入,如放到 Redux 或 父级中;
- 在组件内部维护一个状态量 (isUnmounted),componentWillUnmount中标记为 true,在setState前进行判断;
总结
setState 并非真异步,只是看上去像异步。在源码中,通过
isBatchingUpdates
来判断
setState
是先存进state
队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。- 那么什么情况下
isBatchingUpdates
会为true
呢?在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。 - 但在 React 无法控制的地方,比如原生事件,具体就是在
addEventListener
、setTimeout
、setInterval
等事件中,就只能同步更新。
一般认为,
做异步设计是为了性能优化、减少渲染次数
,React 团队还补充了两点。
- 保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是。
- 启用并发更新,完成异步渲染。
setState
只有在 React 自身的合成事件和钩子函数中是异步的,在原生事件和 setTimeout 中都是同步的setState
的异步并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的异步。当然可以通过 setState 的第二个参数中的 callback 拿到更新后的结果setState
的批量更新优化也是建立在异步(合成事件、钩子函数)之上的,在原生事件和 setTimeout 中不会批量更新,在异步中如果对同一个值进行多次 setState,setState 的批量更新策略会对其进行覆盖,去最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新
- 合成事件中是异步
- 钩子函数中的是异步
- 原生事件中是同步
- setTimeout中是同步
这是一道经常会出现的 React setState 笔试题:下面的代码输出什么呢?
class Test extends React.Component {
state = {
count: 0
};
componentDidMount() {
this.setState({
count: this.state.count + 1});
console.log(this.state.count);
this.setState({
count: this.state.count + 1});
console.log(this.state.count);
setTimeout(() => {
this.setState({
count: this.state.count + 1});
console.log(this.state.count);
this.setState({
count: this.state.count + 1});
console.log(this.state.count);
}, 0);
}
render() {
return null;
}
};
我们可以进行如下的分析:
- 首先第一次和第二次的
console.log
,都在 React 的生命周期事件中,所以是异步的处理方式,则输出都为0
; - 而在
setTimeout
中的console.log
处于原生事件中,所以会同步的处理再输出结果,但需要注意,虽然count
在前面经过了两次的this.state.count + 1
,但是每次获取的this.state.count
都是初始化时的值,也就是0
; - 所以此时
count
是1
,那么后续在setTimeout
中的输出则是2
和3
。
所以完整答案是 0,0,2,3
同步场景
异步场景中的案例使我们建立了这样一个认知:setState 是异步的,但下面这个案例又会颠覆你的认知。如果我们将 setState 放在 setTimeout 事件中,那情况就完全不同了。
class Test extends Component {
state = {
count: 0
}
componentDidMount(){
this.setState({
count: this.state.count + 1 });
console.log(this.state.count);
setTimeout(() => {
this.setState({
count: this.state.count + 1 });
console.log("setTimeout: " + this.state.count);
}, 0);
}
render(){
...
}
}
那这时输出的应该是什么呢?如果你认为是 0,0,那么又错了。
正确的结果是 0,2
。因为 setState
并不是真正的异步函数,它实际上是通过队列延迟执行操作实现的,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新。值为 true 则执行异步操作,false 则直接同步更新
接下来这个案例的答案是什么呢
class Test extends Component {
state = {
count: 0
}
componentDidMount(){
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count)
})
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count)
})
}
render(){
...
}
}
如果你觉得答案是 1,2
,那肯定就错了。这种迷惑性极强的考题在面试中非常常见,因为它反直觉。
如果重新仔细思考,你会发现当前拿到的 this.state.count
的值并没有变化,都是 0
,所以输出结果应该是 1,1
。
当然,也可以在 setState
函数中获取修改后的 state
值进行修改。
class Test extends Component {
state = {
count: 0
}
componentDidMount(){
this.setState(
preState=> ({
count:preState.count + 1
}),()=>{
console.log(this.state.count)
})
this.setState(
preState=>({
count:preState.count + 1
}),()=>{
console.log(this.state.count)
})
}
render(){
...
}
}
这些通通是异步的回调,如果你以为输出结果是 1,2,那就又错了,实际上是 2,2
。
为什么会这样呢?当调用 setState
函数时,就会把当前的操作放入队列中
。React 根据队列内容,合并 state 数据,完成后再逐一执行回调,根据结果更新虚拟 DOM,触发渲染。所以回调时,state 已经合并计算完成了
,输出的结果就是 2,2
了。
----问题知识点分割线----
JavaScript为什么要进行变量提升,它导致了什么问题?
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
- 在解析阶段,JS会检查语法