前端面试知识汇总

406 阅读11分钟

持续更新中。。。

CSS部分

水平居中

  • 行内元素: text-align: center;
  • 块级元素:margin: 0 auto;
  • position: absolute + left:50% + transform: translateX(-50%)
  • display: flex + justify-content: center

垂直居中

  • 设置line-height 等于height
  • position: absolute + top:50% + transform: translateY(-50%)
  • display: flex + align-items: center
  • display:table+display:table-cell + vertical-align: middle;

消除display设置为inline,inline-block产生间距的方法

  • 去掉HTML中的空格,代码连成一行,代码可读性差
  • 使用margin负值
  • 使用font-size:0
  • 标签不闭合
<div>
    <a>1
    <a>2
    <a>3
</div>
//或者
<div>
    <a>1
    <a>2
    <a>3</a>
</div>

盒模型

盒模型的组成,由里向外content,padding,border,margin;

在IE盒子模型中,width表示content+padding+border这三个部分的宽度;

在标准的盒子模型中,width指content部分的宽度。

box-sizing 是用于告诉浏览器如何计算一个元素是总宽度和总高度;

//W3C盒子模型
box-sizing: content-box;
//IE盒子模型
box-sizing: border-box;

清除浮动

  • 父级div定义overflow:auto 或 overflow:hidden;
  • 添加新的块级元素,应用clear:both;
//li是浮动元素
<ul class="cc">
    <li></li>
    <li></li>
    <div style="clear: both;"></div>
</ul>
  • 在父级样式添加伪元素:after或者:before
.parent:after {
	content: '';
	display: block;
	clear: both;
}

画一个三角形

<div></div> 
div {
	width: 0;
	height: 0;
	border-width: 50px;
	border-style: solid;
	border-color: #f00 transparent transparent transparent;
} 

JS部分

数组方法

1.改变原始数组的方法

  • pop() 删除最后一个值,并返回删除的值
  • push() 在数组最后添加一个值
  • shift() 删除数组的第一个元素,并返回第一个元素的值
  • unshift() 向数组的开头添加一个或更多元素,并返回新的长度
  • sort() 在原数组上进行排序
  • splice() 从数组中添加删除项目,然后返回被删除的项目(数组)
  • reverse() 将原来的数组倒序

2.不改变原始数组的方法

  • slice(start,end) 返回选定的元素
  • concat() 用于连接两个或多个数组
  • join() 返回一个字符串
  • forEach()
  • map()
  • filter()
  • every() 检测数组所有元素是否都符合指定条件
  • some() 检测数组任一元素是否符合指定条件
  • find() 返回满足条件的第一个元素的值
  • findIndex() 返回满足条件的第一个元素的位置
  • Object.keys()
  • Object.values()
  • Object.entries()
  • arr.reduce((total, item, index) => {return total + item })

数组去重

//es5
//简单去重法
var uniqueArray = function(arr) {
    var newArr = [];
    arr.forEach(function(v){
        if(newArr.indexOf(v) === -1){
            newArr.push(v);
        }
    })
    return newArr;
}

//数组下标法
var uniqueArray1 = function(arr) {
    var newArr = [];
    for(var i=0; i<arr.length; i++) {
    //如果当前数组的第i项在当前数组中第一次出现的位置是i,才存入数组;否则代表是重复的
        if(arr.indexOf(arr[i]) == i) {
            newArr.push(arr[i]);
        }
    }
    return newArr;
}

/*
* 速度最快, 占空间最多(空间换时间)
*
* 该方法执行的速度比其他任何方法都快, 就是占用的内存大一些。
* 现思路:新建一js对象以及新数组,遍历传入数组时,判断值是否为js对象的键,
* 不是的话给对象新增该键并放入新数组。
* 注意点:判断是否为js对象键时,会自动对传入的键执行“toString()”,
* 不同的键可能会被误认为一样,例如n[val]-- n[1]、n["1"];
* 解决上述问题还是得调用“indexOf”。*/
function uniq(array){
    var temp = {}, r = [], len = array.length, val, type;
    for (var i = 0; i < len; i++) {
        val = array[i];
        type = typeof val;
        if (!temp[val]) {
            temp[val] = [type];
            r.push(val);
        } else if (temp[val].indexOf(type) < 0) {
            temp[val].push(type);
            r.push(val);
        }
    }
    return r;
}
//es6

