阅读 6266

七篇前端面试必考知识点,助你一臂之力

前言

文章大部分内容是做一些知识点的总结,不会面面俱到,对于一些具体的实现步骤和底层原理的代码并不会贴出来,不然篇幅实在是太长啦

不过不必担心,相关知识点的详细讲解会贴出文章链接供大家参考,这些都是博主平常写的笔记和看过的一些优秀的博文,希望能够帮助你查漏补缺,梳理起你的前端知识体系~

文章内容较多,建议先 mark 再看哟。

一、HTML 基础篇

1、doctype 的作用是什么?

  • DOCTYPE 是 html5 标准网页声明,且必须声明在HTML文档的第一行。来告知浏览器的解析器用什么文档标准解析这个文档,不同的渲染模式会影响到浏览器对于 CSS 代码甚至 JavaScript 脚本的解析。

2、HTML、XHTML、XML 有什么区别?

  • HTML(超文本标记语言): 在 html4.0 之前 HTML 先有实现再有标准,导致 HTML 非常混乱和松散
  • XML(可扩展标记语言): 主要用于存储数据和结构,JSON作用类似,但更加轻量高效
  • XHTML(可扩展超文本标记语言): 基于上面两者而来

3、HTML 语义化的理解?

  • 语义化:指使用恰当语义的 html 标签,如 header 标签 代表头部,article 标签代表正文等
  • 好处:增强了可读性、有利于SEO优化

4、常用的 meta 标签?

  • charset,用于描述 HTML 文档的编码形式
<meta charset="UTF-8" >
复制代码
  • http-equiv,相当于http 的文件头作用,比如下面的代码就可以设置 http 的缓存过期日期
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT"复制代码
  • viewport,控制视口的大小和比例
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
复制代码

5、script 标签中 defer 和 async 的区别?

  • defer:script 被异步加载后并不会立刻执行,而是等待文档被解析完毕后执行。
  • async:脚本加载完毕后立即执行

6、前端储存的方式?

  • cookies: 兼容性好,请求头自带 cookie 方便,缺点是大小只有4k,自动请求头加入 cookie 浪费流量,每个 domain 限制20个 cookie,使用起来麻烦需要自行封装
  • localStorage:HTML5 加入的以键值对(Key-Value)为标准的方式,优点是操作方便,永久性储存(除非手动删除),大小为5M,兼容IE8+
  • sessionStorage:与 localStorage 基本类似,区别是 sessionStorage 当页面关闭后会被清理,而且与 cookie、localStorage 不同,他不能在所有同源窗口中共享,是会话级别的储存方式
  • IndexedDB:NoSQL 数据库,用键值对进行储存,可以进行快速读取操作,非常适合 web 场景,同时用 JavaScript 进行操作会非常方便。

二、CSS 篇

1、CSS 盒模型

标准模型:宽高计算不包含 padding 和 border ;通过 box-sizing: content-box; 来设置(浏览器默认)。

IE模型:宽高计算包含 padding 和 border ;通过 box-sizing: border-box; 来设置。

2、BFC(块状格式化上下文)

特点:

  • 是一个独立的容器,里面的元素和外面的元素互不影响;
  • BFC垂直方向的边距会发生重叠;
  • BFC 区域不会与浮动元素区域重叠;
  • 计算 BFC 高度时,浮动元素也参与计算。

创建方式:

  • float 值不为 none;
  • position 的值不为 static 或 relative;
  • display 为 inline-box, table, table-cell 等;
  • overflow 不为 visible

作用:

  • 清除浮动
  • 防止同一 BFC 容器中的相邻元素间的外边距重叠问题

3、实现垂直居中布局

  • 宽高固定
div.parent {
    position: relative; 
}

div.child {
    width: 100px;
    height: 100px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-left: -50px;
    margin-top: -50px;
}
或
div.child {
    width: 100px;
    height: 100px;
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}

复制代码
  • 宽高不固定
div.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}
或
div.parent{
  display:flex;
}
div.child{
  margin:auto;
}
或
div.parent {
    position: relative; 
}
div.child {
    position: absolute; 
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);  
}
或
div.parent {
    display: grid;
}
div.child {
    justify-self: center;
    align-self: center;
}
复制代码

更多布局类型可参考:干货!各种常见布局实现+知名网站实例分析

4、分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景。

  • 结构上:display:none 会从渲染树中消失,元素不占据空间且无法点击;visibility: hidden 不会从渲染树中消失,元素继续占据空间但无法点击;opacity: 0 不会从渲染树消失,元素占据空间且可点击。
  • 继承性:display: none 和 opacity: 0 是非继承属性;父元素设置了 display:none 或 opacity: 0,子元素无论怎么设置都无法显示;visibility: hidden 会被子元素继承,并且子元素可以通过设置设置 visibility: visible; 来取消隐藏。
  • 性能:display: none 会引起重排,性能消耗较大;visibility: hidden 会引起重绘,性能消耗相对较小; opacity: 0 会重建图层,性能较高

5、 link 标签和 import 标签的区别

  • link 属于html 标签,而 @import 是 css 提供的;
  • 页面被加载时,link 会同时被加载,而 @import 引用的 css 会等到页面加载结束后加载;
  • link 方式样式的权重高于 @import 的;
  • link 可以使用 js 动态引入,@import不行;
  • link 此没有兼容性要求,而 @import 要求 IE5 以上才能识别。

