Giter Site home page Giter Site logo

js-learn's Introduction

Hello,我是正在努力学习的小黄

罗列一下我的学习规划

  • ✨ Javascript 基础与进阶
  • 🐨 Vue2.x 源码
  • 🎉 Vue3.x 源码
  • 🤯 React 源码
  • 👺 前端工程化
  • 👾 网络安全
  • 🤖 浏览器原理
  • 🎏 计算机原理
  • 🤔 数据结构和算法
  • 🥳 组件库开发
  • 💬 NodeJS
  • 🛠 单元测试
  • 🗝 linux/docker
  • 🐤 微前端(qiankun)
  • 📱 Flutter
  • 💡 Svelte

即将实现的DEMO

  • 🎮 vue3.x版本的俄罗斯方块
  • 🚀 vue3.x版本的飞机大战
  • 📦 vue2.x 低代码平台
  • 🔗 搭建个人持续集成

js-learn's People

Contributors

hzhxi9 avatar

Watchers

 avatar

js-learn's Issues

浏览器缓存有哪些

强缓存、协商缓存的区别、以及应用场景

一、 概述

浏览器的缓存机制也就是HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的。

浏览器缓存是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。

一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

二、 缓存位置

  1. Service Worker

运行在浏览器背后的独立线程,一般可以用来实现缓存功能。

使用Service Worker的话,传输协议必须为HTTPS。因为Service Worker中涉及了请求拦截,所以必须使用HTTPS协议来保障安全。

  1. Memory Cache

内寸中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面已经下载的样式、脚本、图片等。

读取内存中的数据肯定比磁盘的快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放(一旦我们关闭Tab页面,内存中的缓存也就被释放了)。

内存缓存中有一块重要的缓存资源是preloader相关指令(例如 ),总所周知preloader的相关指令已经是页面优化的常见手段之一,他可以一边解析js/css文件,一边网络请求下一个资源。

  1. Disk Cache

存储在硬盘中的缓存,读取速度虽然慢点,但是什么都能存储到磁盘中, 与Memory Cache 相比,优势是容量和存储时效性。

在所有浏览器缓存中,Disk Cache覆盖面基本上是最大的。

它会根据HTTP Header中的字段判断哪些资源缓存,哪些资源不请求直接使用,哪些资源已经过期需要重新请求。并且即使在夸站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。

绝大部份的缓存都来自Disk Cache。

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?

  • 对于大文件来说,大概率是不存储在内存中的
  • 当前系统内存使用率高的话,文件优先存在硬盘
  1. Push Cache

Push Cache(推送缓存)是HTTP/2中的内容,当以上三种缓存都没有命中时,它才会被使用。

它只在会话(session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。

  1. 如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了

为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种: 强缓存和协商缓存,并且缓存策略都是通过设置HTTP Header来实现的。

三、 缓存过程分析

浏览器与服务器通信的方式为应答式的,即:浏览器发起HTTP请求 >> 服务器响应该请求。

那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?

浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。

image

由图可知

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将结果和缓存标识存入浏览器缓存中

以上两点是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取。

根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强缓存和协商缓存

四、 强缓存

  1. 定义

    不会向服务器发起请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且size显示from disk cache或from memory cache。

    强缓存可以通过设置两种HTTP Header实现: Expires 和 Cache-Control

  2. Expires

    缓存过期时间,用来指定资源到期的时间,是服务端的具体时间点。也就是说Expires = max-age + 请求时间,需要要和Last- modified结合使用

    Expires是Web服务器响应消息头字段,在响应http请求是告诉浏览器在过期时间前浏览器可以直接从浏览器缓存存取数据,而无需再次请求。

    Expires是HTTP/1的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失败

  3. Cache-Control

    在 HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存。

    Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:

    • public: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容(例如,该响应没有max-age指令或Expires消息头)。
    • private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容。
    • no-cache: 在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证。
    • no-store: 缓存不应存储有关客户端请求或服务器响应的任何内容。
    • max-age: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
    • s-maxage: 覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。
    • max-stale: 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。
    • min-fresh: 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。
  4. Expires 和 Cache-Control 两者对比

    其实这两者的差别不大,区别在于Expires 是 http1.0的产物, Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires

    某些不支持HTTP1.1的环境下,Expires就会发挥作用,现阶段它的存在只是一种兼容性的写法。

  5. 弊端

    强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务端最新的内容,那我们如何获知服务端内容是否已经发生了更新,此时需要用到协商缓存策略。

五、 协商缓存

  1. 定义

    协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

  • 协商缓存生效,返回304 和 Not Modified

image

  • 协商缓存成功,返回 200 和请求结果

image

协商缓存可以通过设置两种 HTTP Header 实现: Last-Modified 和 ETag

六、 缓存机制

强缓存优先于协商缓存进行,若强缓存生效则直接使用缓存,不生效则进行协商缓存(Last-Modified/If-modified-Since和Etag/If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失败,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识。再存入浏览器缓存中;生效则返回304,继续使用缓存。

image

七、 实际场景应用

  1. 频繁变动的资源

对于频繁变动的资源,首先需要使用Cache-Contro: no-cache 使浏览器每次都请求服务器,然后配合ETag或者Last- Modified来验证资源是否有效。

这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小

  1. 不常变化的资源

通常在处理这类资源时,给他们的Cache-Control 配置一个很大的max-age=31536000(一年), 这样浏览器之后请求相同的URL会命中缓存。

而为了解决更新的问题,就需要在文件名(或者路径)中添加hash,版本号等动态字符,之后更改动态字符,从而达到更改引用URL的目的,让之前的强制缓存失效(其实并未立即失效,只是不再使用而已)。

八、 用户行为对浏览器缓存的影响

所谓用户行为对浏览器缓存的影响,指的是用户在浏览器如何操作时,会触发怎样的缓存策略。

  1. 打开网页,地址栏输入地址: 查找disk cache中是否有匹配,如果有则使用;没有则发送网络请求

  2. 普通刷新(F5): 因为TAB并没有关闭,因此memory cache是可用的,会被优先使用(如果匹配的话)。其次才是disk cache;

  3. 强制刷新(ctrl + F5): 浏览器不使用缓存,因此发送的请求头部均带有Cache-Control: no-cache(为了兼容,还带了Pragma: no-cache),服务器直接返回200和最新内容。

css的引入方式

一、行内样式

  1. 使用style属性引入css样式

  2. 示例

<h1 style="color:red;">style属性的应用</h1>
<p style="color:red;font-size:30px;">我是p标签</p>

二、内部样式表

  1. 在style标签中书写CSS代码。style标签写在head标签中。

  2. 示例

<head>
   <style type="text/css">
      h3{ color:red; }
   </style>
</head>

三、外表样式表

  1. CSS代码保存在扩展名为.css的样式表中。 HTML文件引用扩展名为.css的样式表,有两种方式:链接式、导入式。

  2. 语法

    • 链接式
    <link type="text/css" rel="styleSheet" href="css文件路径" />
    • 导入式
    // 在html文件内导入
    <style type="text/css">
        @import url("css文件路径");
    </style>
    // 在css文件内导入
    @import url(style.css);
  3. 两种的区别

    • link

      • 输入XHMTL
      • 优先加载CSS文件到页面
      • 标记只能放在中
    • @Important

      • 属于CSS2.1
      • 先加载HTML结构在加载CSS文件

四、 CSS中的优先级

  1. 样式优先级

    行内样式>内部样式>外部样式(后两者是就近原则)

    内部样式表和外部样式表使用就近原则,即谁写在下面以谁为准。
    注意:导入式和链接式的优先级也是使用就近原则。

  2. 选择器优先级

    优先级:id选择器>class选择器>属性选择器>标签选择器>通配符选择器

处理跨域的方式有哪些

一、 跨域的定义

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里的跨域是广义的

  • 广义的跨域
1. 资源跳转:a链接、重定向、表单提交 
2. 资源嵌入:<link><script><img><frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3. 脚本请求:js 发起的ajax请求、dom和js的跨域操作等
  • 狭义的跨域

是由浏览器同源策略限制的一类请求场景

二、 同源策略

同源策略/SOP是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能。
如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。
所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名是指向同一个ip地址,也非同源。

同源策略限制以下几种行为:
1. Cookie、LocalStorage、IndexDB 无法读取
2. DOM 和 JS 对象无法获得
3. AJAX请求不能发送

三、 常见跨越场景

URL                                      说明                    是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js         同一域名,不同文件或路径           允许
http://www.domain.com/lab/c.js

http://www.domain.com:8000/a.js
http://www.domain.com/b.js         同一域名,不同端口                不允许
 
http://www.domain.com/a.js
https://www.domain.com/b.js        同一域名,不同协议                不允许
 
http://www.domain.com/a.js
http://192.168.4.12/b.js           域名和域名对应相同ip              不允许
 
http://www.domain.com/a.js
http://x.domain.com/b.js           主域相同,子域不同                不允许
http://domain.com/c.js
 
http://www.domain1.com/a.js
http://www.domain2.com/b.js        不同域名                         不允许

四、跨域解决方案

  1. 通过jsonp跨域
  2. document.domain + iframe跨域
  3. location.hash + iframe
  4. window,name + iframe跨域
  5. postMessage跨域
  6. 跨域资源共享(CORS)
  7. nginx代理跨域
  8. nodejs中间间代理跨域
  9. WebSocket协议跨域

五、 通过jsonp跨域

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

  1. 原生实现
<script>
var script = document.createElment('script');
script.type = 'text/javascript';

// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.scr = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appenChild(script);

// 回调执行函数
function handleCallbacl(res){
    alert(JSON.stringify(res);
}
</script>

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

handleCallback({"status": true, "user": "admin"})
  1. jquery ajax
$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",    // 自定义回调函数名
    data: {}
});
  1. vue
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

后端nodejs代码

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

server.on('request', function(req, res) {
    var params = qs.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

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

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

六、 document.domain + iframe 跨域

  1. 此方案仅限于主域相同、子域不同的跨域应用场景

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

<!-- 父窗口 ->
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>
<!-- 子窗口 -->
<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

七、 location.hash + iframe跨域

  1. 实现原理

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

  1. 具体实现

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

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

八、 window.name + iframe跨域

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

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

     // 加载跨域页面
     iframe.src = url;
     
     // onload 事件触发两次,第一次加载跨域页,并留存数据与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);
});
  1. proxy.html

中间代理页,与a.html同域,内容为空即可。

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

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

九、 postMessage 跨域

  1. postMessage 是HTML5 XMLHttpRequest Level 2中的API, 且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
  • 页面和打开的新窗口的数据传递
  • 多窗口之间的消息传递
  • 页面与iframe消息传递
  • 上面三个场景的跨域数据传递
  1. 用法: postMessage(data, origin)方法接受两个参数
    data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参最好用JSON.stringify()序列化
    origin: 协议+主机+端口号,也可以设置为"*",表示可以传递任意窗口,如果要指定和当前窗口同源的话设置为"/"

  2. demo

// a.html
<iframe  id="iframe" src="http://www.domain2.com/b.html" style="display:none;" />
<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>
// 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>

十、跨域资源共享(CORS)

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

需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。
如果想实现当前页cookie的写入,可参考下文

  • nginx反向代理中设置proxy_cookie_domain
  • NodeJS中中间件代理中cookieDomainRewrite

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

  1. 前端设置
  • 原生AJAX
// 前端设置是否携带cookie
xhr.withCredentials = true;
// 示例代码
// IE8/9需要用window.XDomainRequest兼容
const xhr = new XMLHttpRequest(); 

// 前端设置是否携带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlecoded');
xhr.send('user=admin');

xhr.inreadystatechange = function(){
     if(xhr.readyState === 4 && xhr.status === 200) alert(xhr.responseText);
}
  • jQuery ajax
$.ajax({
   ...
   xhrFields: {
       // 前端设置是否携带cookie
       withCredentials: true 
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
})
  • vue
// axios 设置
axios.defaults.withCredentials = true
// vue-resource设置
Vue.http.options.credentials = true
  1. 服务端设置

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

  • java后台
/*
 * 导入包:import javax.servlet.http.HttpServletResponse;
 * 接口参数中定义:HttpServletResponse response
 */

// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
  • nodejs后台
var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

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

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

十一、 nginx代理跨域

  1. nginx配置解决confront跨域

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

location / {
  add_header Access-Control-Allow-Origin *;
}
  1. 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;
    }
}
  • 前端代码示例