//方法1
let uniqueArray = [...new Set(arr)];

//方法2
const uniqueArray1 = (arr) => Array.from(new Set(arr));

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象。

Set类似于数组,但是成员的值都是唯一的,没有重复的值。

找出字符串或数组中第一个独立(只出现一次)的字符

/**
example: "strstrabcd@3$~ab" => c
        [1,2,'$',3,3,4,1,2] => $
**/
function getUnique(strOrArr) {
	for(let item of strOrArr) {
		if(strOrArr.indexOf(item) === strOrArr.lastIndexOf(item)) {
			return item;
		}
	}
}

统计字符串中出现次数最多的

function getMaxN(str) {
	let obj = {}, max = 0;
	for(let v of str) {
		obj[v] ? obj[v]++ : obj[v] = 1;
	}
	for(let i in obj) {
		if(obj[i] > max) max = obj[i];
	}
	for(let i in obj) {
		if(obj[i] === max) 
			console.log(`出现最多的字符: ${i}, 最多次数: ${obj[i]}`) ;
	}
}
getMaxN('babcddefghijklmnopqrstuvwxyz0123456789');

将字符串中的分隔符-变成驼峰

//分隔符-变驼峰,get-camel-case -> getCamelCase
//方法1
//replace()如果第2个参数是回调函数,每匹配到一个结果就回调一次,每次回调都会传递以下参数:(1)result: 本次匹配到的结果(2)$1,...$9:正则表达式中有几个(),就会传递几个参数,$1~$9分别代表本次匹配中每个()提取的结果,最多9个(3)offset:记录本次匹配的开始位置(4)ource:接受匹配的原始字符串

function getCamelCase(str) {
	return str.replace(/-([a-z])/g, (v, $1) => $1.toUpperCase());
}
//方法2
function getCamelCase(str) {
	let arr = str.split('-');
	return arr.map((v, i) => {
		if(i === 0) return v;
		return v.charAt(0).toUpperCase() + v.slice(1);
	}).join('');
}
console.log(getCamelCase('get-camel-case')); //getCamelCase

将字符串中的驼峰变分隔符-

//驼峰变分隔符-,getCamelCase -> get-camel-case
//方法1
function getCase(str) {
	return str.replace(/[A-Z]/g, (v) => '-' + v.toLowerCase());
}
//方法2
function getCase(str) {
	let arr = str.split('');
	return arr.map((v, i) => {
		if(v.toUpperCase() === v) {
			return '-' + v.toLowerCase();
		} else {
			return v;
		}
	}).join('');
}
console.log(getCase('getCamelCase')); //get-camel-case

随机生成指定长度的随机字符串

function randomStr(n, str) {
	let newStr = '';
	for (let i=0; i<n; i++) {
		newStr += str.charAt(Math.round(Math.random() * str.length));
	}
	return newStr;
}
console.log(randomStr(10,'abcdefghijklmnopqrstuvwxyz0123456789'));

数字千分位处理

//方法1
let num = 1234567890;
num.toLocaleString(); //"1,234,567,890"
num.toLocaleString('zh', { style: 'currency', currency: 'CNY' }); //"¥1,234,567,890.00"

//方法2
function format (num) {
	return (num+ '').replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g,'$1,');
}
format(1234567890); //"1,234,567,890"

闭包

简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。闭包内最内层函数使用的变量会在自身函数中查找,若找不到向上一级函数查找该变量,以此类推,最后查找全局变量。

for (var i = 0; i < 10; i++) {
	setTimeout(function () {
		console.log(i);
	}, 1000);
}
// 输出 10次10

//解决方法1
for (let i = 0; i < 10; i++) {
	setTimeout(function () {
		console.log(i);
	}, 1000);
}
//解决方法2
for (var i = 0; i < 10; i++) {
	(function() {
		let j = i;
		setTimeout(function () {
		console.log(j);
	}, 1000);
	})();
}

闭包的应用

1.在内存中维持一个变量;2.避免全局变量的污染;3.保护函数内的变量安全,加强了封装性。

闭包的缺陷

闭包会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多。 

解决:闭包不在使用时,要及时释放,将引用内层函数对象的变量赋值为null。

