2019-10-10

2019-10-10  本文已影响0人  执凉
还需要完善的点

TCP/IP五层模型的协议 OSI七层模 -> 应用层下面有表示层和会话层

应用层 // http ftp dns
传输层 // tcp udp
网络层 // ip
数据链路层 // ppp arp
物理层 // 不了解

http https

Http是基于TCP/IP协议的应用程序协议,不包括数据包的传输,主要规定了客户端和服务器的通信格式,默认使用80端口

请求头和响应头部分字段:
Host:指定服务器域名,可用来区分访问一个服务器上的不同服务
Connection:keep-alive表示要求服务器不要关闭TCP连接,close表示明确要求关闭连接,默认值是keep-alive
Accept-Encoding:说明自己可以接收的压缩方式
User-Agent:用户代理,是服务器能识别客户端的操作系统(Android、IOS、WEB)及相关的信息。作用是帮助服务器区分客户端,并且针对不同客户端让用户看到不同数据,做不同操作。
Content-Type:服务器告诉客户端数据的格式,常见的值有text/plain,image/jpeg,image/png,video/mp4,application/json,application/zip。这些数据类型总称为MIME TYPE。
Content-Encoding:服务器数据压缩方式
Transfer-Encoding:chunked表示采用分块传输编码,有该字段则无需使用Content-Length字段。
Content-Length:声明数据的长度,请求和回应头部都可以使用该字段。
Cache-Control:缓存方式

Https协议是以安全为目标的Http通道,简单来说就是Http的安全版。主要是在Http下加入SSL层(现在主流的是SSL/TLS),SSL是Https协议的安全基础。Https默认端口号为443。

SSL/TLS协议基本思路是采用公钥加密法(最有名的是RSA加密算法)。大概流程是,客户端向服务器索要公钥,然后用公钥加密信息,服务器收到密文,用自己的私钥解密。
为了防止公钥被篡改,把公钥放在数字证书中,证书可信则公钥可信。公钥加密计算量很大,为了提高效率,服务端和客户端都生成对话秘钥,用它加密信息,而对话秘钥是对称加密,速度非常快。而公钥用来机密对话秘钥。

流程:
客户端给出协议版本号、一个客户端随机数A(Client random)以及客户端支持的加密方式
服务端确认双方使用的加密方式,并给出数字证书、一个服务器生成的随机数B(Server random)
客户端确认数字证书有效,生成一个新的随机数C(Pre-master-secret),使用证书中的公钥对C加密,发送给服务端
服务端使用自己的私钥解密出C
客户端和服务器根据约定的加密方法,使用三个随机数ABC,生成对话秘钥,之后的通信都用这个对话秘钥进行加密。

Http和Https的区别如下:
https协议需要到CA申请证书,大多数情况下需要一定费用
Http是超文本传输协议,信息采用明文传输,Https则是具有安全性SSL加密传输协议
Http和Https端口号不一样,Http是80端口,Https是443端口
Http连接是无状态的,而Https采用Http+SSL构建可进行加密传输、身份认证的网络协议,更安全。
Http协议建立连接的过程比Https协议快。因为Https除了Tcp三次握手,还要经过SSL握手。连接建立之后数据传输速度,二者无明显区别。

三次握手 四次挥手

第一次握手,A向B发送信息后,B收到信息。B可确认A的发信能力和B的收信能力
第二次握手,B向A发消息,A收到消息。A可确认A的发信能力和收信能力,A也可确认B的收信能力和发信能力
第三次握手,A向B发送消息,B接收到消息。B可确认A的收信能力和B的发信能力

ACK:响应标识,1表示响应,连接建立成功之后,所有报文段ACK的值都为1
SYN:连接标识,1表示建立连接,连接请求和连接接受报文段SYN=1,其他情况都是0
FIN:关闭连接标识,1标识关闭连接,关闭请求和关闭接受报文段FIN=1,其他情况都是0,跟SYN类似
seq number:序号,一个随机数X,请求报文段中会有该字段,响应报文段没有
ack number:应答号,值为请求seq+1,即X+1,除了连接请求和连接接受响应报文段没有该字段,其他的报文段都有该字段

第一次握手:建立连接请求。客户端发送连接请求报文段,将SYN置为1,seq为随机数x。然后,客户端进入SYN_SEND状态,等待服务器确认。
第二次握手:确认连接请求。服务器收到客户端的SYN报文段,需要对该请求进行确认,设置ack=x+1(即客户端seq+1)。同时自己也要发送SYN请求信息,即SYN置为1,seq=y。服务器将SYN和ACK信息放在一个报文段中,一并发送给客户端,服务器进入SYN_RECV状态。
第三次握手:客户端收到SYN+ACK报文段,将ack设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕,客户端和服务券进入ESTABLISHED状态,完成Tcp三次握手。

第一次挥手:主机1(可以是客户端或服务器),设置seq和ack向主机2发送一个FIN报文段,此时主机1进入FIN_WAIT_1状态,表示没有数据要发送给主机2了
第二次挥手:主机2收到主机1的FIN报文段,向主机1回应一个ACK报文段,表示同意关闭请求,主机1进入FIN_WAIT_2状态。
第三次挥手:主机2向主机1发送FIN报文段,请求关闭连接,主机2进入LAST_ACK状态。
第四次挥手:主机1收到主机2的FIN报文段,向主机2回应ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段后,关闭连接。此时主机1等待主机2一段时间后,没有收到回复,证明主机2已经正常关闭,主机1页关闭连接。

tcp udp

TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,在收发数据前,必须和对方建立可靠的连接(三次握手)。
UDP是一个无连接的协议
在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

           UDP  TCP

是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输

常见状态码

1xx(临时响应)
100 //继续 请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。
101 //切换协议 请求者已要求服务器切换协议,服务器已确认并准备切换。

2xx(成功)
200 //成功 服务器已经成功处理了请求。通常,这表示服务器提供了请求的网页。
201 //已创建 请求成功并且服务器创建了新的资源
202 //已接受 服务器已接受请求,但尚未处理
203 //非授权信息 服务器已经成功处理了请求,但返回的信息可能来自另一来源
204 //无内容 服务器成功处理了请求,但没有返回任何内容
205 //重置内容 服务器成功处理了请求,但没有返回任何内容
206 //部分内容 服务器成功处理了部分GET请求

3xx(重定向)
300 //多种选择 针对请求,服务器可执行多种操作。服务器可根据请求者(user agent)选择一项操作,或提供操作列表供请求者选择。
301 //永久移动 请求的网页已永久移动到新位置。服务器返回此响应(对GET或HEAD请求的响应)时,会自动将请求者转到新位置。
302 //临时移动 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
303 //查看其它位置 请求者应当对不同的位置使用单独的GET请求来检索响应时,服务器返回此代码
304 //未修改 自动上次请求后,请求的网页未修改过。服务器返回此响应,不会返回网页的内容
305 //使用代理 请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理
307 //临时性重定向 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有的位置来进行以后的请求

4xx(请求错误)
400 //错误请求 服务器不理解请求的语法
401 //未授权 请求要求身份验证。对于需要登录的网页,服务器可能返回此响应
403 //禁止 服务器拒绝请求
404 //未找到 服务器找不到请求的网页
405 //方法禁用 禁用请求中指定的方法
406 //不接受 无法使用请求的内容特性响应请求的网页
407 //需要代理授权 此状态码与401(未授权)类似,但指定请求者应当授权使用代理
408 //请求超时 服务器等候请求时发生超时
409 //冲突 服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。
410 //已删除 如果请求的资源已永久删除,服务器就会返回此响应
411 //需要有效长度 服务器不接受不含有效内容长度标头字段的请求
412 //未满足前提条件 服务器未满足请求者在请求者设置的其中一个前提条件
413 //请求实体过大 服务器无法处理请求,因为请求实体过大,超出了服务器的处理能力
414 //请求的URI过长 请求的URI(通常为网址)过长,服务器无法处理
415 //不支持媒体类型 请求的格式不受请求页面的支持
416 //请求范围不符合要求 如果页面无法提供请求的范围,则服务器会返回此状态码
417 //未满足期望值 服务器未满足“期望”请求标头字段的要求

5xx(服务器错误)
500 //服务器内部错误 服务器遇到错误,无法完成请求
501 //尚未实施 服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码
502 //错误网关 服务器作为网关或代理,从上游服务器无法收到无效响应
503 //服务器不可用 服务器目前无法使用(由于超载或者停机维护)。通常,这只是暂时状态
504 //网关超时 服务器作为网关代理,但是没有及时从上游服务器收到请求
505 //HTTP版本不受支持 服务器不支持请求中所用的HTTP协议版本

http 缓存设置 Cache-Control

HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置,缓存位置在本地浏览器

强缓存
http/1.1 Cache-Control: max-age=xxxx
http1.0+ Expires: date

Cache-Control:no-store
禁止一切缓存(这个才是响应不被缓存的意思)。缓存通常会像非缓存代理服务器一样,向客户端转发一条 no-store 响应,然后删除对象。
Cache-Control:no-cache
强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。
Cache-Control:max-age
首部表示的是从服务器将文档传来之时起,可以认为此 文档处于新鲜状态的秒数。
Cache-Control:pubilc/private
是否只能被单个用户缓存

