学习笔记

253 阅读52分钟

学习笔记

1.跨域

前端常见跨域解决方案(全)

前端10个灵魂拷问 吃透这些你就能摆脱初级前端工程师

  • 同源策略:协议 域名 端口 同域

www.zf.cn:8081a.zf.cn:8081

  • 为什么浏览器不支持跨域

    cookie ,LocalStorage
    DOM元素也有同源策略 iframe ajax 也不支持跨域

  • 实现跨域

    • jsonp
    • cors
    • postMessage
    • window.name
    • location.hash
    • http-proxy
    • nginx
    • websocket
    • document.domain

    1.jsonp (动态创建scrpit标签,get请求,将client的函数体传给server,server端调用函数体并且,将数据回传给这个回调函数体)

    通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

    1.)原生实现:

     <script>
        var script = document.createElement('script');
        script.type = 'text/javascript';
    
        // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
        script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
        document.head.appendChild(script);
    
        // 回调执行函数
        function handleCallback(res) {
            alert(JSON.stringify(res));
        }
     </script>
    

    服务端返回如下(返回时即执行全局函数):

    handleCallback({"status": true, "user": "admin"})
    

    缺点:只能实现get一种请求。

    2.document.domain + iframe跨域(主域相同,子域不同,强制document.domain)

    此方案仅限主域相同,子域不同的跨域应用场景。

    实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

    1.)父窗口:(www.domain.com/a.html)

    <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
    <script>
        document.domain = 'domain.com';
        var user = 'admin';
    </script>
    

    2.)子窗口:(child.domain.com/b.html)

    <script>
        document.domain = 'domain.com';
        // 获取父窗口中变量
        alert('get js data from parent ---> ' + window.parent.user);
    </script>
    

    3.location.hash + iframe跨域(同域的中间页)

    实现原理: a域与b跨域相互通信,通过与a域相同的中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

    具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

    1.)a.html:(www.domain1.com/a.html)

    <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
    
        // 向b.html传hash值
        setTimeout(function() {
            iframe.src = iframe.src + '#user=admin';
        }, 1000);
        
        // 开放给同域c.html的回调方法
        function onCallback(res) {
            alert('data from c.html ---> ' + res);
        }
    </script>
    

    2.)b.html:(www.domain2.com/b.html)

    <iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
    
        // 监听a.html传来的hash值,再传给c.html
        window.onhashchange = function () {
            iframe.src = iframe.src + location.hash;
        };
    </script>
    

    3.)c.html:(www.domain1.com/c.html)

    <script>
        // 监听b.html传来的hash值
        window.onhashchange = function () {
            // 再通过操作同域a.html的js回调,将结果传回
            window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
        };
    </script>
    

4.window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

在客户端浏览器中每个页面都有一个独立的窗口对象window,默认情况下window.name为空,在窗口的生命周期中,载入的所有页面共享一个window.name并且每个页面都有对此读写的权限,window.name会一直存在当前窗口,但存储的字符串不超过2M。

同步方式:

http://localhost:8383/ccy_client/window_name.html代码

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>window.name-跨域</title>
<script type="text/javascript">
    window.name = "我是 window.name 字符串";
    setTimeout(function(){
    		// 2秒后,切换到 http://localhost:9393/ccy_server/window_name.html页面
        window.location = "http://localhost:9393/ccy_server/window_name.html";
    },2000);
</script>
</head>
<body>

</body>
</html>复制代码

http://localhost:9393/ccy_server/window_name.html代码

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>window.name-跨域</title>
<script type="text/javascript
		// 可以拿到 http://localhost:8383/ccy_client/window_name.html域名里存在 window.name 的值
    var str = window.name;
    console.log('ccy_server.window_name:'+str);
</script>
</head>
<body>

</body>
</html>

异步方式:

1.)a.html:(www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2.)proxy.html:(www.domain1.com/proxy....) 中间代理页,与a.html同域,内容为空即可。

3.)b.html:(www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

5.postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题: a.) 页面和其打开的新窗口的数据传递 b.) 多窗口之间消息传递 c.) 页面与嵌套的iframe消息传递 d.) 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数 data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。 origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1.)a.html:(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2.)b.html:(www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

6.跨域资源共享(CORS)

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

1、 前端设置:**

1.)原生ajax

// 前端设置是否带cookie
xhr.withCredentials = true;

示例代码:

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

2.)vue、react框架

a.) axios设置:

axios.defaults.withCredentials = true

7.nginx代理跨域

1、 nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

2、 nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

1.) 前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

2.Tcp/IP 三次握手

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。

进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常 、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。 进行三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(seq)(c)。此时客户端处于 SYN_SEND 状态。

    • 首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

    • 在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

    • 确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

    发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。

    在socket编程中,客户端执行connect()时,将触发三次握手。

    三次握手.png

2.1. 为什么需要三次握手,两次不行吗?

弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。

  • 第一次握手:客户端发送网络包,服务端收到了。 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常

试想如果是用两次握手,则会出现下面这种情况:

如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一直等待客户端发送数据,浪费资源。

2.2. ISN(Initial Sequence Number)是固定的吗?

当一端为建立连接而发送它的SYN时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的ISN。ISN可以看作是一个32比特的计数器,每4ms加1 。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。

三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

3. 四次挥手