深拷贝 & 浅拷贝

  • 浅拷贝:仅仅是复制了引用,彼此之间的操作会互相影响
  • 深拷贝:在堆中重新分配内存,不同的地址,相同的值,互不影响

主要区别:复制的是引用还是复制的是实例

(1)Array的slice和concat方法和Object.assign()并不是真正的深拷贝,对于第一层的元素这些是深拷贝,而对于第二层元素这些是浅拷贝

let a = [1, 2, 3, 4];
let b = a.slice();
a[0] = 5;
console.log(a); // -> [5, 2, 3, 4]
console.log(b); // -> [1, 2, 3, 4]

let a = [[1, 2], 3, 4];
let b = a.slice();
a[0][0] = 0;
console.log(a); // -> [[0, 2], 3, 4]
console.log(b); // -> [[0, 2], 3, 4]

(2)完全的深拷贝

  • JSON.parse(JSON.stringify(obj))
  • 递归拷贝
function deepCopy(obj) {
	if(!obj || typeof obj !== 'object') throw new Error("Error argument");
	let targetObj = Array.isArray(obj) ? [] : {};
	for(let key in obj) {
		if(obj.hasOwnProperty(key)) {
			if(obj[key] && typeof obj[key] === 'object') {
				targetObj = deepCopy(obj[key]);
			} else {
				targetObj[key] = obj[key];
			}
		}
	}
	return targetObj;
}

微任务与宏任务

宏任务:setTimeout setInterval setImmediate requestAnimationFrame

微任务:Promise.then catch finally process.nextTick MutationObserver

执行顺序: 主线程 -> 微任务 -> 宏任务

//主线程直接执行
console.log('start');
//丢到宏事件队列中
setTimeout(() => {
    console.log('2');
    new Promise(resolve => {
        console.log('4');
        resolve();
    }).then(() => {
        console.log('5')
    })
})
//主线程直接执行
new Promise(resolve => {
    console.log('7');
    resolve();
}).then(() => {
    //微事件2
    console.log('8')
})
//丢到宏事件队列中
setTimeout(function() {
    new Promise(resolve => {
        console.log('11');
        resolve();
    }).then(() => {
        console.log('12')
    })
})
console.log('end');

//上述两个宏任务setTimeout,当第1个宏任务setTimeout执行完所有代码,才会去执行第2个宏任务setTimeout,执行完所有代码,以此类推。
//结果:start 7 end 8 2 4 5 11 12

get和post区别

  • GET请求的数据会附在URL之后,POST把提交的数据则放置在是HTTP包的包体中。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST么有。
  • POST的安全性要比GET的安全性高,get的参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。

Promise

简介

new Promise((resolve, reject) => 异步操作成功 ? resolve(value) : reject(err))

  • Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。
  • Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
  • 状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。

优缺点

  • 优点:1.可以将异步操作以同步操作的流程表达出来,有效的解决了回调地狱的问题;2.Promise对象提供统一的接口,使得控制异步操作更加容易;3.代码直观,可维护性好。
  • 缺点:1.无法取消Promise,一旦新建它就会立即执行,无法中途取消;2.当处于pending状态时,无法得知目前进展到哪一个阶段。

API

  • .then(resolved, rejected) 作用是为 Promise 实例添加状态改变时的回调函数;
  • .resolve() 将现有对象转为 Promise 对象;
  • .reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected;
  • .catch() 用于指定发生错误时的回调函数;
  • .finally() 用于指定不管 Promise 对象最后状态如何,都会执行的操作;
  • .all([p1, p2, p3]) 只要参数实例有一个变成rejected状态,包装实例就会变成rejected状态;如果所有参数实例都变成fulfilled状态,包装实例就会变成fulfilled状态。
  • .race([p1, p2, p3])只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数;
  • .any([p1, p2, p3]) 只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

.any()跟.race()方法很像,只有一点不同,就是不会因为某个Promise变成rejected状态而结束。

Promsie 与事件循环

Promise在初始化时,传入的函数是同步执行的,然后注册 then 回调。注册完之后,继续往下执行同步代码,在这之前,then 中回调不会执行。同步代码块执行完毕后,才会在事件循环中检测是否有可用的 promise 回调,如果有,那么执行,如果没有,继续下一个事件循环。

手写Promise