协商缓存
当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串
Last-Modified 表示本地文件最后修改日期,浏览器会在request header加上If-Modified-Since(上次返回的Last-Modified的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来

Etag就像一个指纹,资源变化都会导致ETag变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的
If-None-Match的header会将上次返回的Etag发送给服务器,询问该资源的Etag是否有更新,有变动就会发送新的资源回来

ETag的优先级比Last-Modified更高
具体为什么要用ETag,主要出于下面几种情况考虑:
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
某些服务器不能精确的得到文件的最后修改时间。

列举三种禁止浏览器缓存的头字段,并写出响应的设置值

Expires:告诉浏览器把回送的资源缓存多长时间 -1或0则是不缓存 简要:添加Expires头能有效的利用浏览器的缓存能力来改善页面的性能,能在后续的页面中有效避免很多不必要的Http请求,WEB服务器使用Expires头来告诉Web客户端它可以使用一个组件的当前副本,直到指定的时间为止。 例如:Expires:Thu,15 Apr 2010 20:00:00 GMT; 他告诉浏览器缓存有效性持续到2010年4月15日为止,在这个时间之内相同的请求使用缓存,这个时间之外使用http请求。

Cache-Control:no-cache Cathe-Control:max-age=315360000

Expires有一个非常大的缺陷,它使用一个固定的时间,要求服务器与客户端的时钟保持严格的同步,并且这一天到来后,服务器还得重新设定新的时间。HTTP1.1引入了Cathe-Control,它使用max-age指定组件被缓存多久,从请求开始在max-age时间内浏览器使用缓存,之外的使用请求,这样就可以消除Expires的限制, 如果对浏览器兼容性要求很高的话,可以两个都使用。

Pragma:no-cache

跨域方式

浏览器的同源策略导致的跨域问题,同源即同协议、同域名、同端口。
主要是跨全域和 iframe 跨域。

1、JSONP
利用 Script 标签不受浏览器同源策略影响:
首先前端先设置好回调函数,并将其作为 url 的参数。
服务端接收到请求后,通过该参数获得回调函数名,并将数据放在参数中将其返回。
收到结果后因为是 script 标签,所以浏览器会当做是脚本进行运行,从而达到跨域获取数据的目的。

// script 标签一般在 js 里生成,然后插入 dom
<script>
    function jsonpCallback(data) {
        alert('获得 X 数据:' + data.x);
    }
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>

支持 GET 请求而不支持 POST 等其它类行的 HTTP 请求。
只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面或 iframe 之间进行数据通信的问题。

2、CORS (Cross-origin resource sharing)

主要是后端设置 Access-Control-Allow-Origin: '域名'

简单请求:
(1) 请求方法是以下三种方法之一:HEAD、GET、POST
(2) HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:application/x-www-form-urlencoded、 multipart/form-data、text/plain

浏览器判断跨域为简单请求时候,会在Request Header中添加 Origin (协议 + 域名 + 端口)字段 , 它表示我们的请求源,CORS服务端会将该字段作为跨源标志。
CORS接收到此次请求后 , 首先会判断Origin是否在允许源(由服务端决定)范围之内,如果验证通过,服务端会在Response Header 添加 Access-Control-Allow-Origin、Access-Control-Allow-Credentials等字段。

必须字段
Access-Control-Allow-Origin:表示服务端允许的请求源,*标识任何外域,多个源,分隔
可选字段
Access-Control-Allow-Credentials:false 表示是否允许发送Cookie,设置为true同时,ajax请求设置withCredentials = true,浏览器的cookie就能发送到服务端
Access-Control-Expose-Headers:调用getResponseHeader()方法时候,能从header中获取的参数

非简单请求:
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
进行非简单请求时候 , 浏览器会首先发出类型为OPTIONS的“预检请求”,请求地址相同 ,
CORS服务端对“预检请求”处理,并对Response Header添加验证字段,客户端接受到预检请求的返回值进行一次请求预判断,验证通过后,主请求发起。

3、Server proxy 就是在 server 端转发。

4、location.hash + iframe 利用 hash 值来传递数据

iframe 修改 parent.location.hash 从而传递数据

5、window.name + iframe 利用 window.name 是个特殊的全局变量

6、postMessage

创建一个 iframe,使用 iframe 的一个方法 postMessage 可以向 http://localhost:8081/b.html 发送消息,然后监听 message,可以获得其他文档发来的消息。

// a.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>a.html</title>
</head>
<body>
    <iframe src="http://localhost:8081/b.html" style='display: none;'></iframe>
    <script>
    window.onload = function() {
        let targetOrigin = 'http://localhost:8081';
        window.frames[0].postMessage('我要给你发消息了!', targetOrigin);
    }
    window.addEventListener('message', function(e) {
        console.log('a.html 接收到的消息:', e.data);
    });
    </script>
</body>
</html>

// b.html
<script>
    window.addEventListener('message', function(e) {
        if(e.source != window.parent) {
        return;
        }
        let data = e.data;
        console.log('b.html 接收到的消息:', data);
        parent.postMessage('我已经接收到消息了!', e.origin);
    });
</script>

7、document.domain 主域相同而子域不同

XSS与CSRF分别是什么,两者有什么联系?如何防御?

XSS即 Cross Site Script,网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。
XSS攻击可以分为3类:反射型(非持久型)、存储型(持久型)、基于DOM。
反射型 XSS 只是简单地把用户输入的数据 “反射” 给浏览器,这种攻击方式往往需要攻击者诱使用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。
存储型 XSS 会把用户输入的数据 "存储" 在服务器端,当浏览器请求数据时,脚本从服务器上传回并执行。这种 XSS 攻击具有很强的稳定性。
基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击。

XSS 攻击的防范
现在主流的浏览器内置了防范 XSS 的措施,例如 CSP。
HttpOnly 防止劫取 Cookie, 浏览器将禁止页面的Javascript 访问带有 HttpOnly 属性的Cookie。
输入检查 输出检查,对用户输入所包含的特殊字符或标签进行编码或过滤,如 <,>,script

CSRF,即 Cross Site Request Forgery,中译是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。
通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。
CSRF 攻击的防范
验证码
Referer Check,根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的"源"。
添加 token 验证,在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

浏览器渲染机制 回流重绘

渲染引擎——webkit和Gecko

处理 HTML 标记并构建 DOM 树。
处理 CSS 标记并构建 CSSOM 树。
将 DOM 与 CSSOM 合并成一个渲染树。
根据渲染树来布局,以计算每个节点的几何信息。
将各个节点绘制到屏幕上。

渲染阻塞当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行,然后继续构建DOM。每次去执行JavaScript脚本都会严重地阻塞DOM树的构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。

replaint:屏幕的一部分重画,不影响整体布局,比如某个CSS的背景色变了,但元素的几何尺寸和位置不变。
reflow: 意味着元件的几何尺寸变了,我们需要重新验证并计算渲染树。是渲染树的一部分或全部发生了变化。这就是Reflow,或是Layout。

防抖节流的实现

debounce 防抖,把连续触发的事件合并一次执行,一定时间内只出发一次,如果指定时间内又触发一次则重新计时。

function debounce(fn) {
  let timeout = null;

  return function() {
    clearTimeout(timeout);

    timeout = setTimeout(() => {
      fn.apply(this, arguments); // 把 this 指向调用者
    }, 500);
  }
}

throttle 节流,一段时间只能执行一次。

function throttle(fn) {
  let running = false;

  return function() {
    if (running) return;

    running = true;

    setTimeout(() => {
      fn.apply(this, arguments);
      running = false;
    }, 500);
  }
}

undersocre 的实现

_.debounce = function(func, wait, immediate) {
    var timeout, result;

    var later = function(context, args) {
        timeout = null;
        if (args) result = func.apply(context, args);
    };

    var debounced = restArguments(function(args) {
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(later, wait);
            if (callNow) result = func.apply(this, args);
        } else {
            timeout = _.delay(later, wait, this, args);
        }

        return result;
    });

    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced;
};

