前端面试问题答案汇总--进阶篇

转载于https://github.com/poetries/FE-Interview-Questions,by poetriesjavascript

1、JS

#1 谈谈变量提高

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。css

b() // call b console.log(a) // undefined var a = 'Hello world' function b() { console.log('call b') } 

想必以上的输出你们确定都已经明白了,这是由于函数和变量提高的缘由。一般提高的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于你们理解。可是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是建立的阶段,JS 解释器会找出须要提高的变量和函数,而且给他们提早在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明而且赋值为 undefined,因此在第二个阶段,也就是代码执行阶段,咱们能够直接提早使用html

  • 在提高的过程当中,相同的函数会覆盖上一个函数,而且函数优先于变量提高
b() // call b second function b() { console.log('call b fist') } function b() { console.log('call b second') } var b = 'Hello world' 

var 会产生不少错误,因此在 ES6中引入了 letlet不能在声明前使用,可是这并非常说的 let 不会提高,let提高了,在第一阶段内存也已经为他开辟好了空间,可是由于这个声明的特性致使了并不能在声明前使用前端

#2 bind、call、apply 区别

  • call 和 apply 都是为了解决改变 this 的指向。做用都是相同的,只是传参的方式不一样。
  • 除了第一个参数外,call 能够接收一个参数列表,apply 只接受一个参数数组
let a = { value: 1 } function getValue(name, age) { console.log(name) console.log(age) console.log(this.value) } getValue.call(a, 'yck', '24') getValue.apply(a, ['yck', '24']) 

bind 和其余两个方法做用也是一致的,只是该方法会返回一个函数。而且咱们能够经过 bind 实现柯里化java

#3 如何实现一个 bind 函数

对于实现如下几个函数,能够从几个方面思考node

  • 不传入第一个参数,那么默认为 window
  • 改变了 this 指向,让新的对象能够执行该函数。那么思路是否能够变成给新的对象添加一个函数,而后在执行完之后删除?