function Promise(excutor) {
	let self = this
	self.status = 'pending'
	self.value = null
	self.reason = null
	self.onFulfilledCallbacks = []
	self.onRejectedCallbacks = []

	function resolve(value) {
		if (self.status === 'pending') {
			self.value = value
			self.status = 'fulfilled'
			self.onFulfilledCallbacks.forEach(item => item())
		}
	}

	function reject(reason) {
		if (self.status === 'pending') {
			self.reason = reason
			self.status = 'rejected'
			self.onRejectedCallbacks.forEach(item => item())
		}
	}
	try {
		excutor(resolve, reject)
	} catch (err) {
		reject(err)
	}
}


Promise.prototype.then = function (onFulfilled, onRejected) {
	onFulfilled = typeof onFulfilled === 'function' ? onFulfilled :  function (data) {resolve(data)}
	onRejected = typeof onRejected === 'function' ? onRejected : function (err) {throw err}
	let self = this
	if (self.status === 'fulfilled') {
		return new Promise((resolve, reject) => {
			try {
				let x = onFulfilled(self.value)
				if (x instanceof Promise) {
					x.then(resolve, reject)
				} else {
					resolve(x)
				}
			} catch (err) {
				reject(err)
			}
		})
	}
	if (self.status === 'rejected') {
		return new Promise((resolve, reject) => {
			try {
				let x = onRejected(self.reason)
				if (x instanceof Promise) {
					x.then(resolve, reject)
				} else {
					resolve(x)
				}
			} catch (err) {
				reject(err)
			}
		})
	}
	if (self.status === 'pending') {
		return new Promise((resolve, reject) => {
			self.onFulfilledCallbacks.push(() => {
				let x = onFulfilled(self.value)
				if (x instanceof Promise) {
					x.then(resolve, reject)
				} else {
					resolve(x)
				}
			})
			self.onRejectedCallbacks.push(() => {
				let x = onRejected(self.reason)
				if (x instanceof Promise) {
					x.then(resolve, reject)
				} else {
					resolve(x)
				}
			})
		})
	}
}

Promise.prototype.catch = function (fn) {
	return this.then(null, fn)
}

async / await

async 是 Generator 函数的语法糖, 也是异步编程的一种解决方案;返回的是一个 Promise 对象,所接收的值就是函数 return 的值。

在 async 函数内部可以使用 await 命令,表示等待一个异步函数的返回。await 后面跟着的是一个 Promise 对象,如果不是的话,系统会调用 Promise.resolve() 方法,将其转为一个 resolve 的 Promise 的对象。

Ajax的封装(JS,Promise,async/await)

//js
function getJSON(options) {
	//创建一个ajax对象
	var xhr = new XMLHttpRequest() || new ActiveXObject("Microsoft,XMLHTTP");
	//处理数据
	let str = '';
	for (let key in options.data) {
		str += `&${key}=${options.data[key]}`;
	}
	str = str.slice(1);
	//处理类型
	if(options.type === 'GET') {
		xhr.open('get', `${options.url}?${str}`);
		xhr.send();
	} else if(options.type === 'POST') {
		xhr.open('post', options.url);
		xhr.setRequestHeader("content-type","application/x-www-form-urlencoded");
		xhr.send(str);
	}
	xhr.onreadystatechange = () => {
		if(xhr.readyState === 4 && xhr.status === 200) {
			options.success && options.success(JSON.parse(xhr.response));
		} else if(xhr.status !== 200) {
			options.error && options.error(xhr.status);
		}
	}
	
}
getJSON({
	type:'GET',
	url:'./a.json',
	options:{a1:1, b2:2},
	success: data => {
		console.log(data);
		getJSON({
			type:'GET',
			url:`${data.next}.json`,
			options:{a1:1, b2:2},
			success: data1 => {
				console.log(data1);
				getJSON({
					type:'GET',
					url:`${data1.next}.json`,
					options:{a1:1, b2:2},
					success: data2 => {
						console.log(data2);
					}
				})
			}
		})
	}
})  

//Promise
function getJSON(url) {
	return new Promise((resolve, reject) => {
		let xhr = new XMLHttpRequest() || new ActiveXObject("Microsoft,XMLHTTP");
		xhr.open('GET', url);
		xhr.send();
		xhr.onreadystatechange = () => {
			if(xhr.readyState === 4 && xhr.status === 200) {
				resolve(JSON.parse(xhr.response));
			} else if(xhr.status !== 200) {
				reject(xhr.status);
			}
		}
	});
}
getJSON('./a.json').then(data => {
			console.log(data);
			return getJSON(`${data.next}.json`);
		}).then(data1 => {
			console.log(data1);
			return getJSON(`${data1.next}.json`);
		}).then(data2 => console.log(data2));