_.throttle = function(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
        previous = options.leading === false ? 0 : _.now();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = _.now();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
            context = this;
            args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function() {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
};

简述document和window两个对象区别

文档和窗口的区别
window.document === document

window是BOM中的一个对象。window是BOM的顶层对象,其他的BOM对象都是window的属性
window 是窗口还包括 history、location、navigator、screen 等
document就是整个DOM树的根节点。可以通过document访问到dom树的所有节点。

深拷贝

(function($) {
    "use strict";

    const types =
        "Array,Object,String,Date,RegExp,Function,Boolean,Number,Null,Undefined".split(",");

    for (let i = types.length; i--; ) {
        $["is" + types[i]] = str =>  Object.prototype.toString.call(str).slice(8, -1) === types[i];
    }

    return $;
})(window.$ || (window.$ = {}));

function deepCopy (obj) {
    const copyed = []; // 用于处理循环引用

    for(let i= 0 ;i < copyed.length; i++){
        if(copyed[i].current === obj){
            return copyed[i].copy;
        }
    }

    if ($.isFunction(obj)) {
        return new Function("return " + obj.toString())();
    }
    
    if (obj === null || typeof obj !== "object"){
        return obj; 
    }

    let name, value;
    const target = $.isArray(obj) ? [] : {};

    for (name in obj) { 
        value = obj[name]; 

        if ($.isArray(value) || $.isObject(value)) {
            target[name] = deepCopy(value);
        } else if ($.isFunction(value)) {
            target[name] = new Function("return " + value.toString())();
        } else {
            target[name] = value;
        } 
    }

    copyed.push({current: obj, copy: target}) 

    return target;
}

apply call bind

func.bind(thisArg[, arg1[, arg2[, ...]]])
func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [arg1, arg2, ...])

// 需要注意的是 bind 的多次传值和构造函数调用时的结果
Function.prototype.myBind = function(context) {
    const self = this;
    const ctx = context || window;
    const args = [...arguments].slice(1);

    const bound = function() {
        var newArgs = [...args, ...arguments];

        // 构造函数调用走这里
        if (this instanceof bound) {
            // 把 bound 的 prototype 指向函数
            // 我在想这样应该也可以吧
            // this.__proto__ = self.prototype;

            bound.prototype = Object.create(self.prototype);

            // 使用 new 的新对象来调用
            const result = self.apply(this, newArgs);

            // new 对象的时候如果返回值是一个对象或者函数则返回的是这个结果
            if((typeof result === 'object' && result !== null) || typeof result === 'function'){
                return result;
            }

            return this;
        }

        return self.apply(ctx, newArgs);
    }

    return bound;
}

Function.prototype.myCall = function(context) {
    const ctx = context || window;
    const args = [...arguments].slice(1);

    ctx.fn = this; // this是指调用myCall的function
    ctx.fn(...args);
    delete ctx.fn;
}

Function.prototype.myApply = function(context) {
    const ctx = context || window;
    const args = [...arguments].slice(1)[0];

    ctx.fn = this;
    ctx.fn(...args);

    delete ctx.fn;
}

事件模型

当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕

js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。

当执行栈的任务执行完了之后,会先去清空微任务队列,再从宏任务队列里获取事件加入执行栈执行。

以下事件属于宏任务:
setInterval()
setTimeout()
以下事件属于微任务
new Promise()
new MutaionObserver()

给出如下虚拟dom的数据结构,如何实现简单的虚拟dom,渲染到目标dom树

let demoNode = ({
tagName: 'ul',
props: {'class': 'list'},
children: [
({tagName: 'li', children: ['douyin']}),
({tagName: 'li', children: ['toutiao']})
]
});

function Element({tagName, props, children}){
if(!(this instanceof Element)){
return new Element({tagName, props, children})
}
this.tagName = tagName;
this.props = props || {};
this.children = children || [];
}

Element.prototype.render = function(){
var el = document.createElement(this.tagName),
props = this.props,
propName,
propValue;
for(propName in props){
propValue = props[propName];
el.setAttribute(propName, propValue);
}
this.children.forEach(function(child){
var childEl = null;
if(child instanceof Element){
childEl = child.render();
}else{
childEl = document.createTextNode(child);
}
el.appendChild(childEl);
});
return el;
};

bfc 两栏布局 三栏布局 垂直水平居中实现 自适应的正方形

块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

触发 bfc:
【1】根元素,即HTML元素
【2】float的值不为none
【3】overflow的值不为visible
【4】display的值为inline-block、table-cell、table-caption
【5】position的值为absolute或fixed