Function.prototype.myBind = function (context) { if (typeof this !== 'function') { throw new TypeError('Error') } var _this = this var args = [...arguments].slice(1) // 返回一个函数 return function F() { // 由于返回了一个函数,咱们能够 new F(),因此须要判断 if (this instanceof F) { return new _this(...args, ...arguments) } return _this.apply(context, args.concat(...arguments)) } } 

#4 如何实现一个 call 函数

Function.prototype.myCall = function (context) { var context = context || window // 给 context 添加一个属性 // getValue.call(a, 'yck', '24') => a.fn = getValue context.fn = this // 将 context 后面的参数取出来 var args = [...arguments].slice(1) // getValue.call(a, 'yck', '24') => a.fn('yck', '24') var result = context.fn(...args) // 删除 fn delete context.fn return result } 

#5 如何实现一个 apply 函数

Function.prototype.myApply = function (context) { var context = context || window context.fn = this var result // 须要判断是否存储第二个参数 // 若是存在,就将第二个参数展开 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result } 

#6 简单说下原型链?

  • 每一个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。
  • 每一个对象都有 __proto__ 属性,指向了建立该对象的构造函数的原型。其实这个属性指向了 [[prototype]],可是 [[prototype]]是内部属性,咱们并不能访问到,因此使用 _proto_来访问。
  • 对象能够经过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象链接起来组成了原型链。

#7 怎么判断对象类型

  • 能够经过 Object.prototype.toString.call(xx)。这样咱们就能够得到相似 [object Type] 的字符串。
  • instanceof 能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype

#8 箭头函数的特色

function a() { return () => { return () => { console.log(this) } } } console.log(a()()()) 

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

#9 This

function foo() { console.log(this.a) } var a = 1 foo() var obj = { a: 2, foo: foo } obj.foo() // 以上二者状况 `this` 只依赖于调用函数前的对象,优先级是第二个状况大于第一个状况 // 如下状况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a) // 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new 

#10 async、await 优缺点

async 和 await 相比直接使用 Promise 来讲,优点在于处理 then 的调用链,可以更清晰准确的写出代码。缺点在于滥用 await 可能会致使性能问题,由于 await 会阻塞代码,也许以后的异步代码并不依赖于前者,但仍然须要等待前者完成,致使代码失去了并发性git

下面来看一个使用 await 的代码。github

var a = 0 var b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 a = (await 10) + a console.log('3', a) // -> '3' 20 } b() a++ console.log('1', a) // -> '1' 1 
  • 首先函数b 先执行,在执行到 await 10 以前变量 a 仍是 0,由于在 await 内部实现了 generators ,generators 会保留堆栈中东西,因此这时候 a = 0 被保存了下来
  • 由于 await 是异步操做,遇到await就会当即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,因此会先执行 console.log('1', a)
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • 而后后面就是常规执行代码了

#11 generator 原理

Generator 是 ES6中新增的语法,和 Promise 同样,均可以用来异步编程web

// 使用 * 表示这是一个 Generator 函数 // 内部能够经过 yield 暂停代码 // 经过调用 next 恢复执行 function* test() { let a = 1 + 2; yield 2; yield 3; } let b = test(); console.log(b.next()); // > { value: 2, done: false } console.log(b.next()); // > { value: 3, done: false } console.log(b.next()); // > { value: undefined, done: true } 

从以上代码能够发现,加上 *的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数能够继续执行被暂停的代码。如下是 Generator 函数的简单实现

// cb 也就是编译过的 test 函数 function generator(cb) { return (function() { var object = { next: 0, stop: function() {} }; return { next: function() { var ret = cb(object); if (ret === undefined) return { value: undefined, done: true }; return { value: ret, done: false }; } }; })(); } // 若是你使用 babel 编译后能够发现 test 函数变成了这样 function test() { var a; return generator(function(_context) { while (1) { switch ((_context.prev = _context.next)) { // 能够发现经过 yield 将代码分割成几块 // 每次执行 next 函数就执行一块代码 // 而且代表下次须要执行哪块代码 case 0: a = 1 + 2; _context.next = 4; return 2; case 4: _context.next = 6; return 3; // 执行完毕 case 6: case "end": return _context.stop(); } } }); } 

#12 Promise

  • Promise 是 ES6 新增的语法,解决了回调地狱的问题。
  • 能够把 Promise当作一个状态机。初始是 pending 状态,能够经过函数 resolve 和 reject,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。
  • then 函数会返回一个 Promise 实例,而且该返回值是一个新的实例而不是以前的实例。由于 Promise 规范规定除了 pending 状态,其余状态是不能够改变的,若是返回的是一个相同实例的话,多个 then 调用就失去意义了。 对于 then 来讲,本质上能够把它当作是 flatMap

#13 如何实现一个 Promise

// 三种状态 const PENDING = "pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; // promise 接收一个函数参数,该函数会当即执行 function MyPromise(fn) { let _this = this; _this.currentState = PENDING; _this.value = undefined; // 用于保存 then 中的回调,只有当 promise // 状态为 pending 时才会缓存,而且每一个实例至多缓存一个 _this.resolvedCallbacks = []; _this.rejectedCallbacks = []; _this.resolve = function (value) { if (value instanceof MyPromise) { // 若是 value 是个 Promise,递归执行 return value.then(_this.resolve, _this.reject) } setTimeout(() => { // 异步执行,保证执行顺序 if (_this.currentState === PENDING) { _this.currentState = RESOLVED; _this.value = value; _this.resolvedCallbacks.forEach(cb => cb()); } }) }; _this.reject = function (reason) { setTimeout(() => { // 异步执行,保证执行顺序 if (_this.currentState === PENDING) { _this.currentState = REJECTED; _this.value = reason; _this.rejectedCallbacks.forEach(cb => cb()); } }) } // 用于解决如下问题 // new Promise(() => throw Error('error)) try { fn(_this.resolve, _this.reject); } catch (e) { _this.reject(e); } } MyPromise.prototype.then = function (onResolved, onRejected) { var self = this; // 规范 2.2.7,then 必须返回一个新的 promise var promise2; // 规范 2.2.onResolved 和 onRejected 都为可选参数 // 若是类型不是函数须要忽略,同时也实现了透传 // Promise.resolve(4).then().then((value) => console.log(value)) onResolved = typeof onResolved === 'function' ? onResolved : v => v; onRejected = typeof onRejected === 'function' ? onRejected : r => throw r; if (self.currentState === RESOLVED) { return (promise2 = new MyPromise(function (resolve, reject) { // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行 // 因此用了 setTimeout 包裹下 setTimeout(function () { try { var x = onResolved(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }); })); } if (self.currentState === REJECTED) { return (promise2 = new MyPromise(function (resolve, reject) { setTimeout(function () { // 异步执行onRejected try { var x = onRejected(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }); })); } if (self.currentState === PENDING) { return (promise2 = new MyPromise(function (resolve, reject) { self.resolvedCallbacks.push(function () { // 考虑到可能会有报错,因此使用 try/catch 包裹 try { var x = onResolved(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (r) { reject(r); } }); self.rejectedCallbacks.push(function () { try { var x = onRejected(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (r) { reject(r); } }); })); } }; // 规范 2.3 function resolutionProcedure(promise2, x, resolve, reject) { // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用 if (promise2 === x) { return reject(new TypeError("Error")); } // 规范 2.3.2 // 若是 x 为 Promise,状态为 pending 须要继续等待不然执行 if (x instanceof MyPromise) { if (x.currentState === PENDING) { x.then(function (value) { // 再次调用该函数是为了确认 x resolve 的 // 参数是什么类型,若是是基本类型就再次 resolve // 把值传给下个 then resolutionProcedure(promise2, value, resolve, reject); }, reject); } else { x.then(resolve, reject); } return; } // 规范 2.3.3.3.3 // reject 或者 resolve 其中一个执行过得话,忽略其余的 let called = false; // 规范 2.3.3,判断 x 是否为对象或者函数 if (x !== null && (typeof x === "object" || typeof x === "function")) { // 规范 2.3.3.2,若是不能取出 then,就 reject try { // 规范 2.3.3.1 let then = x.then; // 若是 then 是函数,调用 x.then if (typeof then === "function") { // 规范 2.3.3.3 then.call( x, y => { if (called) return; called = true; // 规范 2.3.3.3.1 resolutionProcedure(promise2, y, resolve, reject); }, e => { if (called) return; called = true; reject(e); } ); } else { // 规范 2.3.3.4 resolve(x); } } catch (e) { if (called) return; called = true; reject(e); } } else { // 规范 2.3.4,x 为基本类型 resolve(x); } } 

#14 == 和 ===区别,什么状况用 ==

这里来解析一道题目 [] == ![] // -> true ,下面是这个表达式为什么为 true 的步骤

// [] 转成 true,而后取反变成 false [] == false // 根据第 8 条得出 [] == ToNumber(false) [] == 0 // 根据第 10 条得出 ToPrimitive([]) == 0 // [].toString() -> '' '' == 0 // 根据第 6 条得出 0 == 0 // -> true 

===用于判断二者类型和值是否相同。 在开发中,对于后端返回的 code,能够经过 ==去判断

#15 基本数据类型和引⽤类型在存储上的差异

前者存储在栈上,后者存储在堆上

#16 浏览器 Eventloop 和 Node 中的有什么区别

众所周知 JS 是门非阻塞单线程语言,由于在最初 JS 就是为了和浏览器交互而诞生的。若是 JS 是门多线程的语言话,咱们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另外一个线程中删除节点),固然能够引入读写锁解决这个问题。

  • JS 在执行的过程当中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。若是遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,因此本质上来讲 JS 中的异步仍是同步行为
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); console.log('script end'); 
  • 以上代码虽然 setTimeout 延时为 0,其实仍是异步。这是由于 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增长。因此 setTimeout仍是会在 script end 以后打印。
  • 不一样的任务源会被分配到不一样的 Task队列中,任务源能够分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); new Promise((resolve) => { console.log('Promise') resolve() }).then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); // script start => Promise => script end => promise1 => promise2 => setTimeout 
  • 以上代码虽然 setTimeout 写在 Promise 以前,可是由于 Promise 属于微任务而 setTimeout属于宏任务,因此会有以上的打印。
  • 微任务包括 process.nextTick ,promise ,Object.observeMutationObserver
  • 宏任务包括 script , setTimeout ,setIntervalsetImmediate ,I/O ,UI renderin