var xhr = new XMLHttpRequest();

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

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
  • 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...');

十二、 NodeJS中间件代理跨域

  1. 原理

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

  1. 非vue框架的跨域(2次跨域)

利用node + express + http-proxy-middleware搭建一个proxy服务器。

  • 前端代码
var xhr = new XMLHttpRequest();

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

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
  • 中间件服务器
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');
  • nodejs后台同(十一、 niginx)
  1. vue框架的跨域(一次跨域)
  • 实现

利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。

  • webpack.config.js部分配置
module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

十三、 WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

  1. 前端代码
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>
  1. nodejs socket 后台
var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

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

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

面试题

var obj1 = { a: 1 };
function change(obj) {
    obj.a = 2;
    obj = {
      a: 3,
    };
    return obj;
}
var obj2 = change(obj1);
console.log(obj1)
console.log(obj2);
 var Foo = function () {
    getName = function () {
      console.log(1);
    };
  };
  Foo.getName = function () {
    console.log(2);
  };
  Foo.prototype.getName = function () {
    console.log(3);
  };
  var getName = function () {
    console.log(4);
  };
  function getName() {
    console.log(5);
  }

  getName();
  Foo.getName();
  Foo().getName();
  getName();
  new Foo().getName();

css实现一个三角形和半圆环

  1. 三角形
#triangle-up {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 100px solid red;
}
  1. 三角形带边框
.arrow-tip {
    width: 0; 
    height: 0; 
    border-left: 15px solid transparent;
    border-right: 15px solid transparent;
    border-bottom: 15px solid #222;
    position: relative;
}
.arrow-tip:after {
    content: "";
    display: block;
    width: 0; 
    height: 0; 
    position: absolute;
    bottom: -16px;
    left: -17px;
    z-index: -1;
    border-left: 17px solid transparent;
    border-right: 17px solid transparent;
    border-bottom: 17px solid red;
}
  1. 半圆

使用border-radius

div{
  width: 100px;
  height: 50px;
  border:1px solid black;
  background-color: blue;
  border-radius: 100px 100px 0 0;
}

使用clip属性剪裁绝对定位元素

.demo { 
    /*clip 属性剪裁绝对定位元素。也就是说,只有 position:absolute 的时候才是生效的。*/
    position:absolute;
    width: 100px;
    height: 100px;
    border-radius: 50px;
    /*唯一合法的形状值是:rect (top, right, bottom, left)*/
    clip: rect(0px 50px 100px 0px); 
}
  1. 半圆环

使用clip-path属性

div{
  width: 50px;
  height: 50px;
  border: 13px solid orange;
  border-radius: 50px;
  clip-path: polygon(50% 0%, 100% 0%, 100% 3600%, 50% 50%);
  -webkit-clip-path: polygon(50% 0%, 100% 0%, 100% 3600%, 50% 50%);
}

使用borer-radius属性

.circle {
   position: relative;
   box-sizing: border-box;
}
 .big {
    width: 140px;
    height: 140px;
    border-radius: 50%;
    position: absolute;
    margin: auto;
    /*以下五个属性为水平垂直居中的方法*/
    left: 0;
    top: 200px;
    right: 0;
    bottom: 0;
    box-sizing: border-box;
}
 .small {
     width: 136px;
     height: 136px;
     border: 10px solid #9DDBE8;
     border-radius: 50%;
     position: absolute;
     margin: auto;
     /*以下五个属性为水平垂直居中的方法*/
     left: 0;
     top: 200px;
     right: 0;
     bottom: 0;
     box-sizing: border-box;
     border-bottom-color: transparent;
}

content-type有哪些内容

一、 content-type 的概念

content-type是http的实体首部字段,在request的请求行(或response的状态码)之后,也是首部的一部分。用于说明请求或返回的消息主体是用何种方式编码,在request header 和 response header里都存在。

请求信息和响应信息都可以包含实体信息,实体信息一般由实体头域和实体组成。实体头域包含关于实体的原信息,实体头包含Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header。

Content-Type是返回消息中非常重要的内容,表示后面的文档属于什么MIME类型。Content-Type:[type]/[subtype]: parameter。
例如最常见的就是text/html,它的意思是说返回的内容是文本类型,这个文本又是HTML格式的。
原则上浏览器会根据Content-Type来决定如何显示返回的消息体内容。

在http协议消息头中,使用Content-Type来表示具体请求中的媒体类型信息。

二、Type的形式

  1. Text: 用于标准化表示的文本信息,文本信息可以是多种字符集合或者多种格式的
  2. Multipart: 用于连接消息体的多个部分构成一个消息,这些部分可以是不同类型的数据
  3. Application: 用于传输应用程序或者二进制数据
  4. Message: 用于包装一个E-mail消息
  5. Image: 用于传输静态图片数据
  6. Audio: 用于传输音频或者声音数据
  7. Video: 用于传输动态影像数据,可以是与音频编辑在一起的视频数据格式

subtype用于指定type的详细形式。

content-type/subtype配对的集合和与此相关的参数,将随着时间而增长。为了确保这些值在一个有序且公开的状态下开发,MIME使用Internet Assigned Nunber Authority(IANA)作为中心的注册机制来管理这些值

parameter可以用来指定附加的信息,更多情况下是用于指定text/plain和text/html等文字编码方式的charset参数。

MIME根据type制定了默认的subtype,当客户端不能确定消息的subtyoe的情况下,消息被看作默认的subtype进行处理。

Text默认是text/plain、Application默认是application/octet-stream、Multipart默认是multipart/mixed

对于IE6浏览器来说,如果Content-Type中的类型和实际的消息体类型不一致,那么它会根据内容中的类型来分析实际应该是什么类型,对呀jpg、gif等常用图片格式都可以正确的识别出来,而不管Content-Type中写的是什么

如果Content-Type中指定的是浏览器可以直接打开的类型,那么浏览器就会直接打开其内容显示出来。

如果是被关联到其他应用程序的类型,这时候就要查找注册表中关于这种类型的注册情况。

如果是允许直接打开而不需要询问的,就会直接调出这个关联的应用程序来打开这个文件,但如果是不允许直接打开的,就会询问是否打开。

对于没有关联到任何应用程序的类型,IE浏览器不知道它该如何打开,此时IE6就会把它当成XML来尝试打开。

三、 Content-Type 与 Accept

  1. Accept 属于请求头,Content-Type属于实体头

Http报头分为通用报头,请求报头,响应报头和实体报头

请求方的Http报头结构: 通用报头|请求报头|实体报头
响应方的Http报头结构: 通用报头|响应报头|实体报头

  1. Accept代表发送端(客户端)希望接受的数据类型
    比如: Accept: text/xml; 代表客户端希望接受的数据类型是xml类型

  2. Content-Type代表发送端(客服端|服务器)发送的实体数据的数据类型
    比如: Content-Type: text/html; 代表发送端发送的数据格式是html

两者结合起来
Accept: text/xml;
Content-Type: text/html;
即代表希望接受的数据类型是xml格式,本次请求发送的数据的数据格式是html

四、 常用类型

  • 常见的媒体格式类型
text/html: HTML格式
text/plain: 纯文本格式
text/xml: XML格式
image/gif: gif图片格式
image/jpeg: jpg图片格式
image/png: png图片格式
  • 以application开头的媒体格式类型
application/xhtml+xml: XHTML格式
application/xml: XML数据格式
application/atom+xml: Atom XML聚合格式
application/json: JSON数据格式
application/pdf: pdf格式
application/msword: word文档格式
application/octet-stream: 二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded: <form encType=””>中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
  • 以audio开头的常见媒体格式文件
audio/x-wav: wav文件
audio/x-ms-wma: wma文件
audio/mp3: mp3文件
  • 以video开头的常见媒体格式文件
video/x-ms-wmv: wmv文件
video/mpeg4: mp4文件
video/avi: avi文件
  • 上传文件时候使用
multipart/form-data: 需要在表单中进行文件上传时,就需要使用该格式
  1. text/plain: 纯文本格式
  2. text/css: css类型
  3. text/html: html类型
  4. application/x-javascript: js类型
  5. application/json: json类型
  6. application/xhtml+xml: XHTML格式
  7. application/xml: XML数据格式
  8. application/pdf: pdf格式
  9. application/msword: Word文档格式
  10. application/octet-stram: 二进制流数据
  11. application/x-www-form-urlencoded: 表单中默认的encTyoe,表单数据被编码为key/value格式发送给服务器
  12. multipart/form-data: 需要在表单中进行文件上传时,就需要使用该格式
  13. image/png jpg gif: image

五、常见的Content-Type讲解

  1. application/x-www-form-urlencoded

这应该是最常见的POST提交数据的方式了。

浏览器的原声form表单,如果不设置enctype属性,那么最终就会以application/x-www-form-urlencoded方式提交数据。

  • Content-Type 被指定为 application/x-www-form-urlencoded;
  • 提交的数据按照key1=val&key2=val2的方式进行编码,key和val都进行URL转码。大部分服务端语言都对这种方式有很好的支持

很多时候,我们用Ajax提交数据的时候,也是用这种方式。
例如jquery的ajax,Content-Type的默认值都是application/x-www-form-urlencoded;charset=utf-8

  1. multipart/form-data

这又是一个常见的POST数据提交的方式。 我们使用表单上传文件的时候,必须让form的enctype等于这个值。