1.自适应两栏布局 // bfc的区域不会与float 重叠,因此会根据父元素的宽度,和float元素的宽度,自适应宽度。
2.可以阻止元素被浮动元素覆盖 // bfc 元素不会被浮动元素覆盖
3.可以包含浮动元素——清除内部浮动 // 计算 bfc 元素高度的时候,浮动元素也参与计算。
4.分属于不同的BFC时可以阻止margin重叠

两栏布局即左边固定右边自适应

<div class="side"></div>
<div class="bfc"></div>
<style>
    .aside {
        float: left;
        width: 100px;
        height: 150px;
        background: #f66;
    }

    .bfc {
        height: 200px;
        background: #fcc;
        overflow: hidden;
    }
</style>

圣杯布局和双飞翼布局都是三栏布局的方法,左右定宽中间自适应

圣杯布局
首先把left、middle、right都放出来
给它们三个设置上float: left, 脱离文档流;
一定记得给container设置上overflow: hidden; 可以形成BFC撑开文档
left、right设置上各自的宽度
middle设置width: 100%;
给left、middle、right设置position: relative;
left设置 left: -leftWidth, right设置 right: -rightWidth;
container设置padding: 0, rightWidth, 0, leftWidth

<div class="content">
    <div class="middle"></div>
    <div class="left"></div>
    <div class="right"></div>
</div>

.content {
    overflow: hidden;
    padding: 0 100px;
}

.middle {
    position:relative;          
    width: 100%;
    float: left;
    height: 80px;
    background: green;
}

.left {
    position:relative;
    width: 100px;
    float: left;
    left: -100px;
    height: 80px;
    margin-left: -100%;
    background: yellow;
}

.right {
    position: relative;
    width: 100px;
    float: left;
    height: 80px;
    margin-left: -100px;
    right: -100px;
    background: pink
}

首先把left、middle、right都放出来, middle中增加 mid
给它们三个设置上float: left, 脱离文档流;
一定记得给container设置上overflow: hidden; 可以形成BFC撑开文档
left、right设置上各自的宽度
middle设置width: 100%, mid 通过 margin 来撑到 100% 宽度,然后 left right 通过 - margin 放到预想的位置;

<div class="content">
    <div class="middle">
        <div class="mid"></div>
    </div>
    <div class="left"></div>
    <div class="right"></div>
</div>

.content {
    overflow: hidden;
}

.middle {
    width: 100%;
    float: left;
}

.mid {
    margin: 0 100px;
    height: 80px;
    background: green;
}

.left {
    width: 100px;
    float: left;
    height: 80px;
    margin-left: -100%;
    background: yellow;
}

.right {
    width: 100px;
    float: left;
    height: 80px;
    margin-left: -100px;
    background: pink
}

垂直水平居中实现

<div class="wrapper">
    <div class="content"></div>
</div>
// 绝对定位+margin:auto
.wrapper {
    position: relative;
}

.content {
    margin: auto;
    position: absolute;
    left: 0; right: 0; top: 0; bottom: 0;
}
// 绝对定位+margin反向偏移
.wrapper {
    position: relative;
}

.content {
    margin: auto;
    position: absolute;
    left: 50%; top: 50%;
    margin-left: - (width + padding) / 2;
    margin-top: - (height + padding) / 2;
}

// 绝对定位+transform反向偏移
.wrapper {
    position: relative;
}

.content {
    margin: auto;
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
}

// table 
// display: inline-block;
// display: flex;

自适应的正方形
使用 vw,vh,vmin,vmax;
设置垂直方向的 padding 撑开容器;

div { 
    width: 100%;
    height:0;
    padding-bottom: 100%;
}

利用伪元素的 margin(padding)-top 撑开容器

div {
     width: 100%;
     overflow: hidden;
}
div:after {
   content: '';
   display: block;
   margin-top: 100%;
}

描述一下JS 的new操作符具体做了什么并实现一个

// 创建一个新对象obj
// 把obj的__proto__指向 constructor.prototype 实现继承
// 执行构造函数,传递参数,改变this指向 constructor.call(obj, ...args)

function _new(){
    // 这里或者用 Object.Create(Constructor);
    // 这样原型的指向就有了
    const obj = {};
    const Constructor = Array.prototype.shift.call(arguments);
  
    obj.__proto__ = Constructor.prototype;
    const result = Constructor.apply(obj, arguments);
  
    return typeof result === 'object' ? result : obj;
}

实现 String.prototype.trim

String.prototype.trim = function () {
    return this.replace(/^[\s]+|[\s]+$/g, '');
};

柯里化 add(a)(b)(c)和add(a,b,c)

  1. 参数复用;2. 提前返回;3. 延迟计算/运行。
function curry(fn) {
    var length = fn.length; // 闭包引用,判断参数个数是否够了
    var args = [...arguments].slice(1) || [];

    return function() {
        var _args = [...args, ...arguments];

        if (_args.length < length) {
            return curry.call(this, fn, ..._args);
        } else {
            return fn.apply(this, _args);
        }
    }
}

精准获得页面元素的位置

那就是使用getBoundingClientRect()方法。它返回一个对象,其中包含了left、right、top、bottom四个属性,分别对应了该元素的左上角和右下角相对于浏览器窗口(viewport)左上角的距离。

var X= this.getBoundingClientRect().left;
var Y =this.getBoundingClientRect().top;
//再加上滚动距离,就可以得到绝对位置
var X= this.getBoundingClientRect().left+document.documentElement.scrollLeft;
var Y =this.getBoundingClientRect().top+document.documentElement.scrollTop;

proxy

var man = {
name:'jscoder',
age:22
};