//async / await
function getJSON(url) {
	return new Promise((resolve, reject) => {
		let xhr = new XMLHttpRequest() || new ActiveXObject("Microsoft,XMLHTTP");
		xhr.open('GET', url);
		xhr.send();
		xhr.onreadystatechange = () => {
			if(xhr.readyState === 4 && xhr.status === 200) {
				resolve(JSON.parse(xhr.response));
			} else if(xhr.status !== 200) {
				reject(xhr.status);
			}
		}
	});
}
async function asyncGetJSON() {
	let data = await getJSON('./a.json');
	console.log(data);
	let data1 = await getJSON(`${data.next}.json`);
	console.log(data1);
	let data2 = await getJSON(`${data1.next}.json`);
	console.log(data2);
}
asyncGetJSON()

函数防抖与节流

  • 函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。如下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。

//wait时间未到时触发mousemove,定时器都会被清掉;当停止mousemove,并且wait时间到,开始执行定时器timer
function debounce(fn, wait) {
	let timer = null;
	return () => {
		if(timer !== null) clearTimeout(timer);
		timer = setTimeout(fn, wait);
	}
}
function handle() {
	console.log(Math.random());
}
window.addEventListener('mousemove', debounce(handle, 1000));
  • 函数节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。如下图,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。
//定时器法
function throttle(fn, delay) {
	let timer = null;
	return () => {
		let that = this, agrs = arguments;
		if(!timer) {
			timer = setTimeout(() => {
				fn.apply(that, agrs);
				timer = null;
			}, delay);
		}
	}
}
function handle() {
	console.log(Math.random());
}
window.addEventListener('mousemove', throttle(handle, 1000));
		
//时间戳法
function throttle(fn, delay) {
	let prev = Date.now();
	return () => {
		let that = this, now = new Date(), args = arguments;
		if(now - prev >= delay) {
			fn.apply(that, args);
			prev = now;
		}
	}
}
function handle() {
	console.log(Math.random());
}
window.addEventListener('mousemove', throttle(handle, 1000));

多return嵌套函数

function multiReturn() {
	console.log(1);
	return () => {
		console.log(2);
		return () => {
			console.log(3);
		}
	}
}
let rel = multiReturn(); //1
let rel1 = rel();        //2
rel1();                  //3

multiReturn();           //1
multiReturn()();         //1 2
multiReturn()()();       //1 2 3

性能优化

  • 将不影响页面渲染的script放到body的底部;

  • 使用defer,defer规定是否对脚本执行进行延迟,直到页面加载为止;

    <script src="test.js" type="text/javascript" defer></script>

  • 动态加载js文件,按需加载js文件;

  • 尽量避免使用非必要的全局变量;

  • 尽可能的减少对象成员的查找次数和嵌套深度;

  • 尽可能的减少DOM的嵌套层数;

  • 最小化DOM的操作次数,尽可能的使用局部变量储存DOM节点;

  • 尽可能的减少回流和重绘;

  • 对于使用外部字体的,可使用font-dispaly:swap;预先加载浏览器默认字体。

从输入URL到页面加载发生了什么

1、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。

2、DNS域名解析。

3、根据IP建立TCP连接(三次握手)。

4、HTTP发起请求。

5、服务器处理请求,浏览器接收HTTP响应。

6、渲染页面,构建DOM树。

7、关闭TCP连接(四次挥手)。

1、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。

HTTP缓存有多种规则,根据是否需要向服务器重新发起请求,分为强制缓存,对比缓存。

强制缓存判断HTTP首部字段:cache-control,Expires。

  • Expires是一个绝对时间,即服务器时间。浏览器检查当前时间,与Expires做对比,如果还没到失效时间就直接使用缓存文件。但是该方法存在一个问题:服务器时间与客户端时间可能不一致。因此该字段已经很少使用。
  • cache-control中的max-age保存一个相对时间。例如Cache-Control: max-age = 1000,表示浏览器收到文件后,缓存在1000s内均有效。
  • 如果同时存在cache-control和Expires,浏览器总是优先使用cache-control。