<form action="url" enctype="multipart/form-data" method="post"></form>
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
  • 首先生成了一个boundary用于分割不同的字段,为了避免与正文内容重复,boundary很长很复杂
  • 然后Content-Type里指明了数据是以multipart/form-data来编码,本次请求的boundary是什么内容。
  • 消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以- boundary开始,紧接着内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。
  • 如果传输的是文件,还要包含文件名和文件类型信息。
  • 消息主体最后以- boundary-标示结束。

这种方式一般用来上传文件,各大服务端语言对它也有着良好的支持。

  1. application/json

现在越来越多的人把它作为请求头,用来告诉服务端信息主体是序列化后的JSON字符串。
由于JSON规范的流行,除了低版本IE之外的各大浏览器都原生支持JSON.stringify。
服务端语言也都有处理JSON的函数,使用JSON不会遇上什么麻烦

JSON 格式支持比键值对复杂得多的结构化数据

var data = {'title':'test', 'sub' : [1,2,3]};
$http.post(url, data).success(function(result) {
    ...
});

最终发送的请求

POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}

这种方案可以方便的提交复杂的结构化数据,特别适合RESTful的接口。

各大抓包工具如Chrome自带的开发者工具、fireBug、fiddler都会以树型结构展示JSON数据,非常友好。

  1. text/xml

它是一种使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范。典型的 XML-RPC 请求是这样的:

POST http://www.example.com HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
    <param>
        <value><i4>41</i4></value>
    </param>
</params>

XML-RPC协议简单、功能够用,各种语言的实现都有。

它的使用也很广泛,如WordPress的XML-RPC API,搜索引擎的ping服务等等。

JavaScript中也有现成的库支持以这种方式进行数据交互,能很好的支持已有的 XML-RPC 服务。

webpack css-loader的顺序有没有影响

一、 loader 的工作原理

loader(加载器)是webpack的核心之一。它用于将不同类型的文件转换为webpack可识别的模块

webpack 只能直接处理JavaScript格式的代码。任何非js文件都必须呗预先处理转换为js代码,才可以参与打包。

loader就是这样一个代码转换器,它由webpack的loader runner执行调用,接收原始资源数据作为参数(当多个加载器联合使用时,上一个loader的结果会传入下一个loader),最后输出JavaScript代码(和可选的source map)给webpack做进一步编译。

二、 loader 执行顺序

  1. 分类
  • pre: 前置loader
  • normal:普通loader
  • inline:内联loader
  • post:后置loader
  1. 执行优先级
  • 四类loader执行优先级:pre > normal > inline > post
  • 相同优先级的loader执行顺序为:从左往右、从上到下
  1. 前缀的作用

内联loader可以通过添加不同前缀,跳过其他类型的loader

  • ! 跳过normal loader
  • -! 跳过 pre loader、 normal loader
  • !! 跳过 pre loader、normal loader、post loader

三、 less-loader、css-loader、style-loader 插件作用

  1. less-loader:用于加载less文件,将less转化为css
  2. css-loader:用于加载css文件,将css转化为commonjs
  3. style-loader:将样式通过<style>标签插入到header中

四、loader 在webpack的配置

  1. 安装
npm i style-loader css-loader less less-loader -D
  1. 在webpack.config.js文件中加入配置
'use strict';

const path = require('path');

module.exports = {
  mode: 'production',
  entry: {
    index: './src/index.js',
    search: './src/search.js',
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },

      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
    ],
  },
};
  1. 解析过程的执行顺序
// 解析less主要是通过
{
  test: /\.less$/,
  use: [
    'style-loader',
    'css-loader',
    'less-loader'
  ]
}

解析的过程是链式的,所以在use数组中下面的部分会先执行,所以执行顺序其实是less-loader > css-loader > style-loader

  1. webpack为何加载从右往左进行

Webpack 选择了 compose 方式, 而不是pipe的方式而已,在技术上实现从左往右也不会有难度

compose : require("style-loader!css-loader!sass-loader!./my-styles.sass");
pipe : require("./my-styles.sass!sass-loader!css-loader!style-loader");

函数组合是函数式编程中非常重要的**
函数组合有两种形式:一种是pipe、另一种是compose。前者从左往右组合函数,后者方向相反

  1. 函数式编程组合
  • 实现方式从右往左
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const add = n => n + 1;
const double = n => n * 2;
const addThenDouble = compose(double, add);

addThenDouble(2);  //  ((2 + 1) * 2) === 6

先执行了加1, 然后执行了double, 在compose中是采用了reduceRight,所以传入参数的顺序先传入double,后传入add

  • 实现方式从左往右
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const add = n => n + 1;
const double = n => n * 2;
const addThenDouble = compose(add,double);

add1ThenDouble(2); // ((2 + 1 = 3) * 2 === 6)

关于页面的布局概念

一、 静态布局

  1. 定义

传统的Web设计,网页上的所有元素的尺寸一律使用px作为单位

  1. 特点

不管浏览器尺寸具体是多少,网页布局始终按照最初写代码时的布局来显示。常规的pc的网站都是静态(定宽度)布局的,也就是设置了min-width,这样的话,如果小于这个宽度就会出现滚动条,如果大于这个宽度则内容居中外加背景,这种设计常见于pc端。

  1. 设计方法

    • PC
      居中布局,所有样式使用绝对宽度、高度(px),设计了一个Layout,在屏幕宽高有调整时,使用横向和竖向的滚动条来查阅被遮掩部分

    • 移动端
      另外建立移动网站,单独设计一个布局,使用不同的域名如wap.或者m.

  2. 优缺点

    • 优点: 这种布局方式对设计师和CSS编写者来说都是最简单的,亦没有兼容性问题。
    • 缺点:显而易见,即不能根据用户的屏幕尺寸做出不同的表现。当前,大部分门户网站、大部分企业的PC宣传站点都采用了这种布局方式。固定像素尺寸的网页是匹配固定像素尺寸显示器的最简单办法。但这种方法不是一种完全兼容未来网页的制作方法,我们需要一些适应未知设备的方法。

二、 流式布局

  1. 定义

流式布局(Liquid)的特点(也叫"Fluid") 是页面元素的宽度按照屏幕分辨率进行适配调整,但整体布局不变。代表作栅栏系统(网格系统)。

网页中主要的划分区域的尺寸使用百分数(搭配min-*、max-*属性使用),例如,设置网页主体的宽度为80%,min-width为960px。图片也作类似处理(width:100%, max-width一般设定为图片本身的尺寸,防止被拉伸而失真)。

  1. 特点

屏幕分辨率变化时,页面里元素的大小会变化而但布局不变。【这就导致如果屏幕太大或者太小都会导致元素无法正常显示。

  1. 设计方法

使用%百分比定义宽度,高度大都是用px来固定住,可以根据可视区域 (viewport) 和父元素的实时尺寸进行调整,尽可能的适应各种分辨率。往往配合 max-width/min-width 等属性控制尺寸流动范围以免过大或者过小影响阅读。

  1. 优缺点

这种布局方式在Web前端开发的早期历史上,用来应对不同尺寸的PC屏幕(那时屏幕尺寸的差异不会太大),在当今的移动端开发也是常用布局方式,但缺点明显:主要的问题是如果屏幕尺度跨度太大,那么在相对其原始设计而言过小或过大的屏幕上不能正常显示。因为宽度使用%百分比定义,但是高度和文字大小等大都是用px来固定,所以在大屏幕的手机下显示效果会变成有些页面元素宽度被拉的很长,但是高度、文字大小还是和原来一样(即,这些东西无法变得“流式”),显示非常不协调

三、 自适应布局

  1. 定义

自适应布局的特点是分别为不同的屏幕分辨率定义布局,即创建多个静态布局,每个静态布局对应一个屏幕分辨率范围。改变屏幕分辨率可以切换不同的静态局部(页面元素位置发生改变),但在每个静态布局中,页面元素不随窗口大小的调整发生变化。可以把自适应布局看作是静态布局的一个系列。

  1. 特点

屏幕分辨率变化时,页面里面元素的位置会变化而大小不会变化。

  1. 设计方法

使用 @media 媒体查询给不同尺寸和介质的设备切换不同的样式。在优秀的响应范围设计下可以给适配范围内的设备最好的体验,在同一个设备下实际还是固定的布局。

https://www.cnblogs.com/zhuzhenwei918/p/7147303.html

http、https、http2

一、 HTTP 和 HTTPS 协议的区别

  • HTTPS 协议需要CA证书,费用较高;而HTTP协议不需要;
  • HTTP协议是超文本传输协议,信息是明文传输的;HTTPS则是具有安全性的SSL加密传输协议
  • 使用不同的连接方式,端口不同,HTTP协议端口是80,HTTPS协议端口是443;
  • HTTP协议连接很简单,是无状态的;HTTPS协议是有SSL和HTTPS协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全。

二、 HTTP1.0 和 HTTP1.1 的区别

  • 连接方面,http1.0 默认使用非持久连接,而http1.1默认使用持久连接。http1.1通过使用持久连接来使多个http请求复用同一个TCP连接,以此来避免使用非持久连接时每次需要建立连接的时延
  • 资源请求方面,在http1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回时206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接
  • 缓存方面,在http1.0主要使用header里的If-Modified-Since、Expire来作为缓存判断的标准;http1.1则引入了更多的缓存控制策略,例如Etag、If- Unmodified-Since、If-Match、If-None-Match等更多可供选择的缓存头来控制缓存策略
  • http1.1新增了host字段,用来指定服务器的域名。http1.0中认为每台服务器都绑定一个唯一的IP地址,因此请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享了一个IP地址。因此有了host字段,这样就可以将请求发往同一台服务器上的不同网站
  • http1.1相对于http1.0还新增了很多请求方法,如PUT、HEAD、OPTIONS等。

三、 HTTP1.1 和 HTTP2.0 的区别

  • 二进制协议:HTTP/2 是一个二进制协议。在HTTP/1.1版中,报文的头信息必须是文本(ASCII 编码), 数据体可以是文本,也可以是二进制。HTTP/2则是彻底的二进制协议,头信息和数据体都是二进制,并且统称为“帧
    ”,可以分为头信息帧和数据帧。帧的概念是它实现多路复用的基础。
  • 多路复用:HTTP/2实现了多路复用,HTTP/2仍然复用了TCP连接,但是在一个连接里,客服端和服务端都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了队头堵塞的问题。
  • 数据流:HTTP/2使用了数据流的概念,因为HTTP/2的数据包是不按照顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此必须要对数据包进行标记,指出它属于那个请求。HTTP/2将每个请求或回应的所有数据包称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流ID,用来区分它属于哪个数据流。
  • 头信息压缩:HTTP/2实现了头信息压缩,由于HTTP1.1协议不带状态,每次请求都必须附上所有信息。所以请求的很多字段都是重复的,比如Cookie和UserAgent,一摸一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响了数据。HTTP/2对这一点做了优化,引入了头信息压缩机制。一方面头信息使用gzip或compress压缩后再发送;另一方面客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
  • 服务器推送:HTTP/2允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是http2下服务器主动推送的是静态资源,和webSocket以及使用SSE等方式向客户端发送即时数据的推送是不同的。