不少人有个误区,认为微任务快于宏任务,实际上是错误的。由于宏任务中包括了 script,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务

因此正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务须要执行
  • 执行全部微任务
  • 必要的话渲染 UI
  • 而后开始下一轮 Event loop,执行宏任务中的异步代码

经过上述的 Event loop 顺序可知,若是宏任务中的异步代码有大量的计算而且须要操做 DOM 的话,为了更快的 界面响应,咱们能够把操做 DOM 放入微任务中

#17 setTimeout 倒计时偏差

JS 是单线程的,因此 setTimeout 的偏差实际上是没法被彻底解决的,缘由有不少,多是回调中的,有多是浏览器中的各类事件致使。这也是为何页面开久了,定时器会不许的缘由,固然咱们能够经过必定的办法去减小这个偏差。

// 如下是一个相对准备的倒计时实现 var period = 60 * 1000 * 60 * 2 var startTime = new Date().getTime(); var count = 0 var end = new Date().getTime() + period var interval = 1000 var currentInterval = interval function loop() { count++ var offset = new Date().getTime() - (startTime + count * interval); // 代码执行所消耗的时间 var diff = end - new Date().getTime() var h = Math.floor(diff / (60 * 1000 * 60)) var hdiff = diff % (60 * 1000 * 60) var m = Math.floor(hdiff / (60 * 1000)) var mdiff = hdiff % (60 * 1000) var s = mdiff / (1000) var sCeil = Math.ceil(s) var sFloor = Math.floor(s) currentInterval = interval - offset // 获得下一次循环所消耗的时间 console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) // 打印 时 分 秒 代码执行时间 下次循环间隔 setTimeout(loop, currentInterval) } setTimeout(loop, currentInterval) 