var proxy = new Proxy(man, {
get: function(target, property) {
if(property in target) {
return target[property];
} else {
throw new ReferenceError(Property ${property} does not exist.);
}
},

set: function(obj, prop, value) {}

});

cdn gzip 怎么配置

全称Content Delivery Network即内容分发网络。
①、当用户点击APP上的内容,APP会根据URL地址去本地DNS(域名解析系统)寻求IP地址解析。

②、本地DNS系统会将域名的解析权交给CDN专用DNS服务器。

③、CDN专用DNS服务器,将CDN的全局负载均衡设备IP地址返回用户。

④、用户向CDN的负载均衡设备发起内容URL访问请求。

⑤、CDN负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的缓存服务器。

⑥、负载均衡设备告诉用户这台缓存服务器的IP地址,让用户向所选择的缓存服务器发起请求。

⑦、用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。

⑧、如果这台缓存服务器上并没有用户想要的内容,那么这台缓存服务器就要网站的源服务器请求内容。

⑨、源服务器返回内容给缓存服务器,缓存服务器发给用户,并根据用户自定义的缓存策略,判断要不要把内容缓存到缓存服务器上

cdn 获取 cdn host,webpack 打包时 output 到cdn

output: {
    publicPath: `${CDN_HOST}/static/`,
    path: releasePath('./public/static/'),
    filename: `[name].[${HASH}].js`
}

node端很简单,只要加上compress模块即可,代码如下
gzip

var compression = require('compression')
var app = express();

//尽量在其他中间件前使用compression
app.use(compression());

lazy load 实现

主要是先不填写 img src,用其他属性代替,监听滚动事件,判断图片距离顶部高度和滚动高度。

window.onscroll = function () {
  var bodyScrollHeight =  document.documentElement.scrollTop;// body滚动高度
  var windowHeight = window.innerHeight;// 视窗高度
  var imgs = document.getElementsByClassName('tamp-img');
  for (var i =0; i < imgs.length; i++) {
    var imgHeight = imgs[i].offsetTop;// 图片距离顶部高度 
    if (imgHeight  < windowHeight  + bodyScrollHeight - 340) {
       imgs[i].src = imgs[i].getAttribute('data-src');
       imgs[i].className = imgs[i].className.replace('tamp-img','');
    }
  }
};

IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。

IntersectionObserver.thresholds
一个包含阈值的列表, 按升序排列, 列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会生成一个通知(Notification)。如果构造器未传入值, 则默认值为0。

var observer = new IntersectionObserver(function(changes){  
    changes.forEach(function(index,item){
        if(item.intersectionRatio > 0 && item.intersectionRatio < 1){
            //target:被观察的目标元素,是一个 DOM 节点对象
            item.target.src = item.target.dataset.src;
        }
    });
});
function addObserver(){
    var listItems = document.querySelectorAll('.img-item');
    listItems.forEach(function(item){
        //实例的observe方法可以指定观察哪个DOM节点
        //开始观察  observe的参数是一个 DOM 节点对象
        observer.observe(item);
    });
}
addObserver();

复习一下正则

正则复习宝典

看 log 是怎么写和收集的

封装一个 createLogger 函数,传入 options,在需要的时候调用 logger 的写入 log 的方法。

function createLogger(options) {
    logger = bunyan.createLogger({
        name: logName,
        streams: getStreams({debug, logDir, logFileName, errorLogFileName}),
        serializers: {
            ...DEFAULT_SERIALIZERS,
            ...serializers
        },
        ...bunyanOptions
    });

    return logger
}

logger.info(options);
// JSNlog 主要就是结合 logger,当js抛出异常时,可以通过 setOptions 向制定 url 发送请求
// 在 server 端接收到固定 url 请求就调用 logger 写入日志
JN.logger().setOptions({
    defaultAjaxUrl: api.uploadV2Report
});

配置 sourceMap

output: {
    publicPath: `${CDN_HOST}/v2/static/${page}/`,
    path: releasePath(`./static/${page}/`),
    filename: `[name].[${HASH}].js`,
    ...(SRC_MAP ? {} : {sourceMapFilename: '../../maps/[file].map'})
}

原型链

var Fn = function () {};
var fn = new Fn;

fn.__proto__ === Fn.prototype;
Fn.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;

Object.__proto__ === Function.prototype //true Object created by Function
Function.__proto__ === Function.prototype // true Function created by Function
Object.__proto__ === Function.__proto__//true


Function.prototype.__proto__ === Object.prototype // true Function.prototype created by Object
Object.prototype.__proto__ === null;
//因此
Function instanceof Object //true
Object instanceof Function //true

原生 Ajax

request.readyState
0: 请求未初始化
1: 服务器连接已建立
2: 请求已接收
3: 请求处理中
4: 请求已完成,且响应已就绪

var request = new XMLHttpRequest(); // 新建XMLHttpRequest对象

request.onreadystatechange = function () { // 状态发生变化时,函数被回调
    if (request.readyState === 4) { // 成功完成
        // 判断响应结果:
        if (request.status === 200) {
            // 成功,通过responseText拿到响应的文本:
            return success(request.responseText);
        } else {
            // 失败,根据响应码判断失败原因:
            return fail(request.status);
        }
    } else {
        // HTTP请求还在继续...
    }
}

// 发送请求:
request.open('GET', '/api/xxxxx');
request.send();

模板引擎实现