6、移动端 Retina 1px 像素问题的解决方案

  • viewport + rem
  • background-image
  • 伪元素 + transform scale()
  • box-shadow

更多内容可参考:7 种方法解决移动端 Retina 屏幕 1px 边框问题

7、文本显示行数控制

  • 单行
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
复制代码
  • 多行
overflow: hidden;
text-overflow: ellipsis;        // 超出显示'...'
display: -webkit-box;           // 将元素作为弹性伸缩盒子模型显示 。
-webkit-line-clamp: 2;          // 用来限制在一个块元素显示的文本的行数
-webkit-box-orient: vertical;   // 设置或检索伸缩盒对象的子元素的排列方式
复制代码

8、清除浮动的方式

1、
.clearfix:after {
  visibility: hidden;
  display: block;
  font-size: 0;
  content: " ";
  clear: both;
  height: 0;
}
2、clear:both
3、overflow:hidden
复制代码

9、transition 和 animate 有何区别?

  • transition:用于做过渡效果,没有帧概念,只有开始和结束状态,性能开销较小
  • animate:用于做动画,有帧的概念,可以重复触发且有中间状态,性能开销较大

10、实现一个扇形

.sector {
  width: 0;
  height: 0;
  border-width: 50px;
  border-style: solid;
  border-color: red transparent transparent;
  border-radius: 50px;
}
复制代码

三、JS篇

1、JS 的内置类型

  • 基本类型:null、undefined、boolean、number、string、symbol
  • 对象(Object):引用类型

tips: NaN 也属于 number 类型,并且 NaN 不等于自身。

2、类型判断

  • Typeof
console.log(typeof 1);                  // number
console.log(typeof 'a');                // string
console.log(typeof true);               // boolean
console.log(typeof undefined);          // undefined
console.log(typeof function fn(){});    // function
console.log(typeof {});                 // object
console.log(typeof null);               // object
console.log(typeof []);                 // object
console.log(typeof new Error());        // object
复制代码

tips:typeof 对于基本类型,除了 null 都可以显示正确的类型;对于对象,除了函数都会显示 object

  • Object.prototype.toString
var number = 1;             // [object Number]
var string = '123';         // [object String]
var boolean = true;         // [object Boolean]
var und = undefined;        // [object Undefined]
var nul = null;             // [object Null]
var obj = {a: 1}            // [object Object]
var array = [1, 2, 3];      // [object Array]
var date = new Date();      // [object Date]
var error = new Error();    // [object Error]
var reg = /a/g;             // [object RegExp]
var func = function a(){};  // [object Function]

function checkType() {
    for (var i = 0; i < arguments.length; i++) {
        console.log(Object.prototype.toString.call(arguments[i]))
    }
}

checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func)
复制代码

更多内容可参考:JavaScript温故而知新——类型判断

3、原型和原型链的理解

  • 原型:每个函数都有 prototype 属性,该属性指向原型对象;使用原型对象的好处是所有对象实例共享它所包含的属性和方法。
  • 原型链:主要解决了继承的问题;每个对象都拥有一个原型对象,通过__proto__ 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。
  • 下图展示了构造函数、原型对象、实例和原型链的关系:

更多内容可参考:JavaScript温故而知新——原型和原型链

4、执行上下文

  • 全局执行上下文
  • 函数执行上下文
  • eval执行上下文

更多内容可参考:JavaScript温故而知新——执行环境和作用域

5、闭包

  • 闭包是指有权访问另一个函数作用域中的变量的函数。
  • 闭包会使得函数内部的变量都被保存在内存中,造成较大的内存开销,因此不要滥用闭包。解决的方法是在退出函数之前将不使用的局部变量置为 null ;

经典面试题:改造下面的代码,使之输出0 - 9

for (var i = 0; i< 10; i++){
	setTimeout(() => {
		console.log(i);
    }, 1000)
}

方法一、利用 setTimeout 函数的第三个参数,会作为回调函数的第一个参数传入
for (var i = 0; i < 10; i++) {
  setTimeout(i => {
    console.log(i);
  }, 1000, i)
}

方法二、使用 let 变量 的特性
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000)
}
等价于
for (let i = 0; i < 10; i++) {
  let _i = i;// const _i = i;
  setTimeout(() => {
    console.log(_i);
  }, 1000)
}

方法三、利用函数自执行的方式,把当前 for 循环过程中的 i 传递进去,构建出块级作用域。
for (var i = 0; i < 10; i++) {
  (i => {
    setTimeout(() => {
      console.log(i);
    }, 1000)
  })(i)
}
复制代码

6、this 指向的问题

this 的指向取决于函数以哪种方式调用:

  • 作用函数调用:非严格模式下 this 指向全局对象,严格模式为 undefined
  • 作用方法调用:this 指向调用函数的对象
  • 构造函数调用:this 指向 new 创建出来的实例对象
  • call()和apply:它们的第一个参数为 this 的指向

具体可参考:JavaScript温故而知新——函数的4种调用方式

  • 补充:箭头函数中的 this
function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())
复制代码

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。

7、call 和 apply 的实现

Function.prototype.call2 = function(context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    
    var result = eval('context.fn(' + args + ')');
    
    delete context.fn
    return result;
}

Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}
复制代码

实现细节可参考:JavaScript温故而知新——call()和apply()的实现

8、bind 的实现

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new TypeError("error");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    
    // 通过一个空函数作一个中转,避免绑定函数的 prototype 的属性被修改
    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
复制代码