#18 数组降维

[1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4] 

若是想将一个多维数组完全的降维,能够这样实现

const flattenDeep = (arr) => Array.isArray(arr) ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , []) : [arr] flattenDeep([1, [[2], [3, [4]], 5]]) 

#19 深拷贝

这个问题一般能够经过 JSON.parse(JSON.stringify(object)) 来解决

let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE 

可是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
let obj = { a: 1, b: { c: 2, d: 3, }, } obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.c let newObj = JSON.parse(JSON.stringify(obj)) console.log(newObj) 复 

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = { age: undefined, sex: Symbol('male'), jobs: function() {}, name: 'yck' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "yck"} 

可是在一般状况下,复杂数据都是能够序列化的,因此这个函数能够解决大部分问题,而且该函数是内置函数中处理深拷贝性能最快的。固然若是你的数据中含有以上三种状况下,可使用 lodash 的深拷贝函数

#20 typeof 于 instanceof 区别

typeof 对于基本类型,除了 null均可以显示正确的类型

typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 没有声明,可是还会显示 undefined 

typeof 对于对象,除了函数都会显示 object

typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function' 

对于 null 来讲,虽然它是基本类型,可是会显示 object,这是一个存在好久了的 Bug

typeof null // 'object'

instanceof 能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 iprototype