vue-router的钩子函数有哪些

一、 全局路由守卫

  • 全局前置守卫

    router.afterEach((to, from, next) => {})
  • 全局解析守卫

    router. beforeResolve((to, from) => {})
  • 全局后置钩子

    router.afterEach((to, from) => {})

二、路由独享守卫

   beforeEnter: (to, from, next) => {}

三、组件内的守卫

  • beforeRouteEnter 在渲染该组件的对应路由被 confirm 前调用

    beforeRouteEnter(to, from, next){
       // 不!能!获取组件实例 `this`
       // 因为当守卫执行前,组件实例还没被创建
    }
  • beforeRouteUpdate 在当前路由改变,但是该组件被复用时调用

    beforeRouteUpdate(to, from, next){
       // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
       // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
       // 可以访问组件实例 `this`
    }
  • beforeRouteLeave 导航离开该组件的对应路由时调用

    beforeRouteLeave(to, from, next){
        // 可以访问组件实例 `this`
    }

四、 完整的导航解析流程

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

文本单行、多行显示,超出省略号表示

一、 单行省略

overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
white-space: nowrap; // 规定段落中的文本不进行换行

二、 多行省略

display: -webkit-box;  // 作为弹性伸缩盒子模型显示
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
-webkit-box-orient: vertical; // 设置伸缩盒子的子元素排列方式: 从上到下垂直排列
-webkit-line-clamp: 2; // 显示的行数

关于原型、原型链

一、 原型

在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个prototype属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的prototype属性对应的值,在ES5中这个指针被称为对象的原型。

一般来说不应该能够获取这个值的,但是现在浏览器中都实现了proto属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5中新增了Object.getPrototypeOf()方法, 来获取对象的原型

二、 原型链

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。

原型链的尽头一般来说都是Object.prototype,所以这就是新建的对象为什么能够使用toString()等方法的原因。

三、 原型的特点

JavaScript对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。
当修改原型时,与之相关的对象以后继承这一改变。

防抖、节流

一、 防抖

  1. 定义

在事件被触发n秒后在执行回调,如果在这n秒内事件又被触发,则重新计时。

这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求

  1. 应用场景

    • 按钮提交:防止多次提交按钮,只执行最后提交的一次
    • 服务端验证场景: 表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似生存环境请用loadsh.debounce
  2. 实现

function debounce(fn, wait){
   let timer = null;
   return function(){
      let context = this, args = [...arguments];
      // 如果此时存在定时器的话,则取消之前的定时器重新计时
      if(timer){
         clearTimeout(timer);
         timer = null;
      }

      // 设置定时器,使事件间隔指定事件后执行
      timer = setTimeout(() => { 
          fn.apply(context, args);
      }, warit);
   }
}

二、 节流

  1. 定义

指规定一个单位时间内,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件内被触发多次,只有一次能生效。

节流可以使用在scroll函数的事件监听上,通过事件节流来降低事件调用的频率

  1. 应用场景

    • 拖拽场景:固定时间内执行一次,防止超高频次触发位置变动
    • 缩放场景:监控浏览器resize
    • 动画场景:避免短时间内多次触发动画引起性能问题
  2. 实现

// 时间戳版
function throttle(fn, delay){
   let preTime = Date.now();
   return function(){
      let context = this, args = [...arguments], nowTime = Date.now();

      // 如果两次时间间隔超过了指定时间, 则执行函数
      if( nowTime - preTime >= delay){
          preTime = Date.now();
          return fn.apply(context, args);
      }
  }
}

// 定时器版
function throttle(fun, wait){
  let timeout = null;
  return function(){
     let context = this, args = [...arguments];
     if(!timeout){
          timeout = setTimeout(() => {
              fun.apply(context, args);
              timeout = null;
          },  wait);
     }
  }
}

关于vw/vh、rem、em、px

一、 基本概念

  1. 物理像素(physical pixel)

物理像素又被称为设备像素,它是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。正是这些设备像素的微小距离欺骗了我们肉眼看到的图像效果。

  1. 设备独立像素(density-independent pixel)

设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。

  1. CSS像素

CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况之下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。

  1. 屏幕密度

屏幕密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)。

  1. 设备像素比(device pixel ratio)

设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系。

它的值可以按下面的公式计算得到:

设备像素比 = 物理像素 / 设备独立像素

在Javascript中,可以通过 window.devicePixelRatio 获取到当前设备的dpr。

在CSS中可以通过 -webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配。

或者使用 resolution | min-resolution | max-resolution 这些比较新的标准方式

  1. 位图像素

一个位图像素是栅格图像(如:png, jpg, gif等)最小的数据单元。每一个位图像素都包含着一些自身的显示信息(如:显示位置,颜色值,透明度等)。

理论上,1个位图像素对应于1个物理像素,图片才能得到完美清晰的展示

image

如上图:对于dpr=2的retina屏幕而言,1个位图像素对应于4个物理像素,由于单个位图像素不可以再进一步分割,所以只能就近取色,从而导致图片模糊(注意上述的几个颜色值)。

所以,对于图片高清问题,比较好的方案就是两倍图片(@2x)。

如:200×300(css pixel)img标签,就需要提供400×600的图片。

  1. 缩放比 scale

缩放比:scale = 1/dpr

  1. 视窗 viewport

简单的理解,viewport是严格等于浏览器的窗口。在桌面浏览器中,viewport就是浏览器窗口的宽度高度。但在移动端设备上就有点复杂。

移动端的viewport太窄,为了能更好为CSS布局服务,所以提供了两个viewport:虚拟的visualviewport和布局的layoutviewport。

  1. 视窗缩放 viewport scale

在开发移动端页面,我们可以设置meta标签的viewport scale来对视窗的大小进行缩放定义

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

二、 布局单位

  1. em单位

相对长度单位,相对于当前对象内文本的字体尺寸, 根据父元素的大小变化而变化

相当于"倍",比如设置当前的div字体大小为1.5em,则当前的div字体大小为:当前div继承的字体大小 * 1.5倍

  1. rem单位

相对长度单位,相对于跟元素( 即 html 元素)font-size 的倍数, 不会被它的父元素影响。
如果有嵌套的关系,嵌套关系的元素的字体大小始终按照根节点的字体大小进行缩放。

  • 使用rem单位,就按以下三个步骤:

    • 确定基数

    • 设置html的font-size

      /**
       *   百分数 = 基数/16
       *   基数10 百分数 62.5%
       *    基数14 百分数 87.7%
       */
      html{ font-size: 百分数 }; 
    • px换算rem

  • rem 布局

    • 核心: 设置好根html元素的font-size

    • 一般来说,为了防止在高清屏幕下像素不够用导致模糊,设计稿是640px(iPhone5 设备宽为320px)或750px(iphone6 设备宽为375px)的两倍稿,按照设备宽度做了两倍的大小。

    • 假设我们将屏幕宽度平均分为100份,每一份的宽度用rem表示, rem = 屏幕宽度 / 100,如果将rem作为单位,rem前面的数值就代表屏幕宽度的百分比。

    • 如何把UI图中获取的像素单位的值,转化为以rem为单位的值

      • 公式:元素宽度 / UI图宽度 * 100(分成100份)
      • 假如UI图尺寸是640px,UI中一个元素宽度是100px,根据公式得到 100/640*100=15.625
    • 利用scss体验提供一系列的基础支持

      /**移动端页面设计稿宽度*/
      $design-width: 750;
      /**移动端页面设计稿dpr基准值*/
      $design-dpr: 2;
      /**将移动端页面分为10块*/
      $blocks: 10;
      /**缩放所支持的设备最小宽度*/
      $min-device-width: 320px;
      /**缩放所支持的设备最大宽度*/
      $max-device-width: 540px;
      
      /**
       *  rem与px的对应关系
       * 1rem代表在js中设置的html,font-size的值(为一块的宽度)
       * $rem即为$px对应占多少块
       * 
       *  $px / $design-width === $rem / $blocks
       */
      
      /**单位px转化为rem*/
      @function px2rem($px){
           @return #{ $px / $design-width * $blocks }rem;
      }
      
      /**单位rem转化为px, 可用于根据rem单位快速计算原px*/
      @function rem2px($rem){
           @return #{ $rem / $blocks * $design-width)px;
      }
  1. 视窗单位
  • vw: 1vw 等于视窗宽度的1%
  • vh: 1vh 等于视窗高度的1%
  • vmin: 选取vw和vh中最小的那个
  • vmax: 选取vw和vh中最大的那个

image

兼容性: 在移动端IOS 8以上Android 4.4以上获得支持

如何判断数组和对象

一、 数据类型检测的方式有哪些

  1. typeof
console.log(typeof 2);  // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object

对象、数组、null都会被判断为object,其他判断都正确

  1. instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型
instanceof 只能正确判断引用数据类型,而不能判断基本数据类型
instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性

console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); //  false
console.log('str' instanceof String); // false

console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
console.log((() => {}) instanceof Function); // true
  1. instanceof 操作符的实现原理以及实现

instanceof 运算符用于判断构造函数的prototype属性是否出现在对象的原型链中的任何位置

function instanceof(left, right){
   // 获取对象的原型
  let proto = Object.getPrototypeOf(left);
  // 获取构造函数的prototype对象
  let prototype = right.prototype;
  // 判断构造函数的prototype对象是否在对象的原型链上
  while(true){
     if(!proto) return false;
     if(proto === prototype) return true;
     // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
     proto = Object.getPrototypeOf(proto);
  }
}
  1. constructor

constructor 有两个作用, 一是判断数据的类型,二是对象实例通过constructor对象访问它的构造函数

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log([].constructor === Array); // true
console.log((function(){}).constructor === Function); // true
console.log(({}).constructor === Object); // true

需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了

function Fn(){};
Fn.prototype = new Array();

var f = new Fn();
console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true
  1. Object.prototype.toString.call()

Object.prototype.toString.call() 使用Object对象的原型方法toString来判断数据类型

var a = Object.prototype.toString;

console.log(a.call(2)); // [object Number]
console.log(a.call(true)); // [object Number]
console.log(a.call('str')); // [object String]
console.log(a.call([]));  // [object Array]
console.log(a.call(function(){})); // [object Function]
console.log(a.call({})); // [object Object]
console.log(a.call(undefined)); // [object Undefined]
console.log(a.call(null)); // [object Null]
  1. obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

因为toString是Object的原型方法,而Array、Function等类型作为Object的实例,都重写了toString方法。

不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应重写之后的toString方法( function 类型返回内容为函数体的字符串,Array类型返回元素的字符串),而不会去调用Object上原型的toString方法(返回对象的具体类型)。

所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此在想要得到对象的具体类型时,应该调用Object原型上的toString方法。

二、 判断数组的方式有哪些

  1. Object.prototype.toString.call
Object.prototype.toString.call(arr).slice(8, -1) === 'Array';
  1. 通过原型链判断