var TemplateEngine = function(html, options) {
    var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0, match;
    var add = function(line, js) {
        js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
        return add;
    }
    while(match = re.exec(html)) {
        add(html.slice(cursor, match.index))(match[1], true);
        cursor = match.index + match[0].length;
    }
    add(html.substr(cursor, html.length - cursor));
    code += 'return r.join("");';
    return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
}

var template = 
'My skills:' + 
'<%if(this.showSkills) {%>' +
    '<%for(var index in this.skills) {%>' + 
    '<a href="#"><%this.skills[index]%></a>' +
    '<%}%>' +
'<%} else {%>' +
    '<p>none</p>' +
'<%}%>';
console.log(TemplateEngine(template, {
    skills: ["js", "html", "css"],
    showSkills: true
}));

模板引擎实现
我在想能不能直接用 es6 字符串模板 hack。

实现 promise

非常简单的了解了 vuex vue-router 实现原理

Vue.use(Vuex)
install 方法拿到 Vue 实例,使用 mixin 在 beforeCreate 方法里混入 $store 的挂载。
然后 new Vuex.Store({modules}),实例化 store。

webpack 打包速度优化、产物优化

webpack-bundle-analyzer

速度优化:
减小文件搜索范围:使用 alias 让 webpack 更快的检索到文件路径;

resolve: {
    alias: {
        // Ref: https://cn.vuejs.org/v2/guide/installation.html#独立构建-vs-运行时构建
        'vue$': 'vue/dist/vue.common.js',
        'app-v2': srcPath('.'),
        'yqg-common': srcPath('../../common')
    }
}

设置 test|exclude|include 处理需要处理的目录

cacheDirectory
cacheDirectory是loader的一个特定的选项,默认值是false。指定的目录(use: 'babel-loader?cacheDirectory=cacheLoader')将用来缓存loader的执行结果,减少webpack构建时Babel重新编译过程。

Happypack:将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展多进程模式,从而加速代码构建;

产物优化:
按需异步加载模块,动态 import

const Foo = () => import('./Foo.vue')

splitChunks: {
    maxAsyncRequests: 12,
    maxInitialRequests: 12,
    cacheGroups: {
        vue: {
            chunks: 'all',
            test: /\/node_modules\/vue/
        },

        util: {
            chunks: 'all',
            test: /\/node_modules\/(exif-js|lrz|moment|qr-image|underscore)/
        },

        vendor: {
            chunks: 'all',
            test: /\/node_modules\//,
            priority: -10
        }
    }
}

复习 react rn

react 16.4.1
react-native 0.56.0

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

const [count, setCount] = useState(0);

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

useEffect(() => {
    subscribe()
    return () => {
      unsubscribe();
    };
});

JSBridge

JavaScript 调用 Native 的方式,主要有两种:注入 API 和 拦截 URL SCHEME。
注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。

拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作

Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。(闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)

项目中使用的是 WebViewJavascriptBridge
IOS主要使用的是 拦截 URL SCHEME 的方式。(看了下 android 似乎是类似的实现)
初始化时,在 window 上建立一个 WebViewJavascriptBridge 对象,并且初始化 iframe。

window.WebViewJavascriptBridge = {
    init: init,
    send: send,
    registerHandler: registerHandler,
    callHandler: callHandler,
    _fetchQueue: _fetchQueue,
    _handleMessageFromObjC: _handleMessageFromObjC
}

var doc = document
_createQueueReadyIframe(doc)
var readyEvent = doc.createEvent('Events')
readyEvent.initEvent('WebViewJavascriptBridgeReady')
readyEvent.bridge = WebViewJavascriptBridge
doc.dispatchEvent(readyEvent)

function _createQueueReadyIframe(doc) {
    messagingIframe = doc.createElement('iframe')
    messagingIframe.style.display = 'none'
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
    doc.documentElement.appendChild(messagingIframe)
}

然后调用 WebViewJavascriptBridge 的 init 方法,调用时使用 callHandler 方法,他会调用 _doSend 方法,如果有回调的话,会生成一个 callbackId,把 callback 加入 responseCallbacks 中,并把 callbackId 添加到发送给 native 的 message 中, 然后把 message 添加到 sendMessageQueue 中, 设置 iframe src,发送请求。

function callHandler(handlerName, data, responseCallback) {
    _doSend({ handlerName:handlerName, data:data }, responseCallback)
}

function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime()
        responseCallbacks[callbackId] = responseCallback
        message['callbackId'] = callbackId
    }
    sendMessageQueue.push(message)
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
}

native 拦截到这个 SCHEME 后,会调用 WebViewJavascriptBridge._fetchQueue(注意 android 这里不能直接拿到返回值,所以把返回值拼到 SCHEME 后面,相当于再发一次请求),其实就是将 sendMessageQueue JSON.stringify,拿到这个字符串去解析,native 解析到 callbackId 就把 callbackId 转成 responseId 向 js 发一个请求(我感觉像是往 receiveMessageQueue 里塞了个回调函数,然后触发了 _doDispatchMessageFromObjC)。

_doDispatchMessageFromObjC 有两种情况,一种是回调,一种是 native 主动调用

function _handleMessageFromObjC(messageJSON) {
    if (receiveMessageQueue) {
        receiveMessageQueue.push(messageJSON)
    } else {
        _dispatchMessageFromObjC(messageJSON)
    }
}