咱们也能够试着实现一下 instanceof function instanceof(left, right) { // 得到类型的原型 let prototype = right.prototype // 得到对象的原型 left = left.__proto__ // 判断对象的类型是否等于类型的原型 while (true) { if (left === null) return false if (prototype === left) return true left = left.__proto__ } } 

#2、浏览器

#1 cookie和localSrorage、session、indexDB 的区别

特性 cookie localStorage sessionStorage indexDB
数据生命周期 通常由服务器生成,能够设置过时时间 除非被清理,不然一直存在 页面关闭就清理 除非被清理,不然一直存在
数据存储大小 4K 5M 5M 无限
与服务端通讯 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

从上表能够看到,cookie 已经不建议用于存储。若是没有大量数据存储需求的话,可使用 localStorage和 sessionStorage 。对于不怎么改变的数据尽可能使用 localStorage 存储,不然能够用 sessionStorage 存储。

对于 cookie,咱们还须要注意安全性

属性 做用
value 若是用于保存用户登陆态,应该将该值加密,不能使用明文的用户标识
http-only 不能经过 JS访问 Cookie,减小 XSS攻击
secure 只能在协议为 HTTPS 的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie,减小 CSRF 攻击

#2 怎么判断页面是否加载完成?

  • Load 事件触发表明页面中的 DOMCSSJS,图片已经所有加载完毕。
  • DOMContentLoaded 事件触发表明初始的 HTML 被彻底加载和解析,不须要等待 CSSJS,图片加载

#3 如何解决跨域

由于浏览器出于安全考虑,有同源策略。也就是说,若是协议、域名或者端口有一个不一样就是跨域,Ajax请求会失败。

咱们能够经过如下几种经常使用方法解决跨域的问题

JSONP

JSONP 的原理很简单,就是利用 <script>标签没有跨域限制的漏洞。经过 <script>标签指向一个须要访问的地址并提供一个回调函数来接收数据当须要通信时

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script> <script> function jsonp(data) { console.log(data) } </script> 

JSONP 使用简单且兼容性不错,可是只限于 get 请求

  • 在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就须要本身封装一个 JSONP,如下是简单实现
function jsonp(url, jsonpCallback, success) { let script = document.createElement("script"); script.src = url; script.async = true; script.type = "text/javascript"; window[jsonpCallback] = function(data) { success && success(data); }; document.body.appendChild(script); } jsonp( "http://xxx", "callback", function(value) { console.log(value); } ); 

CORS

  • ORS须要浏览器和后端同时支持。IE 8 和 9 须要经过 XDomainRequest 来实现。
  • 浏览器会自动进行 CORS 通讯,实现CORS通讯的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就能够开启 CORS。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源。

document.domain

  • 该方式只能用于二级域名相同的状况下,好比 a.test.com 和 b.test.com 适用于该方式。
  • 只须要给页面添加 document.domain = 'test.com' 表示二级域名都相同就能够实现跨域

postMessage

这种方式一般用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另外一个页面判断来源并接收消息

// 发送消息端 window.parent.postMessage('message', 'http://test.com'); // 接收消息端 var mc = new MessageChannel(); mc.addEventListener('message', (event) => { var origin = event.origin || event.originalEvent.origin; if (origin === 'http://test.com') { console.log('验证经过') } }); 

#4 什么是事件代理

若是一个节点中的子节点是动态生成的,那么子节点须要注册事件的话应该注册在父节点上

<ul id="ul"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> <script> let ul = document.querySelector('#ul') ul.addEventListener('click', (event) => { console.log(event.target); }) </script> 
  • 事件代理的方式相对于直接给目标注册事件来讲,有如下优势
    • 节省内存
    • 不须要给子节点注销事件

#5 Service worker

service worker

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。他们还容许访问推送通知和后台同步API

目前该技术一般用来作缓存文件,提升首屏速度,能够试着来实现这个功能

// index.js if (navigator.serviceWorker) { navigator.serviceWorker .register("sw.js") .then(function(registration) { console.log("service worker 注册成功"); }) .catch(function(err) { console.log("servcie worker 注册失败"); }); } // sw.js // 监听 `install` 事件,回调中缓存所需文件 self.addEventListener("install", e => { e.waitUntil( caches.open("my-cache").then(function(cache) { return cache.addAll(["./index.html", "./index.js"]); }) ); }); // 拦截全部请求事件 // 若是缓存中已经有请求的数据就直接用缓存,不然去请求数据 self.addEventListener("fetch", e => { e.respondWith( caches.match(e.request).then(function(response) { if (response) { return response; } console.log("fetch source"); }) ); }); 

打开页面,能够在开发者工具中的 Application 看到 Service Worker已经启动了

#6 浏览器缓存

缓存对于前端性能优化来讲是个很重要的点,良好的缓存策略能够下降资源的重复加载提升网页的总体加载速度。

  • 一般浏览器缓存策略分为两种:强缓存和协商缓存。

强缓存

实现强缓存能够经过两种响应头实现:Expires 和 Cache-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-Modified 和 If-Modified-Since

  • Last-Modified表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。
  • 可是若是在本地打开缓存文件,就会形成 Last-Modified被修改,因此在 HTTP / 1.1 出现了 ETag

ETag 和 If-None-Match

ETag 相似于文件指纹,If-None-Match 会将当前 ETag发送给服务器,询问该资源 ETag 是否变更,有变更的话就将新的资源发送回来。而且 ETag 优先级比 Last-Modified 高

选择合适的缓存策略

对于大部分的场景均可以使用强缓存配合协商缓存解决,可是在一些特殊的地方可能须要选择特殊的缓存策略

  • 对于某些不须要缓存的资源,可使用 Cache-control: no-store ,表示该资源不须要缓存
  • 对于频繁变更的资源,可使用 Cache-Control: no-cache并配合 ETag 使用,表示该资源已被缓存,可是每次都会发送请求询问资源是否更新。
  • 对于代码文件来讲,一般使用 Cache-Control: max-age=31536000 并配合策略缓存使用,而后对文件进行指纹处理,一旦文件名变更就会马上下载新的文件

#7 浏览器性能问题

重绘(Repaint)和回流(Reflow)

  • 重绘和回流是渲染步骤中的一小节,可是这两个步骤对于性能影响很大。
  • 重绘是当节点须要更改外观而不会影响布局的,好比改变 color就叫称为重绘
  • 回流是布局或者几何属性须要改变就称为回流。
  • 回流一定会发生重绘,重绘不必定会引起回流。回流所需的成本比重绘高的多,改变深层次的节点极可能致使父节点的一系列回流。

因此如下几个动做可能会致使性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

不少人不知道的是,重绘和回流其实和 Event loop 有关。

  • 当 Event loop 执行完 Microtasks后,会判断 document 是否须要更新。- 由于浏览器是 60Hz 的刷新率,每 16ms才会更新一次。
  • 而后判断是否有resize 或者 scroll ,有的话会去触发事件,因此 resize 和 scroll 事件也是至少 16ms 才会触发一次,而且自带节流功能。
  • 判断是否触发了 media query
  • 更新动画而且发送事件
  • 判断是否有全屏操做事件
  • 执行 requestAnimationFrame回调
  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差
  • 更新界面
  • 以上就是一帧中可能会作的事情。若是在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

减小重绘和回流

使用 translate 替代 top

<div class="test"></div> <style> .test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } </style> <script> setTimeout(() => { // 引发回流 document.querySelector('.test').style.top = '100px' }, 1000) </script> 
  • 使用 visibility 替换 display: none ,由于前者只会引发重绘,后者会引起回流(改变了布局)
  • 把 DOM 离线后修改,好比:先把 DOM 给 display:none(有一次 Reflow),而后你修改100次,而后再把它显示出来
  • 不要把 DOM结点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) { // 获取 offsetTop 会致使回流,由于须要去获取正确的值 console.log(document.querySelector('.test').style.offsetTop) } 
  • 不要使用 table 布局,可能很小的一个小改动会形成整个 table 的从新布局 动画实现的速度的选择,动画速度越快,回流次数越多,也能够选择使用 requestAnimationFrame
  • CSS选择符从右往左匹配查找,避免 DOM 深度过深
  • 将频繁运行的动画变为图层,图层可以阻止该节点回流影响别的元素。好比对于 video 标签,浏览器会自动将该节点变为图层。

CDN

静态资源尽可能使用 CDN 加载,因为浏览器对于单个域名有并发请求上限,能够考虑使用多个 CDN 域名。对于 CDN 加载静态资源须要注意 CDN 域名要与主站不一样,不然每次请求都会带上主站的 Cookie

使用 Webpack 优化项目

  • 对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
  • 使用 ES6 模块来开启 tree shaking,这个技术能够移除没有使用的代码
  • 优化图片,对于小图可使用 base64 的方式写入文件中
  • 按照路由拆分代码,实现按需加载

#3、Webpack



#1 优化打包速度

  • 减小文件搜索范围
    • 好比经过别名
    • loader 的 testinclude & exclude
  • Webpack4 默认压缩并行
  • Happypack 并发调用
  • babel 也能够缓存编译

#2 Babel 原理

  • 本质就是编译器,当代码转为字符串生成 AST,对 AST 进行转变最后再生成新的代码
  • 分为三步:词法分析生成 Token,语法分析生成 AST,遍历 AST,根据插件变换相应的节点,最后把 AST转换为代码

#3 如何实现一个插件

  • 调用插件 apply 函数传入 compiler 对象
  • 经过 compiler 对象监听事件

好比你想实现一个编译结束退出命令的插件

apply (compiler) { const afterEmit = (compilation, cb) => { cb() setTimeout(function () { process.exit(0) }, 1000) } compiler.plugin('after-emit', afterEmit) } } module.exports = BuildEndPlugin
相关文章
相关标签/搜索