个人回故

120 阅读23分钟

防抖

在一定的时间再次执行,重新记时,只执行最后一次

function debounce(fn,delay=300){
	let timer = null;
	return function (){
		if(timer){
			clearTimeout(timer);
		}
		timer = setTimeout(() => {
			fn.apply(this, arguments)
		}, delay);
	}
}

节流

时间段内只执行一次

function throttle(fn, delay){
	let timer = null;
	return () => {
		if(timer) return;
		timer = setTimeout(()=>{
			fn();
			timer = null
		}, delay)
		
	}
}

闭包

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。

bind / call / apply 区别

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,apply、call 则是立即执行, bind是一个函数体需要加括号执行
  • xxx.call(foo, '参数1', '参数2', '参数3') xxx.apply(foo, ['参数1', '参数2', '参数3']) xxx.bind(foo, 'P1', 'P2' )('参数1', '参数2', '参数3')

new 关键字

	function Person(name){
		this.name = name;
	}
	Person.prototype.getName = function(){
		console.log(this.name);
	}
	const p = new Person('swf');

// 1. new 创建一个对象,指向构造函数的原型 p.__proto__ = Person.prototype; // prototype 是Person的静态方法

// 2. 构造函数上,有个原型(是个对象),里面有个 constructor 函数,就是这个构造函数本身。 Person.prototype.constructor = Person;

// 3. p对象的构造函数,是Person p.constructor === Person

写一个new关键字 -> 没办法用new关键字所以用newFunc

function newFunc(Father){
	if(typeof Father !== 'function'){
		throw new Error('new operator function the firist param must be a function')
	}
	var obj = Object.create(Father.prototype);
	var result = Father.apply(obj, Array.prototype.slice.call(arguments, 1));
	return result && typeof === 'object' && result !== null ? result : obj;
}
const p = newFunc (Person, name)

输入URL到渲染页面的全过程

1、URL解析