对比缓存通过HTTP的last-modified,Etag字段进行判断。

  • Last-Modified 是由服务器发送给客户端的HTTP请求头标签; If-Modified-Since 则是由客户端发送给服务器的HTTP请求头标签。
  • last-modified是第一次请求资源时,服务器返回的字段,表示最后一次更新的时间。下一次浏览器请求资源时就发送if-modified-since字段。服务器用本地Last-modified时间与if-modified-since时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送304状态码,让浏览器继续使用缓存。
  • Etag:该字段存储的是文件的特殊标识(一般都是hash生成的),服务器存储着文件的Etag字段,可以在与每次客户端传送If-no-match的字段进行比较。如果相等,则表示未修改,响应304;反之,则表示已修改,响应200状态码,返回数据。

2、DNS域名解析

域名解析的过程实际是将域名还原为IP地址的过程。

首先在本地域名服务器中查询IP地址,如果没有找到的情况下,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也不存在该域名时,本地域名会向com顶级域名服务器发送一个请求,依次类推下去。

3.根据IP建立TCP连接(三次握手)

在获取到IP地址后,便开始建立一次连接,由TCP协议完成,主要通过三次握手进行连接。

第一次握手: 客户端向服务器发出连接请求报文,这时报文首部中的同部位SYN=1,同时随机生成初始序列号 seq=x,此时,客户端进程进入了 SYN-SENT状态,等待服务器的确认。

第二次握手: 服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己随机初始化一个序列号 seq=y,此时,服务器进程进入了SYN-RCVD状态,询问客户端是否做好准备。

第三次握手: 客户端进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,此时,连接建立,客户端进入ESTABLISHED状态,服务器端也进入ESTABLISHED状态。

4.浏览器向服务器发送HTTP请求

HTTP请求报文是由三部分组成: 请求行, 请求报头和请求正文。

  • 请求行:常用的方法有: GET, POST, PUT, DELETE, OPTIONS, HEAD。
  • 请求报头:允许客户端向服务器传递请求的附加信息和客户端自身的信息。
  • 请求正文:当使用POST,PUT等方法时,通常需要客户端向服务器传递数据(储存在请求正文中)。

5.服务器处理请求,浏览器接收HTTP响应

服务器在收到浏览器发送的HTTP请求之后,会将收到的HTTP报文封装成HTTP的Request对象,处理完的结果以HTTP的Response对象返回,主要包括状态码,响应头,响应报文(服务器返回给浏览器的文本信息,通常HTML, CSS, JS,图片等文件就放在这一部分。)三个部分。

6、渲染页面,构建DOM树

浏览器是一个边解析边渲染的过程。首先浏览器解析HTML文件构建DOM树,然后解析CSS文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。

回流(Reflow):元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。

重绘(Repaint):元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等)。

同步任务

因为JavaScript的单线程,因此同个时间只能处理一个任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务,但是,如果前一个任务的执行时间很长,比如文件的读取操作或ajax操作,后一个任务就不得不等着,拿ajax来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验。

异步任务

因此,JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务。

异步任务是不会进入主线程,而是会先进入任务队列(栈)。比如说文件读取操作,因为这是一个异步任务,因此该任务会被添加到任务队列中,等到IO完成后,就会在任务队列中添加一个事件,表示异步任务完成,当主线程处理完其它任务有空时,就会读取任务队列,读取里面有哪些事件,排在前面的事件会被优先进行处理,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码。

事件循环

单线程从从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等待,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环。

浏览器渲染外部资源

浏览器在解析过程中遇到请求外部资源时(图片,JS等)。浏览器将重复1-6过程下载该资源。请求过程是异步的,并不会影响HTML文档进行加载,但是当文档加载过程中遇到JS文件,HTML文档会挂起渲染过程,不仅要等到文档中JS文件加载完毕还要等待解析执行完毕,才会继续HTML的渲染过程。原因是因为JS有可能修改DOM结构,这就意味着JS执行完成前,后续所有资源的下载是没有必要的,这就是JS阻塞后续资源下载的根本原因。CSS文件的加载不影响JS文件的加载,但是却影响JS文件的执行。JS代码执行前浏览器必须保证CSS文件已经下载并加载完毕。

7.关闭TCP连接