function _doDispatchMessageFromObjC() {
    var message = JSON.parse(messageJSON);
    var messageHandler;
    var responseCallback;
    //回调
    if (message.responseId) {
        responseCallback = responseCallbacks[message.responseId];
        if (!responseCallback) {
            return;
        }
        responseCallback(message.responseData);
        delete responseCallbacks[message.responseId];
    } else {//主动调用
        if (message.callbackId) {
            var callbackResponseId = message.callbackId;
            responseCallback = function(responseData) {
                _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
            };
        }

        //获取JS注册的函数
        var handler = messageHandlers[message.handlerName];
        if (!handler) {
            console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
        } else {
            //调用JS中的对应函数处理
            handler(message.data, responseCallback);
        }
    }
}

react native

setup props and state

componentWillMount

render

componentDidMount

props:
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate

state:
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate

componentWillUnmount

promise 实现

const PROMISE_STATUS = {
    pending: 'pending',
    fulfilled: 'fulfilled',
    rejected: 'rejected'
}

function MyPromise(fn) {
    const self = this;
    // 初始化状态,回调队列及结果(这个结果可以用于透传)
    self.status = PROMISE_STATUS.pending;
    self.onResolvedCallback = [];
    self.onRejectedCallBack = [];
    self.data = null;

    function resolve(value) {
        self.data = value;
        self.status = PROMISE_STATUS.fulfilled;

        // 如果回调队列有值,说明调用 then 时异步任务还未完成,所以 then 时把
        // 回调加入回调队列,当异步任务结束时,执行回调队列的任务,要变成异步任务。
        if (self.onResolvedCallback.length) {
            setTimeout(() => {
                for (callback of self.onResolvedCallback) {
                    callback(value);
                }
            }, 0);
        }
    }

    function reject(error) {
        self.data = error;
        self.status = PROMISE_STATUS.rejected;

        if (this.onRejectedCallBack.length) {
            setTimeout(() => {
                for (callback of this.onRejectedCallBack) {
                    callback(reason);
                }
            }, 0);
        }
    }

    try {
        fn(resolve, reject);
    } catch(err) {
        reject(err);
    }
}

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const self = this;

    // then 的返回值是一个 promise,当前 promise 当返回值还是 promise 实例时,
    // 调用实例的 then 方法,把 resolve 和 reject 当作 onFulfilled, onRejected,使其自执行。
    if (self.status === PROMISE_STATUS.fulfilled) {
        return new MyPromise((resolve, reject) => {
            const result = onFulfilled();

            if (result instanceof MyPromise) {
                // 当执行结果返回的是一个promise实例
                // 等待这个promise实例状态改变后,把promise实例的结果透传给 then 这个 promise 的 resolve,
                // 其实就是 resolve(result的结果)
                result.then(resolve, reject);
            } else {
                resolve(result);
            }
        });
    } else if (self.status === PROMISE_STATUS.rejected) {
        return new MyPromise((resolve, reject) => {
            const result = onRejected();

            if (result instanceof MyPromise) {
                result.then(resolve, reject);
            } else {
                reject(result);
            }
        });
    } else {
        // 当状态还是 pending 时,说明异步任务还未执行完,所以把回调添加到回调队列。
        return new MyPromise((resolve, reject) => {
            self.onResolvedCallback.push((value) => {
                setTimeout(() => {
                    try {
                        var result = onFulfilled(self.data);
                        if (result instanceof Promise) {
                            result.then(resolve, reject);
                        } else {
                            resolve(result);
                        }
                    } catch (err) {
                        reject(err);
                    }
                });
            });
            
            self.onRejectedCallBack.push((value) => {
                setTimeout(function() {
                    try {
                        var result = onFulfilled(self.data);
                        if (result instanceof Promise) {
                            result.then(resolve, reject);
                        } else {
                            resolve(result);
                        }
                    } catch (err) {
                        reject(err);
                    }
                });
            });
        })
    }
}

MyPromise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected);
}

const p = new MyPromise(function(resolve, reject) {
  setTimeout(function() {
    resolve(1);
  }, 2000);
});

p.then(function(v) {
  console.log(v);
  return 2;
}).then(function(v) {
  console.log(v);
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve(3);
    }, 3000);
  });
}).then(function(v) {
  console.log(v);
});

promise.all 实现

1、接收一个 Promise 实例的数组或具有 Iterator 接口的对象
2、如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象
3、如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调4、只要有一个失败,状态就变为 rejected,返回值将直接传递给回调all() 的返回值也是新的 Promise 对象

function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if (!isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }
    let resolvedCounter = 0;
    const promiseNum = promises.length;
    const resolvedValues = new Array(promiseNum);
    for (let i = 0; i < promiseNum; i++) {
        Promise.resolve(promises[i]).then(function(value) {
          resolvedCounter++;
          resolvedValues[i] = value;
          if (resolvedCounter === promiseNum) {
            return resolve(resolvedValues)
          }
        }).catch(err => (reject(err)));
    }
  })
}

node event loop

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...

timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
idle, prepare: 这个阶段仅在内部使用,可以不必理会。
poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
check: setImmediate()的回调会在这个阶段执行。
close callbacks: 例如socket.on('close', ...)这种close事件的回调。

poll阶段
当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,收到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。

值得注意的是,poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回调执行完毕。2.执行数超过了node的限制。

check阶段
check阶段专门用来执行setImmediate()方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。

close阶段
当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去。

timer阶段
这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。

I/O callback阶段
如上文所言,这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。

闲置阶段(idle, prepare)

process.nextTick()
尽管没有提及,但是实际上node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用process.nextTick()方法会导致node进入一个死循环。。直到内存泄漏。

因为在I/O事件的回调中,setImmediate方法的回调永远在timer的回调前执行。

上一篇下一篇

猜你喜欢

热点阅读