1. 判断是URL还是搜索关键字
2. HSTS (Web安全协议,HSTS的作用是强制客户端使用HTTPS与服务器创建连接)
3. 缓存检查 (浏览器会先去查看强缓存(Expires和cache-control)判断是否过期,如果强缓存生效,直接从缓存中读取资源;若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag/If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,并重新返回资源和缓存标识,再次存入浏览器缓存中;生效则返回304,并从缓存中读取资源。(协商缓存之前要经过DNS域名解析,之后建立TCP链接

注意:输入网址之后,会查找内存缓存,没有再找硬盘,都没有就发生网络请求。 普通刷新(F5):因为TAB没有关闭,所以内存缓存可用,如果匹配上会被优先使用,其次是磁盘缓存 强制刷新(Ctrl+F5):浏览器不使用缓存,因此发送的请求头均带有Cache-control:no-cache,服务器直接返回200和最新内容。

2、DNS解析

(1)、DNS:把域名和ip地址相互映射分布式数据库,让用户能更方便的访问互联网,DNS协议运行在UDP协议之上
(2)、DNS解析:通过域名最终得到对应ip地址的过程。
(3)、DNS缓存:浏览器,操作系统,路由器,本地DNS,根域名服务器都会对DNS结果作出一定的缓存

DNS解析过程

image.png 自身DNS缓存 -> hosts文件 -> 本地DNS服务器(一般这里找到) 本地DNS服务器没有找到时 根域 -> 顶级域 -> 第二层域 -> 子域 -> 主机名

现在,已经获取到服务器的IP地址,并且准备向这个IP地址发送请求了。

3、建立TCP链接

TCP三次握手

image.png

4、客户端发送请求 image.png 5、服务器处理和响应请求

image.png

6、浏览器解析并渲染响应内容

image.png

image.png

image.png 7、TCP四次挥手断开连接

image.png

image.png

一般http中存在如下问题:

  • 请求信息明文传输,容易被窃听截取。
  • 数据的完整性未校验,容易被篡改
  • 没有验证对方身份,存在冒充危险

HTTPS 协议(HyperText Transfer Protocol over Secure Socket Layer):一般理解为HTTP+SSL/TLS,通过 SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密。

那么SSL又是什么?

SSL(Secure Socket Layer,安全套接字层):1994年为 Netscape 所研发,SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。

TLS(Transport Layer Security,传输层安全):其前身是 SSL,它最初的几个版本(SSL 1.0、SSL 2.0、SSL 3.0)由网景公司开发,1999年从 3.1 开始被 IETF 标准化并改名,发展至今已经有 TLS 1.0、TLS 1.1、TLS 1.2 三个版本。SSL3.0和TLS1.0由于存在安全漏洞,已经很少被使用到。TLS 1.3 改动会比较大,目前还在草案阶段,目前使用最广泛的是TLS 1.1、TLS 1.2

浏览器在使用HTTPS传输数据的流程

image.png

  1. 首先客户端通过URL访问服务器建立SSL连接。
  2. 服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端。
  3. 客户端的服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
  4. 客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
  5. 服务器利用自己的私钥解密出会话密钥。
  6. 服务器利用会话密钥加密与客户端之间的通信。

HTTPS的缺点

  • HTTPS协议多次握手,导致页面的加载时间延长近50%;
  • HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗;
  • 申请SSL证书需要钱,功能越强大的证书费用越高。
  • SSL涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大。

vue 中 $route$router 的区别

this.$router 是 router 实例

通过 this.$router 访问路由器,相当于获取了整个路由文件,在$router.option.routes中,或查看到当前项目的整个路由结构 具有实例方法

router.beforeEach((to, from, next) => { /* 必须调用 `next` */ }) 
router.beforeResolve((to, from, next) => { /* 必须调用 `next` */ }) 
router.afterEach((to, from) => {}) 动态导航到新路由 
router.push 
router.replace 
router.go 
router.back 
router.forward

this.$route

this.$route 访问的是当前路由,获取和当前路由有关的信息,只读

fullPath: "" // 当前路由完整路径,包含查询参数和 hash 的完整路径 
hash: "" // 当前路由的 hash 值 (锚点) 
matched: [] // 包含当前路由的所有嵌套路径片段的路由记录 
meta: {} // 路由文件中自赋值的meta信息 
name: "" // 路由名称 
params: {} // 一个 key/value 对象,包含了动态片段和全匹配片段就是一个空对象。
path: "" // 字符串,对应当前路由的路径 
query: {} // 一个 key/value 对象,表示 URL 查询参数。跟随在路径后用'?'带的参数

ES6 new Set()、 new Map / WeakSet、WeakMap 的区别

Set

Set数据结构是一个类似于数组,但是与数组不同的是它具有唯一性,里面的元素都是不重复的,而且他本身也是一个构造函数。 constructor、size、add、delete、has、clear、forEach、entries、values、keys

WeakSet

从字面上的意思看来,它是一个弱的Set结构,具体的弱体现在WeakSet的成员只能是对象,还有一个就是WeakSet的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用。也就是说WeakSet里面的引用都不计入垃圾回收机制 add、delete、has

Map

上面我们讲到的Set结构是没有键的只有值,而Map结构是键值的组合,这里就解决了我们以往只能使用字符串作为键的限制。也就是说我们可以使用各种的数据类型(或者对象)作为键。 constructor、size、set、get、has、delete、clear、forEach、entries、keys、values

WeakMap

WeakMap与Map一样可以生成键值对的集合,但是也有不同的地方,主要是有以下的两点: WeakMap只接受对象作为键名(不包括null) 键名所指向的对象不计入垃圾回收机制

与WeakSet相似,WeakMap也是没有遍历的操作,也没有size属性,没有办法列出所有键名(由于垃圾回收机制的运行),也不能清空。 get、set、has、delete

箭头函数与普通函数的区别

  1. 语法更加简洁、清晰
  2. 箭头函数不会创建自己的this
  3. 箭头函数继承而来的this指向永远不变
  4. .call()/.apply()/.bind()无法改变箭头函数中this的指向
  5. 箭头函数不能作为构造函数使用
  6. 箭头函数没有自己的arguments
  7. 箭头函数没有原型prototype
  8. 箭头函数不能用作Generator函数,不能使用yeild关键字

js异步处理发展史

callback -> Promise -> Generator -> Async

垃圾回收机制

标记清除算法

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  • 然后从各个根对象开始遍历,把不是垃圾的节点改成1
  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

优点

标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单

缺点

标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题

注:标记清除算法的缺点补充

归根结底,标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续,所以只要解决这一点,两个缺点都可以完美解决了

标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存

引用计数算法

优点

引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾

而标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了

缺点

引用计数的缺点想必大家也都很明朗了,首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的

V8对GC的优化

分代式垃圾回收

新老生代

新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大。对于新老两块内存区域的垃圾回收,V8 采用了两个垃圾回收器来管控

新生代垃圾回收

Cheney算法 中将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区,一个是处于闲置状态的空间我们称之为 空闲区。新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作

活动对象标记 -> 从活动区复制到空闲区 -> 清除原活动区 -> 原活动区域变成空闲区

当一个对象经过多次复制后依然存活,认为是生命周期较长的对象;或较大对象占空闲区25% 时,被移动到老生代中,采用老生代的垃圾回收策略进行管理。

老生代垃圾回收

然后标记清除算法标记整理算法来解决这一问题来优化空间(移动至一头),

并行回收(Parallel)

增量标记与懒性清理

三色标记法(暂停与恢复)

写屏障(增量中修改引用)

V8中GC优化

V8 的垃圾回收策略主要基于分代式垃圾回收机制,关于新生代垃圾回收器,使用并行回收可以很好的增加垃圾回收的效率,那老生代垃圾回收器用的哪个策略呢?并行回收、增量标记与惰性清理。这三种方式各有优缺点,所以在老生代垃圾回收器中这几种策略都是融合使用的

老生代主要使用并发标记,主线程在开始执行 JavaScript 时,辅助线程也同时执行标记操作(标记操作全都由辅助线程完成)

标记完成之后,再执行并行清理操作(主线程在执行清理操作时,多个辅助线程也同时执行清理操作)

同时,清理的任务会采用增量的方式分批在各个 JavaScript 任务之间执行

手写promise

//刚开始是等待态,pending
let promise = new Promise( (resolve,reject) =>{if(err) return reject(err) //失败了返回失败信息 失败态
        resolve(data) //成功了返回数据 成功态
})
//状态改变了调用
promise.then(data=>{ //成功了调用
    console.log(data)
},err=>{             //失败了调用
    console.log(err)
})

这是一个promise实例,它有三种状态,pending(等待态)、fullfilled(成功态)、rejected(失败态),resolve为成功时调用,状态由等待态变为成功态,reject为失败,状态由等待态变为失败态。

当状态改变的了的时候会执行then方法,如果是成功就输出值成功的值,如果是失败就返回失败的原因。但是他们两个只能调用一个,不可能有即成功由失败的情况。

根据这个实例我们就可以实现一版简单的功能。

第一版:定义整个流程,实现同步功能:

这一版我们主要实现流程能走通,能执行同步任务

class MyPromise{
    constructor(executorCallback){ //executorCallback执行的回调函数
        let _this = this;
        _this.status = 'pending'; //记录状态
        _this.value = ''; //记录返回的数据
        function resolveFn(result){
            if(_this.status === 'pending'){ //状态只能是从pending到成功
                _this.status = 'resolved'
                _this.value = result
            }
        }
        function rejectFn(reason){
            if(_this.status === 'pending'){ //状态只能是从pending到失败
                _this.status = 'rejected'
                _this.value= reason
            }
        }
     executorCallback(resolveFn,rejectFn)
    }
    //判断状态成功还是失败,成功了执行onFullfiled,失败了执行onRejected
    then(onFullfiled,onRejected) {
        if(this.status === 'resolved'){
            onFullfiled(this.data)
        }
        if(this.status === 'rejected'){
            onRejected(this.err)
        }
    }
}
1.定义_this保证每次都能获取到正确的this
2.定义一个常量status用来记录promise的状态
3.定义一个常量value用来保存执行结果的返回值
4.executorCallback是立即执行函数,需要我们手动传入两个参数,这两个参数代表成功和失败时候调用,需要我们在内部定义好。
5.resolveFn()和rejectedFn()函数,在执行前先判断一下状态,如果状态为pending就执行,并且让状态变为相应的成功态或者失败态,这样每次就只能执行一个,要么是resolveFn(),
要么是rejectedFn(),并且把相应的返回值赋值给value。
6.状态改变了之后就会调用Promise.then()方法,如果状态是成功态就执行onFullfilled(),如果状态是失败态,就执行onRejected()

第二版:实现异步功能

我们先来分析一下上一版为什么实现不了异步功能,当代码执行到setTimeout时,不会立即执行里面的函数,而是先放到一个异步调用栈里面,等到同步代码执行完了在执行里面的resolveFn函数,这个时候then已经执行完了,不会再执行,所以就不会输出任何东西,我们可以在resolveFn执行之前就将then的回调函数先保存起来,等到resolveFn执行的时候再去一个一个执行这些回调函数,这个时候就可以实现异步功能。

在原来的基础上修改

class myPromise{
    constructor(executorCallback){
        var _this = this
        _this.status = 'pending'
        _this.value 
        _this.onFullfilledCallback  = [] //存放成功时的回调函数
        _this.onRejectedCallback = [] //存放时的失败的回调函数
        function resolveFn (result) {
            let timer = setTimeout( () =>{ //异步任务
                clearTimeout(timer)  //
                // console.log('chengg')
                if(_this.status === 'pending'){
                    _this.status = 'resolved'
                    _this.value = result
                    _this.onFullfilledCallback .forEach(item =>item(_this.value)); //
                }
            })
        }
        function rejectFn (err) {
            let timer = setTimeout( () =>{ //异步任务
                clearTimeout(timer) //
                if(_this.pending === 'pending'){
                    _this.status = 'rejected'
                    _this.value = err
                    _this.onRejectedCallback.forEach(item =>item(_this.value)) //
                }
            })
        }
        executorCallback(resolveFn,rejectFn)
    }
    
    then(onFullfiled,onRejected){
        if(this.status === 'pending') {
            this.onFullfilledCallback .push(onFullfiled) //
            this.onRejectedCallback.push(onRejected) //
        }
    }
}

第三版:实现链式调用

首先分析,Promise为什么可以实现链式调用,因为Promise.then()方法它返回的是一个新的Promise实例,将这个新的Promise实例的返回值传递到下一个then中,作为下次onFullfilled()或者onRejected()的值。那这个新的Promise的返回值会有哪几种情况呢?我们来分析一下 1.如果返回的是一个普通值就直接执行成功,resolve(x)

2.如果返回的是一个promise实例,就继续new

3.如果出错了,就执行失败reject(x)

4.如果参数是null,就会输出undefined

所以要对返回值进行解析。

改写then方法。

then(onFulfiled,onRejected){
        // 声明返回的promise2     
        let promise2 = new myPromise((resolveFn, rejectFn)=>{       
            if (this.status === 'fulfilled') {         
                let x = onFulfiled(this.value);         
                // resolvePromise函数,处理自己return的promise和默认的promise2的关系         
                resolvePromise(promise2, x, resolveFn, rejectFn);       
            };       
            if (this.status === 'rejected') {         
                let x = onRejected(this.reason);         
                resolvePromise(promise2, x, resolveFn, rejectFn);       
            };       
            if (this.status === 'pending') {         
                this.onFullfilledCallback.push(()=>{           
                    let x = onFulfiled(this.value);           
                    resolvePromise(promise2, x, resolveFn, rejectFn);         
                })         
                this.onRejectedCallback.push(()=>{           
                    let x = onRejected(this.reason);           
                    resolvePromise(promise2, x, resolveFn, rejectFn);         
                })       
            }
        });     
        // 返回promise,完成链式     
        return promise2;   
    }

我们首先定义一个新的Promise实例,在这个实例内部需要判断状态,当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数。将then回调函数的返回值记为x,由于这个返回值可能有多种情况,所以需要对各种情况进行解析。所以另外封装一个方法resolvePromise(),接下来我们就来封装一下这个函数。

function resolvePromise(promise2, x, resolve, reject){   
                // 循环引用报错   
                if(x === promise2){     
                    // reject报错     
                    return reject(new TypeError('Chaining cycle detected for promise'));    
                }     
                let called;   //控制调用次数
                // x不是null 且x是对象或者函数   
                if (x !== null && (typeof x === 'object' || typeof x === 'function')) {     
                    try {       
                        // PromiseA+规定,声明then 等于 x的then方法       
                        let then = x.then;       
                        // 如果then是函数,就默认是promise了       
                        if (typeof then === 'function') {          
                            // 就让then执行 第一个参数是this,   后面是成功的回调 和 失败的回调         
                            then.call(x, y => {           
                                // 成功和失败只能调用一个           
                                if (called) return;           
                                called = true;           
                                // resolve的结果依旧是promise 那就继续解析           
                                resolvePromise(promise2, y, resolve, reject);         
                            }, err => {           
                                // 成功和失败只能调用一个           
                                if (called) return;           
                                called = true;           
                                reject(err);     
                            })       
                        } else { //如果不是函数,是普通对象直接resolve
                            resolve(x); // 直接成功即可       
                        }     
                    } catch (e) {       
                        // 如果在执行的过程中报错了,就被then的失败捕获       
                        if (called) return;       
                        called = true;       // 取then出错了那就不要在继续执行了       
                        reject(e);      
                    }   
                } else { //如果是普通值     
                    resolve(x);   
                } 
            }
1.PromiseA+规定 x 不能与 promise2 相等,这样会发生循环引用的问题
2.定义一个called来控制调用次数,成功和失败只能调用一个,一旦调用完就将called = true,防止下一个再调用。
3.接着判断x的类型,如果不是对象或者函数,那就是普通值,直接resolve(x)
4.如果是对象或者函数,将x.then赋值给then,如果then是函数,就让then执行回调函数。如果下一次的执行结果还是一个Promise,就接着处理。如果then是个普通对象,就直接执行
resolve方法
5.再执行这段代码的过程中可能会发生异常,我们用try catch去捕获错误,如有错误,就直接执行reject方法。

Promise.all的实现

Promise,all方法接收一个数组,等到数组里面的所有Promise实例的状态都变成成功态之后才成功,但只要有一个失败了就返回失败。

 
//all 获取所有的promise,都执行then,把结果放到数组,一起返回
   static all(promiseArr =[]) {
       return new Promise((resolve,reject) =>{
           let index = 0;
           let arr = []
           for(let i =0; i<promiseArr.length; i++){
               promiseArr[i].then(result =>{
                   index++
                   arr[i] = result
                   if(index === arr.length){
                       resolve(arr)
                   }
               },reason =>{
                   reject(reason)
               })
           }
       })
   }

Promise.race的实现

race方法是哪个先成功就先返回哪个,

    //谁先执行先返回谁
  static race (promises){   
      return new Promise((resolve,reject)=>{     
          for(let i=0;i<promises.length;i++){       
              promises[i].then(resolve,reject)     
          };   
      }) 
  }

Promise.resolve的实现

resolvereject 是分别执行成功和是失败。

   //resolve方法 
  static resolve(result){   
      return new Promise((resolve,reject)=>{     
          resolve(result)   
      }); 
  } 
     //reject方法 
  static reject (reason){   
      return new Promise((resolve,reject)=>{     
          reject(reason)   
      }); 
  }

WebSocket

WebSocketHTTP一样,是一个应用层的网络协议,它的作用是可以使客户端和服务器在应用层建立一个全双工的连接,http连接时效性较短,轮询效率低下,浪费资源;只能客户端请求。 WebSocket就是为了解决这些问题而被发明出来的。WebSocket是一个应用层的协议,它依靠HTTP实现连接前的握手,而底层依赖TCP协议传递消息

WebSocket建立连接的过程

WebSocket基于HTTP进行握手从而建立连接,使用HTTP协议是为了兼容服务于HTTPWeb服务器以及中间代理服务器。

握手的第一个阶段:客户端与服务器建立HTTP连接,并发送请求报文,请求报文的格式如下:

GET / HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: example.com
Origin: HTTP://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

这个请求是一个GET请求,请求中必须包含这些首部信息

  • Host:需要建立连接的服务器名称;
  • Connection:必须包含此首部,且值必须为Upgrade,表明这是一个协议升级请求;
  • Upgrade:必须包含此首部,且值必须为WebSocket,表明当前连接请求升级为WebSocket连接;
  • Origin:如果这个请求是从浏览器发出的,那么还必须带有Origin请求头;
  • Sec-WebSocket-Key:这是浏览器随机生成的值,用来验证服务器身份;
  • Sec-WebSocket-Version:这个表示的是WebSocket的版本号码;

握手第二个阶段:服务器接收到了客户端发来的协议升级请求,会先判断请求报文是否合法,若没有什么异常,则会给客户端发送响应

HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=

需要注意一点,响应报文的状态码是101101状态码的含义是:服务器正在根据客户端的指定,将协议切换为请求报文中Upgrade指定的协议。下面我们来说一说响应报文中的首部:

  • Connection:值和请求报文中一致,表明这是一个协议升级请求的响应报文;
  • Upgrade:值为WebSocket,表明这个响应报文,响应的协议升级请求,是希望升级为WebSocket协议;
  • Sec-WebSocket-Accept:这个值是服务器接收到客户端发来的Sec-WebSocket-Key值后,经过加密计算出来的值,用来向客户端表明自己的身份

客户端接收到服务器发回来的响应报文后,将对响应报文进行校验:

  • 首先客户端将检测响应的状态码是否为101,若不是,表明协议升级失败,连接自然无法建立;
  • 若状态码正常,则接着检测Sec-WebSocket-Accept的值。客户端使用相同的加密方式,对Sec-WebSocket-Key进行计算,若计算的结果与服务器发来的Sec-WebSocket-Accept的值一致,表明服务器身份没有问题,于是连接成功建立,否则放弃建立连接;

  连接建立后,客户端和服务器就可以基于TCP,进行全双工的通信了。

使用流程

1.先下载websocket模块 npm install ws --save

   var ws = new WebSocket("ws://127.0.0.1:8000/chat/");
    // 1 请求链接成功之后  自动触发
    ws.onopen = function () {
        {#alert('链接成功')#}
    }
    // 2 向服务端发消息
    function sendMsg() {
        ws.send($('#txt').val())
    }
    // 3 服务端给客户端回复消息的时候 自动触发
    ws.onmessage = function (args) {
        {#alert(args)  // 是一个对象 #}
        var res = args.data
        {#alert(res)#}
        var pEle = $('<p>');
        pEle.text(res);
        $('.record').append(pEle)
    }
    // 4 断开链接之后自动触发
    ws.onclose = function () {
        console.log('断开链接了')
    }

typeof和instanceof的区别是什么

typeof的返回值是一个字符串,用来说明变量的基本数据类型(number, boolean, string, function, object, undefined。)对于 Array, Null 等特殊对象使用 typeof 一律返回 object, 这正是 typeof 的局限性。

instanceof的返回值是布尔值,用于判断一个变量是否属于某个对象的实例。

/**
  * @description 判断对象是否属于某个构造函数
  * @prams left: 实例对象  right: 构造函数
  * @return boolean
*/
function myInstanceof(left, right) {
  let rightPrototype = right.prototype; // 获取构造函数的显式原型
  let leftProto = left.__proto__; // 获取实例对象的隐式原型
  while (true) {
    // 说明到原型链顶端,还未找到,返回 false
    if (leftProto === null) {
      return false;
    }
    // 隐式原型与显式原型相等
    if (leftProto === rightPrototype) {
      return true;
    }
    // 获取隐式原型的隐式原型,重新赋值给 leftProto
    leftProto = leftProto.__proto__
  }
}

['1','2','3'].map(parseInt) 输出答案和解析

map方法的参数是一个回调函数,这个函数会有三个参数:当前元素、当前元素的下标和当前数组。所以这道题可以转换成一下写法:

parseInt('1',0,['1','2','3'])
parseInt('2',1,['1','2','3'])
parseInt('3',2,['1','2','3'])

parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于2-36之间的数。们着重看下第二个参数,第二个参数标识进制,默认是10进制的,如果第一个字符不能被转换成数字,parseInt返回NaN。 0x===16进制、0===8进制或10进制

所有无法转换 1进制和2进制,所以返回NaN,返回结果[1,NaN,NaN]

['1','2','3'].map(Number) === [1,2,3]

怎么实现接口防刷

大多放在后端 reads+拦截器, 前端 做nginx缓存。

JWT json web token

删除升 序链表 中重复出现的所有节点[1,2,3,3,4,4,5] => [1,2,5]

let deleteDuplicates = function(head){
    var fir = head;
    while(fir && fir.next){
        if(fir.val === fir.next.val){
            fir.next = fir.next.next
        }else{
            fir=fir.next
        }
    }
    return head
    };

线程和进程

  • 进程:进程是系统进行资源调度和分配的基本单位,每个进程都有自己独立的一块内存空间。

  • 线程:线程是进程的子任务,是CPU调度和分配的基本单位。

进程和线程的区别

根本区别:进程是操作系统进行资源调度和分配的基本单位,而线程是CPU进行资源调度和分配的基本单位。
从资源开销上讲:每个进程都有自己的内存空间,进程之间切换性能开销比较大,线程与线程之间是共享代码和内存空间的,每个线程都有自己独立的运行栈和程序计数器,线程之间的开销小。
从包含关系上讲:一个进程可以有多个线程,线程是进程的一部分。
从内存分配上讲:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
从影响关系上讲:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都会挂掉,所以多进程程序相对多线程要更加健壮。
从执行过程上讲:进程可以独立执行,但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

并发和并行的区别

  • 并发:一个处理器同时处理多个任务。类似于两队等一个咖啡机。
  • 并行:多个处理器或者多核处理器同时处理多个不同的任务,类似于两队等待两个咖啡机。

进程之间如何进行通信?

消息队列、共享内存、Socket通信

线程之间如何进行通信?

线程间通信的目的主要是用于线程同步 锁机制:(互斥锁、读写锁)、信号量机制、阻塞唤醒机制

进程调度策略

  1. 先来先服务
  2. 最短作业优先调度
  3. 时间片轮转调度(给每个进程分配一个时间片,CPU轮流切换进程执行。)
  4. 最高优先级调度(在时间片轮转的基础上,加上了优先级,调度程既希望给每个进程轮转调度,又希望每次轮转尽可能选择高优先级的进程)

Chrome浏览器为什么采用多进程架构?

浏览器刚被设计出来的时候,网页资源占有率比较小,因此一个进程处理多个网页是可行的,但是随着流媒体资源的丰富,网页也变得越来越复杂,把所有网页都放在一个进程在健壮性、响应速度、安全性方面都面临很多挑战,如果是多线程架构,一个网页崩溃会导致其他网页受到影响。多线程架构之间的内存共享也会带来安全问题。

浏览器都有哪些进程事件?

Browser进程:浏览器的主线程,负责浏览器界面的显示与交互,各个页面的管理,创建和销毁其他进程,网络资源的管理和下载等。
Renderer进程:也称为浏览器渲染进程或浏览器内核,其内部是多线程的,主要负责页面渲染,脚本执行,事件处理等。
第三方插件进程。
GPU进程:用于3D绘制等。

浏览器内核都有哪些线程?

  • GUI渲染线程
  • JS引擎线程
  • 定时触发器线程
  • 事件触发线程
  • 异步http请求线程

浏览器处理AJAX请求和渲染页面是同一个进程吗,为什么?

浏览器处理Ajax请求是发生在浏览器内核进程中的异步http请求线程中,GUI渲染也是在内核中,所以他们在同一个进程中,但是不是同一个线程。

浏览器的Worker线程和主线程是如何进行通信的?

主线程采用new命令,调用Worker()新建一个Worker线程。Worker()构造函数的参数是一个脚本文件,该文件就是Worker线程所要执行的任务。
主线程调用  Woker.postmessage  方法想worker发送消息。
worker线程通过监听函数onmessage收到消息,处理数据然后通过postmessage进行返回。
主线程通过  decodeWorker.onmessage  监听函数,接收子线程发回来的消息。
等Worker完成任务后,主线程就可以把它关掉了。

http1.0,http1.1,http2,http3

http1.0

HTTP1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,是一种无状态、无连接的应用层协议,几年后被HTTP1.1代替并广泛使用

http1.1

  1. http1.1基于文本解析,把所有请求和响应作为纯文本
  2. http1.1加入了缓存处理(强缓存和协商缓存)
  3. http1.1拥有长连接,并支持请求管道化pipelining),
  4. http1.1流控制基于tcp连接。当连接建立时,两端通过系统默认机制建立缓冲区。并通过ack报文来通知对方接收窗口大小,因为http1.1 依靠传输层来避免流溢出,每个tcp连接需要一个独立的流控制机制

缓存处理(强缓存和协商缓存)

浏览器缓存能优化性能,而浏览器缓存分为强缓存协商缓存,都是从客户端读取缓存 强缓存

  1. 强缓存不发送请求,直接读取资源,可以获得返回200的状态码
  2. 利用http头中的ExpiresCache-Control两个字段来控制,都用来表示资源的缓存时间,Expires能设置失效时间,而Cache-Control能做到更多选项更细致,如果同时设置的话,其优先级高于Expires

协商缓存

  1. 通过服务器来确定缓存资源是否可用,通过request header判断是否命中请求,命中后返回304状态码,并返回新的request header通知客户端从缓存里取
  2. 普通刷新会启用弱缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用强缓存
  3. 如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求,回到1

http2

  1. http2相比于http1.1,性能大幅度提升
  2. http2通过一个连接来多路复用
  3. http2拥有头部压缩
  4. http2拥有新的二进制格式,使用二进制框架层把所有消息封装成二进制,且仍然保持http语法
  5. http2允许客户端和服务器端实现他们自己的流控制机制,而不是依赖传输层,两端在传输层交换可用的缓冲区大小,来让他们在多路复用流上设置自己的接收窗口
  6. http2让服务器可以将响应主动“推送”到客户端缓存中

htpp2头部压缩

  1. http2头部压缩又称为HAPCK设计简单而灵活,是因为HPACK格式有意地简单不灵活能降低由于实现错误而导致的互操作性或安全问题的风险
  2. http1.1没有头部压缩,随着请求增加,冗余头部字段会不必要地占用带宽,从而显着增加延迟,而头部压缩可消除冗余报头字段,限制已知安全攻击的漏洞,并且在受限环境中使用有限的内存要求

http2多路复用

  1. http 性能优化的关键并不在于高带宽,而是低延迟
  2. tcp 连接会随着时间进行自我「调谐」,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度,这种调谐则被称为 tcp 慢启动,由于这种原因,让原本就具有突发性和短时性的 http 连接变的十分低效
  3. http/2 通过让所有数据流共用同一个连接,可以更有效地使用 tcp 连接,让高带宽也能真正的服务于 http 的性能提升。而http1.1存在低性能的线头阻塞,一旦有一个请求超时,便会出现阻塞等待的情况

http3

之前说了http2,那么http3就是为了解决http2相关问题而诞生,它基于一个新的传输层协议QUIC,而http3就是建立一个在QUIC上运行的HTTP新规范,而http3之前的版本都是基于TCP,QUIC就是为了替代TCP,解决TCP的一些缺陷

tcp

  1. 不支持流级复用,TCP会将所有对象序列化在同一个流中,因此,它不知道TCP段的对象级分区,无法在同一个流中复用数据包
  2. 会产生冗余通信,tco三次连接握手会有冗余的消息交换序列
  3. 可能会间歇性地挂起数据传输,tcp中有个因为序列顺序处理丢失的问题的缺陷称为行头阻塞

QUIC

  1. 同样拥有头部压缩,并优化了对乱序发送的支持,也优化了压缩率
  2. 放弃tcp,通过udp建立,提高了连接建立的速度,降低了延迟
  3. tcp本身是无法解决队头拥塞,quic则解决了这个问题
  4. Connection ID使得http3支持连接迁移以及NAT的重绑定

HTTP1.0对比HTTP1.1

HTTP1.1主要改进了以下几点内容

  • keep-alive

  • 客户端缓存

  • 连接代宽优化

  • 请求Host域

  • 请求状态码

  • 请求方法(HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。)

HTTP2.0

http2.0是一种安全高效的下一代http传输协议。安全是因为http2.0建立在https协议的基础上,高效是因为它是通过二进制分帧来进行数据传输。

他对于HTTP1.x主要有以下改进

  • 二进制分帧(Binary Format)

  • 头部压缩(Header Compression)

  • 服务端推送(Server Push)

  • 多路复用 (Multiplexing) / 连接共享,让多个请求合并在同一 TCP 连接内

  • 请求优先级(Request Priorities)

HTTP3

HTTP3是一个基于UDP协议的应用层协议,并且集成了HTTP2的加密传输、多路复用的几个特点

数组随机排序

数组方法sort实现随机排序

var arr = [1, 2, 3, 4, 5]; 
arr.sort(function () {
  return Math.random() - 0.5
})
console.log(arr);

洗牌算法

function shuffle(array){
    let res = [], random;
    while(array.length>0){
        random = Math.floor(Math.random()*array.length);
        res.push(array[random]);
        array.splice(random, 1);
    }
    return res;
}
 console.log(shuffle([1,2,3,4,5]));
 
 var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle (arr) {
  var len = arr.length;
  for (var i = 0; i < len; i++) {
    // 生成 0 到 i 之间的随机整数
    var index = Math.floor(Math.random() * (i + 1));
    // 使用 ES6 中的解构赋值完成两个变量值的交换
    [arr[i], arr[index]] = [arr[index], arr[i]];
  }
  return arr;
}
console.log('Shuffled arr: ', shuffle(arr));

排序

冒泡排序

function bubbleSort(arr) {  
    var len = arr.length;  
    for (var i = 0; i < len - 1; i++) {  
        for (var j = 0; j < len - 1 - i; j++) {  
            if (arr[j] > arr[j+1]) {        // 相邻元素两两对比  
                var temp = arr[j+1];        // 元素交换  
                arr[j+1] = arr[j];  
                arr[j] = temp;  
            }  
        }  
    }  
    return arr;  
}

选择排序

function selectionSort(arr) {  
    var len = arr.length;  
    var minIndex, temp;  
    for (var i = 0; i < len - 1; i++) {  
        minIndex = i;  
        for (var j = i + 1; j < len; j++) {  
            if (arr[j] < arr[minIndex]) {     // 寻找最小的数  
                minIndex = j;                 // 将最小数的索引保存  
            }  
        }  
        temp = arr[i];  
        arr[i] = arr[minIndex];  
        arr[minIndex] = temp;  
    }  
    return arr;  
}

插入排序

从头到尾依次取出未排序序列,将取到的元素插入有序序列的适当位置。

function insertionSort(arr) {  
    var len = arr.length;  
    var preIndex, current;  
    for (var i = 1; i < len; i++) {  
        preIndex = i - 1;  
        current = arr[i];  
        while(preIndex >= 0 && arr[preIndex] > current) {  
            arr[preIndex+1] = arr[preIndex];  
            preIndex--;  
        }  
        arr[preIndex+1] = current;  
    }  
    return arr;  
}

希尔排序(插入排序的优化版)

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

function shellSort(arr) {  
    var len = arr.length,  
        temp,  
        gap = 1;  
    while(gap < len/3) {          //动态定义间隔序列  
        gap =gap*3+1;  
    }  
    for (gap; gap > 0; gap = Math.floor(gap/3)) {  
        for (var i = gap; i < len; i++) {  
            temp = arr[i];  
            for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {  
                arr[j+gap] = arr[j];  
            }  
            arr[j+gap] = temp;  
        }  
    }  
    return arr;  
}

数组扁平、去重、升序排列

Array.from(new Set(arr.flat(Infinity))).sort((a, b) => {
  return a - b;
});

vue-router完整的导航解析流程:

  1. 导航被触发
  2. 在即将离开的组件里调用 beforeRouteLeave守卫
  3. 调用全局的beforeEach守卫(全局前置守卫)
  4. 在重用的组件里调用 beforeRouteUpdate守卫(2.2+)
  5. 在路由配置里调用beforeEnter
  6. 解析异步路由组件
  7. 在被激活的组件里调用beforeRouterEnter
  8. 调用全局的beforeResolve守卫(2.5+)
  9. 导航被确认
  10. 调用全局的afterEach钩子(全局后置守卫)
  11. 触发DOM更新
  12. 用创建好的实例调用beforeRouteEnter守卫中传给next的回调函数

this.$route:当前激活的路由的信息对象,this.$router:全局的 router 实例

setState 实现原理

1、当 setState 方法被调用后,方法会将状态传递给组件更新器,让组件更新器将状态临时存储起来。每个组件都会有自己的组件更新器,当需要更新组件时调用组件更新器。

2、状态临时保存完成后判断当前是否为批量更新模式,如果是,将组件更新器添加到更新队列中;如果不是,直接更新组件

3、更新组件时,先判断是否有状态需要更新,如果有就先计算最新状态,将得出的最新状态重新设置给组件。

计算状态时,如果状态是函数类型,调用函数传入当前状态,返回最新状态。如果状态是对象类型,使用对象状态覆盖原有状态。

4、组件状态计算完成后,通过调用组件内部的 render 方法获取新的 VirtualDOM,再通过 DOM 对象获取旧的虚拟 DOM,然后调用 diff 方法进行比对,对比完成后将差异更新到真实 DOM 对象中。

Loader 和 Plugin 有哪些不同?

loader:loader让webpack去处理那些非JavaScript文件(webpack自身只理解javascript)。
loader可以将所有类型的文件转换成webpack能处理的有效模块。然后利用webpack的打包能力,对它们进行处理。
plugin:插件可以用于执行其他范围更广的任务,插件的范围包括,打包优化,代码压缩,甚至可以重新定义环境中的变量。
插件的目的是用于解决loader无法实现的其他事。

Webpack 的完整构建流程

  1. 初始化 - Webpack 函数 - compiler
根据配置生成编译器实例 compiler,然后处理参数,执行 WebpackOptionsApply().process,根据参数加载不同内部插件
  1. 创建编译器 Compiler 实例。
  2. 根据 Webpack 参数加载参数中的插件,以及程序内置插件。
  3. 执行编译流程:创建编译过程 Compilation 实例,从入口递归添加与构建模块,模块构建完成后冻结模块,并进行优化。
  4. 构建与优化过程结束后提交产物,将产物内容写到输出文件中。

Compiler Hooks

构建器实例的生命周期可以分为 3 个阶段:初始化阶段、构建过程阶段、产物生成阶段。下面我们就来大致介绍下这些不同阶段的 Hooks :

**初始化阶段**

-   environment、afterEnvironment:在创建完 compiler 实例且执行了配置内定义的插件的 apply 方法后触发。
-   entryOption、afterPlugins、afterResolvers:在 WebpackOptionsApply.js 中,这 3Hooks 分别在执行 EntryOptions 插件和其他 Webpack 内置插件,以及解析了 resolver 配置后触发。

**构建过程阶段**

-   normalModuleFactory、contextModuleFactory:在两类模块工厂创建后触发。
-   beforeRun、run、watchRun、beforeCompile、compile、thisCompilation、compilation、make、afterCompile:在运行构建过程中触发。

**产物生成阶段**

-   shouldEmit、emit、assetEmitted、afterEmit:在构建完成后,处理产物的过程中触发。
-   failed、done:在达到最终结果状态时触发。

JS运行机制(Event Loop)

JS执行是单线程的,它是基于事件循环的。

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,会存在一个任务队列,只要异步任务有了结果,就在任务队列中放置一个事件。
  3. 当执行栈中的所有同步任务执行完后,就会读取任务队列。那些对应的异步任务,会结束等待状态,进入执行栈。
  4. 主线程不断重复第三步。

这里主线程的执行过程就是一个tick,而所有的异步结果都是通过任务队列来调度。Event Loop 分为宏任务和微任务,无论是执行宏任务还是微任务,完成后都会进入到一下tick并在两个tick之间进行UI渲染

Vue.nextTick()

MutationObserver:MO是HTML5中的API,是一个用于监视DOM变动的接口,它可以监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等。

队列控制的最佳选择是microtask,而microtask的最佳选择是Promise.但如果当前环境不支持Promise,vue就不得不降级为macrotask来做队列控制了 setTimeout 略微有点延迟 4ms。

nextTick 源码主要分为两块:能力检测和根据能力检测以不同方式执行回调队列。

能力检测

由于宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,再使用宏任务。

next-tick.js 对外暴露了nextTick这一个参数,所以每次调用Vue.nextTick时会执行:

  • 把传入的回调函数cb压入callbacks数组
  • 执行timerFunc函数,延迟调用 flushCallbacks 函数
  • 遍历执行 callbacks 数组中的所有函数

这里的 callbacks 没有直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行nextTick,不会开启多个异步任务,而是把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

keep-alive

// 在动态组件中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount"> 
    <component :is="currentComponent"></component> 
</keep-alive>

// 在vue-router中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <router-view></router-view>
</keep-alive>

include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;max定义缓存组件上限,超出上限使用置换缓存数据。

浅谈iframe的优缺点及应用场景

  • 模块分离
  • 加载三方资源
  • 重载时不需要重载整个页面
  1. 每一个都是单独模块,增加http请求
  2. 爬虫无法解读,不利于seo
  3. iframe会阻塞主页面的Onload事件及iframe和主页面共享连接池,会影响页面的并行加载。
    • .window的onload 事件需要在所有 iframe 加载完毕后(包含里面的元素)才会触发,就会影响网页加载速度。通过 JavaScript 动态设置 iframe的SRC可以避免这种阻塞情况。