这是我在掘金写得第二篇文章,文章中一个问题就是一个知识点,建议大家多看看其他文章,多查阅资料,最好自己总结一下,真正搞懂,这样在面试的时候就不是背出来给面试官听,而是用自己的话说出来。文章还没有写完,js的相关知识点太多了。
1. 介绍防抖与节流,说出它们之间的区别,并用js实现
- 防抖
- 概念:短时间内大量触发同一事件,只会执行一次函数。
- 实现:维护一个计时器,规定在触发事件后等待n秒执行事件处理函数,如果在这n秒内再次触发事件,则清除计时器,重新计时,直到n秒内没有触发事件才会执行相应的事件处理函数。
- 使用场景:搜索框联想;按钮提交;
- 代码实现
// 简单实现
function debounce(func,wait){
let timeout = null;
return function(){
const context = this;
const args = arguments;
if(timeout) clearTimeout(timeout)
timeout = setTimeout(()=>{
func.apply(context,args)
},wait)
}
}
// 立即执行
function debounce(func,wait,immediate){
let timeout = null;
return function(){
const context = this;
const args = arguments;
if(immediate){
const callNow = !timeout;
if(callNow) func.apply(context,args)
timeout = setTimeout(()=>{
clearTimeout(timeout);
},wait)
} else {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(()=>{ func.apply(context,args) },wait)
}
}
}
- 节流
- 概念:单位时间内,多次触发事件,但是只能执行一次事件处理函数。
- 实现:使用事件戳或者定时器实现
- 使用场景:在处理类似mousemove/scroll/touchmove等事件的时候,控制事件处理函数触发的次数。
- 实现代码:
// 时间戳:使用两个时间戳prev旧时间戳和now新时间戳,每次触发事件都判断二者的时间差,如果达到规定的时间,就执行函数并重置旧时间戳
function throttle(func, wait){
let prev = 0;
return function(){
const context = this;
const args = arguments;
const now = Date.now();
if(now-prev > wait){
func.apply(context,args);
prev = now;
}
}
}
// 定时器的方式实现
function throttle(func, wait){
let timeout = null;
return function(){
const context = this;
const args = arguments;
if(!timeout){
timeout = setTimeout(()=>{
func.apply(context,args)
clearTimeout(timeout);
},wait)
}
}
}
2. 说一下闭包原理以及应用场景
- 概念:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数访问定义时的词法作用域。(当函数可以记住并访问所在的词法作用域时,就产生的闭包,及时函数是在当前词法作用域之外执行的)。本质上定时器、事件监听器、ajax请求等任何异步任务中,只要使用了回调函数,实际上就是在使用闭包。
- 特点:正常函数在执行完毕后,局部活动对象会被销毁。但由于闭包的作用域链包含函数的活动对象,这会让函数在执行完后变量不能销毁掉,使用不当会造成内存泄漏。
- 使用场景:模拟私有变量;异步回调本质上就是在使用闭包。
3. 浏览器的进程与线程,js单线程带来的好处?
- 进程概念(process):具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。
- 线程概念:一个进程可能包含了很多顺序执行流,每个顺序执行流就是一个线程。
- 进程与线程的区别:进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。一个进程可以由一个或多个线程组成,线程可以理解为一个进程中代码的不同执行路线。举个例子:当你打开了一个tab页面的时候,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、js引擎线程、http请求线程等,当你发起一个请求的时候,其实就是创建了一个线程,当请求结束后,该线程就可能会被销毁。
- 浏览器的进程分类:
- 用户界面线程: 负责浏览器界面显示、用户交互(地址栏的前进、后退)等。
- 浏览器进程: 浏览器的主进程(负责协调、主控),该进程只有一个;负责各个页面的管理,创建和销毁其他进程;网络资源的管理、下载等等;在用户界面和渲染引擎之间传达指令。
- 渲染进程(重要): 常说的浏览器内核;每个tab页面都有一个渲染进程,互不影响;渲染进程是多线程的,其中有ajax请求、页面渲染、事件触发线程、ui线程、定时器线程,其中ui线程和js线程是互斥的,共用同一条线程。
- 持久层进程:存放cookie、sessionStorage、localStorage、indexedDB等。
- 浏览器渲染线程分类:
- GUI渲染线程: 负责渲染浏览器界面,解析HTML/css,构建render树,布局和绘制等,当界面需要重绘或由于某种操作引发回流的时候,该线程就执行;
- JS引擎线程(主线程):js内核,负责处理js脚本程序、解析js脚本、运行代码(v8引擎),js引擎线程和GUI渲染线程是互斥的。
- 事件触发线程:当js执行碰到异步操作(如setTimeout/ajax请求),会走将对应事件添加到对应的线程中(定时器线程/http请求线程)执行,等异步操作有了结果的时候,就会通知事件触发线程将它们的回调操作添加到事件队列中,等js空闲的时候来处理;当对应的事件被触发时(onclick、onload),事件触发线程会把对应的回调操作添加到事件队列中。
- 定时器触发线程:setTimeout和setInterval所在的线程,
- 异步http请求线程:当执行一个http请求的时候,就把异步请求操作放到http请求线程中,等收到响应,事件触发线程会把回调函数添加到事件队列中,等待执行。
- js单线程的好处:避免dom渲染的冲突。
4. 谈谈你对event loop事件循环机制的了解
- 概念:为了提高cpu的利用率,js把任务分为同步任务和异步任务,同步任务在主线程中顺序执行,异步任务进入到回调队列中。主线程会形成一个执行栈,等执行栈中所有的同步任务执行完毕后,主线程会从回调队列中读取一个事件,放入执行栈中,当这个事件执行完之后清空执行栈。然后事件触发线程将会读取回调队列中的下一个事件,放入执行栈中,这个过程是循环不断的,因此这种机制称为事件循环。(主线程之外,事件触发线程管理着任务队列)
5. 什么是宏任务什么是微任务?
- 常见宏任务(计划由标准机制来执行的任何js代码):程序的初始化;事件触发回调;setTimeout;setInterval;requestAnimationFrame
- 常见微任务:promise.then;promise.catch;promise.finally;MutationObserver;queueMicrotask
- 区别:
- 浏览器会先执行一个宏任务,紧接着执行当前执行栈中的微任务,再进行渲染,然后再执行下一个宏任务。
- 微任务跟宏任务不在一个任务队列中。
- 微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前执行完所有的微任务,可以提供一个更好的访问级别。
6. 谈谈你对原型的理解,解释一下什么是原型链?原型链解决了什么问题?
- 原型:每个函数都有一个prototype属性,指向一个对象,这个对象就是通过调用构造函数创建对象实例的原型。使用原型的好处就是所有的对象实例共享它所包含的属性和方法。
- 原型链:每个构造函数都有一个prototype属性指向原型,原型中有一个constructor属性指向构造函数,实例中有一个内部指针_proto_指向原型。如果一个原型是另一个类型的实例,就意味着这个原型内部有个_proto_指针指向另一个原型,如此循环,就在实例、构造函数、原型之间构造了一条原型链。
- 作用:通过原型链可以实现一个对象继承另一个对象的属性与方法,以及实现实例共享属性与方法。
7. 谈谈ES5继承以及ES6继承
- 组合继承(ES5):综合了原型链和借用构造函数,使用原型链继承原型上的属性和方法,而通过借用构造函数继承实例属性。这样即可以在原型上以实现方法的重用,又可以让每个实例都有自己的属性。缺点就是:执行了两次父类构造函数;
function SuperType(name){
this.name = name;
this.colors = ['red','blue','yellow']
}
SuperType.prototype.sayName = function(){
console.log('hello world') }
function SubType(){
// 函数就是在特定上下文中执行代码的简单对象
SuperType.call(this,name)
}
SubType.prototype = new SuperType();
let instance1 = new SubType()
- 寄生式组合继承(ES5最佳模式):通过借用构造函数继承属性,使用混合式原型链继承方法。不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本,直接给子类赋值。
function inheritPrototype(subType,superType){
let prototype = object(superType.prototype) // 创建父类构造函数原型对象
prototype.constructor = subType // 增强对象,解决重写原型导致默认constructor丢失的问题
subType.prototype = prototype; // 赋值对象
}
function SuperType(name){
this.name = name;
this.colors = ['red','blue','yellow']
}
SuperType.prototype.sayName = function(){
console.log(this.name)
}
function SubType(name,age){
SuperType.call(this,name);
}
inheritPrototype(SubType,SuperType);
- 类继承(ES6继承方法) 使用extends关键字,并在子类构造函数中调用super (可以看成Parent.call(this,value)
class Parent {
constructor(name){
this.name = name
}
getName(){
console.log(this.name)
}
}
class Child extends Parent {
constructor(age){
super(value);
this.age = age
}
}
let child = new Child('dandan');
child.getName()
- 寄生式组合继承与类继承的区别:通过babel将ES6代码编译成ES5后,发现类继承的核心就是寄生式组合继承。
8. 类型判断的方式?
- typeof判断原始类型(Number、String、Undefined、Boolean、Symbol)
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolan'
typeof Symbol // 'symbol':表示独一无二的值
- instanceof 判断引用类型:用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上(对比的是实例与构造函数)
function Foo(){};
let myFoo = new Foo();
console.log(myfoo instanceof Foo) // true
console.log(myfoo instanceof Object) // true
- Array.isArray() 判断一个值是否为数组。
Array.isArray([1,2,3]) //true
- Object.prototype.toString() 对于这个方法,会返回一个形如 "[object XXX]"的字符串.。可以判断几乎所有类型的数据。
// Boolean 类型,tag 为 "Boolean"
Object.prototype.toString.call(true); // => "[object Boolean]"
// Number 类型,tag 为 "Number"
Object.prototype.toString.call(1); // => "[object Number]"
// String 类型,tag 为 "String"
Object.prototype.toString.call(""); // => "[object String]"
// Array 类型,tag 为 "String"
Object.prototype.toString.call([]); // => "[object Array]"
// Arguments 类型,tag 为 "Arguments"
Object.prototype.toString.call((function() { return arguments; })()); // => "[object Arguments]"
// Function 类型, tag 为 "Function"
Object.prototype.toString.call(function(){}); // => "[object Function]"
// Error 类型(包含子类型),tag 为 "Error"
Object.prototype.toString.call(new Error()); // => "[object Error]"
// RegExp 类型,tag 为 "RegExp"
Object.prototype.toString.call(/\d+/); // => "[object RegExp]"
// Date 类型,tag 为 "Date"
Object.prototype.toString.call(new Date()); // => "[object Date]"
// 其他类型,tag 为 "Object"
Object.prototype.toString.call(new class {}); // => "[object Object]"
9. 判断this的绑定
- 默认绑定:在严格模式下,绑定到undefined;在非严格模式下,绑定到全局对象;
- 隐式绑定:函数在某个上下文对象中调用,this绑定的是那个上下文对象(obj.foo());
- 显示绑定:函数通过call,apply,bind调用,那么this绑定的就是指定的对象;fn.call(target, 1, 2);fn.apply(target,[1,2]);fn.bind(target)(1,2)
- new绑定:函数如果是new绑定,那么this绑定的是new中新创建的对象;
- 箭头函数没有自己的this,它的this继承于上一层代码块中的this;
- new绑定 > 显式绑定 > 隐式绑定 > 默认绑定;
10. call/apply/bind的实现原理,以及内部如何实现的
- call 、apply、bind是挂在Function对象上的三个方法,调用这三个方法必须是一个函数;调用他们可以为函数指定this执行,也可以传参。
- 调用方式
func.call(target,1,2); // 使用特定对象调用函数方法,并给函数传参
func.apply(target,[1,2]);
func.bind(target)(1,2)
- 实现一个call函数
Function.prototype.call = function(context){
if(typeof this !== "function"){
throw new Error("not a function");
}; // 判断调用对象是否是一个函数,如果不是则抛出错误
context = context ? context : window // context为空时绑定到window上
const key = Symbol('key'); // 创建一个独一无二的属性值,防止已有属性的覆盖
context[key] = this; // 给context添加一个属性,其中this指向当前函数
const rest = [...arguments].slice(1) // 获取除this指向对象以外的参数
const res = rest.length > 0 ? context[key](...rest) : context[key]() // 隐式绑定,当前函数的this指向了context
delete context[key] // 删除属性fn
return res
}
- 实现一个apply函数
Function.prototype.apply = function(context,args){
if(typeof this !== 'function'){
throw new Error('not a function')
}
context = context ? context : window
const key = Symbol('key')
context[key] = this
const res = args.length > 0 ? context[key](...args) : context[key]()
delete context[key]
return res
}
- 实现一个bind函数
Function.prototype.bind = function(context,...rest){
if(typeof this !== 'function'){
throw new Error('not a function')
}
context = context ? context : window
const func = this
return function(){
const args = [].slice.call(arguments)
return func.apply(context, rest.concat(args))
}
}
- 作用
- 判断类型
Object.prototype.toString.call(obj)
- 类数组借用方法
let arrayLike = {
0:'java',
1:'html'
}
Array.prototype.push.call(arrayLike,'css','node')
console.log(arrayLike) // {0:'java',1:'html',2:'css',3:'node'}
- 获取数组的最大值、最小值:使用apply直接传递数组作为调用方法的参数,可以减少异步展开数组的操作
let arr = [1,8,9,15,23];
const max = Math.max.apply(Math,arr);
const min = Math.min.apply(Math,arr);
console.log(max) // 23
console.log(min) // 1
- 子类构造函数内部调用父类构造函数,从而实现实例属性的继承。
11. js中为什么会存在变量提升?
- 变量的赋值操作会执行两个动作(先编译,后运行),首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,并为其赋值。
12. 深浅拷贝,区别以及手写
- 浅拷贝:浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性值是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝:将一个对象从内存中完整拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
- 实现浅拷贝的方式
- Object.assign()
- 函数库lodash的_.clone方法
- 展开运算符 ...
- Array.prototype.concat()
- Array.prototype.slice()
- 手写浅拷贝
- 实现深拷贝
- JSON.parse(JSON.stringify) : 可以实现数组或对象的深拷贝,但是不能处理函数和正则;
- 函数库lodash的_.cloneDeep方法
- 手写递归方法(需要考虑的类型有点多,这边只列出几个类型)
// 判断类型
function cloneType(target){
return Object.prototype.toString.call(target)
}
// 深拷贝
function cloneDeep(target){
let cloneTarget;
if(typeof target !== 'object') return target
let type = cloneType(target)
if(type.includes('Null')) return null
if(type.includes('Date')) return new Date()
if(type.includes('RegExp')) return new RegExp(target)
if(type.includes('Array')){
cloneTarget = []
cloneTarget = target.map(item => { return cloneDeep(item) })
return cloneTarget
}
if(type.includes('Object')){
cloneTarget = {}
let keys = Object.keys(target)
for(key of keys){
cloneTarget[key] = cloneDeep(target[key])
}
return cloneTarget
}
}
13. 简述new的实现过程
- 步骤:
- 生成一个新对象
- 链接到原型
- 绑定this
- 返回新对象
- 实现
function create(Con, ...args){
let obj = {} // 创建一个空对象
Object.setPrototypeOf(obj, Con.prototype) // ES6中的方法,用于设置一个指定对象的原型
let result = Con.apply(obj,args) // 绑定this并执行构造函数
return result instanceof Object ? result : obj // 确保返回值为对象
}
- 补充:new有返回值与没有返回值的区别 一般情况下,构造函数没有返回值,但是用户可以主动返回对象,来覆盖正常的创建对象;如果return 返回的是对象,则会覆盖正常创建的对象;如果返回值为基本类型,则不会覆盖正常的返回对象;
14. 为什么0.1+0.2 != 0.3?
- 原因:在进制转换和对阶运算的过程中出现精度损失
- 进制转换:计算机无法直接对十进制的数字进行运算,需要对照IEEE 754规范转换成二进制, 0.1与0.2转换成二进制后会无限循环,又由于IEEE 754尾数位数的限制,需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。
- 对阶运算:运算时需要对阶运算,这部分可能产生精度损失。
0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)
0.1 + 0.2 === 0.30000000000000004 // true
- 解决
// toFixed()将运算转为字符串
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
15. var/let/const的区别
- 全局作用域中,var声明的变量会挂到window上,而let和const不会;
- var声明的变量存在变量提升,而let和const不会;
- let和const声明形成了块级作用域,只能在块级作用域里访问,不能跨块访问;
- 同一作用域下let 、const不能同时声明同名变量,而var可以;
- 暂时性死区,let和const声明的变量不能在声明前被使用。
16. 全局下,var声明的变量会挂在在window下,let(const)声明的变量会挂在哪里呢?
let pjj = 'a';
var person = 'b';
debugger
打印结果如图所示,发现全局中let、const声明的变量会挂在块级作用域script下。
17. 数组扁平化方式
- ES6的flat(level):参数level表示拉平的层数;
[1,2,[3,4]].flat() // [1,2,3,4] 拉平1层
[1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5] 拉平2层
- 序列化后正则:把传入的数组转换成字符串,然后通过正则表达式把方括号去掉;
let arr = [1,[2,[3,[4,5]]],6];
function flatten(arr){
let str = JSON.stringify(arr);
str = str.replace(/(\[||\])/g, '');
str = '[' + str + ']';
return JSON.parse(str);
}
- reduce()递归(常用)
let arr = [1,[2,[3,[4,5]]],6];
function flatten(arr){
return arr.reduce((acc,cur)=>{
return acc.concat(
Array.isArray(cur) ? flatten(cur) : cur
)
},[])
}
- split() 和 toString()共同处理
let arr = [1,[2,[3,[4,5]]],6];
function flatten(arr){
return arr.toString().split(',')
}
18. 什么是同源策略?
- 概念:一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。协议+域名+端口三者都相同才是同源。
- 同源策略的限制内容: 1.无法获取Cookie、localStorage、indexedDB等存储性内容; 2.AJAX请求发送后,返回结果被浏览器拦截;
19. 浏览器跨域方式?
- JSONP
- 原理:jsonp通过动态创建<script>元素并为src属性指定跨域URL实现的,此时<script>能够不受限制地从其他域加载资源,并且在资源被加载后立即执行回调。
- 优点:简单、兼容性好
- 缺点:仅支持get方法,不安全,可能会遭受xss攻击;
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
// 定义回调函数,将其挂载在window对象上,这样就能在加载过来的js代码中直接访问这个回调函数
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback } // wd=b&callback=show
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script) // 返回callback这个回调函数 show("hello world")
})
};
jsonp({
url: 'http://localhost:3000/say',
params: { wd: '123456' },
callback: 'show'
}).then(data => {
console.log(data)
})
- CORS 原理:服务器可以选择,允许跨域请求访问它们的资源,如果服务器决定响应请求,那么应该发送Access-Control-Allow-Origin头部,包含相同的源,或者如果资源是公开的则包含 *
- postMessage 原理:在一个窗口上调用postWindow.postMessage()方法发送一个MessageEvent消息。接收消息的窗口(targetOrigin)可以根据需要自由处理此事件。
// 发送消息的窗口
// data: 将要发送到其他窗口的数据
// targetOrigin: 指定哪些窗口能接收到消息事件,其值可以是字符串*或者一个url
postWindow.postMessage(data,targetOrigin,[transfer])
// 接收消息的窗口
window.addEventListener('message',receiveMessage,false)
function receiveMessage(event){
event.data // 从其他窗口传递过来的数据
event.origin // 调用postMessage() 时发送消息的origin
event.source // 对发送消息的窗口对象的引入
}
- websocket 原理:可以在用户的浏览器和服务器之间打开交互式通信会话,在建立连接后,WebSocket的server与client都能主动向对方发送或接收数据。
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open',event=>{ socket.send('hello') // 向服务器发送数据 })
socket.addEventListener('message', event=>{ console.log('Message from server ', event.data); // 接收来自服务器返回的数据 })
- nginx反向代理 原理: 服务器向服务器请求无需遵循同源策略。用户访问网站的时候首先会访问nginx服务器,然后nginx服务器再从服务器集群中选择压力较小的服务器,将该访问请求引向该服务器。
20. 浏览器缓存机制
- 作用:重用已获取的资源,提高网页的整体加载速度;
- 缓存分类:
- 浏览器缓存机制:强缓存(通过Expires和Cache-Control两种响应头实现)
- Expires: 它描述了缓存过期的一个绝对时间,由服务器返回。Expires受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
- Cache-Control:它描述了缓存过期的一个相对时间, max-age=259000,优先级高于Expires;
Cache-Control: no-cache // 强制源服务器再次验证
Cache-Control: no-store // 不缓存数据到本地
Cache-Control: public // 可以被所有用户缓存,包括终端和CDN等中间代理服务器
Cache-Control: private // 只能被终端浏览器缓存,不允许中继服务器中缓存
Cache-Control: max-age=256000 // 规定缓存过期的相对时间
- 浏览器缓存机制:协商缓存(利用Last-Modified && If-Modified-Since 和 ETag && If-None-Match)
- Last-Modified && If-Modified-Since: 浏览器第一次请求资源时,服务器会在响应头上加上 Last-Modified(文件最后修改时间),当浏览器再次请求该资源的时候,会在请求头中带上If-Modified-Since字段(上一次Last-Modified的值),服务器对比这两个时间,若相同则返回304,否则返回新资源,并更新Last-Modified。
- ETag && If-None-Match : ETag表示文件唯一标识,只要文件改动,ETag就会重新计算。服务器发送ETag字段, 浏览器再次请求时发送If-None-Match, 服务器对比这两个值,若相同则返回304,否则返回新资源,并更新Last-modified。
21. 浏览器的渲染流程
- 构建dom树:解析HTML文件,构建DOM树;
- 生成样式表:解析CSS文件,生成样式表;
- 构建render树:将dom树跟样式表关联起来,构建一棵render树;
- 确定节点坐标:布局渲染树(Layout/reflow),负责渲染树中的元素尺寸、位置等计算;
- 绘制页面:根据render树和节点显示坐标,然后调用每个节点的paint方法,将它们绘制出来;
22. 在地址栏中输入一个url到这个页面完全呈现出来,中间会发生什么?
- DNS域名解析:将域名解析成对应的ip地址; DNS域名解析过程(递归查询与迭代查询):浏览器缓存 —> 系统缓存 —> hosts文件 —> 路由器缓存 —> DNS缓存 —> DNS递归查询
- TCP连接:TCP三次握手;
- 第一次握手:客户端给服务器发一个SYN报文,并指明客户端的初始化序列号ISN;
- 第二次握手:服务器收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也指定了自己的初始化序列号ISN,同时会把客户端的ISN + 1 作为ACK的值,表示自己已经收到了客户端的SYN;
- 第三次握手:客户端收到SYN报文之后,会发送一个ACK报文,把服务器的ISN+1作为ACK的值,表示已经收到了服务端的SYN报文,此时,双方建立起来了连接; 为什么需要三次握手:确认双方的接收能力、发送能力是否正常;指定自己的初始化序列号,为后面可靠传送做准备;如果是https协议,三次握手过程还会进行数字验证以及加密秘钥生成;
- TCP链接建立后发送http请求;
- 服务器处理请求并返回HTTP报文;
- 浏览器解析渲染页面;
- html解析,构建DOM;
- css解析,生成css规则树;
- 合并DOM树和CSS规则,生成render树;
- 布局render树(layout/reflow),负责各元素的尺寸、位置计算;
- 绘制render树(paint),绘制页面像素信息;
- 断开链接:TCP四次挥手;
- 第一次挥手:客户端发送一个FIN报文,并指定客户端序列号;
- 第二次挥手:服务端收到FIN之后,会发送ACK报文,且把客户端的序列号 +1作为ACK报文的序列号值,同时指定服务端序列号,表明已经收到客户端的报文了;
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给FIN报文,且指定一个序列号;
- 第四次挥手:客户端收到FIN报文之后,发送一个ACK报文作为应答,且把服务端的序列号+1作为自己ACK报文的序列号值;
23. 什么是点击穿透?
- 原因:页面有两个元素,b元素在a元素上面,当点击b元素的时候,b元素被隐藏,同时a元素的click事件也被触发。这是因为移动端浏览器中,点击事件执行顺序是touchstart —— touchmove —— touchend —— click,因为click是有300ms的延迟,当b上的touchstart事件被触发后,b被隐藏,300ms之后,a的click事件被触发。
- 解决办法:
- 引入fastclick;
- 在touchend事件上调用preventDefault,取消之后的click点击事件;