实现细节可参考:JavaScript温故而知新——bind()方法的实现

9、new 的实现原理

new 操作符做了什么?

  • 创建一个空对象
  • 然后让这个空对象的__proto__指向函数的原型prototype
  • 执行构造函数中的代码,构造函数中的this指向该对象
  • 如果构造函数有返回值,则以该对象作为返回值。若没有return或return了基本类型,则将新对象作为返回值
function objectFactory() {

    var obj = new Object(),

    Constructor = [].shift.call(arguments);

    obj.__proto__ = Constructor.prototype;

    var ret = Constructor.apply(obj, arguments);

    return typeof ret === 'object' ? ret : obj;

};
复制代码

实现细节可参考:JavaScript温故而知新——new操作符的实现

9、instanceof 的实现

function instance_of(L, R) {
  //L 表示左表达式,R 表示右表达式
  var O = R.prototype; // 取 R 的显示原型
  L = L.__proto__; // 取 L 的隐式原型
  while (true) {
    if (L === null) return false;
    if (O === L)
      // 这里重点:当 O 严格等于 L 时,返回 true
      return true;
    L = L.__proto__;
  }
}
复制代码

10、深浅拷贝

  • 浅拷贝——如果被拷贝对象的元素是基本类型,就会拷贝出一份,并且互不影响。而如果被拷贝对象的元素是对象或者数组,就只会拷贝对象和数组的引用,此时若是在新旧对象上进行修改,都会相互影响。
// 数组浅拷贝:slice()、concat()
// 对象浅拷贝:Object.assign()、ES6的扩展运算符
复制代码
  • 深拷贝——完全的拷贝一个对象,即使嵌套了对象,两者也互相分离,修改对象的属性,也不会影响到另一个。
// 递归实现
function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // 如果是引用类型,则继续遍历
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}
复制代码

当然这只是简单的实现,没有考虑到特殊的情况,如对象或数组中的函数,正则等特殊类型的拷贝等。

// JSON.parse(JSON.stringify)
var arr = [
   { value: 1 },
   { value: 2 },
   { value: 3 }
];
var copyArr = JSON.parse(JSON.stringify(arr))
copyArr[0].value = 0;
console.log(arr);       // [{value: 1}, { value: 2 }, { value: 3 }]
console.log(copyArr);   // [{value: 0}, { value: 2 }, { value: 3 }]
复制代码

上面这种方法简单粗暴,缺点是不能拷贝函数。

了解深拷贝更多实现细节,可以参考:深拷贝的终极探索(90%的人都不知道)

11、防抖和节流的实现(简易版)

  • 防抖:触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
funtion debounce(fn) {
    // 创建一个标记用来存放定时器的返回值
    let timeout = null;
    return function() {
        // 每次触发事件时都取消之前的延时调用方法
        clearTimeout(timeout);
        // 然后又创建一个新的 setTimeout, 这样就能保证 1000ms 间隔内如果重复触发就不会执行 fn 函数
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, 1000);
    };
}
复制代码
  • 节流:高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率
function throttle(fn) {
    // 通过闭包保存一个标记
    let canRun = true;
    return function(){
        // 每次开始执行函数时都先判断标记是否为 true,不为 truereturn
        if (!canRun) return;
        // 上一次定时器执行完后 canRun 为 true,所以要先设置为false
        canRun = false;
        setTimeout(() => {
            fn.apply(this, arguments);
            // 最后在 setTimeout 执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是 false,在开头被 return 掉
            canRun = true;
        }, 1000)
    }
}
复制代码

12、ES5继承的实现

// 组合继承
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
    console.log(this.age);
}
复制代码

13、JS 异步解决方案的发展历程以及优缺点

  • 回调函数(callback)
ajax('XXX1', () => {
    // callback 函数体
    ajax('XXX2', () => {
        // callback 函数体
        ajax('XXX3', () => {
            // callback 函数体
        })
    })
})
复制代码

优点:解决了同步的问题

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

  • Promise
ajax('XXX1')
  .then(res => {
      // 操作逻辑
      return ajax('XXX2')
  }).then(res => {
      // 操作逻辑
      return ajax('XXX3')
  }).then(res => {
      // 操作逻辑
  })
复制代码

优点:解决了回调地狱的问题

缺点:无法取消 Promise ,错误需要通过回调函数来捕获

  • Generator