arr.__proto__ === Array.prototype
  1. 通过Array.isArray()判断
Array.isArray(arr)
  1. 通过instanceof判断
arr instanceof Array
  1. 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr)

大数据列表优化如何实现

一、 当需要一次性向页面中插入大量数据时,怎么样才能不卡主页面的情况下渲染数据?一般有两种做法

  • 时间分片
  • 虚拟列表

二、 粗暴法

首先使用最粗暴的做法,一次性将大量数据插入页面中

<ul id="container"></ul>
  /**记录任务开始的时间*/
  let now = Date.now();
  /**插入十万条数据*/
  const total = 100000;
  /**获取容器*/
  const ul = document.getElementById("container");
  /**将数据插入容器中*/
  for (let i = 0; i < total; i++) {
    let li = document.createElement("li");
    li.innerHTML = ~~(Math.random() * total);
    ul.appendChild(li);
  }
  console.log("JS运行时间:", Date.now() - now); /**JS运行时间: 375*/
  setTimeout(() => {
    console.log("总时间", Date.now() - now); /**总时间 3267*/
  }, 0);

如何两次console.log的结果时间差异巨大,并且是如何简单统计JS运行时间和总渲染时间

  • 在JS的Event Loop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完毕后,才会触发渲染线程对页面进行渲染

  • 第一个console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为js运行所需要的时间

  • 第二个console.log是放到setTimeout中,它的触发时间是在渲染完成,在下一次Event Loop中执行

  • 结论:对于大量数据的渲染,JS运算并不是性能的瓶颈,性能的瓶颈主要在于渲染阶段

三、 时间分片

  1. 使用定时器

页面卡顿是由于同事渲染大量DOM所引起的,所以考虑将渲染过程分批进行

  /**获取容器*/
  const ul = document.getElementById("container");
  /**插入十万条数据*/
  const total = 100000;
  /**一次插入20条*/
  const once = 20;
  /**每条记录的索引*/
  let idx = 0;
  /**总页数*/
  let page = total / once;

  /**循环加载数据*/
  function loop(curTotal, curIdx) {
    if (curTotal <= 0) return false;
    /**每页多少条*/
    let pageCount = Math.min(curTotal, once);
    setTimeout(() => {
      /**将数据插入容器中*/
      for (let i = 0; i < pageCount; i++) {
        let li = document.createElement("li");
        li.innerText = curIdx + i + " : " + ~~(Math.random() * total);
        ul.appendChild(li);
      }
      loop(curTotal - pageCount, curIdx + pageCount);
    }, 0);
  }

  loop(total, idx);

页面加载的时间已经非常快了,每次刷新可以很快看到第一屏的所有数据,但是当我们快速滚动页面的时候,发现页面出现闪屏或者白屏的现象。

  1. 为什么会出现闪屏现象呢

需要明白的一些概念

  • FPS: 每秒钟画面更新的次数。我们平时所看到的连续画面都是由一幅幅静止画面组成的,每幅画面称为一帧,FPS是描述帧变化速度的物理量。
  • 大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次,FPS为60frame/s,为这个值的设定受屏幕分辨率、屏幕尺寸和显卡的影响。
  • 因此,当你对着电脑屏幕什么也不做的情况下,大多显示器也会以每秒60次的频率正在不断的更新屏幕上的图像。

为什么你感觉不到这个变化?

  • 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了, 这中间只间隔了16.7ms(1000/60≈16.7),所以会让你误以为屏幕上的图像是静止不动的。
  • 而屏幕给你的这种感觉是对的,试想一下,如果刷新频率变成1次/秒,屏幕上的图像就会出现严重的闪烁, 这样就很容易引起眼睛疲劳、酸痛和头晕目眩等症状。
  • 大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。 因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。

直观感受,不同帧率的体验:

  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;
  • 帧率波动很大的动画,亦会使人感觉到卡顿。
  1. 简单聊一下 setTimeout 和闪屏现象为什么会出现闪屏现象呢
  • setTimeout的执行时间并不是确定的。在JS中,setTimeout任务被放进事件队列中,只有主线程执行完才会去检查事件队列中的任务是否需要执行,因此setTimeout的实际执行时间可能会比其设定的时间晚一些。
  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的刷新频率可能会不同,而setTimeout只能设置一个固定时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致。

在setTimeout中对dom进行操作,必须要等到屏幕下次绘制时才能更新到屏幕上,如果两者步调不一致,就可能导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素,从而导致丢帧现象。

  1. 使用 requestAnimationFrame

requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机

如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次
如果屏幕刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms
换句话说就是requestAnimationFrame的步伐跟着系统的刷新步伐走。
他能保证回调函数在屏幕每一次的刷新间隔只被执行一次,这样就不会引起丢帧现象

<ul id="container"></ul>
  /**获取容器*/
  const ul = document.getElementById("container");
  /**插入十万条数据*/
  const total = 100000;
  /**一次插入20条*/
  const once = 20;
  /**每条记录的索引*/
  let idx = 0;
  /**总页数*/
  let page = total / once;

  /**循环加载数据*/
  function loop(curTotal, curIdx) {
    if (curTotal <= 0) return false;

    /**每页多少条*/
    let pageCount = Math.min(curTotal, once);

    window.requestAnimationFrame(() => {
      /**将数据插入容器中*/
      for (let i = 0; i < pageCount; i++) {
        let li = document.createElement("li");
        li.innerText = curIdx + i + " : " + ~~(Math.random() * total);
        ul.appendChild(li);
      }
      loop(curTotal - pageCount, curIdx + pageCount);
    }, 0);
  }

  loop(total, idx);
  1. 使用 DocumentFragment

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染),且不会导致性能等问题。
可以使用document.createDocumentFragment方法或者构造函数来创建一个空的DocumentFragment

从MDN的说明中,我们得知DocumentFragment是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流

当append元素到document中时,被append进去的元素的样式表的计算是同步发生的,此时调用get ComputedStyle可以得到样式的计算值。
而append元素到documentFragment中时,是不会计算元素的样式表,所以documentFragment性能更优。当然现在浏览器的优化已经做的很好了,当append元素到document中后,没有访问getComputedStyle之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。

<ul id="container"></ul>
  /**获取容器*/
  const ul = document.getElementById("container");
  /**插入十万条数据*/
  const total = 100000;
  /**一次插入20条*/
  const once = 20;
  /**每条记录的索引*/
  let idx = 0;
  /**总页数*/
  let page = total / once;

  /**循环加载数据*/
  function loop(curTotal, curIdx) {
    if (curTotal <= 0) return false;

    /**每页多少条*/
    let pageCount = Math.min(curTotal, once);

    window.requestAnimationFrame(() => {
      const fragment = document.createDocumentFragment();
      /**将数据插入容器中*/
      for (let i = 0; i < pageCount; i++) {
        let li = document.createElement("li");
        li.innerText = curIdx + i + " : " + ~~(Math.random() * total);
        fragment.appendChild(li);
      }
      ul.appendChild(fragment);
      loop(curTotal - pageCount, curIdx + pageCount);
    }, 0);
  }

  loop(total, idx);

通过时间分片的方式来同时加载大量简单DOM。

四、 虚拟列表

  1. 定义

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域的数据不渲染或部分渲染的技术,从而达到极高的渲染性能

假设有一万条记录同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需要加载10条即可。

image

说完首次加载,再分析一下当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域应该显示的列表项

假设滚动发生,滚动条距顶部的位置为150px,则我们可得知可见区域内的列表项为第4项至第13项。

image

  1. 实现

在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

  • 计算当前可视区域起始索引(startIdx)
  • 计算当前可视区域结束索引(endIdx)
  • 计算当前可视区域数据,并渲染到页面中
  • 计算startIdx对应的数据在整个列表中的偏移位置startOffset并设置到列表上

image

由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可以正常的处罚滚动,将html结构设计成如下结构

<div class="infinite-list-container">
    <div class="infinite-list-phantom"></div>
    <div class="infinite-list">
      <!-- item-1 -->
      <!-- item-2 -->
      <!-- ...... -->
      <!-- item-n -->
    </div>
</div>
  • infinite-list-container 为可视区域的容器
  • infinite-list-phantom 为容器的占位,高度为总列表高度,用于形成滚动条
  • infinite-list 为列表项的渲染区域

接着监听infinite-list-container的scroll事件,获取滚动条的scrollTop

  • 假定可视区域高度固定,称之为screenHeight
  • 假定列表项目高度固定,称之为itemSize
  • 假定列表数据称之为listData
  • 假定当前滚动位置称之为scrollTop

则可推算出

  • 列表总高度 listHeight = listData.length * itemSize
  • 可显示的列表项数 visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的起始索引 startIdx = Math.floor(scrollTop / itemSize)
  • 数据的结束索引 endIdx = startIdx + visibleCount
  • 列表显示数据为 visibleData = listData.slice(startIdx, endIdx)

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移到可视区域中

  • 偏移量 startOffset = scrollTop - ( scrollTop % itemSize)
  1. 列表项高度固定代码实现

虚拟列表component部分

<template>
  <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
    <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }" />
    <div class="infinite-list" :style="{ transform: getTransform }">
      <div
        ref="items"
        v-for="item in visibleData"
        :key="item.id"
        :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"
      >
        {{ item.value }}
      </div>
    </div>
  </div>
</template>
export default {
  name: "Infinite",
  props: {
    /**所有列表数据 */
    listData: {
      type: Array,
      default: () => [],
    },
    /**每项的高度 */
    itemSize: {
      type: Number,
      default: 200,
    },
  },
  computed: {
    /**列表总高度 */
    listHeight() {
      return this.listData.length * this.itemSize;
    },
    /**可显示的列表项目 */
    visibleCount() {
      return Math.ceil(this.screenHeight / this.itemSize);
    },
    /**偏移量对应的style */
    getTransform() {
      return `translate3d(0, ${this.startOffset}px, 0)`;
    },
    /**获取真实显示列表数据 */
    visibleData() {
      return this.listData.slice(
        this.start,
        Math.min(this.end, this.listData.length)
      );
    },
  },
  data() {
    return {
      /**可视区域高度 */
      screenHeight: 0,
      /**偏移量 */
      startOffset: 0,
      /**起始索引 */
      start: 0,
      /**结束索引 */
      end: null,
    };
  },
  mounted() {
    this.screenHeight = this.$el.clientHeight;
    this.start = 0;
    this.end = this.start + this.visibleCount;
  },
  methods: {
    scrollEvent() {
      /**当前滚动位置 */
      const scrollTop = this.$refs.list.scrollTop;
      /**此时的开始索引 */
      this.start = Math.floor(scrollTop / this.itemSize);
      /**此时的结束索引 */
      this.end = this.start + this.visibleCount;
      /**此时的偏移量 */
      this.startOffset = scrollTop - (scrollTop % this.itemSize);
    },
  },
};
.infinite-list-container {
  height: 100%;
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
}

.infinite-list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.infinite-list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
  text-align: center;
}