对于一个已经建立的连接,TCP使用改进的四次握手来释放连接(使用一个带有FIN附加标记的报文段),客户端或服务器均可主动发起挥手动作。TCP关闭连接的步骤如下:

  • 第一步,当主机A的应用程序通知TCP数据已经发送完毕时,TCP向主机B发送一个带有FIN附加标记的报文段(FIN表示英文finish)。

  • 第二步,主机B收到这个FIN报文段之后,并不立即用FIN报文段回复主机A,而是先向主机A发送一个确认序号ACK,同时通知自己相应的应用程序:对方要求关闭连接(先发送ACK的目的是为了防止在这段时间内,对方重传FIN报文段)。

  • 第三步,主机B的应用程序告诉TCP:我要彻底的关闭连接,TCP向主机A送一个FIN报文段。

  • 第四步,主机A收到这个FIN报文段后,向主机B发送一个ACK表示连接彻底释放。

刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。

  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。

  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。

  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

      状态简写:
      * F : FIN - 结束; 结束会话 				// 四次挥手阶段
      * S : SYN - 同步; 表示开始会话请求 // 三次握手阶段 
      * R : RST - 复位;中断一个连接
      * P : PUSH - 推送; 数据包立即发送
      * A : ACK - 应答->确认号    // acknowledgment number 
      * U : URG - 紧急
      * E : ECE - 显式拥塞提醒回应
      * W : CWR - 拥塞窗口减少
    

    收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

在socket编程中,任何一方执行close()操作即可产生挥手操作。

image.png

3.1 挥手为什么需要四次?

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,"你发的FIN报文我收到了"。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

3.2 2MSL等待状态

TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。

对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。

这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。

3.3 四次挥手释放连接时,等待2MSL的意义?

MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

为了保证客户端发送的最后一个ACK报文段能够到达服务器。 因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。

两个理由:
  1. 保证客户端发送的最后一个ACK报文段能够到达服务端。 这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
  2. 防止“已失效的连接请求报文段”出现在本连接中。 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。

3.4 为什么TIME_WAIT状态需要经过2MSL才能返回到CLOSE状态?

理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文