function *fetch() {
    yield ajax('XXX1', () => {})
    yield ajax('XXX2', () => {})
    yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

// 配合 co 库使用
const co = require('co')

function *fetch() {
    yield ajax('XXX1', () => {})
    yield ajax('XXX2', () => {})
    yield ajax('XXX3', () => {})
}

co(fetch()).then(data => {
    //code
}).fetch(err => {
    //code
})

复制代码

优点:可以控制函数的执行,配合自动执行器 co 模块 简化了手动执行的步骤

缺点:不配合 co 函数库的话使用起来比较麻烦

  • async/await
// async其实是一个语法糖,它的实现就是将 Generator 函数和自动执行器(co),包装在一个函数中
async function test() {
  // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
  // 如果有依赖性的话,其实就是解决回调地狱的例子了
  await fetch('XXX1')
  await fetch('XXX2')
  await fetch('XXX3')
}
read().then((data) => {
    //code
}).catch(err => {
    //code
});

复制代码

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

14、setTimeout、Promise、Async/Await 的区别

对于这个问题首先要弄明白 JS 的事件循环(Event Loop)机制,推荐大家先看一下这篇文章:这一次,彻底弄懂 JavaScript 执行机制

  • setTimeout —— setTimeout的回调函数会放到宏任务队列里,等到执行栈清空以后执行
console.log('script start')	//1. 打印 script start
setTimeout(function() {
    console.log('settimeout')	// 4. 打印 settimeout
})	// 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end')	//3. 打印 script start
// 输出顺序:script start->script end->settimeout
复制代码
  • Promise —— Promise本身是同步的立即执行函数, 当在 executor 中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行。
console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function() {
    console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
复制代码
  • async/await —— async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
async function async1() {
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}

console.log('script start');
async1();
console.log('script end')

// 输出顺序:script start->async1 start->async2->script end->async1 end
复制代码

15、Promise的简单实现

promise 的使用(有关 Promise 的详细用法,可参考 阮一峰老师的ES6文档

var promise = new Promise((resolve,reject) => {
    if (操作成功) {
        resolve(value)
    } else {
        reject(error)
    }
})
promise.then(function (value) {
    // success
},function (value) {
    // failure
})
复制代码

简单实现

function myPromise(constructor) {
    let self = this;
    self.status = "pending"   // 定义状态改变前的初始状态
    self.value = undefined;   // 定义状态为resolved的时候的状态
    self.reason = undefined;  // 定义状态为rejected的时候的状态
    function resolve(value) {
       if(self.status === "pending") {
          self.value = value;
          self.status = "resolved";
       }
    }
    function reject(reason) {
       if(self.status === "pending") {
          self.reason = reason;
          self.status = "rejected";
       }
    }
    // 捕获构造异常
    try {
       constructor(resolve,reject);
    } catch(e) {
       reject(e);
    }
}
复制代码

添加 then 方法

myPromise.prototype.then = function(onFullfilled,onRejected) {
   let self = this;
   switch(self.status) {
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

var p = new myPromise(function(resolve,reject) {
    resolve(1)
});
p.then(function(x) {
    console.log(x) // 1
})
复制代码

有关 Promise 原理的更多细节推荐 Promise实现原理(附源码),这篇文章讲的很仔细,比较好理解。

16、前端模块化发展历程

  • IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
(function(){
  return {
	data:[]
  }
})()
复制代码
  • AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好。
define('./index.js',function(code){
	// code 就是index.js 返回的内容
})
复制代码
  • CMD: 使用 seaJS 来编写模块化,特点:对于依赖的模块是延迟执行,依赖可以就近书写,等到需要用这个依赖的时候再引入这个依赖,支持动态引入依赖文件。
define(function(require, exports, module) {  
  var indexCode = require('./index.js');
});
复制代码
  • CommonJS: nodejs 中自带的模块化。
var fs = require('fs');
复制代码
  • ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js 。
import a from 'a';
复制代码

17、ES6 模块和 CommonJS 模块的差异?

  • ES6模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量;CommonJS 模块,运行时加载。
  • ES6 模块自动采用严格模式,无论模块头部是否写了 "use strict";
  • require 可以做动态加载,import 语句做不到,import 语句必须位于顶层作用域中。
  • ES6 模块中顶层的 this 指向 undefined,CommonJS 模块的顶层 this 指向当前模块。
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

四、网络基础篇

1、HTTP 协议的主要特点

  • 简单快速、灵活、无连接、无状态

2、HTTP 报文的组成部分

  • 请求报文:
    • 请求行 ( http 方法 + 页面地址 + http 协议 + 版本)
    • 请求头( key + value 值)
    • 空行(服务端通过空行来判断下一部分不再是请求头,而当做请求体来解析)
    • 请求体(数据部分)
  • 响应报文:状态行 + 响应头 + 空行 + 响应体

3、HTTP 方法

  • GET => 获取资源
  • POST => 传输资源
  • PUT => 更新资源
  • DELETE => 删除资源
  • HEAD => 获得报文首部

4、POST 和 GET 的区别

  • GET在浏览器回退时是无害的,而POST会再次提交请求 *
  • GET请求会被浏览器主动缓存,而POST不会,除非手动设置 *
  • GET请求参数会被完整保留在浏览器的历史记录里,而POST中的参数不会被保留 *
  • GET请求在URL中传送的参数是有长度限制的,而POST没有限制 *
  • GET参数通过URL传递,POST放在Request body中 *
  • GET请求只能进行 url 编码,而POST支持多种编码方式
  • GET产生的URL地址可以被收藏,而POST不可以
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息

5、HTTP 状态码

  • 1xx:指示信息 —— 表示请求已接受,继续处理

  • 2xx: 成功 —— 表示请求已被成功接受

    • 200:OK,表示从客户端发来的请求在服务器端被正确处理
    • 204:No content,表示请求成功,但响应报文不含实体的主体部分
    • 205:Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
    • 206:Partial Content,客户发送了一个带有Range头的GET请求,服务器完成了它(例如请求较大的文件,如用vedie标签或audio标签播放音视频地址)
  • 3xx:重定向 —— 要完成请求必须进行更进一步操作

    • 301:moved permanently,永久性重定向,表示资源已被分配了新的 URL
    • 302:found,临时性重定向,表示资源临时被分配了新的 URL
    • 303:see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
    • 304:not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
    • 301:temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
  • 4xx:客户端错误——请求有语法错误或请求无法实现

    • 400:bad request,请求报文存在语法错误
    • 401:unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
    • 403:forbidden,表示对请求资源的访问被服务器拒绝
    • 404:not found,表示在服务器上没有找到请求的资源
  • 5xx:服务端错误——服务器未能实现合法的请求

    • 500:internal sever error,表示服务器端在执行请求时发生了错误
    • 501:Not Implemented,表示服务器不支持当前请求所需要的某个功能
    • 503:service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

6、TCP 三次握手和四次挥手的理解

三次握手和四次挥手可以模拟成对讲机通话的过程

  • 三次握手:
A: 你好,我是A
B:收到,我是B
A:好的,我们可以开始通话啦
复制代码
  • 四次挥手:
A:我已经没什么话说了,结束通话吧
B:稍等,我还有最后一句话要说
B:我已经说完啦
A:好的,你可以关掉对讲机了,不用回复了(然后A等待2MSL无回复,也关掉对讲机)
复制代码

更详细的内容可参考:面试 -- 网络 TCP/IP

7、HTTPS

HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

TLS 中的加密:

  • 对称加密 —— 两边拥有相同的秘钥,两边都知道如何将密文加密解密。
  • 非对称加密 —— 有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

HTTPS 握手过程:

  • 第一步,客户端给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
  • 第二步,服务端确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
  • 第三步,客户端确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给服务端。
  • 第四步,服务端使用自己的私钥,获取客户端发来的随机数(即Premaster secret)。
  • 第五步,客户端和服务端根据约定的加密方法,使用前面的三个随机数,生成"对话密钥"(session key),用来加密接下来的整个对话过程。

7、HTTP 2.0的特性

  • 二进制传输:将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码
  • 多路复用:一个 TCP 连接中可以发送多个请求
  • Header 压缩
  • 服务器推送:服务器可以额外的向客户端推送资源,而无需客户端明确的请求

http1.0 http1.1 http2.0特性及区别

8、用户输入 url 到页面呈现的过程

  • 用户输入url
  • 浏览器查找域名的 IP 地址 域名解析(DNS解析)
  • 找到 IP 地址后,建立 TCP 三次握手 ,与目标服务器建立连接
  • 握手成功后,通过规定的协议(http),浏览器向目标主机发送 http 请求,请求数据包
  • 服务器处理收到的请求,将数据返回至浏览器
  • 浏览器收到 HTTP 响应报文
  • 关闭连接 浏览器解析文档
  • 读取页面内容,浏览器渲染,解析html源码
  • 生成 Dom 树、解析 css 样式、js 交互
  • 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了

五、浏览器与 Web 安全篇

1、事件机制

  • 事件触发的三个阶段:
捕获阶段 —— window 往事件触发处传播,遇到注册的捕获事件会触发
目标阶段 —— 传播到事件触发处时触发注册的事件
冒泡阶段 —— 从事件触发处往 window 传播,遇到注册的冒泡事件会触发
复制代码
  • 捕获DOM事件的具体流程:
window对象 => document对象 => html标签 => body标签 => ... => 目标元素(冒泡反之)
复制代码
  • Event 对象的常见应用
event.preventDefault()              // 阻止默认事件,例如a标签的跳转行为
event.stopPropagation()             // 阻止冒泡
event.stopImmediatePropagation()    // 事件响应优先级:例如同一元素绑定不同事件时,触发a事件不让b事件触发
event.currentTarget                 // 当前绑定事件的元素
event.target                        // 当前被点击的元素
复制代码

2、跨域

同源策略:

  • 浏览器出去安全考虑所做的限制,协议、域名、端口有一个不同就是跨域,此时 Ajax 请求不能发送,Cookie、LocalStorage 和 IndexDB 无法获取,DOM也无法获取。

跨域的几种解决方法:

  • JSONP —— 利用 <script> 标签没有跨域限制的特点,通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据。
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
    	console.log(data)
	}
</script>
复制代码

封装一个 jsonp 方法

function jsonp({url, params, cb}) {
    return new Promise((resolve, reject) => {
        //创建script标签
        let script = document.createElement('script');
        //将回调函数挂在 window 上
        window[cb] = function(data) {
            resolve(data);
            //代码执行后,删除插入的script标签
            document.body.removeChild(script);
        }
        //回调函数加在请求地址上
        params = {...params, cb}
        let arrs = [];
        for(let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
    });
}
//使用
function sayHi(data) {
    console.log(data);
}
jsonp({
    url: 'http://localhost:3000/say',
    params: {
        //code
    },
    cb: 'sayHi'
}).then(data => {
    console.log(data);
});
复制代码
  • CORS —— 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
  • document.domain —— 只适用于二级域名相同的情况,如a.test.comb.test.com,只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域。
  • postMessage —— H5新增的跨域通信方式,如窗口 A(http:A.com) 向跨域的窗口 B(http:B.com) 发送信息
# 在A中发送数据
window.postMessage('data', 'http://B.com');
# 在窗口B中监听
window.addEventListener('message', function(event){
    console.log(event.origin);
    console.log(event.source);
    console.log(event.data);
}, false)
复制代码
  • Hash:url 地址中 '#' 后面的部分,hash 的改变页面不会刷新
# 使用场景:当页面A通过iframe或frame嵌入了跨域的页面B
# 在A中的代码:
var B = document.getElementByTagName('iframe');
B.src = B.src + '#' + 'data';
# 在B中的代码:
window.onhashchange = function () {
    var data = window.location.hash;
}
复制代码

3、渲染机制

  • 浏览器渲染过程:
    • 处理 HTML 并构建 DOM 树
    • 处理 CSS 构建 CSS 规则树
    • 将 DOM 树 与 CSS 规则树 合并成一个渲染树
    • 根据渲染树来布局,计算每个节点的位置
    • 调用 GPU 绘制,合成图层,显示在屏幕上

  • 重绘(Repaint)和回流(Reflow)
    • 重绘 —— 当节点需要更改外观而不会影响布局的,比如改变 color
    • 回流 —— 布局或者几何属性需要改变
  • 何时发生重绘和回流(回流必定会发生重绘,重绘不一定会引发回流。)
    • 添加或删除可见的DOM元素
    • 元素的位置发生变化
    • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
    • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
    • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
  • 减少重绘和回流
    • 使用 translate 替代 top
    • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
    • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来
    • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
    • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
    • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
    • CSS 选择符从右往左匹配查找,避免 DOM 深度过深
    • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

重绘和回流更详细的内容可参考:你真的了解回流和重绘吗

4、浏览器的缓存机制

缓存的原理 —— 将请求来的资源存放到本地磁盘当中,下次获取资源则直接在磁盘当中读取而不再去向服务器发送请求。

缓存的分类:

  • 强缓存 —— 通过两种响应头实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200
Expires: Wed, 22 Oct 2018 08:41:00 GMT
// Expires 是 HTTP / 1.0 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
// 并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-control: max-age=30
// Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires 。该属性表示资源会在 30 秒后过期,需要再次请求。
复制代码
  • 协商缓存 —— 如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304。 协商缓存需要客户端和服务端共同实现
    • Last-ModifiedIf-Modified-Since —— 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。(但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag 。)
    • ETagIf-None-Match —— ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified

了解更多有关浏览器缓存机制可参考:深入理解浏览器的缓存机制

5、Web 安全

XSS

  • 基本概念:XSS,也称跨域脚本攻击,英文名 Cross-site scripting
  • 如何攻击:向页面注入恶意的标签或 js 代码来攻击网站。
  • 如何防御:让恶意插入的标签或js代码不可执行,如转义输入输出的内容

CSRF

  • 基本概念:CSRF,也称跨站请求伪造,英文名 Cross-site request forgery
  • 如何攻击:利用用户的登录态发起恶意请求。
  • 如何防御:
    • Get 请求不对数据进行修改
    • 不让第三方网站访问到用户 Cookie
    • 阻止第三方网站请求接口
    • 请求时附带验证信息,比如验证码或者 token

关于 Web 安全更多内容可参考:【面试篇】寒冬求职之你必须要懂的Web安全

六、性能优化篇

1、网络相关

  • DNS 预解析
<link rel="dns-prefetch" href="//host_name_to_prefetch.com" />
复制代码
  • 缓存(见上文浏览器缓存机制)
  • 使用 HTTP / 2.0
  • 预加载 —— 可以将一些不影响首屏但重要的文件延后加载,能够降低首屏加载的时间,缺点是兼容性不好
<link rel="preload" href="http://example.com" />
复制代码
  • 预渲染 —— 将要下载的文件预先在后台渲染
<link rel="prerender" href="http://example.com" />
复制代码

2、优化渲染过程

  • 代码层面的优化(参考浏览器篇如何减少重绘和回流)
  • 懒执行 —— 将某些逻辑放到使用时再进行,可以通过定时器或事件进行唤醒
  • 懒加载 —— 将不关键的资源延后加载,如图片、视频资源等。

3、文件优化

图片优化:

  • 可以用 css 模拟代替的尽量不要用图片
  • 小图片用 base64 格式
  • 雪碧图
  • 选择正确的图片格式
    • 选择使用 WebP 格式,体积较小,缺点是兼容性不好
    • 小图使用 PNG ,图标类可以使用 SVG 代替
    • 照片使用 JPEG

其他文件优化:

  • CSS 文件放在 head
  • 服务端开启文件压缩功能
  • 将 script 标签放在 body 底部,因为 JS 文件执行会阻塞渲染。
  • script 文件异步加载
    • defer:在 script 标签上加上 defer 属性,defer 是在 HTML 解析完之后才会执行,如果是多个,按照加载的顺序依次执行
    • async:在 script 标签上加上 async 属性,async 是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关
  • 对于需要很多时间计算的代码可以考虑使用 Webworker,Webworker 可以让我们另开一个线程执行脚本而不影响渲染。
  • 使用CDN

4、其他

使用 Webpack 优化项目:

  • 对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
  • 开启 tree shaking,移除没有用的代码
  • 优化图片,对于小图可以使用 base64 的方式写入文件中
  • 按照路由拆分代码,实现按需加载
  • 给打包出来的文件名添加哈希,实现浏览器缓存文件

错误监控:

  • 即时运行错误
    • try...catch
    • window.onerror
  • 资源加载错误
    • object.onerror
    • 高级浏览器下的 performance.getEntries() 可以获取已经加载完成的资源进而监控加载失败的资源
  • 跨域的 js 运行错误 —— 在 script 标签增加 crossorigin 属性,然后设置 js 资源响应头 Access-Control-Allow-Origin
  • 打包时生成 sourceMap 文件便于 debug

错误上报

  • 采用 Ajax 通信的方式上报
  • 通过 img 标签的 src 发起一个请求。

七、框架篇

1、如何理解MVVM模式?

顾名思义,即 Model-View-ViewModel 模式

  • View:界面
  • Model:数据模型
  • ViewModel:作为桥梁负责沟通 View 和 Model

优点:

  • 分离视图 (View) 和模型 (Model),降低代码耦合,提高视图或逻辑的重用性
  • 自动更新 DOM,避免了对 DOM 的频繁操作
  • 提高可测试性

缺点:

  • Bug 难以调试,比如在 View 写了一些指令,但这是没办法通过 debug 的手段去调试的
  • 模块较大时,Model 也会变大,容易造成较高的内存开销
  • 对于大型的图形应用层序,视图状态较多,ViewModel 的构建和维护成本也会较高

2、Vue 响应式原理

Vue 采用数据劫持结合发布—订阅模式的方法,通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

  • Observer 遍历数据对象,给所有属性加上 setter 和 getter,监听数据的变化
  • compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  • Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情:
    • 在自身实例化时往属性订阅器 (dep) 里面添加自己
    • 待属性变动 dep.notice() 通知时,调用自身的 update() 方法,并触发 Compile 中绑定的回调

模拟实现可参考 面试题:你能写一个Vue的双向数据绑定吗?,这里就不贴代码了。

3、虚拟DOM(Virtual Dom)实现原理

  • 通过 JS 对象模拟实现 DOM
  • Dom 树的 diff 算法,同层对比(因为实际业务中很少会跨层移动 DOM 元素),需要判断三种情况
    • 没有新的节点,则什么都不用做
    • 新的节点的 tagName 和 key 和旧的不同,直接替换节点
    • 新的节点的 tagName 和 key(可能没有)和旧的相同,则判断属性是否变更,并继续遍历子树
      • 判断属性是否变更
        • 遍历旧的属性列表,查看每个属性是否还存在于新的属性列表中
        • 遍历新的属性列表,判断都存在的属性值是否有变化(同时查看是否出现新的属性)
  • 渲染差异
    • 通过 diff 算法可以得到两个树的差异
    • 深度遍历树,根据差异来进行 DOM 的更新操作

在遍历子节点的时候还有一个列表对比的算法,是针对带有 key并且新旧列表同时存在的节点做处理的。当子节点仅仅只是发生位置改变的情况,如果按照同层对比,它们就会被替换掉,造成较大的 DOM 开销,而列表对比算法会通过对比新旧节点列表的顺序来移动节点进行 DOM 的更新,在一些场合下就可以起到一定的性能优化的作用。

对于 Virtual Dom 详细的代码实现可参考:深度剖析:如何实现一个 Virtual DOM 算法

4、前端路由原理

本质:监听 URL 的变化,然后匹配路由规则,并且无需刷新页面。

实现方式:

  • 基于 Hash —— 兼容性更好,但存在 '#' 不够美观
    • 点击跳转或浏览器历史跳转:触发 hashchange 事件 -> 解析 url -> 匹配到对应的路由规则
    • 手动刷新: 触发 load 事件 -> ...
  • 基于 History API —— HTML5 新路由方案,更加方便可读,兼容性较差
    • 浏览器动作,如前进后退:触发 popstate 事件 -> 解析 url -> 匹配到对应的路由规则
    • 点击跳转:调用 pushState 函数向浏览器历史添加一个状态 -> ...
    • 刷新页面或输入 URL:会向服务器请求,所以使用 history 需要后端配合重定向 -> ...

详细内容可参考:面试官: 你了解前端路由吗?

5、Proxy与Object.defineProperty的优劣势对比

Proxy的优势:

  • 可以直接监听对象而非属性
  • 可以直接监听数组的变化
  • 拦截方式较多
  • Proxy返回一个新对象,可以只操作新对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化

Object.defineProperty的优势如下:

  • 兼容性好,支持IE9

6、Vue 生命周期

指创建初始化数据、编译模板、挂载 DOM、渲染、更新、卸载一系列的过程。

  • beforeCreate:组件实例被创建之初,组件的属性生效之前
  • created:组件实例已经完全创建,属性也绑定,但真实DOM还未生成,$el不可用
  • beforeMount: 在挂载开始之前被调用,相关的render函数首次被调用
  • mounted: el被创建的vm.$el替换,并挂载到实例上去之后调用
  • beforeUpdate: 组件数据更新之前调用,发生在虚拟DOM打补丁之前
  • update:组件数据更新之后
  • activited:keep-alive专属,组件被激活时调用
  • deadctivated: keep-alive专属,组件被销毁时调用
  • beforeDestory:组件销毁前调用
  • destoryed:组件销毁后调用

7、v-if 和 v-show 的区别

  • v-if 切换状态时会造成 dom 的销毁和重建,初始渲染条件为 false 时,将不会渲染元素;
  • v-show 只是简单的控制显隐藏,不管初始条件如何,元素总会被渲染;
  • v-if适用于很少改变条件的场景,v-show适用于频繁切换条件的场景。

8、computed 和 watch 的区别

  • computed:常用于比较消耗性能的计算场景,具有缓存性,getter执行后会被缓存,只有它依赖的属性值变化后,下一次获取的computed值才会重新计算
  • watch:常用于某些数据的监听,观察props$emit或本组件值的变化来执行回调进行后续操作,无缓存性,页面重新渲染时值不变化也会执行。

9、Vue 中的 key 有什么用

  • 带上唯一标识的 key 可以提高 diff 算法的效率,并且在一些复杂场景下的列表组件,可以更加准确的进行更新渲染
  • 在渲染简单的无状态列表组件的场景下,不带 key 实际上性能要比带 key 要好,因为不带 key 的情况下,vue 默认会对节点进行复用,省去了销毁/创建组件的开销。

推荐使用唯一标识作为 key 的原因:

  • 带上唯一的 key 可以保证更新组件的状态是正确的,避免了一些场景下会出现 bug,虽然会增加性能开销,但对于用户而言基本感受不到差距。

10、nextTick

nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。

  • Vue2.4 前使用的是 microtasks
  • 新版本默认使用 microtasks,v-on 中使用 macrotasks
  • macrotasks 的实现:会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout

11、vue 组件间通信的方法

  • props / $emit
  • $children / $parent —— 父组件中用 this.$children 获取到子组件实例数组,子组件中可以使用 this.$parent 获取父组件实例对象。
    • tips:在 #app 上获取 $parent 得到的是 new Vue()的实例,在这之上再获取 $parent 则是 undefined ;而底层子组件获取 $children 得到的是空数组。
  • provide / inject —— vue2.2 新增的 api ,父组件通过 provide 属性来提供变量,然后在子组件中用 inject 来注入变量。
  • ref / $refs —— ref 在普通 DOM 元素上使用,引用的就是 DOM 元素;如果在子组件上,引用的是组件实例
  • $attrs / $listeners —— vue2.4新增,可以进行跨级的组件通信
  • eventBus
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

// A.vue
<template>
  <div>
    <button @click="sendFirstName"></button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js'
export default {
  data(){
    return{
      firstName:'leborn'
    }
  },

  methods:{
    sendFirstName(){
      EventBus.$emit('getFirstName', {
        firstName:this.firstName
      })
    }
  }
}
</script>


// B.vue
<template>
  <div>姓名: {{name}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      name: ''
    }
  },

  mounted() {
    EventBus.$on('getFirstName', param => {
      this.name = param.firstName + 'james';
    })
  }
}
</script>
复制代码

12、vue 项目性能优化

代码层面:

  • 合理适用 v-if 和 v-show
  • 区分 computed 和 watch 的使用
  • v-for 遍历为 item 添加 key
  • v-for 遍历避免同时使用 v-if
  • 通过 addEventListener 添加的事件在组件销毁时要用 removeEventListener 手动移除这些事件的监听
  • 图片懒加载
  • 路由懒加载
  • 第三方插件按需引入
  • SSR服务端渲染,首屏加载速度快,SEO效果好

Webpack 层面优化:

  • 对图片进行压缩
  • 使用 CommonsChunkPlugin 插件提取公共代码
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析,利用 webpack-bundle-analyzer 可视化分析工具

基础的Web技术优化:

  • 开启gzip压缩
  • 浏览器缓存
  • CDN的使用
  • 使用Chrome Performance分析性能

详细内容可参考:Vue 项目性能优化 — 实践指南(网上最全 / 详细)

13、React 生命周期

class ExampleComponent extends React.Component {
  // 用于初始化 state
  constructor() {}
  // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
  // 因为该函数是静态函数,所以取不到 `this`
  // 如果需要对比 `prevProps` 需要单独在 `state` 中维护
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判断是否需要更新组件,多用于组件性能优化
  shouldComponentUpdate(nextProps, nextState) {}
  // 组件挂载后调用
  // 可以在该函数中进行请求或者订阅
  componentDidMount() {}
  // 用于获得最新的 DOM 数据
  getSnapshotBeforeUpdate() {}
  // 组件即将销毁
  // 可以在此处移除订阅,定时器等等
  componentWillUnmount() {}
  // 组件销毁后调用
  componentDidUnMount() {}
  // 组件更新后调用
  componentDidUpdate() {}
  // 渲染组件函数
  render() {}
  // 以下函数不建议使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}
复制代码

14、React 中 setState 什么时候是同步的,什么时候是异步的?

  • 由 React 引发的事件处理(如 onClick ),调用 setState 不会同步更新 this.state,除此之外(addEventListener,setTimeout/setInterval)的 setState 调用会同步执行 this.state 。

原因:在 setState 函数实现中,会根据 isBatchingUpdates 这个变量来决定是否同步更新 this.state。isBatchingUpdates 默认是 false 表示 setState 会同步更新 this.state。但 React 在调用事件处理函数时会调用一个 batchedUpdates 函数,将 isBatchingUpdates 改为 true,此时 setState 不会同步更新 this.state

  • 一道 setState 的笔试题:下面代码会输出什么
class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};
复制代码
  • 第一次和第二次都是在 react 自身生命周期内,isBatchingUpdates 为 true,此时不会直接更新 state,都是输出 0
  • setTimeout 外的两次 setState 会被合并只执行一次,此时 state.val 为 1
  • setTimeout 中会触发 isBatchingUpdates 为 false,此时 setState 同步执行,输出 2, 3

输出:0 0 2 3

结尾

前端的知识点实在是太多太杂了,对于一些没有涉及到的内容,例如 webpack,babel 什么的后续可能会补上。本篇文章如果能帮助到你的话点赞就完事儿了,没能帮到你也敬请见谅哈~

最后建议大家在看一些技术文章的时候可以自己一边做做总结,不然很容易过目即忘的,没错博主就是如此,然后文章如果有不严谨的地方,欢迎大家评论区指出。

参考

关注下面的标签,发现更多相似文章
评论