.infinite-list-item {
  padding: 10px;
  color: #555;
  box-sizing: border-box;
  border-bottom: 1px solid #999;
}

父组件

<template>
  <div id="app">
    <Infinite :listData="data" :itemSize="100" />
  </div>
</template>
import Infinite from "./components/Infinite";
let d = [];
for (let i = 0; i < 1000; i++) {
  d.push({ id: i, value: i });
}
export default {
  name: "App",
  components: {
    Infinite,
  },
  data() {
    return {
      data: d,
    };
  },
};
html {
  height: 100%;
}
body {
  height: 100%;
  margin: 0;
}
#app {
  height: 100%;
}
  1. 列表项动态高度代码

实际应用的时候,当列表中包含文本之类的可变内容,会导致列表项的高度并不相同

在虚拟列表中应用动态高度的解决方案一般有如下三种

  1. 对组件数学itemSize进行扩展,支持传递类型为数字、数组、函数
  • 可以是一个固定值,如100,此时列表项是固定的
  • 可以是一个包含所有列表项高度的数据,如[50, 20, 100, 80, ...]
  • 可以是一个根据列表项索引返回其高度的函数,(index: number): number

这种方式虽然有比较好的灵活度,但仅适用于可以预先知道或通过计算得知列表项高度的情况,依然无法解决列表项高度由内容撑开的情况。

  1. 将列表项渲染到屏幕外,对其高度进行测量并缓存,然后再将其渲染至可视区域内

由于预先渲染至屏幕外,在渲染至屏幕内,这导致渲染成本增加一倍,这对于数百万用户在低端移动端设备上使用的产品来说是不切实际的

  1. 以预估高度先行渲染,然后获取真实高度并缓存

定义组件属性estimatedItemSize, 用来接收预告高度

props:{
   // 预告高度
   estimatedItemSize:{
     type: Number
  }
}

定义positions,用于列表项渲染后存储每一项的高度以及位置信息

this.positions = [
   top: 0,
   bottom: 100,
   height: 100
]

并在初始化根据estimatedItemSize对positions进行初始化

initPositions(){
   this.positions = this.listData.map((item, index) => {
        return {
           index,
           height: this.estimatedItemSize,
           top: index * this.estimatedItemSize,
           bottom: (index + 1) * this.estimatedItemSize
        }
   })
}

由于列表项高度不定,并且我们维护了positions,用于记录每一项的位置,而列表高度实际就等于列表中最后一项的底部距离列表顶部的位置

// 列表总高度
listHeight(){
   return this.positions[this.positions.length - 1].bottom
}

由于需要在渲染完成后,获取列表每项的位置信息并缓存,所以使用钩子函数updated来实现

updated(){
   let nodes = this.$refs.items;
   nodes.forEach(node => {
         let rect = node.getBoundingClientRect();
         let height = rect.height;
         let index = +node.id.slice(1);
         let oldHeight = this.positions[index].height;
         let dValue = oldHeight - height;
         // 存在差值
         if(dValue){
             this.positions[index].bottom = this.positions[index].bottom - dValue;
             this.positions[index].height = height;
             for(let k = index + 1;  k < this.positions.length; k++){
                  this.positions[k].top = this.positons[k-1].bottom;
                  this.positions[k].bottom = this.positions[k-1].bottom - dValue;
             }
         }
   })
}

滚动后获取列表开始索引的方法修改为通过缓存获取】

//  获取列表起始索引
getStartIndex(scrollTop = 0){
  let item = this.positions.find(i => i && i.bottom > scrollTop);
  return item.index
}

由于我们的缓存数据,本身就是有顺序的,所以获取开始索引的方法可以考虑通过二分查找的方式来降低检索次数

// 获取列表起始索引
getStartIndex(scrollTop = 0){
   // 二分查找
  return this.binarySearch(this.positions, scrollTop)
}
// 二分法查找
binarySearch(list, value){
   let start = 0;
   let end = list.length - 1;
   let tempIndex = null;
   while(start <= end){
      let midIndex = parsetInt((start + end) / 2);
      let midValue = list[midIndex].bottom;
      if(midValue === value){
          return midIndex + 1;
      }else if(midValue < value){
          start = midIndex + 1;
      }else if(midValue > value){
          if(tempIndex === null || tempIndex > midIndex) tempIndex = midIndex;
          end = end -1;
      }
   }
   return tempIndex
}

滚动后将偏移量的获取方式变更

scrollEvent(){
 // ... 省略  
 if(this.start >= 1) this.startOffset = this.positions[this.start - 1].bottom
 else this.startOffset = 0
}

通过faker.js来创建一些随机数据

let data = [];
for(let i = 0; i < 100000;i++){
   data.push({
      id: i,
      value: faker.lorem.sentences(); // 长文本
   })
}

为了使页面平滑滚动,我们还需要在可见区域的上方和下方渲染额外的项目,在滚动时给予一些缓存,所以将屏幕分为三个区域

  • 可视区域上方 above
  • 可视区域 screen
  • 可视区域下方 below

image

定义组件属性bufferScale, 用于接收缓存区数据与可视区数据的比例

props:{
   // 缓存区比例
   bufferScale:{
       type: Number;
       default: 1
   }
}

可视区上方渲染条数 aboveCount 获取方式如下

aboveCount(){
   return  Math.min(this.start, this.bufferScale * this.visibleCount)
}

可视区下方渲染条数 belowCount 获取方式如下

belowCount(){
     return Math.min(this.listData.length - this.end, this.bufferScale * this.visibleCount)
}

真实渲染数据visibleData获取方式如下

visibleData(){
   let start = this.start - this.abvoeCount;
   let end = this.end + this.belowCount;
  return this._listData.slice(start, end);
}

完整代码

虚拟列表组件

<template>
  <div
    ref="list"
    :style="{ height }"
    class="infinite-list-container"
    @scroll="scrollEvent($event)"
  >
    <div ref="phantom" class="infinite-list-phantom" />
    <div ref="content" class="infinite-list">
      <div
        class="infinite-list-item"
        ref="items"
        :id="item._index"
        :key="item._index"
        v-for="item in visibleData"
      >
        <slot ref="slot" :item="item.item" />
      </div>
    </div>
  </div>
