面试题目1
面试题
https://juejin.im/post/5c8e409ee51d4534977bc557#heading-1
https://juejin.im/post/5d136700f265da1b7c6128db
https://juejin.im/post/5a6547d0f265da3e283a1df7
https://juejin.im/post/5e55272e6fb9a07ca453436f
一、页面布局
(一)三栏布局
float 布局
- 简单且兼容性好,需要清除浮动
<div class="box">
<div class="left" style="float: left;">200px</div>
<div class="right">200px</div>
<div class="center" style="float: right;">自适应</div>
</div>
绝对布局
- 简单且兼容性好,但脱离文档流
<div class="box" style="position: relative;">
<div class="left" style="position: absolute;left: 0;">200px</div>
<div class="right" style="position: absolute;right: 0;">200px</div>
<div class="center" style="margin: 0 200px;">自适应</div>
</div>
flex 布局
- 简单,不支持 IE8 及以下
<div class="box" style="display: flex;">
<div class="left" style="width:200px">200px</div>
<div class="center" style="flex:1">自适应</div>
<div class="right" style="width:200px">200px</div>
</div>
grid布局
- 简单,不支持 IE8 及以下
<style>
.box {
width: 100%;
display: grid;
grid-template-columns: 200px auto 200px;
grid-template-rows: 400px;
}
</style>
<div class="box">
<div class="left">200px</div>
<div class="right">200px</div>
<div class="center">自适应</div>
</div>
(二)水平垂直居中
绝对定位 position: absolute
+ transform
.container {
position: relative;
}
.box {
position:absolute;
top:50%;
left:50%;
transform:translate(-50%, -50%);
/*已知高度宽度,margin-top margin-left设置为本身高度宽度的1/2负值*/
}
绝对定位 position: absolute
+ margin: auto
.container {
width: 500px;
height: 500px;
border: 1px solid #000;
position: relative;
}
.box {
width: 200px;
height: 200px;
border: 1px solid #000;
/*需知道子元素的宽高*/
position: absolute;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
table
组合使用display:table-cell和vertical-align、text-align,使父元素内的所有行内元素水平垂直居中
/*利用 table 的单元格居中效果展示*/
.container {
width: 300px;
height: 300px;
border: 1px solid #000;
display: table-cell;
text-align: center;
vertical-align: middle;
}
.box {
width: 100px;
height: 100px;
display: inline-block;
}
flex
.container {
display:flex;
justify-content: center;
align-items:center;
}
grid
/*方法一:父元素指定子元素的对齐方式*/
.container {
width: 500px;
height: 500px;
display:grid;
justify-content: center;
/*整个内容区域在容器里面的水平位置*/
align-content: center;
/*整个内容区域的垂直位置*/
}
/*方法二:子元素自己指定自己的对齐方式*/
.out{
width: 300px;
height: 300px;
display: grid;
}
.inner{
width: 100px;
height: 100px;
justify-self: center;
/*设置单元格内容的水平位置*/
align-self:center;
/*设置单元格内容的垂直位置*/
}
二、CSS盒模型
(一)基础概念
盒子模型组成:分为内容(content)、填充(padding)、边框(border)和边界(margin)
-
IE盒模型:属性width,height包含content、border和padding
-
W3C标准盒模型:属性width,height只包含内容content,不包含border和padding
(二)设置盒模型
/*设置IE模型*/
box-sizing: border-box;
/*设置标准模型*/
box-sizing: content-box;
(三)块级格式化上下文 Block Formatting Context
页面的基本单位是Box, 而元素的类型和 display 属性,决定了 Box 的类型
不同类型的 Box 会参与不同的 Formatting Context ,Box内的元素会以不同的方式渲染
Formatting Context —— CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用
盒子类型
-
block-level box
- display 属性为 block, list-item, table 等的元素
- 生成 block-level box且参与 block fomatting context
-
inline-level box
- display 属性为 inline, inline-block, inline-table 等的元素
- 生成 inline-level box且参与 inline formatting context
-
run-in box
BFC的布局规则
- 内部的盒子从顶端开始垂直地一个接一个地排列
- 盒子之间垂直的间距是由 margin 决定的
- 在同一个 BFC 中,两个相邻的块级盒子的垂直外边距会发生重叠
- BFC 区域不会和 float box 发生重叠
- BFC 能够识别并包含浮动元素,当计算其区域的高度时,浮动元素也参与计算
- BFC在页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此
创建BFC
(1)根元素
(2)浮动元素float=left|right或inherit(≠none)
(3)绝对定位元素position=absolute或fixed
(4)overflow=hidden|auto或scroll(≠visible)
(5)display=inline-block|flex|inline-flex|table-cell或table-caption
BFC 的作用
-
包含浮动元素(清除浮动)
-
浮动元素会脱离文档流(绝对定位元素也会脱离文档流),导致无法计算准确的高度,这种问题称为高度塌陷
-
解决高度塌陷问题的前提是能够识别并包含浮动元素,也就是清除浮动
解决方法:在容器(container)中创建 BFC
-
-
避免外边距折叠
外边距折叠(Margin collapsing)只会发生在属于同一BFC的块级元素之间。如果它们属于不同的 BFC,它们之间的外边距则不会折叠
三、DOM事件
(一)事件流
- 事件冒泡(Event Bubbling)——从下往上的传播方式,当一个元素(文档中嵌套层次最深的节点)接收到事件的时会将该事件传给自己的父级直到顶部
- 事件捕获(Event Capturing)——与事件冒泡相反
(二)事件模型
DOM0级模型
-
绑定监听函数
- HTML代码中直接绑定:
<button onclick="handleClick()">Press me</button>
- JS代码指定属性值:
element.onclick = function(){}
- HTML代码中直接绑定:
-
删除事件处理
element.onclick = null
-
事件不会传播,即没有事件流的概念
DOM2级模型
-
绑定监听函数
element.addEventListener('click', function(){}, false)
-
删除事件处理
element.removeEventListener('mousedown', handleMouseDown, false)
-
可以添加多个相同事件的处理程序
-
事件传播:2级DOM的事件传播分三个阶段进行
(三)事件对象
当一个事件被触发时,会创建一个事件对象(Event Object), 这个对象里面包含了与该事件相关的属性或者方法。该对象会作为第一个参数传递给监听函数
- DOM事件模型中的事件对象常用属性:
type
用于获取事件类型target
获取事件目标stopPropagation()
阻止事件冒泡preventDefault()
阻止事件默认行为
- IE事件模型中的事件对象常用属性:
type
用于获取事件类型srcElement
获取事件目标cancelBubble
阻止事件冒泡returnValue
阻止事件默认行为
(四)事件代理 / 委托
事件代理本质
- 利用事件冒泡的机制,父级元素可以通过事件对象获取到触发事件的目标元素,可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件
事件代理优势
- 减少事件注册,节省内存占用
- 新增子对象时无需再次对其绑定事件,适合动态添加元素
事件代理实例
<ul id="parent">
<li class="child">one</li>
<li class="child">two</li>
<li class="child">three</li>
</ul>
<script type="text/javascript">
//父元素
var dom= document.getElementById('parent');
//父元素绑定事件,代理子元素的点击事件
dom.onclick= function(event) {
var event= event || window.event;
var curTarget= event.target || event.srcElement;
if (curTarget.tagName.toLowerCase() == 'li') {
//事件处理
}
}
</script>
(五)事件广播
var event= new Event('build');
// listener for the event
element.addEvenetListener('build', function(e) {...}, false);
//Dispatch the event
element.dispatchEvent(event);
四、HTTP协议
五、原型链
(一)原型、构造函数、实例、原型链
- 每个函数都有一个prototype属性指向原型对象。原型对象包含所有实例共享的属性和方法
- 每个原型都有一个constructor指针,用来指向Prototype属性所在的函数
- 浏览器为实例对象提供一个
__proto__
属性,用来指向构造函数的原型对象- 获取实例的原型建议使用
Object.getPrototypeOf(obj)
返回指定对象的原型(内部[[Prototype]]
属性的值)
- 获取实例的原型建议使用
- 当原型对象等于其他类型的实例,形成递进关系直到
Object.prototype
,叫做原型链
(二)instanceof 的原理
instanceof
用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
(三) 判断变量的类型
typeof
- 基础数据类型:除了
null
返回"object"
,其他的正确返回 - 引用类型:除了函数会返回
"function"
,其他的返回"object"
- 基础数据类型:除了
instanceof
Date instanceof Object
Object.prototype.toString.call(..)
——tpyeof
返回object
的对象,内部具有[[class]]属性用于进一步区分类型Object.prototype.toString.call( [1,2,3] ); //"[object Array]"
variableName === void 0
—— 判断是否等于 undefined
(四) new操作符
new调用构造函数的步骤
- 创建空对象
- 将this指向创建的空对象——构造函数作用域赋给空对象
- 执行构造函数的代码——为新对象添加属性
- 返回新对象
手动实现new
function Person(name, age){
this.name = name;
this.age = age;
}
function _new() {
//1.获取构造函数
let Func = [].shift.call(arguments)
//2.创建一个空对象obj,并让其继承Func.prototype
let obj = Object.create(Func.prototype)
// var obj = {}
// obj.__proto__ = Func.prototype
//3.执行构造函数,并将this指向创建的空对象obj
let res = Func.apply(obj, arguments)
//4.如果构造函数返回的值是对象则返回,不是对象则返回创建的对象obj
return typeof res === 'object' ? res : obj
}
var p1 = _new(Person, '小鱼', 18)
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的
__proto__
六、面向对象
(一)面向对象 OOP
- 封装:低耦合高内聚
- 多态:重载和重写
- 重载:方法名相同,形参的个数或类型相同
- 重写:在类的继承中,子类可以重写父类中的方法
- 继承:子类继承父类的方法
(二)创建对象的方法
工厂模式
- 描述:用函数封装以特定接口创建对象的细节
function createPerson(name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName() {
console.log(this.name);
}
return o
}
var p1 = createPerson('小鱼',18,'web');
p1.sayName(); //小鱼
var p2 = createPerson('xiaoyu',22,'java');
p2.sayName(); //xiaoyu
- 缺点:不能识别对象类型
构造函数模式
- 描述:创建自定义的构造函数,从而定义特定类型对象的属性和方法
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function() {
console.log(this.name);
}
}
var p1 = new Person('小鱼', 18, 'Web');
var p2 = new Person('xiaoyu', 22, 'java');
//创建对象具有特定类型
p1.constructor === Person //true
//不同的实例上的同名函数是不相等的
p1.sayName === p2.sayName;
- 缺点:每个实例的属性和方法重新创建,浪费内存空间
如果将构造函数中的函数定义为全局的函数,可以解决同名函数不共享的问题。但是破坏了自定义类型的封装性
原型模式
- 描述:创建的每个函数都有原型属性prototype,指向特定类型所有共享的属性和方法
function Person() {}
Person.prototype = {
name:'小鱼',
age:18,
job:'Web',
sayName:function(){
console.log(this.name);
}
};
var p1 = new Person();
console.log(p1.constructor); //[Function: Object]
- 缺点:对于引用类型的属性,一个实例对其进行修改,也会反映在其他实例上
组合使用构造函数和原型模式
- 描述:构造函数模式定义实例属性,原型模式定义方法和共享的属性
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Shelby','Court'];
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
}
};
var p1 = new Person('小鱼',18,'Web');
var p2 = new Person('xioayu',22,'Java');
p1.friends.push('biu');
console.log(p1.friends); //[ 'Shelby', 'Court', 'biu' ]
console.log(p2.friends); //[ 'Shelby', 'Court' ]
动态原型模式
- 描述:把所有信息都封装在构造函数中,通过判断某一方法是否存在来决定是否需要初始化原型
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Shelby','Court'];
if(typeof this.sayName !== 'function'){
Person.prototype.sayName = function(){
console.log(this.name);
}
}
}
var p1 = new Person('小鱼', 18,'Web');
p1.sayName(); //小鱼
(三)类的继承
构造函数继承
借用构造函数:在子类构造函数内部调用超类构造函数
- 👍 可以定义私有属性方法
- 👍 子类可以传递参数给父类
- ❌ 不能定义共享属性方法/或写在外面失去了封装性
function Parent(name, friends) {
this.name = name
this.friends = friends // 👍 可以定义私有 引用类型不会被共享
this.share = share // ❌ 可以定义公有 但需要放在外部
this.log = log // ❌ 避免重复声明,为了复用需要放在外面
}
// ❌ 公有属性和方法定义在外面失去了封装性
let share = [1, 2, 3]
function log() {
return this.name
}
function Child(name, friends, gender) {
Parent.call(this, name, friends) // 👍 可以在子类传递参数给父类
this.gender = gender
}
原型链继承
原型链模式:通过将子类的原型指向父类的实例实现继承,注意此时子类的constructor
指向了父类
需要手动重新绑定 constructor 而且不能定义私有变量
- 👍 可以定义公有属性方法
- ❌ 无论是定义还是继承都需要手动修改 constructor
- ❌ 封装性一般
- ❌ 不能定义私有属性方法
- ❌ 没办法向父类传递参数
function Parent() {}
Parent.prototype = {
constructor: Parent, // ❌ 需要手动绑定 constructor
name: 'oli', // ❌ 不能定义私有属性,全部都是公有
friends: ['alice', 'troy'], // 👍 可以定义公有属性 所有实例都引用这个
log: function() { // 👍 方法被共享了
return this.name
}
}
function Child() {} // ❌ 没办法向父类传递参数
Child.prototype = new Parent() // 使用 new 操作符创建并重写 prototype
Child.prototype.constructor = Child // ❌ 每次继承都需要手动修改 constructor
组合继承
组合继承模式:上面两者结合,使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承
- 👍 公有的写在原型
- 👍 私有的写在构造函数
- 👍 可以向父类传递参数
- ❌ 需要手动绑定 constructor
- ❌ 封装性一般
- ⚡ 重复调用父类性能损耗
function Parent(name, friends) {
// 😀 私有的写这里
this.name = name // 👍 可以定义私有属性
this.friends = friends // 👍 可以定义公有引用属性不会被共享
}
Parent.prototype = {
// 😀 公有的写这里
constructor: Parent, // ❌ 需要手动绑定 constructor
share: [1, 2, 3], // 👍 这里定义的公有属性会被共享
log: function() { // 👍 方法被共享了
return this.name
}
}
// ❌ 封装性一般
function Child(name, friends, gender) {
Parent.call(this, name, friends) // 👍 可以向父类传递参数 ⚡ 调用一次 Parent
this.gender = gender
}
Child.prototype = new Parent() // 使用 new 操作符创建并重写 prototype ⚡ 调用一次 Parent
Child.prototype.constructor = Child // ❌ 每次继承都需要手动修改 constructor
原型式继承
原型式继承直接使用 ES5 Object.create
方法,
创建一个新对象,使用现有的对象来提供新创建的对象的
__proto__
该方法的原理是创建一个构造函数,构造函数的原型指向对象,然后调用 new 操作符创建实例,并返回这个实例,本质是一个浅拷贝
- 👍 父类方法可以复用
- ❌ 父类引用属性全部被共享
- ❌ 子类不可传递参数给父类
let parent = {
name: 'parent',
share: [1, 2, 3], // ❌ 父类的引用属性全部被子类所共享
log: function() { // 👍 父类方法可以复用
return this.name
}
}
let child = Object.create(parent) // ❌ 子类不能向父类传递参数
寄生式继承
原型式继承的基础上为子类增加属性和方法
- 👍 父类方法可以复用
- 👍 增加了别的属性和方法
- ❌ 父类引用属性全部被共享
- ❌ 子类不可传递参数给父类
let parent = {
name: 'parent',
share: [1, 2, 3],
log: function() {
return this.name
}
}
function create(obj) {
let clone = Object.create(obj) // 本质上还是 Object.create
clone.print = function() { // 增加一些属性或方法
console.log(this.name)
}
return clone
}
let child = create(parent)
寄生组合式继承
杂糅了原型链式、构造函数式、组合式、原型式、寄生式而形成的一种方式:
不必为了指定子类的原型而调用超类的构造函数,需要的无非就是超类原型的一个副本而已
组合继承的方法会调用两次 Parent,一次是在
Child.prototype = new Parent()
,一次是在Parent.call()
。寄生组合式解决了这个问题
- 👍 公有的写在原型
- 👍 私有的写在构造函数
- 👍 可以向父类传递参数
- 👍 不会重复调用父类
- ❌ 需要手动绑定 constructor (如果重写 prototype)
- ❌ 需要调用额外的方法封装性一般
function Parent(name, friends) {
this.name = name // 👍 可以定义私有属性
this.friends = friends // 👍 可以定义公有引用属性不会被共享
}
Parent.prototype = {
constructor: Parent, // ❌ 需要手动绑定 constructor
share: [1, 2, 3], // 👍 这里定义的公有属性会被共享
log: function() { // 👍 方法被共享了
return this.name
}
}
function Child(name, friends, gender) {
Parent.call(this, name, friends) // 👍 可以向父类传递参数 ⚡ 调用了一次 Parent
this.gender = gender
}
function inherit(child, parent) {
let clonePrototype = Object.create(parent.prototype) //创建父类的原型副本
child.prototype = clonePrototype //改变子类的原型
child.prototype.constructor = child //纠正子类的构造函数
}
inherit(Child, Parent)
ES6 继承
提供4个关键字,用于解决上面问题
class
——类声明constructor
——构造函数的声名extends
——继承super
——超类/父类
class Parent {
constructor(name, friends) { // 该属性在构造函数上,不共享
this.name = name
this.friends = friends
}
log() { // 该方法在原型上,共享
return this
}
}
Parent.prototype.share = [1, 2, 3] // 原型上的属性,共享
class Child extends Parent {
constructor(name, friends, gender) {
super(name, friends)
this.gender = gender
}
}
继承方式的比较
继承方式 | 优点 | 缺陷 |
---|---|---|
借用构造函数 | 1.可以定义私有属性 2.子类可以向父类传参 |
1.不能定义公共属性和方法 2.或在在外部失去封装性 |
原型链继承 | 1.可以定义公共属性和方法 | 1.不能定义私有属性和方法 2.不可以向父类传参 3.需要修改子类的constructor |
组合继承 | 1.私有的在构造函数 2.公共的在原型 3.可以向父类传参 |
1.需要修改子类的constructor 2.创建子类调用两次父类的构造函数 |
原型式继承 | 1.父类方法可以复用 | 1.父类引用属性全部被共享 2.不可以向父类传参 |
寄生式继承 | 1.父类方法可以复用 2.增加其他属性和方法 |
1.父类引用属性全部被共享 2.不可以向父类传参 |
寄生组合继承 | 1.相比组合继承不会重复调用父类 | 1.需要修改子类的constructor 2.调用额外的方法封装性一般 |
七、通信类
(一)同源策略及限制
同源策略
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。查看同源策略-MDN
同源策略是浏览器的安全功能用于隔离潜在恶意文件,如果缺少同源策略浏览器容易受到XSS,CSFR等攻击
所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源
不同源的限制
-
Cookie、LocalStorage、IndexDB 无法获取
访问存储在浏览器中的数据,如 localStorage 和 IndexedDB,是以源进行分割。每个源都拥有自己单独的存储空间,一个源中的 JavaScript 脚本不能对属于其它源的数据进行读写操作
Cookies 使用不同的源定义方式。一个页面可以为本域和其父域设置 cookie
-
DOM 无法获取
-
Ajax 请求不能发送(Ajax 只限于同源使用,不能跨域使用)
缺少同源策略的危险场景
(二)前后端如何通信
- Ajax
- WebSocket
- CORS
(三)跨域解决方案
八、安全类
前端安全分两类:CSRF、XSS
常考点:基本概念和缩写、攻击原理、防御措施
九、算法
十、渲染机制
(一)DOCTYPE 及其作用
Doctype作用
<!DOCTYPE>
声明叫做文件类型定义(DTD)- 目的是告知浏览器该文件类型,让浏览器解析器知道使用什么文档类型规范来解析这个文档
<!DOCTYPE>
声明必须在 HTML 文档的第一行,并不是 HTML 标签
呈现模式
严格模式:
- 又称标准模式,是指浏览器按照 W3C 标准解析代码
- 浏览器以其支持的最高标准呈现页面
混杂模式(quirks mod)
- 又称怪异模式或兼容模式,是指浏览器用自己的方式解析代码
- 页面以一种比较宽松的向后兼容(兼容老的版本)的方式显示
- 混杂模式通常模拟老式浏览器的行为以防止老站点无法工作
(二)关键渲染路径
关键渲染路径(Critical Rendering Path)是指与当前用户操作有关的内容
例如用户刚刚打开一个页面,首屏的显示就是当前用户操作相关的内容,具体就是浏览器收到 HTML、CSS 和 JavaScript 等资源并对其进行处理从而渲染出 Web 页面
了解浏览器渲染的过程与原理,很大程度上是为了优化关键渲染路径例如为了保障首屏内容的最快速显示,通常会提到渐进式页面渲染
(三)输入URL到页面渲染完成
从耗时的角度,浏览器请求、加载、渲染一个页面需要花费时间在 DNS 查询、TCP 连接、发送HTTP 请求、服务器处理请求返回报文、客户端解析渲染页面
DNS 解析
DNS解析就是网址到IP地址的转换的过程
- DNS缓存:浏览器
->
操作系统- 浏览器会在缓存中查找URL是否存在,并比较缓存是否过期
- 本地hosts文件
- win操作系统保存在
C:\Windows\System32\drivers\etc
- win操作系统保存在
- 分级查询
- 本地DNS服务器
->
根域名服务器->
COM顶级域名服务器
- 本地DNS服务器
建立TCP连接——三次握手
- 第一次握手:客户端发送
SYN=1
字段和客户端序列号seq=n
,并进入SYN_SENT状态,等待服务器确认; - 第二次握手: 服务器收到客户端的
SYN
字段,返回SYN=1
表示同意建立连接ack=n+1
用于确定收到客户端信息和服务端本身的序号seq=x
,此时服务器进入SYN_RECV状态; - 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包
ack=x+1
,此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手
客户端 -> 服务端:SYN=1(请求进行连接) seq=n(序列号)
服务端 -> 客户端:SYN=1(同意建立连接) ack=n+1(确认收到信息) seq=x(服务端序列号)
客户端 -> 服务端:SYN=0(开始发送信息) ack=x+1(确认收到信息) seq=n+1
客户端发送HTTP 请求
- tcp将
http请求报文
切割为报文段
,并在各个报文上打上标记序号以及端口号,将每个报文段可靠地传给网络层
- 协议在
网络层
通过ip地址找到mac地址(ARP协议
,解析地址,根据通信方的ip地址反查出对应的MAC地址),在各个路由中间进行路由中转
传送到数据链路层
- 服务器端在数据链路层收到数据,按数据链路层→网络层→传输层→应用层顺序逐层发送数据,期间,之前加在数据报上的报头信息被层层解析丢弃
服务器处理请求返回报文
客户端解析渲染页面
断开连接——四次挥手
(四)浏览器渲染页面的过程
HTML解析,构建DOM树
浏览器从网络或硬盘中获得HTML字节数据后会经过以下流程将字节解析为DOM树:
- 字符编码:先将HTML的原始字节数据转换为文件指定编码的字符
- 令牌化:然后浏览器会根据HTML规范来将字符串转换成各种令牌(
<html>
、<body>
、<p>
等标签以及标签中的字符串和属性等都会被转化为令牌,每个令牌具有特殊含义和规则) - 生成节点对象:接着每个令牌都会被转换成定义其属性和规则的对象,即节点对象
- 构建DOM树:最后将节点对象构建成树形结构,即DOM树
字节 -> 字符 -> 令牌-> 节点对象 -> 对象模型
CSS解析,构建CSSOM树
浏览器解析遇到<link>
标签时,浏览器就开始解析CSS,像构建DOM树一样构建CSSOM树
Render Tree 渲染树
在构建了DOM树和CSSOM树之后,浏览器只是拥有2个相互独立的对象集合,DOM树描述的文档结构和内容,CSSOM树描述了对应文档的样式规则
想要渲染出页面,就需要将DOM树、CSSOM树结合在一起,构建Render Tree渲染树
layout布局
渲染树构建好后,浏览器得到了每个节点的内容与样式,下一步就是需要计算每个节点在浏览器窗口的确切位置与大小,即layout布局
布局阶段,从渲染树的根节点开始遍历,采用盒子模型的模式来表示每个节点与其他元素之间的距离,从而确定每个元素在屏幕内的位置与大小
Paint绘制页面
当Layout布局完成后,浏览器会立即发出Paint事件,开始将渲染树绘制成像素,绘制所需要的时间跟CSS样式的复杂度成正比,绘制完成后,用户才能看到页面在屏幕中的最终呈现效果
(五)渲染优化方案
优化渲染关键路径方案
通过优化渲染关键路径,可以优化页面渲染性能,减少页面白屏时间
- 优化JS:JavaScript文件加载会阻塞DOM树的构建,可以给
<script>
标签添加异步属性async
,这样浏览器的HTML解析就不会被js文件阻塞 - 优化CSS:浏览器每次遇到
<link>
标签时,浏览器就需要向服务器发出请求获得CSS文件,然后才继续构建DOM树和CSSOM树,可以合并所有CSS成一个文件,减少HTTP请求,减少关键资源往返加载的时间,优化渲染速度
其他优化方案
- 加载部分HTML:浏览器先加载主要HTML初始化静态部分,动态变化的HTML内容通过Ajax请求加载。这样可以减少浏览器构建DOM树的工作量,让用户感觉页面加载速度很快
- 压缩:对HTML、CSS、JavaScript这些文件去除冗余字符(例如不必要的注释、空格符和换行符等),再进行压缩,减小文件数据大小,加快浏览器解析文件编码
- 图片加载优化
- 小图标合并成雪碧图,进而减少img的HTTP请求次数
- 图片加载较多时,采用懒加载的方案,用户滚动页面可视区时再加载渲染图片
- HTTP缓存
(六)回流与重绘
回流必将引起重绘,重绘不一定会引起回流。回流的成本比重绘高很多
回流 (Reflow)
当元素的几何尺寸或结构发生改变,需要对Render Tree
重新验证并计算。浏览器重新渲染部分或全部文档
回流需要重新计算Render Tree
而每一个DOM Tree
都有一个reflow
方法
一旦某个节点发生重排,就有可能导致子元素和父元素甚至是同级其他元素的reflow
导致回流的操作
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的
DOM
元素 - 激活
CSS
伪类(例如::hover
) - 查询某些属性或调用某些方法
导致回流的属性和方法
重绘 (Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时称为重绘
例如:color
、background-color
、visibility
等,浏览器会将新样式赋予给元素并重新绘制
导致重绘的属性和方法
性能优化
CSS
- 对动画元素使用
absolute / fixed
属性 - 隐藏在屏幕外或在页面滚动时,停止动画
- 避免使用
CSS
表达式(例如:calc()
) - 尽量少使用
dispaly:none
,可以使用visibility:hidden
代替,dispaly:none
会造成重排,visibility:hidden
会造成重绘 - 避免使用
table
布局 - 避免设置大量的
style
属性,通过style
属性改变样式触发重排。
JavaScript
- 使用
resize
事件时,做防抖和节流处理 - 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中 - 用事件委托来减少事件处理器的数量
十一、JS运行机制
(一)进程与线程的区分
进程——工厂
- cpu资源分配的最小单位,具有独立的一块内存空间
- 进程之间相互独立
- 不同进程之间也可以通信,不过代价较大
- 一个进程由一个或多个线程组成
线程——工人
- 线程是cpu调度的最小单位
- 多个线程在进程中协作完成任务
- 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
(二)多进程的浏览器
浏览器是多进程的
- Browser 进程:浏览器的主进程负责协调和主控
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 网络资源的管理,下载等
- GPU 进程:用于 3D 绘制等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- 浏览器渲染进程:
- 内部是多线程的,每打开一个新网页就会创建一个进程
- 主要用于页面渲染,脚本执行,事件处理等
浏览器渲染进程
浏览器的渲染进程是多线程的
- GUI渲染线程:
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起。GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
- JS引擎线程
- JS引擎线程负责解析Javascript脚本,运行代码
- JS执行的时间过长会造成页面的渲染不连贯,导致页面渲染加载阻塞
- 事件触发线程
- 归属于浏览器,用来控制事件循环
- JS引擎执行代码块如setTimeOut、AJAX异步请求等,会将对应任务添加到事件线程中
- 定时触发器线程
setInterval
与setTimeout
所在线程- 通过单独线程来计时并触发(添加到事件队列中)
- 异步http请求线程
(三)单线程的 JavaScript
单线程是指在 JavaScript 引擎中负责解释和执行 JavaScript 代码的线程唯一,同一时间上只能执行一件任务
单线程为了防止防止的同步问题,如一个线程在某个DOM节点上添加内容,另一个线程删除该节点
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM
Call Stack 调用栈
调用栈是解释器(如浏览器中 JavaScript 解释器)追踪函数执行流的一种机制
也称执行栈,拥有后进先出(LIFO)的数据结构,被用来存储代码运行时创建的所有执行上下文
JvaScript 是一种单线程编程语言,这意味着它只有一个 Call Stack 。因此,它一次仅能做一件事
当V8引擎遇到你编写的代码时,会创建全局的执行上下文并压入当前调用栈中,每当引擎遇到一个函数调用,它会为该函数创建一个新的函数执行上下文并压入栈的顶部
引擎会执行位于栈顶的函数,正在调用栈中执行的函数如果调用了其他函数,新函数也将添加到调用栈顶,立即执行
当前函数执行完毕后,解释器将该函数执行上下文从栈中弹出,继续执行当前执行环境下的剩余的代码
当分配的调用栈空间被占满时,会引发“堆栈溢出”错误
function first() {
console.log('Inside first function');
second();
}
function second() {
console.log('Inside second function');
}
first();
任务队列 / 消息队列
由于 JavaScript 单线程,理论上当某段代码运行变慢(比如网络请求、下载图片)时就会发生阻塞,导致浏览器不能执行后面的简单操作。所以任务分为同步任务和异步任务
- 同步任务:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
- 异步任务:
- 主线程之外,事件触发线程管理着一个
任务队列
,存在于任务队列(task queue)的任务为异步任务 - 只要异步任务有了运行结果,就在任务队列之中放置一个事件
- 异步任务
- setTimeout 和 setInterval
- DOM 事件
- Promise
- 网络请求
- I/O
- 主线程之外,事件触发线程管理着一个
异步执行的运行机制
一旦执行栈
中的所有同步任务执行完毕,系统就会读取任务队列
,将可运行的异步任务添加到可执行栈中,开始执行
异步与事件
消息队列中的每条消息实际上都对应着一个事件,DOM事件是一个重要的异步过程
var button = document.getElementById('button')
button.addEventListener('click', function(e) {
console.log('事件')
})
从异步的角度看,addEventListener
函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。
事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放在消息队列中,等待主线程执行。
宏任务 & 微任务
除了广义的同步任务和异步任务,我们对任务有更精细的定义
- macro-task(宏任务):整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue
事件循环的顺序
- 先执行所有同步任务
- 遇到的异步任务分发到对应Event Queue
- 主线程任务执行完毕
- 先执行微任务Event Queue :
- 再执行宏任务Event Queue
十二、页面性能
(一)提升页面性能的方法
- 资源压缩合并,减少http请求
- 非核心代码异步加载
- 利用浏览器缓存
- 使用CDN
- 预解析DNS
异步加载
异步加载的方式
- 动态脚本加载
- defer
- async
异步加载的区别
当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载
当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发load事件
- defer
- 脚本会被延迟到整个页面都解析完毕后再运行,相当于告诉浏览器立即下载,但延迟执行
- 如果是多个,按照加载的顺序依次执行
defer
脚本会在DOMContentLoaded
和load
事件之前执行
- async
- 只适用于外部脚本文件,并告诉浏览器立即下载文件
- 如果是多个,下载和执行是异步的,不能确保彼此的先后顺序
async
会在load
事件之前执行,但并不能确保与DOMContentLoaded
的执行先后顺序
绿色线代表 HTML 解析,蓝色线代表网络读取,红色线代表执行时间,蓝红线是针对脚本的;
浏览器缓存
预解析DNS
//强制打开 <a> 标签的 dns 解析
<meta http-equiv="x-dns-prefetch-controller" content="on">
//DNS预解析
<link rel="dns-prefetch" href="//host_name_to_prefetch.com">
dns-prefeth
使得域名转化ip地址的工作提前进行,缩短了请求资源的耗时
什么时候使用呢?在页面中使用其他域名的资源时,可以将静态资源放在cdn上,就可以对cdn的域名进行预解析
十三、错误监控
十四、防抖节流
十五、ES6常见用法
let和const
用于声明块级作用域的变量且不具备变量提升
- 块级作用域内有效,不会污染全局变量
- 存在暂时性死区,不能变量提升
let
用于声明变量const
用于声明常量, 一经声明不能改变- 引用类型保证指向内存地址不改变,堆中的数据可以改变
解构赋值
数组的结构赋值——数组是按位置匹配
let [a, b, c] = [1, 2, 3];
对象的结构赋值——对象是按属性名匹配
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
交换变量的值—— [a,b] = [b,a]
服务器返回数据时,提取 JSON 数据—— let {data, code} = res
输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
模板字符串
使得字符串的拼接更加的简洁,支持变量、HTML文档与换行
let a = "小鱼"
let b = `你好,${name}`
箭头函数
用于简化函数表达式和改善函数this指向的问题
箭头函数的
this
是在定义函数时绑定的,不是在执行过程中绑定的函数在定义时,
this
就继承了定义函数的对象。this
一旦确定以后不会改变
var sum = (num1, num2) => num1 + num2;
扩展运算符(...)
用于函数(剩余参数)——用于获取函数的多余参数,不需要使用arguments
对象
function add(...values) {
let sum = 0;
for (var val of values) { //数组
sum += val;
}
return sum;
}
用于数组或字符串——将内容依次取出
var str = "asdfghjkl"
console.log(...str) //a s d f g h j k l
var arr = [1,2,3,4,5,6]
console.log(...arr) //1 2 3 4 5 6
//复制数组
let arr2 = [...arr1]
//合并数组
[...arr1, ...arr2, ...arr3]
用于对象——克隆或者属性拷贝
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }
var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }
类(class)
让面向对象编程变得更加简单和易于理解
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
sayHi() {
alert(`my name is ${this.name}`)
}
}
class Worker extends Person {
constructor(name,age,job) {
super(name,age)
this.job = job
}
sayJob() {
alert(`my job is ${this.job}`)
}
}
模块化(Module)
每一个模块都有自己单独的作用域
- 为模块创造了命名空间,防止函数的命名冲突
模块的功能主要由 export 和 import 组成
- 通过 export 来规定模块对外暴露的接口
- 通过import来引用其它模块提供的接口
Promise
异步编程的一种解决方案,比传统的解决方案callback更加的优雅
Promise
是一个容器,保存着在某个未来结束的异步操作的结果
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then().catch().finally()
-
成功调用
resolve
,失败调用reject
-
.then
获取结果,.catch
捕获异常。捕获异常还可通过.then
的第二个参数 -
.finally
无论成功失败都一定会调用 -
多个并发的请求,用
Promise.all()
let p = Promise.all([p1,p2,p3]) p.then(([res1, res2,res3]) => {};
- 只有
p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数 - 只要
p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数
- 只有
async/await
async
- 用于申明一个 function 是异步的
async
函数的返回值是 Promise 对象
await
- 用于等待一个异步方法执行完成
await
只能出现在 async 函数中
const axios = require("axios");
async function getZhihuTopSearch(id) {
const url = "https://www.zhihu.com/api/v4/search/top_search";
//将异步操作的值赋给变量
const response = await axios(url);
console.log(response);
}
getZhihuTopSearch(5);
十六、移动端的 H5 兼容性和适配
十七、Vue 相关问题
https://juejin.im/post/5d59f2a451882549be53b170