4.HTTP

  • 1.从输入url地址栏到所有内容显示到界面上做了哪些事?**

    • 1.浏览器向DNS服务器请求解析该 URL 中的域名所对应的 IP 地址;
    • 2.解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;(三次握手);
    • 3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
    • 4.服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
    • 5.释放 TCP连接;(四次挥手)
    • 6.浏览器下载 web 服务器返回的数据及解析 html 源文件;
    • 7.生成 DOM 树,解析 css 和 js,渲染页面,直至显示完成。
  • 2.DNS 域名解析

    • DNS`解析:将域名解析为ip地址 ,由上往下匹配,只要命中便停止
      • 走缓存
      • 浏览器DNS缓存,浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束。
      • 本机DNS缓存,在windows系统中可通过c盘里一个叫hosts的文件来设置
      • 路由器DNS缓存
      • 网络运营商服务器DNS缓存 (80%的DNS解析在这完成的)
      • 递归查询

优化策略:尽量允许使用浏览器的缓存,能给我们节省大量时间,下面有dns-prefetch的介绍,每次dns解析大概需要20-120秒

  • 3.DNS详细解释:

    • 1.浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置。
    • 2.如果浏览器缓存中没有(专业点叫还没命中),浏览器会检查操作系统缓存中有没有对应的已解析过的结果。而操作系统也有一个域名解析的过程。在windows中可通过c盘里一个叫hosts的文件来设置,如果你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这 个ip地址。
    • 3.但是这种操作系统级别的域名解析规程也被很多黑客利用,通过修改你的hosts文件里的内容把特定的域名解析到他指定的ip地址上,造成所谓的域名劫持。所以在windows7中将hosts文件设置成了readonly,防止被恶意篡改。
    • 4.如果至此还没有命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了。
    • 5.如果LDNS仍然没有命中,就直接跳到Root Server 域名服务器请求解析
    • 6.根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器如.com .cn .org等)地址
    • 7.此时LDNS再发送请求给上一步返回的gTLD
    • 8.接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器
    • 9.Name Server根据映射关系表找到目标ip,返回给LDNS
    • 10.LDNS缓存这个域名和对应的ip
    • LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束
    1. 浏览器解析数据,绘制渲染页面的过程

    • 先预解析(将需要发送请求的标签的请求发出去)
    • 从上到下解析html文件
    • 遇到HTML标签,调用html解析器将其解析DOM
    • 遇到css标记,调用css解析器将其解析CSSOM
    • link 阻塞 - 为了解决闪屏,所有解决闪屏的样式
    • style 非阻塞,与闪屏的样式不相关的
    • DOM树和CSSOM树结合在一起,形成render
    • layout布局 render渲染(repaint/reflow),负责各元素尺寸、位置等的计算。
    • 遇到script标签,阻塞,调用js解析器解析js代码,可能会修改DOM树,也可能会修改CSSOM
    • DOM树和CSSOM树结合在一起,形成render
    • layout布局 render渲染(重排重绘)
    • script标签的属性 asnyc defer
    • 浏览器将各层的信息发送给GPU进程,GPU会将各层合成(composite)显示在页面上,渲染完毕后就是load事件了,之后就是自己的JS逻辑处理了

    img

    GUI渲染线程与JS引擎线程是互斥的,阻碍页面的渲染 .

    解决方案:

    建议把 <script> 标签放到 <body> 的最后面。如果不放在最后,也可使用defer或者async属性,异步加载js文件。

    二者的区别是:async 会在下载完之后立即执行;而 defer 会等到DOM Tree解析完成之后再去执行。

    img

  • 5.重绘和回流(重排)

    • 回流:

      回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。

    • 重绘:

      重绘是由于节点的几何属性发生改变或者由于样式发生改变但不会影响布局。例如outline, visibility, color、background-color等,重绘的代价是高昂的,因为浏览器必须验证DOM树上其他节点元素的可见性。

    • 什么时候会发生回流呢?

      • 1、添加或删除可见的DOM元素

      • 2、元素的位置发生变化

      • 3、元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)

      • 4、内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。

      • 5、页面一开始渲染的时候(这肯定避免不了)

      • 6、浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

      而重绘是指在布局和几何大小都不变得情况下,比如次改一下background-color,或者改动一下字体颜色的color等。 注意:回流一定会触发重绘,而重绘不一定会回流

    • 如何减少回流和重绘

      1、CSS优化法

      • (1)使用 transform 替代 top
      • (2)使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局
      • (3)避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局。
      • (4)尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。
      • (5)避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
      • (6)将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame。
      • (7)避免使用CSS表达式,可能会引发回流。
      • (8)将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。
      • (9)CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

      2、JavaScript优化法

      • (1)避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
      • (2)避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中, Document.createDocumentFragment()创建文档碎片,不会引发页面回流。
      • (3)避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

    5.HTTP状态码

    • 1xx消息——请求已被服务器接收,继续处理

      100 continue        //迄今为止的所有内容都是可行的,客户端应该继续请求
      102 processing      //服务器已收到并正在处理该请求,但没有响应可用
      
    • 2xx成功——请求已成功被服务器接收、理解、并接受

      200 OK
      201 Created        //该请求已成功,并因此创建了一个新的资源。这通常是在POST请求,或是某些PUT请求之后返回的响应。
      202 Accept         //请求已经接收到,但还未响应,没有结果。
      204 No Content     //服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。
      205 Reset Content  //服务器成功处理了请求,且没有返回任何内容。
      
    • 3xx重定向——需要后续操作才能完成这一请求

      300 Multiple Choice      //被请求的资源有一系列可供选择的回馈信息,用户或浏览器能够自行选择一个首选的地址进行重定向。
      301 Moved Permanently    //永久移动。请求的资源已被永久的移动到新URI,浏览器会自动定向到新URI。
      302 Found                //临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
      303 See Other            //对应当前请求的响应可以在另一个 URI 上被找到,客户端应当采用 GET 的方式访问那个资源。
      304 Not Modified         //请求内容未修改
      305 Use Proxy            //被请求的资源必须通过指定的代理才能被访问。
      307 Temporary Redirect   //临时重定向,与302类似。不允许改变请求方式
      308 Permanent Redirect   //永久重定向,不允许改变请求方式
      

      301,302是http1.0的内容,303、307、308是http1.1的内容。

      301和302本来在规范中是不允许重定向时改变请求方法的(将POST改为GET),但是许多浏览器却允许重定向时改变请求方法(这是一种不规范的实现)。

      303的出现正是为了给上面的301,302这种行为作出个规范(将错就错吧),也就是允许重定向时改变请求方法。此外303响应禁止被缓存。

      大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的,所以303基本用的很少,一般用302。

      307和308的出现也是给上面的行为做个规范,不过是不允许重定向时改变请求方法

    • 4xx请求错误——请求含有词法错误或者无法被执行

      400 Bad Request      //客户端请求有语法错误,不能被服务器所理解
      401 Unauthorized     //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 
      403 Forbidden        //服务器收到请求,但是拒绝提供服务
      404 Not Found        //请求资源不存在,eg:输入了错误的URL
      405 Method Not Allowed
      406 Not Acceptable
      408 Request Timeout  //请求超时
      
    • 5xx服务器错误——服务器在处理某个正确请求时发生错误

      500 Internal Server Error     //服务器发生不可预期的错误
      502 Bad Gateway
      503 Server Unavailable        //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
      504 Gateway Timeout           // 当服务器作为网关,不能及时得到响应时返回此错误代码。
      

6.HTTP请求方法

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

  • HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

    GET     请求指定的页面信息,并返回实体主体。
    HEAD     类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
    POST     向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
    PUT     从客户端向服务器传送的数据取代指定的文档的内容。
    DELETE      请求服务器删除指定的页面。
    CONNECT     HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
    OPTIONS     允许客户端查看服务器的性能。
    TRACE     回显服务器收到的请求,主要用于测试或诊断。
    
  • http和https区别

http:

  • 1.无状态,每个请求结束后都会被关闭,每次的请求都是独立的,它的执行情况和结果与前面的请求和之后的请求是无直接关系的,它不会受前面的请求应答情况直接影响,也不会直接影响后面的请求应答情况;服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器,就像是“人生只如初见”,比如说用户需要请求某个数据,需要登录权限,用户登录之后进行请求,结果因为http的无状态,等用户下一次还想请求一份数据,还需要再次登录,这样不就很烦了吗,所以就需要session和cookie来进行状态管理了。

  • 2.明文传输(未经过加密的报文),为什么通信时不加密是一个缺点,这是因为,按TCP/IP 协议族的工作机制,通信内容在所有的通信线路上都有可能遭到窥视。

  • 3.不验证通信方的身份,因此有可能遭遇伪装。HTTP 协议中的请求和响应不会对通信方进行确认。也就是说存在“服务器是否就是发送请求中 URI 真正指定的主机,返回的响应是否真的返回到实际提出请求的客户端”等类似问题。

  • 4.无法证明报文的完整性。因此,在请求或响应送出之后直到对方接收之前的这段时间内,即使请求或响应的内容遭到篡改,也没有办法获悉;换句话说,没有任何办法确认,发出的请求 / 响应和接收到的请 求 / 响应是前后相同的。

https解决的问题:

  • 1.信任主机的问题.采用https 的服务端必须从 CA(数字证书认证机构处于客户端与服务器双方都可信赖的第三方机构的 立场上)申请一个用于证明服务器用途类型的证书,该证书有了CA的签名,客户端才能知道访问的服务器是安全的。目前基本所有的在线购物和网银等网站或系统,关键部分应用都是https 的,客户通过信任该证书,从而信任了该主机,这样才能保证安全。

  • 2.通讯过程中的数据的泄密和被窜改.使用https协议,服务端和客户端之间的所有通讯都是加密的。客户端和服务端各有自己的一对非对称的密钥,一把叫做私有密钥(private key),另一把叫做公开密钥(public key),顾名思义,私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。使用公开密钥加密方式,发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密。利用这种方式,不需要发送用来解密的私有密钥,也不必担心密钥被攻击者窃听而盗走。

简单点说就是:HTTPS = HTTP + 认证 + 加密 + 完整性保护

7.HTTPS

HTTPS,全称Hyper Text Transfer Protocol Secure,相比http,多了一个secure,也就是TLS(SSL),一个安全套接层。https和http都属于应用层(application layer),基于TCP(以及UDP)协议,但是又完全不一样。TCP用的port是80, https用的是443。

HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用SSL(Secure Socket Layer)和 TLS(Transport Layer Security)协议代替而已。通常,HTTP 直接和 TCP 通信。当使用 SSL时,则演变成先和 SSL通信,再由 SSL和 TCP 通信了。简言之,所谓 HTTPS,其实就是身披SSL协议这层外壳的 HTTP。

img

HTTPS及加密算法

建立连接的过程:

img img

  • 一、客户端发起https连接

​ 当用户在浏览器(客户端)地址栏敲击www.scwipe.com时(浏览器去到DNS服务器获取此url对应的ip,然后客户端连接上服务端的443端口,将此请求发送给到服务端,此时客户端同时将自己支持的加密算法带给服务端;

  • 二、服务端发送证书

​ 在讲这一段之前插播一条小知识点:私钥加密的密文只有公钥才能解开;公钥加密的密文只有私钥才能解开。

服务端收到这套加密算法的时候,和自己支持的加密算法进行对比(也就是和自己的私钥进行对比),如果不符合,就断开连接;如果符合,服务端就将SSL证书发送给客户端,此证书中包括了数字证书包含的内容:1、证书颁发机构;2、使用机构;3、公钥;4、有效期;5、签名算法;6、指纹算法;7、指纹。

这里服务端发送的东西是用私钥进行加密的,公钥都能解开,并不能保证发送的数据包不被别人看到,所以后面的过程会和客户端商量选择一个对称加密(只能用私钥解开,这里详情请移步非对称、对称加解密相关问题)来对传输的数据进行加密。

  • 三、客户端验证服务端发来的证书

    • 1.验证证书

    客户端验证收到的证书,包括发布机构是否合法、过期,证书中包含的网址是否与当前访问网址一致等等。

    • 2.生成随机数(此随机数就是后面用的对称加密的私钥)

    ​ 客户端验证证书无误后(或者接受了不信任的证书),会生成一个随机数,用服务端发过来的公钥进行加密。如此一来,此随机数只有服务端的私钥能解开了。

    • 3.生成握手信息

    ​ 用证书中的签名hash算法取握手信息的hash值,然后用生成的随机数将[握手信息和握手信息的hash值]进行加密,然后用公钥将随机数进行加密后,一起发送给服务端。其中计算握手信息的hash值,目的是为了保证传回到服务端的握手信息没有被篡改。

  • 四、服务端接收随机数加密的信息,并解密得到随机数,验证握手信息是否被篡改。

    ​ 服务端收到客户端传回来的用随机数加密的信息后,先用私钥解密随机数,然后用解密得到的随机数解密握手信息,获取握手信息和握手信息的hash值,计算自己发送的握手信息的hash值,与客户端传回来的进行对比验证。

    ​ 如果验证无误,同样使用随机字符串加密握手信息和握手信息hash值发回给到客户端

  • 五、客户端验证服务端发送回来的握手信息,完成握手

    ​ 客户端收到服务端发送过来的握手信息后,用开始自己生成的随机数进行解密,验证被随机数加密的握手信息和握手信息hash值。

      验证无误后,握手过程就完成了,从此服务端和客户端就开始用那串随机数进行对称加密通信了(常用的对称加密算法有AES)。
    
  • 六、另外:

    1.常用的加密算法:

    • 对称密码算法:是指加密和解密使用相同的密钥,典型的有DES、RC5、IDEA(分组加密),RC4(序列加密);
    • 非对称密码算法:又称为公钥加密算法,是指加密和解密使用不同的密钥(公开的公钥用于加密,私有的私钥用于解密)。比如A发送,B接收,A想确保消息只有B看到,需要B生成一对公私钥,并拿到B的公钥。于是A用这个公钥加密消息,B收到密文后用自己的与之匹配的私钥解密即可。反过来也可以用私钥加密公钥解密。也就是说对于给定的公钥有且只有与之匹配的私钥可以解密,对于给定的私钥,有且只有与之匹配的公钥可以解密。典型的算法有RSA,DSA,DH;
    • 散列算法:散列变换是指把文件内容通过某种公开的算法,变成固定长度的值(散列值),这个过程可以使用密钥也可以不使用。这种散列变换是不可逆的,也就是说不能从散列值变成原文。因此,散列变换通常用于验证原文是否被篡改。典型的算法有:MD5,SHA,Base64,CRC等。

​ 2.非对称加解密算法的效率要比对称加解密要低的多。所以SSL在握手过程中使用非对称密码算法来协商密钥,实际使用对称加解密的方法对http内容加密传输。

8.浏览器进程、JS事件循环机制、宏任务和微任务

9.重绘和回流

repaint(重绘):元素的某一部分属性发生改变,如字体颜色,背景颜色等改变,尺寸并未改变,这时发生的改变过程就是repaint。

reflow(回流): 因为浏览器渲染是一个由上而下的过程,当发现某部分的变化影响了布局时,就需要倒回去重新渲染,这个过程就称之为reflow。

10.实现forceUpdate

方法1:

// 函数组件必须以大写开头
const ElementApp=()=>{
		//方法1:useState,调用 setUpdate方法触发更新,但是不设置state值;
    const [, setUpdate] = useState();
    const handleForceUpdate = useCallback(() => {
        console.log("点击刷新了");
        // 原理:触发setState函数
        return setUpdate({});
    }, []);
    
    //方法2:useReducer,调用setForceUpdate(其实是dispatch)函数,不断触发reducer函数,更新state(reducer函数触发返回新的state值,这里会触发页面刷新,应该是会调用类似setState函数)
    const [ignore,setForceUpdate]=useReducer(x=>x+1,0);
    const forceUpdateMethods=()=>{
    		//这里dispatch函数没有传 {type:''},所以只要调用,就会触发reducer函数执行,从而触发setState更新state值,页面刷新
        setForceUpdate();
    }
    
    return (
        <div>
            <p>111</p>
            <p>222</p>
            <p>333</p>
            <button style={{width:'250px',height:'50px'}} onClick={handleForceUpdate}>测试forceUpdate方法</button>
        </div>
    )
}

10.React组件事件代理机制和原理是什么?

深入理解React:事件机制原理

  • DOM事件流:事件捕获阶段,事件目标阶段,事件冒泡阶段;
  • React事件概述:
    • 事件注册:document上注册事件
    • 事件回调函数listener存储,事件回调函数注册完毕后需要存储起来,以便触发时进行回调
    • 事件分发,document.addEventListener(eventType,dispatchEvent,false),dispatchEvent统一分发;

1.DOM事件流

1)事件捕获阶段、处于目标阶段和事件冒泡阶段

​ 在正式讲解 React 事件之前,有必要了解一下 DOM 事件流,其包含三个流程:事件捕获阶段、处于目标 阶段和事件冒泡阶段

1)事件捕获阶段:事件对象通过目标节点的祖先 Window 传播到目标的父节点。

(2)处于目标阶段:事件对象到达事件目标节点。如果阻止事件冒泡,那么该事件对象将在此阶段完成后停止传播。

(3)事件冒泡阶段:事件对象以相反的顺序从目标节点的父项开始传播,从目标节点的父项开始到 Window 结束。

(2)addEventListener 方法

​ DOM 的事件流中同时包含了事件捕获阶段和事件冒泡阶段,而作为开发者,我们可以选择事件处理函数在哪 一个阶段被调用

addEventListener() 方法用于为特定元素绑定一个事件处理函数。addEventListener 有三个参数:

element.addEventListener(event, function, useCapture)

		如果一个元素(element)针对同一个事件类型(event),多次绑定同一个事件处理函数(function),那么重复的实例会被抛弃。当然如果第三个参数capture值不一致,此时就算重复定义,也不会被抛弃掉;
		事件类型:'click','change',,,

3.React 事件概述

React并不像原生事件一样将事件和DOM一一对应,而是将所有的事件都绑定在网页的document,通过统一的事件监听器dispatchEvent处理并分发,找到对应的回调函数并执行

React 根据W3C 规范来定义自己的事件系统,其事件被称之为合成事件 (SyntheticEvent)。而其自定义事件系统的动机主要包含以下几个方面:

  • (1)抹平不同浏览器之间的兼容性差异。最主要的动机。

  • (2)事件"合成",即事件自定义。事件合成既可以处理兼容性问题,也可以用来自定义事件(例如 React 的 onChange 事件)。

  • (3)提供一个抽象跨平台事件机制。类似 VirtualDOM 抽象了跨平台的渲染方式,合成事件(SyntheticEvent)提供一个抽象的跨平台事件机制。

  • (4)可以做更多优化。例如利用事件委托机制,几乎所有事件的触发都代理到了 document,而不是 DOM 节点本身,简化了 DOM 事件处理逻辑,减少了内存开销,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次。(React 自身模拟了一套事件冒泡的机制)

  • (5)可以干预事件的分发。V16引入 Fiber 架构,React 可以通过干预事件的分发以优化用户的交互体验。

对于React合成事件中合成的理解:
1.对原生事件的封装:
	handleClick=(e)=>{
		debugger;
		console.log(e);//这个e是React封装了原生nativeEvent后的合成事件对象(SyntheticEvent),可以通过e.nativeEvent获取到原生事件;React给事件对象也添加了e.persist(),解决异步事件中合成事件的持久化;
	}
	//1.SyntheticEvent是react合成事件的基类,定义了合成事件的基础公共属性和方法。
	//2.react会根据当前的事件类型来使用不同的合成事件对象,比如鼠标单击事件 - SyntheticMouseEvent,焦点事件-SyntheticFocusEvent等,但是都是继承自SyntheticEvent;
	
2.对某些原生事件的升级和改造:
	对于有些dom元素事件,我们进行事件绑定之后,react并不是只处理你声明的事件类型,还会额外的增加一些其他的事件,帮助我们提升交互的体验;
	
3.不同浏览器兼容性的处理:
	addEventListener,removeEventListener事件做了兼容处理,ie浏览器用 attachEvent,detachEvent;
这里就举一个例子来说明下:
		当我们给input声明个onChange事件,看下 react帮我们做了什么?
		1)可以看到react不只是注册了一个onchange事件,还注册了很多其他事件。
		2)而这个时候我们向文本框输入内容的时候,是可以实时的得到内容的。
		3)然而原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。

React事件概述:

事件注册:document上注册事件;
事件回调函数存储:事件回调函数注册完毕后需要存储起来,以便触发时进行回调事件分发;
事件分发:document.addEventListener(eventType,dispatchEvent,false),dispatchEvent统一分发;
注:「几乎」所有事件都代理到了 document,
		说明有例外,比如`audio``video`标签的一些媒体事件(如 onplay、onpause 等),是 document 所不具有,这些事件只能够在这些标签上进行事件进行代理,但依旧用统一的入口分发函数(dispatchEvent)进行绑定。

1.React事件注册

  • React 的事件注册过程主要做了两件事:document 上注册、存储事件回调。

    1. document 上注册

    • 1)在 React 组件挂载阶段,根据组件内的声明的事件类型(onclick、onchange 等),在 document 上注册事件(使用addEventListener),并指定统一的回调函数 dispatchEvent。 例如:document.addEventListener('click'或者'change',dispatchEvent,false);
    • 2换句话说,document 上不管注册的是什么事件,都具有统一的回调函数 dispatchEvent。也正是因为这一事件委托机制,具有同样的回调函数 dispatchEvent,所以对于同一种事件类型,不论在 document 上注册了几次,最终也只会保留一个有效实例,这能减少内存开销。
function TestComponent() {
  handleFatherClick=()=>{
		// ...
  }

  handleChildClick=()=>{
		// ...
  }

  return <div className="father" onClick={this.handleFatherClick}>
	<div className="child" onClick={this.handleChildClick}>child </div>
  </div>
}
上述代码中,事件类型都是onclick,由于 React 的事件委托机制,会指定统一的回调函数 dispatchEvent,所以最终只会在 document 上保留一个 click 事件,类似document.addEventListener('click', dispatchEvent),从这里也可以看出 React 的事件是在 DOM 事件流的冒泡阶段被触发执行。
  • 2.存储事件回调

    React 为了在触发事件时可以查找到对应的回调去执行,会把组件内的所有事件统一地存放到一个对象中(listenerBank)。

    而存储方式如下图:首先会根据事件类型分类存储,例如 click 事件相关的统一存储在一个对象中,回调函数的存储采用键值对(key/value)的方式存储在对象中,key 是组件的唯一标识 id,value 对应的就是事件的回调函数。

    React 的事件注册的关键步骤如下图:

  • 3.事件分发

    事件分发也就是事件触发。React 的事件触发只会发生在 DOM 事件流的冒泡阶段,因为在 document 上注册时就默认是在冒泡阶段被触发执行。

    react 把所有的事件和事件类型以及react 组件进行关联,把这个关系保存在了一个 map里,也就是一个对象里(键值对),然后在事件触发的时候去根据当前的 组件id事件类型查找到对应的 事件fn

事件分发(触发)的大致流程如下:

  1. 触发事件,开始 DOM 事件流,先后经过三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段
  2. 当事件冒泡到 document 时,触发统一的事件分发函数 ReactEventListener.dispatchEvent
  3. 根据原生事件对象(nativeEvent)找到当前节点(即事件触发节点)对应的 ReactDOMComponent 对象
  4. 事件的合成
    1. 根据当前事件类型生成对应的合成对象
    2. 封装原生事件对象和冒泡机制
    3. 查找当前元素以及它所有父级
    4. 在 listenerBank 中查找事件回调函数并合成到 events 中
  5. 批量执行合成事件(events)内的回调函数
  6. 如果没有阻止冒泡,会将继续进行 DOM 事件流的冒泡(从 document 到 window),否则结束事件触发
  7. 事件触发完毕之后立即移除监听事件;

**结论:**React 合成事件和原生 DOM 事件的主要区别:

(1)React 组件上声明的事件没有绑定在 React 组件对应的原生 DOM 节点上,其实都绑定在了document上统一监听。

(2)React 利用事件委托机制,将几乎所有事件的触发代理(delegate)在 document 节点上,事件对象(event)是合成对象(SyntheticEvent),不是原生事件对象,但通过 nativeEvent 属性访问原生事件对象。

(3)由于 React 的事件委托机制,React 组件对应的原生 DOM 节点上的事件触发时机总是在 React 组件上的事件之前。

原生事件(阻止冒泡)会阻止合成事件的执行

合成事件(阻止冒泡)不会阻止原生事件的执行

11.介绍React高阶组件,使用场景?

React 中的高阶组件及其应用场景

高阶组件 :如果一个函数接收一个或多个组件作为参数并返回一个新的组件,就称之为高阶组件,当返回的是一个无状态组件时,此时高阶组件也是一个高阶函数,无状态组件其实就是一个纯函数;

function HigherOrderComponent(WrappedComponent) {
    return (props)=><WrappedComponent {...props}/>;
}
HigherOrderComponent(WrappedComponent)(props);
高阶函数:如果一个函数接收一个或多个函数作为参数并返回一个函数,就称之为高阶函数;
function withGreeting(greeting = () => {}) {
    return greeting;
}

高阶组件两种主要形式:** 属性代理 **和 反向继承

属性代理 (Proxys,Proxy)

最简单的属性代理实现:

// 无状态
function HigherOrderComponent(WrappedComponent) {
    return props => <WrappedComponent {...props} />;
}
// or
// 有状态
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

属性代理可以做哪些:

  • 1.操作Props
  • 2.抽离state
  • 3.通过ref访问组件实例
  • 4.用其他元素包裹WrappedComponent组件,包装传入的组件返回新组件
function withOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                name: '',
            };
        }
        onChange = () => {
            this.setState({ //2.抽离state
                name: '大板栗',
            });
        }
        render() {
            const newProps = {//1.操作props
                name: {
                    value: this.state.name,
                    onChange: this.onChange,
                },
            };
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    };
}
// 使用:这样就将 input 转化成受控组件了
const NameInput = props => (<input name="name" {...props.name} />);
export default withOnChange(NameInput);// withOnChange高阶组件向NameInput组件传了name={value: this.state.name,onChange: this.onChange}属性

通过Ref访问组件实例

ref获取组件实例,只能获取声明在class类组件上,不能获取函数式(无状态)组件;

ref的值可以是字符串(不推荐)或者回调函数

ref是回调函数的执行时机:

  • 组件被挂载后(componentDidMount),回调函数立即执行,回调函数的参数为该组件的该组件的实例;
  • 组件被卸载(componentUnMount)或者原有的ref属性发生变化的时候,此时回调函数也会立即执行,且回调函数的参数为null;

如何在高阶函数中获取到组件实例

回调函数的方式:WrapperedComponent组件的ref属性,该属性会在componentDidMount时候执行ref回调函数,并将组件实例传给回调函数的参数;

function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        executeInstanceMethod = (wrappedComponentInstance) => {
            wrappedComponentInstance.someMethod();
        }
        render() {
            return(
            			<div style={{ backgroundColor: '#fafafa' }}>//用其他元素包裹传入的组件 WrappedComponent,包一层背景色为 #fafafa的div
            				<WrappedComponent {...this.props} ref={this.executeInstanceMethod} />;
            			</div>
            )  
        }
    };
}
注意:不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。

反向继承

反向继承其实就是一个函数接收一个WrapperedComponent组件,返回一个继承WrapperedComponent组件的新组件,且在改新的类组件的render中调用super.render()方法;

最简单的反向继承:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}
反向继承和属性代理的区别是:
		都是返回了一个继承父类的组件,属性代理返回的组件继承的是React.Component,反向继承中继承的是WrapperedComponent

反向继承可以做哪些:

  • 操作state
  • 渲染劫持

操作state

反向继承的是WrapperedComponent组件,所以该子组件可以访问到WrapperedComponent组件的state属性,并且可以操作父组件的state,但是非常不建议这么做,会导致state难以维护;

function withLogging(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return (
                <div>
                    <h2>Debugger Component Logging...</h2>
                    <p>state:</p>
                    <pre>{JSON.stringify(this.state, null, 4)}</pre>//this.state访问到父组件的state
                    <p>props:</p>
                    <pre>{JSON.stringify(this.props, null, 4)}</pre>
                    {super.render()}//父组件的 render()方法
                </div>
            );
        }
    };
}
在这个例子中利用高阶函数中可以读取 state 和 props 的特性,对 WrappedComponent 组件做了额外元素的嵌套,把 WrappedComponent 组件的 state 和 props 都打印了出来,

渲染劫持

之所以可以渲染劫持是因为高阶组件控制着WrapperedComponent组件的render方法,也包括state,props;

渲染劫持可以做哪些:

1.有条件的展示WrapperedComponent组件树;2.操作由super.render()输出的元素树,并可以添加props;3.用其他元素包裹WrapperedComponent(通属性代理)

条件渲染

function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                return super.render();
            }
        }
    };
}

修改由 render() 输出的 React 元素树

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            const tree = super.render();//父组件的render()
返回的元素            const newProps = {};
            if (tree && tree.type === 'input') {
                newProps.value = 'something here';
            }
            const props = {
                ...tree.props,
                ...newProps,
            };
            const newTree = React.cloneElement(tree, props, tree.props.children);
            return newTree;
        }
    };
}

高阶组件存在的问题

  • 静态方法丢失,因为原始组件被包裹于一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法,也就是新组建不能直接获取到父组件的静态方法,只能通过父组件获取;

    function HigherOrderComponent(WrappedComponent) {
        class Enhance extends React.Component {}
        // 必须得知道要拷贝的方法
        Enhance.staticMethod = WrappedComponent.staticMethod;
        return Enhance;
    }
    
  • ref不能透传

    • 你向一个由高阶组件创建的组件的元素添加 ref 引用,那么 ref 指向的是最外层容器组件实例的,而不是被包裹的 WrappedComponent 组件。

    • React 为我们提供了一个名为 React.forwardRef 的 API 来解决这一问题(在 React 16.3 版本中被添加)

      function withLogging(WrappedComponent) {
          class Enhance extends WrappedComponent {
              componentWillReceiveProps() {
                  console.log('Current props', this.props);
                  console.log('Next props', nextProps);
              }
              render() {
                  const {forwardedRef, ...rest} = this.props;
                  // 把 forwardedRef 赋值给 ref
                  return <WrappedComponent {...rest} ref={forwardedRef} />;
              }
          };
      
          // React.forwardRef 方法会传入 props 和 ref 两个参数给其回调函数
          // 所以这边的 ref 是由 React.forwardRef 提供的
          function forwardRef(props, ref) {
              return <Enhance {...props} forwardRef={ref} />
          }
      
          return React.forwardRef(forwardRef);
      }
      const EnhancedComponent = withLogging(SomeComponent);
      
  • 反向继承不能保证完整的子组件树被解析,如果渲染 elements tree 中包含了 function 类型的组件的话,这时候就不能操作组件的子组件了

高阶组件使用场景

  • 权限控制

    // HOC.js
    const withAuth = role => WrappedComponent => {
        return class extends React.Component {
            state = {
                permission: false,
            }
            async componentWillMount() {
                const currentRole = await getCurrentUserRole();
                this.setState({
                    permission: currentRole === role,
                });
            }
            render() {
                if (this.state.permission) {
                    return <WrappedComponent {...this.props} />;
                } else {
                    return (<div>您没有权限查看该页面,请联系管理员!</div>);
                }
            }
        };
    }
    
  • 组件性能追踪 ,页面渲染时长

  • 页面复用

  • 错误统一处理

  • 日志记录

React16声明周期到React Fiber架构

React16新的生命周期

上图是基于 React 16.4 之后的生命周期图解。如感觉不对,请先查看 React 版本

1. React16废弃的生命周期有3个will:

componentWillMount

componentWillReceiveProps

componentWillUpdate

废弃的原因,是在React16的Fiber架构中,调和过程会多次执行will周期,不再是一次执行,失去了原有的意义。此外,多次执行,在周期中如果有setState或dom操作,会触发多次重绘,影响性能,也会导致数据错乱

2. componentWillReceiveProps的执行时机

触发时机: 在render期间不会执行,当父组件导致子组件更新的时候, 即使接收的 props 并没有变化, 这个函数也会被调用

    1. 在props变化时触发
    1. 在父组件导致子组件重新渲染(rerender)时,即使props没有变化,也触发componentWillReceiveProps本身是存在一些问题的。

3. React16的2个新的生命周期

  • getDerivedStateFromProps

  • getSnapshotBeforeUpdate

3.1 getDerivedStateFromProps(nextProps, prevState)的用法

触发时机: 会在每次组件被重新渲染前被调用, 这意味着无论是父组件的更新, props 的变化, 或是组件内部执行了 setState(), 它都会被调用.

  • 当组件实例化的时候,这个方法替代了componentWillMount();

  • 而当接收到新的 props 时,该方法替代了 componentWillReceiveProps() 和 componentWillUpdate();

这个生命周期很难用:

    1. 触发时机频繁,16.3是在props变化时触发,16.4则改为在每次组件渲染器调用,即无论props变化,组件自己setState,父组件render 都会触发
    1. 静态方法,本意是隔离访问组件实例,却造成访问组件的数据和方法十分不便,难以进行数据比较
    1. 不能setState,而是返回一个对象来更新state,使用不便,也可能触发多次更新状态

3.2 getSnapshotBeforeUpdate(prevProps, prevState)

在render之后,state更新后,真实dom元素更新之前(在节点挂载前)。它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置);我们可以获取当前rootNode的scrollHeight,传到componentDidUpdate 的参数perScrollHeight

getSnapshotBeforeUpdate这个周期在Fiber架构中,只会调用一次,实现了类似willMount的效果。

4.为什么需要新的生命周期

因为新的react引入了异步渲染机制,主要的功能是,在渲染完成前,可以中断任务,中断之后不会继续执行生命周期,而是重头开始执行生命周期。这导致componentWillMount**,componentWillReceivePropscomponentWillUpdate可能会被中断,中断后重新,导致执行多次,带来意想不到的情况。