</template>
export default {
  name: "VirtualList",
  props: {
    /**所有列表数据 */
    listData: {
      type: Array,
      default: () => [],
    },
    /**预估高度 */
    estimatedItemSize: {
      type: Number,
      required: true,
    },
    /**缓存区比例 */
    bufferScale: {
      type: Number,
      default: 1,
    },
    /**容器高度 100px or 50vh */
    height: {
      type: String,
      default: "100%",
    },
  },
  data() {
    return {
      /**可视区域高度 */
      screenHeight: 0,
      /**开始索引 */
      start: 0,
      /**结束索引 */
      end: 0,
    };
  },
  computed: {
    _listData() {
      return this.listData.map((item, index) => {
        return {
          _index: `_${index}`,
          item,
        };
      });
    },
    visibleCount() {
      return Math.ceil(this.screenHeight / this.estimatedItemSize);
    },
    aboveCount() {
      return Math.min(this.start, this.bufferScale * this.visibleCount);
    },
    belowCount() {
      return Math.min(
        this.listData.length - this.end,
        this.bufferScale * this.visibleCount
      );
    },
    visibleData() {
      let start = this.start - this.aboveCount;
      let end = this.end + this.belowCount;
      return this._listData.slice(start, end);
    },
  },
  created() {
    this.initPositions();
    window.vm = this;
  },
  mounted() {
    this.screenHeight = this.$el.clientHeight;
    this.start = 0;
    this.end = this.start + this.visibleCount;
  },
  updated() {
    this.$nextTick(function () {
      if (!this.$refs.items || !this.$refs.items.length) return;
      /**获取真实元素大小, 修改对应的尺寸缓存 */
      this.updateItemSize();
      /**更逊真实元素大小,修改对应的尺寸缓存 */
      let height = this.positions[this.positions.length - 1].bottom;
      this.$refs.phantom.style.height = height + "px";
      /**更新真实偏移量 */
      this.setStartOffset();
    });
  },
  methods: {
    initPositions() {
      this.positions = this.listData.map((d, index) => ({
        index,
        height: this.estimatedItemSize,
        top: index * this.estimatedItemSize,
        bottom: (index + 1) * this.estimatedItemSize,
      }));
    },
    /**获取列表起始索引 */
    getStartIndex(scrollTop = 0) {
      /**二分查找 */
      return this.binarySearch(this.positions, scrollTop);
    },
    binarySearch(list, value) {
      let start = 0,
        end = list.length - 1,
        tempIndex = null;
      while (start <= end) {
        let midIndex = parseInt((start + end) / 2);
        let mindValue = list[midIndex].bottom;
        if (mindValue === value) return midIndex + 1;
        else if (mindValue < value) start = midIndex + 1;
        else if (mindValue > value) {
          if (tempIndex === null || tempIndex > midIndex) tempIndex = midIndex;
          end = end - 1;
        }
      }
      return tempIndex;
    },
    /**获取列表项的当前尺寸 */
    updateItemSize() {
      let nodes = this.$refs.items;
      nodes.forEach((node) => {
        let rect = node.getBoundingClientRect();
        let height = rect.height;
        let index = +node.id.slice(1);
        let oldHeight = this.positions[index].height;
        let dValue = oldHeight - height;
        /**存在差值 */
        if (dValue) {
          this.positions[index].bottom = this.positions[index].bottom - dValue;
          this.positions[index].height = height;
          for (let k = index + 1; k < this.positions.length; k++) {
            this.positions[k].top = this.positions[k - 1].bottom;
            this.positions[k].bottom = this.positions[k].bottom - dValue;
          }
        }
      });
    },
    /**获取当前的偏移量 */
    setStartOffset() {
      let startOffset = 0;
      if (this.start >= 1) {
        let size =
          this.positions[this.start].top -
          (this.positions[this.start - this.aboveCount]
            ? this.positions[this.start - this.aboveCount].top
            : 0);
        startOffset = this.positions[this.start - 1].bottom - size;
      } else {
        startOffset = 0;
      }
      this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`;
    },
    /**滚动事件 */
    scrollEvent() {
      /**当前滚动位置 */
      let scrollTop = this.$refs.list.scrollTop;
      /**此时的开始索引 */
      this.start = this.getStartIndex(scrollTop);
      /**此时的结束索引 */
      this.end = this.start + this.visibleCount;
      /**此时的偏移量 */
      this.setStartOffset();
    },
  },
};
<style scoped>
.infinite-list-container {
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
}

.infinite-list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.infinite-list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}

.infinite-list-item {
  padding: 5px;
  color: #555;
  box-sizing: border-box;
  border-bottom: 1px solid #999;
  /* height:200px; */
}
</style>

Item组件

<template>
  <p>
    <span style="color: red">{{ item.id }}</span
    >{{ item.value }}
  </p>
</template>
export default {
  props: {
    item: {
      type: Object,
      default: () => {},
    },
  },
};

父组件

<template>
  <div id="app">
    <VirtualList :listData="data" :estimatedItemSize="100" v-slot="slotProps">
      <Item :item="slotProps.item" />
    </VirtualList>
  </div>
</template>
import Item from "./components/Item";
import VirtualList from "./components/VirtualList.vue";

import faker from "faker";

let d = [];
for (let i = 0; i < 1000; i++) {
  d.push({ id: i, value: faker.lorem.sentences() });
}
export default {
  name: "App",
  components: {
    VirtualList,
    Item,
  },
  data() {
    return {
      data: d,
    };
  },
};

聊聊你所知道的设计模式

一、 单例模式
二、 策略模式
三、 代理模式
四、 发布订阅模式
五、 迭代器模式
六、 命令模式
七、 组合模式
八、 模版方法模式
九、 享元模式
十、 指责链模式
十一、 中介者模式
十二、 装饰器模式
十三、 状态模式
十四、 适配器模式
十五、 外观模式

js图片预加载的三种方法

一、 预加载的概念
当页面打开图片提前加载,并且缓存在用户本地,需要用到时候直接进行渲染; 在浏览器图片较多的网页,可以有更好的用户体验

二、 预加载的三种方式

  1. load预加载
var num = 1;
var list = [];
loadImage();

function loadImage(){
  var img = new Image(); // 创建一张图片
  img.addEventListnener('load',  loadHander); // 给这张图片添加事件监听load
  img.src='./img/' + num + '.jpg';
}

function loadHeader(){
  list.push(thos.cloneNode()); // 加载完成后,将加载进来的图片复制一个新的,放入数组中
  num++; // 修改num, 大于100停止执行
  if(num > 100) return; 
  // 给这个图片的地址赋予一个新地址,因为改变图片的地址就会重新出发load, 会继续进入loadHander函数,不断加载,直到加载完成
  this.src = './img/' + num + '.jpg'; 
}
  1. 生成器函数来实现预加载
function loadImage(src){
  return new Promise(function(resolve, reject){
     // promise 中创建图片
      const img = new Image();
      img.onload = function(){
         resolve(img); // 加载时执行resolve函数
      }
      img.onerror = function(){
         reject(src + '地址错误'); // 抛出异常时执行reject函数
      }
      img.src = src;
  })
}

function* fn(){
  for(let i = 1; i<100; i++){
    // 在执行器函数中一次性执行loadImage函数
    yield loadImage('./img/'+ i + '.jpg');
  }
}

const s = fn();
let value = s.next().value;
resume();

function resume(){
  // 执行一次resume函数, 并在函数里面执行promise在resolve状态下的函数
  value.then(img => {
    // 反复执行s.next().value, 直到全部图片加载完成
    value = s.next().value;
     if(value) resume()
  })
}
  1. async / await
function loadImage(src){
  const p = new Promise(function(resolve, reject){
      const img = new Image();
      img.onload = function(){ resolve(img) };
      img.onerror = function(){ reject(src) };
      img.src= src;
  })
  return p;
}

// 使用async表示这个函数是一个异步函数
async function fn(){
  const arr = [];
  for(let i = 3; i < 80; i++){
    // 使用await, 作用就是让异步函数变为同步等待, 异步变成了阻塞式等待
    // 当异步全部完成时,再继续向后进行
   //  async函数中的await后面跟的是promise对象
   //  async函数执行后返回一个promise对象
    await loadImage(`./img/${i}.jpg`).then(img => arr.push(img))
  }
  return arr
};

fn().then(list => {console.log(list)}); 

实现垂直居中显示

  1. 绝对定位 + translate, 考虑兼容性问题
.parent{
  position: relative;
}
.children{
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
  1. 绝对定位,适用于盒子有宽高的情况
.parent{
  position: relative;
}
.children{
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}
  1. 绝对定位 + margin,适用于盒子有宽高的情况
.parent{
  position: relative;
}
.children{
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left:  -50px;  /*自身height 的一半**/
  margin-top: -50px;  /*自身width的一半**/
}
  1. flex 布局,考虑兼容性,在移动端用的比较多
.parent{
  display: flex;
  jusity-content: center;
 align-items: center;
}

关于js的事件流

一、 事件

事件是文档或者浏览器窗口中发生的,特定的交互瞬间

事件是由用户或浏览器自身执行的某种动作

事件是JavaScript和DOM之间交互的桥梁

二、事件流

事件流描述的是从页面中接收事件的顺序

事件发生时会在元素节点于根节点之间按照特定顺序的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流

  1. 事件冒泡

事件的传播是从最特定的事件目标到最不特定的事件目标(document对象)。即从DOM树的叶子到根,也就是说事件会从最内层的元素开始发生,一直向上传播,直到document对象。

  1. 事件捕获

事件的传播是从最不特定的事件目标(document对象)到最特定的事件目标。即从DOM树的根到叶子,事件会从最外层开始发生,直到最具体的元素。

  1. DOM 事件流

DOM标准采用捕获 + 冒泡,两种事件流都会触发DOM的所有对象,从document对象开始,也在document对象结束

DOM标准规定事件流包括三个阶段: 事件捕获阶段、处于目标阶段、事件冒泡阶段

事件捕获阶段:实际目标在捕获阶段不会接收事件。也就是在捕获阶段,事件从document到再到就停止了。

处于目标阶段:事件在

上发生并处理。但是事件处理会被看成冒泡阶段的一部分

冒泡阶段: 事件又传播回文档

  1. element.addEventListener(event, function, useCapture)

用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法,其传入三个参数,分别是没有on的事件类型、事件处理函数、控制事件阶段,第三个参数是boolean类型,默认是false,表示在事件冒泡阶段调用事件处理函数,传入true就表示在事件捕获的阶段调用事件处理函数。

  1. 事件代理

事件委托是利用事件的冒泡原理来实现的,在元素最外层的元素绑定事件,里面的元素触发事件的时候,都会冒泡到最外层元素,所以都会被触发。这就是事件委托,委托它们父级代为执行事件。

  1. 阻止事件冒泡

(1) event.stopPropagation()

(2) window.event.cancelBubble = true (谷歌、IE8兼容,火狐不兼容)

(3) 合并取消 return false
在JavaScript的return false只会阻止默认行为,而用jquery的话既阻止了默认行为又防止对象冒泡

隐藏元素的方法有哪些

  1. display: none: 渲染树不会包含该渲染对象,因此该元素不会在页面中占据位置,也不会响应绑定的监听事件
  2. visibility: hidden: 元素在页面中仍占据空间,但是不会响应绑定的监听事件
  3. opacity: 0: 将元素的透明度设置为0,以此来实现元素的隐藏。元素在页面中仍然占据空间,并且能够响应元素绑定的监听事件。
  4. position: absolute: 通过使用绝对定位将元素移除到可视区域内,以此来实现元素的隐藏
  5. z-index: 负值: 来使用其他元素遮盖该元素,实现元素隐藏
  6. clip/clip-path: 使用元素裁剪的方法来实现元素的隐藏。元素仍在页面占据位置,但是不会响应绑定的监听事件
  7. transform: scale(0,0): 将元素缩放为0,来实现元素隐藏。元素仍在页面占据位置,但是不会响应绑定的监听事件

关于闭包

一、 变量

  1. 全局变量

可以在任意位置访问的变量

var age = 20;
function a(){ console.log(age) };
a() // 20
  1. 局部变量

函数中用var定义的变量,只能在函数中访问这个变量,函数外部访问不了

function a(){ var age = 20; }
a();
console.log(age); //  Uncaught ReferenceError: age is not defined
  1. 关于变量需要注意的点
  • 在函数中如果不使用var定义变量那么js引擎会自动添加成全局变量
  • 全局变量从创建的那一刻起就会一直保存在内存中,除非你关闭这个页面。
  • 局部变量当函数运行完以后就会销毁这个变量,假如有多次调用这个函数它下一次调用的时候又会重新创建,即运行完销毁,回到最初的状态。

二、 函数

  1. 一个函数内可以嵌套多个函数
  2. 函数里面的子函数可以访问它上级定义的变量,注意不只是一级,如果上级没有会继续往上级找,直到找到为止,如果找到全局变量找不到就会报错
function a(){
   var name = 'test';
   function b(){ console.log(name); }
   b()
}
a(); // test
  1. 函数的另外一种调用形式,叫做自调用,自己调用自己,达到自执行的效果
var a = 0;
(function(){ console.log(++a) })()  // 1

三、 闭包

  1. 概念

如果某个函数被它的父函数之外的一个变量引用,就形成了闭包
父对象的所有变量对子对象都是可见的,反之则不成立

所以闭包就是能够读取其他函数内部变量的函数,是将函数内部和函数外部连接起来的桥梁

  1. 为什么使用闭包
function  a(){
   var num = 0;
   console.log(++num);
}
a(); // 1
a(); // 1

上面代码都输出了1,为什么呢

因为函数执行完以后,里面的变量(即局部变量)就会销毁,下一次运行又会重新创建那个变量,所以虽然第一次++num了,但是这个变量在第一次执行完后就会被销毁了

怎么样才能确保第一次的变量不被销毁,那么就需要使用闭包了

  1. 闭包的第一种写法
function a(){
    var b = 0;
    function c(){
         b++;
         console.log(b);
    }
    return c;
}
var res = a();
res(); // 1
res(); // 2

里面的变量的值没有被销毁,因为函数a被外部res引用,所以变量b没有被回收

  1. 闭包的第二种写法
var bi = (function(){
      var a = 0;
      function b(){
          a++;
          console.log(a);
      }
      return b;
})();
bi(); // 1
bi(); // 2

执行过程分析

  • 首先把一个自调用函数赋值给bi,这个自调用函数运行完之后bi的值就变成了
    function b(){
        a++;
        console.log(a);
    }
    因为我们在上面的代码return了b,然后因为这个自调用函数被bi引用所以里面的变量a并没有因为这个自调用函数执行完而销毁,而是保存到了内存中,所以可以多次打印1、2、3。
  1. 闭包的使用场景
  • 没有使用闭包的版本

     window.onload = function(){
          var ul = document.getElementsByTagName("ul")[0];
          var li = ul.getElementsByTagName("li");
          for(var i = 0; i < li.length; i++){
             li[i].onclick = function(){ console.log(i) }; // 不管点那个都是返回6
          }
     }
  • 使用了闭包的版本

    window.onload = function(){
        var ul = document.getElementsByTagName("ul")[0];
        var li = ul.getElementsByTagName("li");
        for(var i = 0; i < li.length; i++){
            (function(i){
                li[i].onclick = function(){ console.log(i) }; // 点击第几个就返回第几个
            })(i)
        }
    }
  1. 使用闭包的注意点
  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题,在IE中可能导致内存泄露。
    • 解决方法: 在退出函数之前,将不再使用的局部变量全部删除
  • 闭包会在父函数内外部,改变父函数内部变量的值。所以如果把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有熟悉,这时候一定要小心,不要随便改变父函数内部变量的值。
  1. 关于闭包的面试题
var name = "The Windows";
var object = {
   name: "My Object",
   getNameFunc: function(){
        return function(){ return this.name };
   }
}
console.log(object.getNameFunc()()) // The Windows
var name = "The Windows";
var object = {
   name: "My Object",
   getNameFunc: function(){
        var that = this
        return function(){ return that.name };
   }
}
console.log(object.getNameFunc()()); // My Object
var name = "The Windows";
var object = {
   name: "My Object",
   getNameFunc: function(){
        var that = this
        return () => this.name
   }
}
console.log(object.getNameFunc()()); // My Object
function foo(x) {
    var tmp = 3;
    return function f2(y) {
        alert(x + y + (++tmp));
    };
}
var bar = foo(3); // bar 现在是一个闭包
bar(10);  // 17
bar(1); // 9
// 解释arguments
  function sum() {
    var cache;
    if (arguments.length === 1) {
      cache = arguments[0];
      return function (args) {
        return cache + args;
      };
    } else {
      return arguments[0] + arguments[1];
    }
  }
  console.log(sum(1,2))  // 3
  console.log(sum(2)(3)) // 5
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
  const btnes = document.querySelectorAll('.btn');
  for (var i = 0, len = btnes.length; i < len; i++) {
    btnes[i].onclick = function () {
      console.log(i);  // 7
    };
  }

修改一下

  // 使用let声明变量
  const btnes = document.querySelectorAll('.btn');
  for (let i = 0, len = btnes.length; i < len; i++) {
    btnes[i].onclick = function () {
      console.log(i);
    };
  }
  //  参数的传递  桥梁
  const btnes = document.querySelectorAll('.btn');
  for (var i = 0, len = btnes.length; i < len; i++) {
    btnes[i].idx = i
    btnes[i].onclick = function () {
      console.log(this.idx); 
    }
  }
  // 使用闭包
  const btnes = document.querySelectorAll('.btn');
  for (var i = 0, len = btnes.length; i < len; i++) {
    (function (j) {
      btnes[j].onclick = function () {
        console.log(j);
      };
    })(i);
  }
function fnnn(){
   	var arr = [];
   	for(var i = 0;i < 5;i ++){
		arr[i] = function(){
			return i;
	 	}
   	}
   	return arr;
}
var list = fnnn();
for(var i = 0,len = list.length;i < len ; i ++){
   console.log(list[i]());  // 55555
}

修改一下

function fnnn(){
   	var arr = [];
   	for(let i = 0;i < 5;i ++){
		arr[i] = function(){
			return i;
	 	}
   	}
   	return arr;
}
var list = fnnn();
for(var i = 0,len = list.length;i < len ; i ++){
   console.log(list[i]());  // 12345
}

或者

function fnnn(){
   	var arr = [];
   	for(var i = 0;i < 5;i ++){
		arr[i] = ( function(j){
			return j;
	 	})(i)
   	}
   	return arr;
}
var list = fnnn();
for(var i = 0,len = list.length;i < len ; i ++){
   console.log(list[i]);  // 12345
}
  function fun(n, o) {
    console.log(o);
    return {
      fun: function (m) {
        return fun(m, n);
      },
    };
  }
  var a = fun(0); a.fun(1); a.fun(2); a.fun(3); // undefined,0,0,0
  var b = fun(0).fun(1).fun(2).fun(3);  // undefined,0,1,2
  var c = fun(0).fun(1); c.fun(2); c.fun(3); // undefined,0,1,1
  1. 分析三个fun函数的关系

注意:所有声明的匿名函数都是一个新的函数

  // 这里的fun函数属于标准具名函数声明,是新创建的函数
  function fun(n, o) {
    console.log(o);
    // 函数的返回值是一个对象字面量表达式,属于一个新的object
    return {
      // 这个新的对象内部包含一个也叫fun的属性,
      // 通过上述介绍可得知,属于匿名函数表达式,
      // 即fun这个属性中存放的是一个新创建匿名函数表达式。
      // 所以第一个fun函数与第二个fun函数不相同,均为新创建的函数
      fun: function (m) {
        return fun(m, n);
      },
    };
  }
  1. 关于函数作用域链的问题

在函数表达式内部能不能访问存放当前函数的变量

测试一,对象内部的函数表达式

var o = {
   fn: function(){
       console.log(fn);
   }
}
o.fn(); // Uncaught ReferenceError: fn is not defined

测试二,非对象内部的函数表达式

var fn = function(){
    console.log(fn);
}
fn(); // function(){ console.log(fn) }

结论是:使用var或是非对象内部的函数表达式内,可以访问到存放当前函数的变量,在对象内部的不能访问到。

原因是:因为函数作用域链的问题,采用var的是在外部创建了fn变量。函数内部当然在内部寻找不到fn后向上的作用域查找,而在创建对象内部时,因为没有在函数作用域内创建fn,所以无法访问。

综上可知,最内层return 出去的fun函数不是第二层fun函数,而是最外层的fun函数

所以三个fun函数的关系就是第一个等于第三个,他们都不等于第二个。

  1. 到底在调用哪个函数

第一行a

var a = fun(0); a.fun(1); a.fun(2); a.fun(3);

第一个fun(0)是在调用第一层fun函数。第二个fun(1)是在调用前一个fun的返回值的fun函数,所以第后面几个fun(1),fun(2),fun(3),都是在调用第二层fun函数

所以在第一次调用fun(0)时,o为undefined

第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1, 0),所以o为0

第三次调用fun(2)时m为2,但依然是调用a.fun,所以还是闭包了第一次调用时的n,所以内部调用第一层的fun(2, 0); 所以还是0

第四次同理

即得到最终答案为undefined, 0, 0, 0

第二行b

var b = fun(0).fun(1).fun(2).fun(3);

先从fun(0)开始看,肯定是调用的第一层fun函数
而它的返回值是一个对象,所以第二个fun(1)调用的是第二层fun函数,后面几个也是调用的第二层fun函数,后面几个也是调用的第二层fun函数。

所以在第一次调用第一层fun(0)时,o为undefined

第二次调用.fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1, 0);所以o为0;

第三次调用.fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun函数时fun(1,0),所以n=1,o=0,返回时闭包了第二次的n

所以在第三次调用第三层fun函数时,m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1

第四次调用.fun(3)时m为3,闭包了第三次调用的n,同理最终调用第一层fun函数为fun(3,2);所以o为2

即最终答案: undefined, 0, 1, 2

第三行c

var c = fun(0).fun(1);  c.fun(2);  c.fun(3);

fun(0)为执行第一层fun函数,.fun(1)执行的是fun(0)返回的第二层fun函数,这里语句结束,这里语句结束。
所以c存放的是fun(1)的返回值,而不是fun(0)的返回值。
所以c中的闭包的也是fun(1)第二次执行的n的值。
c.fun(2)执行的是fun(1)返回的第二层函数。
c.fun(3)执行的也是fun(1)返回的第二层fun函数

所以在第一次调用第一层fun(0)时,o为undefined

第二次调用.fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0

第三次调用.fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1;

第四次调用.fun(3)时同理,但依然是调用的第二次的返回值,所以最终调用第一层fun函数fun(3,1),所以o还为1

即最终是undefined, 0, 1, 1

浏览器输入URL后发生了什么

  1. 输入网址, 浏览器交给浏览器进程开始处理你输入的内容
  2. 如果不是网址,则开始跳到默认搜索引擎,执行搜索
  3. 如果是网站,就执行beforeUnload事件,卸载当前页面
  4. 判断是否存在缓存,是的话读取缓存并渲染,否则像服务器发送请求
  5. 之后把控制权交给网络进程
  6. 网络进程会开始执行DNS解析,获取IP地址,并开始建立连接
    • 首先先去找本地的hosts文件,检查是否有相应的域名、IP对应关系
      • 有,则向其IP地址发送请求
      • 没有,再去找DNS服务器
  7. 请求会发送给对方服务器,然后交给nginx进行处理(如果有负载均衡,会发送到各地对应的服务器进行处理)
  8. 建立TCP连接
    • HTTP连接(三次握手)
      • 客户端发送SYN到服务器
      • 服务器接收到SYN,并生成ACK,发送给客户端
      • 客户端接收到服务器的SYN和ACK,标志连接建立成功
    • HTTPS连接
      • 客户端发送支持的加密协议和版本给服务器
      • 服务器找到适合的加密协议
      • 服务器返回证书和公钥
      • 客户端使用根证书严重证书合法性,生成对称密钥,通过证书的公钥加密,发送给服务器
      • 服务器使用私钥解密,获取对称密钥,使用对称密钥加密数据
      • 客户端解密数据,建立SSL连接
  9. 连接建立完成后,浏览器开始读取服务器返回的数据
  10. 读取HTTP或者HTTPS响应的状态码
    • 是否返回301和302,如果是浏览器将读取HTTP的Location字段,执行重定向,并执行beforeUnload之后的过程
    • 返回200,开始读取Content-Type字段,判断文件的MIMIE类型
      • 并根据MIMIE构建其他请求进行请求渲染
    • 返回404,nginx是否有404页面或者自定义404内容
      • 没有,执行浏览器默认异常
      • 有,nginx返回相关数据,开始渲染
    • 其他异常
  11. 所有数据传输完成,断开连接
    • HTTP
      • 客户端发送FIN=1到服务器
      • 服务器接收到之后,发送ACK到客户端,进行等待关闭状态,此时仍然可以发送接收数据
      • 服务器发送FIN=1,表示数据已经发送完成
      • 客服端收到内容后,断开连接
    • HTTPS
      • 客户端断开连接,发送close_notify
  12. 渲染过程,将交给渲染进程和GPU进程去负责
  13. 解析过程,其实是根据相关语义,通过词法分析和语法分析,开始构建Token,如果不合法,将会在解析过程抛出异常
  14. 解析HTML,构建DOM树
  15. 解析CSS,构建styleSheets(css 规则树)(可以通过document.styleSheets查看浏览器解析好的一个css结构)
  16. 合并HTML、CSS,生成render tree
  17. 根据生成好的render tree,计算每个节点的层级、transform等,并生成layout tree
  18. 浏览器执行完分层后,会开始执行光栅化,并对每个渲染区域进行分块,这里浏览器进行了优化。根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。(渲染的时候,并不是全部都渲染的,由于内容过多,所以浏览器之后渲染可视区域的部分)
    • 重绘
      只改变颜色,不改变位置、宽高
      屏幕的一部分重画,不影响整体布局。
    • 重排
      导致元素宽高、位置发生变化,重排一定会触发重绘
      意味着元件的几何尺寸变了,需要重新验证并计算渲染树
  19. 浏览器通过显卡把需要显示的内容发送给显示器,显示器读取缓冲区的内容,根据逐行扫描的方式进行渲染
  20. 最后浏览器绘制各个节点,将页面展示给用户,渲染完成

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.