balabala

131 阅读56分钟

Knowledge points

浏览器

HTTP 和 HTTPS

  • http 和 https 的基本概念

    • http: 是一个客户端和服务器端请求和应答的标准(TCP),用于从 WWW 服务器传输超文本到本地浏览器的超文本传输协议。
    • https:是以安全为目标的 HTTP 通道,即 HTTP 下 加入 SSL 层进行加密。其作用是:建立一个信息安全通道,来确保数据的传输,确保网站的真实性。
  • http 和 https 的区别及优缺点

    • http 是超文本传输协议,信息是明文传输; HTTPS 协议要比 http 协议安全,https 是具有安全性的 ssl 加密传输协议,可防止数据在传输过程中被窃取、改变,确保数据的完整性(当然这种安全性并非绝对的,对于更深入的 Web 安全问题,此处暂且不表)。
    • http 协议的默认端口为 80,https 的默认端口为 443。
    • http 的连接很简单,是无状态的; https 握手阶段比较费时,会使页面加载时间延长 50%,增加 10%~20%的耗电。
    • https 缓存不如 http 高效,会增加数据开销。
    • Https 协议需要 ca 证书,费用较高,功能越强大的证书费用越高。
    • SSL 证书需要绑定 IP,不能再同一个 IP 上绑定多个域名,IPV4 资源支持不了这种消耗。
  • https 协议的工作原理

    • 客户端使用 https url 访问服务器,则要求 web 服务器建立 ssl 链接。
    • web 服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),传输给客户端。
    • 客户端和 web 服务器端开始协商 SSL 链接的安全等级,也就是加密等级。
    • 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站。
    • web 服务器通过自己的私钥解密出会话密钥。
    • web 服务器通过会话密钥加密与客户端之间的通信。

TCP 三次握手

  • 第一次握手:建立连接时,客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SENT 状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
  • 第二次握手:服务器收到 syn 包并确认客户的 SYN(ack=j+1),同时也发送一个自己的 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
  • 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED(TCP 连接成功)状态,完成三次握手。

TCP 四次挥手

  • 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为 seq=u(等于前面已经传送过来的数据的最后一个字节的序号加 1),此时,客户端进入 FIN-WAIT-1(终止等待 1)状态。 TCP 规定,FIN 报文段即使不携带数据,也要消耗一个序号。
  • 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号 seq=v,此时,服务端就进入了 CLOSE-WAIT(关闭等待)状态。TCP 服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个 CLOSE-WAIT 状态持续的时间。
  • 客户端收到服务器的确认请求后,此时,客户端就进入 FIN-WAIT-2(终止等待 2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最 后的数据)。
  • 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为 seq=w,此时,服务器就进入了 LAST-ACK(最后确认)状态,等待客户端的确认。
  • 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是 seq=u+1,此时,客户端就进入了 TIME-WAIT(时间等待)状态。注意此时 TCP 连接还没有释放,必须经过 2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的 TCB 后,才进入 CLOSED 状态。
  • 服务器只要收到了客户端发出的确认,立即进入 CLOSED 状态。同样,撤销 TCB 后,就结束了这次的 TCP 连接。可以看到,服务器结束 TCP 连接的时间要比客户端早一些。

跨域问题

  • 跨域的原理

    • 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的。

    • 同源策略,是浏览器对 JavaScript 实施的安全限制,只要协议、域名、端口有任何一个不同,都被当作是不同的域。

    • 跨域原理,即是通过各种方式,避开浏览器的安全限制。

  • 跨域解决方案

    • JSONP

      script 标签 src 属性中的链 接却可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是 返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。

      JSONP 的缺点 : JSONP 只支持 get,因为 script 标签只能使用 get 请求; JSONP 需要后端配合返回指定格式的数据。

    • CORS

      CORS(Cross-origin resource sharing)跨域资源共享 服务器设置对 CORS 的支持原理:服务器设置 Access-Control-Allow-Origin HTTP 响应头之后,浏览器将会允许跨域请求

    • proxy 代理

      目前常用方式,通过服务器设置代理;

      使用 http-proxy-middleware 等 node 服务中间件,可以代发请求

    • window.postMessage()

      利用 h5 新特性 window.postMessage()

Cookie、sessionStorage、localStorage 的区别

  • 相同点:

    • 存储在浏览器客户端
  • 不同点:

    • cookie 数据大小不能超过 4k;sessionStorage 和 localStorage 的存储比 cookie 大得多,可以达到 5M+
    • cookie 设置的过期时间之前一直有效;localStorage 永久存储,浏览器关闭后数据不丢失除非主动删除数据;
    • sessionStorage 数据在当前浏览器窗口关闭后自动删除
    • cookie 的数据会自动的传递到服务器;sessionStorage 和 localStorage 数据保存在本地

浏览器的缓存机制

浏览器与服务器通信的方式为应答模式,即是:浏览器发起 HTTP 请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中 HTTP 头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中

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

    强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。

  • 协商缓存

    协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

进程和线程

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程 ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

进程和线程的区别与联系

  • 【区别】:
    • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
    • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
    • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
    • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
  • 【联系】:
    • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
    • 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
    • 处理机分给线程,即真正在处理机上运行的是线程;
    • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

从输入 URL 到页面加载的全过程

  1. 首先在浏览器中输入 URL

  2. 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。

    • 浏览器缓存:浏览器会记录 DNS 一段时间,因此,只是第一个地方解析 DNS 请求;
    • 操作系统缓存:如果在浏览器缓存中不包含这个记录,则会使系统调用操作系统, 获取操作系统的记录(保存最近的 DNS 查询缓存);
    • 路由器缓存:如果上述两个步骤均不能成功获取 DNS 记录,继续搜索路由器缓存;
    • ISP 缓存:若上述均失败,继续向 ISP 搜索。
  3. DNS 域名解析:浏览器向 DNS 服务器发起请求,解析该 URL 中的域名对应的 IP 地址。DNS 服务器是基于 UDP 的,因此会用到 UDP 协议。

  4. 建立 TCP 连接:解析出 IP 地址后,根据 IP 地址和默认 80 端口,和服务器建立 TCP 连接

  5. 发起 HTTP 请求:浏览器发起读取文件的 HTTP 请求,,该请求报文作为 TCP 三次握手的第三次数据发送给服务器

  6. 服务器响应请求并返回结果:服务器对浏览器请求做出响应,并把对应的 html 文件发送给浏览器

  7. 关闭 TCP 连接:通过四次挥手释放 TCP 连接

  8. 浏览器渲染:客户端(浏览器)解析 HTML 内容并渲染出来,浏览器接收到数据包后的解析流程为:

    • 构建 DOM 树:词法分析然后解析成 DOM 树(dom tree),是由 dom 元素及属性节点组成,树的根是 document 对象
    • 构建 CSS 规则树:生成 CSS 规则树(CSS Rule Tree)
    • 构建 render 树:Web 浏览器将 DOM 和 CSSOM 结合,并构建出渲染树(render tree)
    • 布局(Layout):计算出每个节点在屏幕中的位置
    • 绘制(Painting):即遍历 render 树,并使用 UI 后端层绘制每个节点。
  9. JS 引擎解析过程:调用 JS 引擎执行 JS 代码(JS 的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)

    • 创建 window 对象:window 对象也叫全局执行环境,当页面产生时就被创建,所有的全局变量和函数都属于 window 的属性和方法,而 DOM Tree 也会映射在 window 的 doucment 对象上。当关闭网页或者关闭浏览器时,全局执行环境会被销毁。

    • 加载文件:完成 js 引擎分析它的语法与词法是否合法,如果合法进入预编译

    • 预编译:在预编译的过程中,浏览器会寻找全局变量声明,把它作为 window 的属性加入到 window 对象中,并给变量赋值为'undefined';寻找全局函数声明,把它作为 window 的方法加入到 window 对象中,并将函数体赋值给他(匿名函数是不参与预编译的,因为它是变量)。而变量提升作为不合理的地方在 ES6 中已经解决了,函数提升还存在。

    • 解释执行:执行到变量就赋值,如果变量没有被定义,也就没有被预编译直接赋值,在 ES5 非严格模式下这个变量会成为 window 的一个属性,也就是成为全局变量。string、int 这样的值就是直接把值放在变量的存储空间里,object 对象就是把指针指向变量的存储空间。函数执行,就将函数的环境推入一个环境的栈中,执行完成后再弹出,控制权交还给之前的环境。JS 作用域其实就是这样的执行流机制实现的。

浏览器重绘与重排的区别

  • 概念

    • 重排(Reflow):也叫回流,当 DOM 的变化影响了元素的几何信息,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。表现为重新生成布局,重新排列元素。

    • 重绘(Repaint): 当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。表现为某些元素的外观被改变

      重排和重绘代价是高昂的,它们会破坏用户体验,并且让 UI 展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。

  • 如何触发重排和重绘

    任何改变用来构建渲染树的信息都会导致一次重排或重绘:

    • 添加、删除、更新 DOM 节点
    • 通过 display: none 隐藏一个 DOM 节点-触发重排和重绘
    • 通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化
    • 移动或者给页面中的 DOM 节点添加动画
    • 添加一个样式表,调整样式属性
    • 用户行为,例如调整窗口大小,改变字号,或者滚动。
  • 如何避免重绘或重排

    • 集中改变样式,不要一条一条地修改 DOM 的样式。
    • 不要把 DOM 结点的属性值放在循环里当成循环里的变量。
    • 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
    • 不使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。
    • 尽量只修改 position:absolute 或 fixed 元素,对其他元素影响不大
    • 动画开始 GPU 加速,translate 使用 3D 变化

常见浏览器 code 码

  • 1xx 信息,服务器收到请求,需要请求者继续执行操作

    • 100 Continue 继续。客户端应继续其请求
    • 101 Switching Protocols 切换协议。服务器根据客户端的请求(如包含 Upgrade 请求头信息)切换协议。切换协议可能与 WebSockets 一起使用。
  • 2xx 成功,操作被成功接收并处理

    • 200 OK 请求成功。
    • 201 Created 已创建。 成功请求并创建了新的资源
    • 202 Accepted 已接受。 已经接受请求,但未处理完成
    • 204 No Content 无内容。 服务器成功处理,但未返回内容,网页内容不更新
  • 3xx 重定向,需要进一步的操作以完成请求

    • 301 Moved Permanently 永久移动。请求的资源已被永久的移动到新 URI,返回信息会包括新的 URI,浏览器会重定向到新 URI。今后任何新的请求都应使用新的 URI 代替
    • 302 Found 临时移动。与 301 类似。但资源只是临时被移动。客户端应继续使用原有 URI
    • 304 Not Modified 未修改。所请求的资源未修改,服务器不会返回内容,使用浏览器缓存
  • 4xx 客户端错误,请求包含语法错误或无法完成请求

    • 400 Bad Request 客户端请求的语法错误,服务器无法理解
    • 401 Unauthorized 未授权。 请求要求用户的身份认证
    • 403 Forbidden 服务器拒绝执行此请求
    • 404 Not Found 服务器未找到资源(网页)
    • 405 Method Not Allowed 客户端请求中的方法被禁止
  • 5xx 服务器错误,服务器在处理请求的过程中发生了错误

    • 500 Internal Server Error 服务器内部错误,无法完成请求
    • 501 Not Implemented 服务器不支持请求的功能,无法完成请求
    • 502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
    • 503 Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。
    • 504 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求

介绍下 304 过程

  • a. 浏览器请求资源时首先命中资源的 Expires 和 Cache-Control,Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效,可以通过 Cache-control: max-age 指定最大生命周期,状态仍然返回 200,但不会请求数据,在浏览器中能明显看到 from cache 字样。
  • b. 强缓存失效,进入协商缓存阶段,首先验证 ETagETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据客户端上送的 If-None-Match 值来判断是否命中缓存。
  • c. 协商缓存 Last-Modify/If-Modify-Since 阶段,客户端第一次请求资源时,服务服返回的 header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间。再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存。

HTML5 新特性、语义化

  • 概念: HTML5 的语义化指的是合理正确的使用语义化的标签来创建页面结构。

  • 语义化标签: header nav main article section aside footer

  • 语义化的优点:

    • 在没 CSS 样式的情况下,页面整体也会呈现很好的结构效果
    • 代码结构清晰,易于阅读,
    • 利于开发和维护 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
    • 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重

CSS

CSS 选择器及优先级

  • 选择器

    • id 选择器(#myid)
    • 类选择器(.myclass)
    • 属性选择器(a[rel="external"])
    • 伪类选择器(a:hover, li:nth-child)
    • 标签选择器(div, h1,p)
    • 相邻选择器(h1 + p)
    • 子选择器(ul > li)
    • 后代选择器(li a)
    • 通配符选择器(*)
  • 优先级:

    • !important
    • 内联行内样式(1000)
    • ID 选择器(0100)
    • 类选择器/属性选择器/伪类选择器(0010)
    • 元素选择器/伪元素选择器(0001)
    • 关系选择器/通配符选择器(0000)

带!important 标记的样式属性优先级最高; 样式表的来源相同时:!important > 行内样式 > ID 选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性

position 属性的值有哪些

  • 固定定位 fixed: 元素的位置相对于浏览器窗口是固定位置,即使窗口是滚动的它也不会移动。Fixed 定 位使元素的位置与文档流无关,因此不占据空间。 Fixed 定位的元素和其他元素重叠。

  • 相对定位 relative: 如果对一个元素进行相对定位,它将出现在它所在的位置上。然后,可以通过设置垂直 或水平位置,让这个元素“相对于”它的起点进行移动。 在使用相对定位时,无论是 否进行移动,元素仍然占据原来的空间。因此,移动元素会导致它覆盖其它框。

  • 绝对定位 absolute: 绝对定位的元素的位置相对于最近的已定位父元素,如果元素没有已定位的父元素,那 么它的位置相对于。absolute 定位使元素的位置与文档流无关,因此不占据空间。 absolute 定位的元素和其他元素重叠。

  • 粘性定位 sticky: 元素先按照普通文档流定位,然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。而后,元素定位表现为在跨越特定阈值前为相对定 位,之后为固定定位。

  • 默认定位 Static: 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声 明)。 inherit: 规定应该从父元素继承 position 属性的值。

页面布局

  1. Flex 布局

    布局的传统解决方案,基于盒状模型,依赖 display 属性 + position 属性 + float 属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。
    
    Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。指定容器 display: flex 即可。 简单的分为容器属性和元素属性。
    
    • 容器的属性:

      flex-direction:决定主轴的方向(即子 item 的排列方法)flex-direction: row | row-reverse | column | column-reverse; flex-wrap:决定换行规则 flex-wrap: nowrap | wrap | wrap-reverse; flex-flow: .box { flex-flow: || ; } justify-content:对其方式,水平主轴对齐方式 align-items:对齐方式,竖直轴线方向 align-content

    • 项目的属性(元素的属性):

      order 属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为 0 flex-grow 属性:定义项目的放大比例,即使存在空间,也不会放大 flex-shrink 属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小,如果 定义个 item 的 flow-shrink 为 0,则为不缩小 flex-basis 属性:定义了在分配多余的空间,项目占据的空间。 flex:是 flex-grow 和 flex-shrink、flex-basis 的简写,默认值为 0 1 auto。 align-self:允许单个项目与其他项目不一样的对齐方式,可以覆盖 align-items,默认属 性为 auto,表示继承父元素的 align-items 比如说,用 flex 实现圣杯布局

  2. Rem 布局

    首先 Rem 相对于根(html)的 font-size 大小来计算。简单的说它就是一个相对单例 如:font-size:10px;,那么(1rem = 10px)了解计算原理后首先解决怎么在不同设备上设置 htmlfont-size 大小。其实 rem 布局的本质是等比缩放,一般是基于宽度。
    
    • 优点:可以快速适用移动端布局,字体,图片高度

    • 缺点:

      ① 目前 ie 不支持,对 pc 页面来讲使用次数不多;

      ② 数据量大:所有的图片,盒子都需要我们去给一个准确的值;才能保证不同机型的适配;

      ③ 在响应式布局中,必须通过 js 来动态控制根元素 font-size 的大小。也就是说 css 样式和 js 代码有一定的耦合性。且必须将改变 font-size 的代码放在 css 样式之前。

  3. 百分比布局

    通过百分比单位 " % " 来实现响应式的效果。通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。 直观的理解,我们可能会认为子元素的百分比完全相对于直接父元素,height 百分比相 对于 heightwidth 百分比相对于 widthpaddingbordermargin 等等不论是垂直方向还是水平方向,都相对于直接父元素的 width。 除了 border-radius 外,还有比如 translate、background-size 等都是相对于自身的。
    

    缺点:

    • 计算困难
    • 各个属性中如果使用百分比,相对父元素的属性并不是唯一的。造成我们使用百分比单位容易使布局问题变得复杂。
  4. 浮动布局

        浮动布局:当元素浮动以后可以向左或向右移动,直到它的外边缘碰到包含它的框或者另外一个浮动元素的边框为止。元素浮动以后会脱离正常的文档流,所以文档的普通流中的框就变的好像浮动元素不存在一样。
    
    • 优点

      这样做的优点就是在图文混排的时候可以很好的使文字环绕在图片周围。另外当元素浮动了起来之后,它有着块级元素的一些性质例如可以设置宽高等,但它与 inline-block 还是有一些区别的,第一个就是关于横向排序的时候,float 可以设置方向而 inline-block 方向是固定的;还有一个就是 inline-block 在使用时有时会有空白间隙的问题

    • 缺点

      最明显的缺点就是浮动元素一旦脱离了文档流,就无法撑起父元素,会造成父级元素高度塌陷。 如何使用 rem 或 viewport 进行移动端适配

隐藏页面中某个元素的方法

  • opacity:0

    该元素隐藏起来了,但不会改变页面布局,并且,如果该元素已经绑定 一些事件,如 click 事件,那么点击该区域,也能触发点击事件的

  • visibility:hidden

    该元素隐藏起来了,但不会改变页面布局,但是不会触发该元素已 经绑定的事件 ,隐藏对应元素,在文档布局中仍保留原来的空间(重绘)

  • display:none,

    把元素隐藏起来,并且会改变页面布局,可以理解成在页面中把该元素。 不显示对应的元素,在文档布局中不再分配空间(回流+重绘)

CSS 盒子模型

CSS 盒模型本质上是一个盒子,它包括:边距,边框,填充和实际内容。CSS 中的盒子模型包括 IE 盒子模型和标准的 W3C 盒子模型。

在标准的盒子模型中,widthcontent 部分的宽度。

在 IE 盒子模型中,width 表示 content+padding+border 这三个部分的宽度。

故在计算盒子的宽度时存在差异:
- 标准盒模型: 一个块的总宽度 = width+margin(左右)+padding(左右)+border(左右)
- 怪异盒模型: 一个块的总宽度 = width+margin(左右)(既 width 已经包含了 paddingborder 值)

前端工程化

  • 背景

    在企业级的前端项目开发中,复杂的 Web 应用,需要众多人共同开发维护, 工程复杂了就会产生许多问题,如何进行高效的多人协作?如何保证项目的可维护性?如何提高项目的开发质量?... 落实到细节上,就是前端的“4 个现代化”:模块化、组件化、规范化、自动化

  • 概念

    • 模块化(js 的模块化、css 的模块化、其它资源的模块化)
    • 组件化(复用现有的 UI 结构、样式、行为)
    • 规范化(目录结构制定、编码规范、接口规范、文档规范、 Git 分支管理、Commit 描述、Code Review...)
    • 自动化(自动化构建、自动化测试、自动部署)

水平居中和垂直居中

  • 水平居中

    • 对于 行内元素 : text-align: center;

    • 对于确定宽度的块级元素:

      • width 和 margin 实现。margin: 0 auto;
      • 绝对定位和 margin-left: (父 width - 子 width)/2, 前提是父元素 position: relative
    • 对于宽度未知的块级元素

      • table 标签配合 margin 左右 auto 实现水平居中。使用 table 标签(或直接将块级元素设值为 display:table),再通过给该标签添加左右 margin 为 auto。
      • inline-block 实现水平居中方法。display:inline-block 和 text-align:center 实现水平居中。
      • 绝对定位+transform,translateX 可以移动本身元素的 50%。
      • flex 布局使用 justify-content:center
  • 垂直居中

    • 利用 line-height 实现居中,这种方法适合纯文字类

    • 通过设置父容器 相对定位 ,子级设置 绝对定位,标签通过 margin 实现自适应居中

    • 弹性布局 flex :父级设置 display: flex; 子级设置 margin 为 auto 实现自适应居中

    • 父级设置相对定位,子级设置绝对定位,并且通过位移 transform 实现

    • table 布局,父级通过转换成表格形式,然后子级设置 vertical-align 实现。(需要注意的是:vertical-align: middle 使用的前提条件是内联元素以及 display 值为 table-cell 的元素)。

BFC(块级格式上下文)

  • BFC 的概念 BFC 是 Block Formatting Context 的缩写,即块级格式化上下文。BFC 是 CSS 布局的一个概念,是一个独立的渲染区域,规定了内部 box 如何布局, 并且这个区域的子元素不会影响到外面的元素,其中比较重要的布局规则有内部 box 垂直放置,计算 BFC 的高度的时候,浮动元素也参与计算。

  • BFC 的原理布局规则 内部的 Box 会在垂直方向,一个接一个地放置 Box 垂直方向的距离由 margin 决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠 每个元素的 margin box 的左边, 与包含块 border box 的左边相接触(对于从左往右的格式化,否则相反 BFC 的区域不会与 float box 重叠 BFC 是一个独立容器,容器里面的子元素不会影响到外面的元素 计算 BFC 的高度时,浮动元素也参与计算高度 元素的类型和 display 属性,决定了这个 Box 的类型。不同类型的 Box 会参与不同的 Formatting Context。

  • 如何创建 BFC? 根元素,即 HTML 元素 float 的值不为 none position 为 absolute 或 fixed display 的值为 inline-block、table-cell、table-caption overflow 的值不为 visible

  • BFC 的使用场景 去除边距重叠现象 清除浮动(让父元素的高度包含子浮动元素) 避免某元素被浮动元素覆盖 避免多列布局由于宽度计算四舍五入而自动换行

rem 和 em

清除浮动的方式

当父容器没有设置高度,里面的盒子没有浮动的情况下会将父容器的高度撑开。
一旦父容器中的盒子设置浮动,脱离标准文档流,父容器立马没有高度,下面的盒子会跑到浮动的盒子下面。
出现这种情况,我们需要清除浮动。清除浮动不是不浮动,是清除浮动产生的不利影响
  1. 添加额外标签

    <div class="parent">
      //添加额外标签并且添加clear属性
      <div style="clear:both"></div>
      //也可以加一个br标签
    </div>
    
  2. 父级添加 overflow 属性,或者设置高度

  3. 建立伪类选择器清除浮动

    //在css中添加:after伪元素
    .parent:after {
      /* 设置添加子元素的内容是空 */
      content: "";
      /* 设置添加子元素为块级元素 */
      display: block;
      /* 设置添加的子元素的高度0 */
      height: 0;
      /* 设置添加子元素看不见 */
      visibility: hidden;
      /* 设置clear:both */
      clear: both;
    }
    

Javascript

JS 中的 8 种数据类型及区别

  • 数据类型

    包括值类型(基本对象类型)和引用类型(复杂对象类型)

    • 基本类型(值类型):

      • Number(数字)
      • String(字符串)
      • Boolean(布尔)
      • Symbol(符号)
      • null(空)
      • undefined(未定义)在内存中占据固定大小,保存在栈内存中
    • 引用类型(复杂数据类型):

      • Object(对象)
      • Function(函数)
      • Array(数组)
      • Date(日期)
      • RegExp(正则表达式)
  • 数据类型检测方案

    • typeof

      console.log(typeof 1); // number
      console.log(typeof true); // boolean
      console.log(typeof "mc"); // string
      console.log(typeof Symbol); // function
      console.log(typeof function () {}); // function
      console.log(typeof console.log()); // function
      console.log(typeof []); // object
      console.log(typeof {}); // object
      console.log(typeof null); // object
      console.log(typeof undefined); // undefined
      
      • 优点:能够快速区分基本数据类型

      • 缺点:不能将 Object、Array 和 Null 区分,都返回 object

    • instanceof

      console.log(1 instanceof Number); // false
      console.log(true instanceof Boolean); // false
      console.log("str" instanceof String); // false
      console.log([] instanceof Array); // true
      console.log(function () {} instanceof Function); // true
      console.log({} instanceof Object); // true
      
      • 优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象

      • 缺点:Number,Boolean,String 基本数据类型不能判断

    • Object.prototype.toString.call()

      var toString = Object.prototype.toString;
      console.log(toString.call(1)); //[object Number]
      console.log(toString.call(true)); //[object Boolean]
      console.log(toString.call("mc")); //[object String]
      console.log(toString.call([])); //[object Array]
      console.log(toString.call({})); //[object Object]
      console.log(toString.call(function () {})); //[object Function]
      console.log(toString.call(undefined)); //[object Undefined]
      console.log(toString.call(null)); //[object Null]
      
      • 优点:精准判断数据类型

      • 缺点:写法繁琐不容易记,推荐进行封装后使用

数组方法

  • 返回新数组,不改变原数组

    • join

      把数组中所有元素放入一个字符串中,返回字符串。

      const arr = ["pr", "is", 18];
      
      console.log(arr.join(" ")); // pr is 18
      console.log("=>");
      console.log(arr); // [ 'pr', 'is', 18 ]
      
    • concat

      连接多个(含两个)数组,两边的原始数组都不会变化,返回被连接数组的一个副本,可继续 concat。

      const arr = [1, 2, 3, 4];
      const arr1 = ["pr", "is", "a", "boy"];
      const arr2 = [5, 6, 7];
      
      console.log(arr.concat(arr1, arr2).concat(8, 9)); // [1, 2, 3, 4, 'pr', 'is', 'a', 'boy', 5, 6, 7, 8, 9 ]
      console.log("=>");
      console.log(arr); // [ 1, 2, 3, 4 ]
      
    • slice

      从开始到结束([)左闭右开,即不包括结束)选择数组的一部分浅拷贝到一个新数组。

      const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
      
      console.log(arr.slice(1, 5)); // [ 1, 2, 3, 4 ]
      console.log("=>");
      console.log(arr); // [ 0,1,2,3,4,5,6,7,8,9 ]
      

      slice(1, 5) 可见里面最多含 4(5 - 1) 个元素,而且从第 1 位开始取。

    • map

      创建一个新数组并返回,新数组的每个元素由原数组中的每一个元素执行提供的函数而来,其中原始数组不会发生改变。

      const arr = [1, 2, 3, 4];
      
      console.log(arr.map(i => i \* 10 - 5)); // [ 5, 15, 25, 35 ]
      console.log('=>');
      console.log(arr); // [ 1, 2, 3, 4 ]
      
    • every

      检测数组所有元素是否都符合指定条件.

      如果数组中检测到有一个元素不满足,则整个表达式返回 false,且剩余的元素不会再进行检测; 如果所有元素都满足条件,则返回 true;

      const arr = [1, 2, 3, 4];
      
      console.log(arr.every((i) => i > 2)); // false
      console.log(arr.every((i) => i > 0)); // true
      console.log([].every((i) => i === "pr")); // true
      console.log("=>");
      console.log(arr); // [ 1, 2, 3, 4 ]
      
    • some

      用于检测数组中的元素是否满足指定条件。

      如果有一个元素满足条件,则表达式返回 true, 剩余的元素不会再执行检测; 如果没有满足条件的元素,则返回 false;

      const arr = [1, 2, 3, 4];
      
      console.log(arr.some((i) => i > 4)); // false
      console.log(arr.some((i) => i > 0)); // true
      console.log([].some((i) => i === "pr")); // false
      console.log("=>");
      console.log(arr); // [ 1, 2, 3, 4 ]
      
    • filter

      创建一个新的数组,新数组中的元素是通过检查符合条件的所有元素。

      const arr = [1, 2, 3, 4];
      
      console.log(arr.filter((i) => i > 2)); // [3, 4]
      console.log([].filter((i) => i === "pr")); // []
      console.log("=>");
      console.log(arr); // [ 1, 2, 3, 4 ]
      
    • forEach

      用于调用数组的每个元素,并将元素传递给回调函数,返回 undefiend。

      const arr = [1, 2, 3, 4];
      const copy = [];
      
      console.log(arr.forEach(i => {
          copy.push(i \* 2);
      }));
      console.log(copy); // [ 2, 4, 6, 8 ]
      console.log('=>');
      console.log(arr); // [ 1, 2, 3, 4 ]
      
    • reduce

      接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。若是空数组是不会执行回调函数的;

      const arr = [1, 2, 3, 4];
      
      console.log(arr.reduce((prev, cur) => prev + cur, 0)); // 10
      console.log("=>");
      console.log(arr); // [ 1, 2, 3, 4 ]
      
  • 返回新数组,改变原数组

    • pop

      删除数组的最后一个元素,并返回这个元素(即被删除的元素)。 如果数组为空,则不改变数组,返 undefined;

      const arr = [1, 2, 3, 4];
      const arr1 = [];
      
      console.log(arr.pop()); // 4
      console.log(arr1.pop()); // undefined
      console.log("=>");
      console.log(arr); // [ 1, 2, 3 ]
      console.log(arr1); // []
      
    • push

      将一个或多个元素添加到数组的末尾,返回值是改变后的数组的长度。

      const arr = [1, 2, 3, 4];
      
      console.log(arr.push(5)); // 5
      console.log(arr.push([1, 2])); // 6
      console.log("=>");
      console.log(arr); // [ 1, 2, 3, 4, 5, [ 1, 2 ] ]
      
    • shift

      删除数组的第一个元素,并返回这个元素。

      const arr = [1, 2, 3, 4];
      
      console.log(arr.shift()); // 1
      console.log("=>");
      console.log(arr); // [ 2, 3, 4 ]
      
    • unshift

      将一个或多个元素添加到数组的开头,返回值是改变后的数组的长度。

      const arr = [1, 2, 3, 4];
      
      console.log(arr.unshift(5, 6)); // 6
      console.log(arr.unshift([1, 2])); // 7
      console.log("=>");
      console.log(arr); // [ [ 1, 2 ], 5, 6, 1, 2, 3, 4 ]
      
    • reverse

      颠倒数组中元素的位置,返回该数组的引用。

      const arr = [1, 2, 3, 4];
      const hello = "hello";
      const helloArray = hello.split("");
      
      console.log(helloArray.reverse().join("")); // olleh
      console.log(arr.reverse()); // [ 4, 3, 2, 1 ]
      console.log("=>");
      console.log(arr); // [ 4, 3, 2, 1 ]
      console.log(helloArray); // [ 'o', 'l', 'l', 'e', 'h' ]
      
    • sort

      对数组的元素进行排序,并返回数组。排序不一定是稳定的。默认排序顺序是根据字符串 Unicode 码点。

      const arr = [1, 2, 3, 4, 10, 12, 22];
      
      console.log(arr.sort()); // [ 1, 10, 12, 2, 22, 3, 4];
      console.log("=>");
      console.log(arr); // [ 1, 10, 12, 2, 22, 3, 4];
      
    • splice

      向数组中添加/删除项目,然后返回被删除项目。

      const arr = [1, 2, 3, 4];
      
      console.log(arr.splice(1, 2, 10, 12)); // [ 2, 3 ]
      console.log("=>");
      console.log(arr); // [ 1, 10, 12, 4 ]
      

数组去重

  • 冒泡 splice 方法实现

    var arr = [1, "a", "a", "b", "d", "e", "e", 1, 0];
    function test() {
      for (var i = 0; i < arr.length; i++) {
        for (var j = i + 1; j < arr.length; j++) {
          if (arr[i] === arr[j]) arr.splice(j, 1); //如果前一个值与后一个值相等,那么就去掉后一个值,splice()可以修改原数组
        }
      }
      return arr;
    }
    test();
    
  • filter 方法实现

    const arr = [1, 2, 11, 22, 11, 1, 11, 22, 1, 2];
    const unique = (arr) =>
      arr.filter((element, index, self) => {
        // self.indexOf(element) 每次都从数组第一个元素开始往下查
        return self.indexOf(element) === index;
      });
    console.log(unique(arr)); // [1, 2, 11, 22]
    
  • reducer 方法实现

    const unique = (arr) =>
      arr.sort().reduce((cache, current) => {
        if (cache.length === 0 || cache[cache.length - 1] !== current) {
          cache.push(current);
        }
        return cache;
      }, []);
    
    const arr = [1, 2, 11, 22, 11, 1, 11, 22, 1, 2];
    console.log(unique(arr)); //[1, 2, 11, 22]
    
  • ES6 Array.from(new Set())

    Array.from 返回一个新的数组实例。

    var arr = [1, 1, 4, 50, 50, 6, 2, 2];
    function unique(arr) {
      return Array.from(new Set(arr));
    }
    unique(arr); //[1, 4, 50, 6, 2]
    

数组排序

  • sort

    var arr = [1, 9, 4, 50, 49, 6, 3, 2];
    function test() {
      return arr.sort(sortNumber);
    }
    function sortNumber(a, b) {
      return a - b;
    }
    test();
    
  • 冒泡排序

    var arr = [1, 9, 4, 50, 49, 6, 3, 2];
    function test() {
      for (var i = 0; i < arr.length - 1; i++) {
        for (var j = i + 1; j < arr.length; j++) {
          var tempi = arr[i]; //获取第一个值,并与后一个值比较
          var tempj = arr[j];
          if (tempi > tempj) {
            arr[i] = tempj;
            arr[j] = tempi; //如果前一个值比后一个值大,那么相互交换
          }
        }
      }
      console.log(arr); //return arr;
    }
    test(); //调用函数
    
  • 快速排序

    var arr = [1, 9, 4, 50, 49, 6, 3, 2];
    function test(arr) {
      if (arr.length <= 1) return arr; //如果数组只有一位,就没有必要比较了
      var index = Math.floor(arr.length / 2); //获取中间值的索引
      var cur = arr.splice(index, 1); //截取中间值
      var left = [],
        right = []; //小于中间值的放在left数组里,大于的放在right数组
      for (var i = 0; i < arr.length; i++) {
        if (cur > arr[i]) {
          left.push(arr[i]);
        } else {
          right.push(arr[i]);
        }
      }
      return test(left).concat(cur, test(right)); //通过递归,上一轮比较好的数组合并,并且再次进行比较
    }
    test(arr);
    

var 、 let 、 const

ES6 之前创建变量用的是 var,之后创建变量用的是 letconst
  • 三者区别:

    1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。 let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。 const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改。

    2. var 可以先使用,后声明,因为存在变量提升;let 必须先声明后使用。

    3. var 是允许在相同作用域内重复声明同一个变量的,而 let 与 const 不允许这一现象。

    4. 在全局上下文中,基于 let 声明的全局变量和全局对象 window 没有任何关系

    5. 在代码块内,使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”:

      • 检测一个未被 var 声明的变量类型时,不会报错,会返回 undefined
      • 在 let、const 未声明之前使用,会返回 ReferenceError
    6. let /const/function 会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于 let 的块作用特征即可解决

JS 垃圾回收机制

  • 项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。

  • 浏览器垃圾回收机制/内存回收机制:

      浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
    
    • 标记清除:在 js 中,最常用的垃圾回收机制是标记清除:当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。垃圾回收器会销毁那些带标记的值并回收它们所占用的内存空间。

    • 谷歌浏览器:“查找引用”,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。

    • IE 浏览器:“引用计数法”,当前内存被占用一次,计数累加 1 次,移除占用就减 1,减到 0 时,浏览器就回收它。

  • 优化手段:

      内存优化 ; 手动释放:取消内存的占用即可。
    
      (1)堆内存:fn = null 【null:空指针对象】
    
      (2)栈内存:把上下文中,被外部占用的堆的占用取消即可。
    
  • 内存泄漏

      在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器
    

作用域和作用域链

  • 定义:

    简单来说作用域就是变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成

    • 全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。
    • 函数作用域:在固定的代码片段才能被访问
  • 作用:

    作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

  • 作用域链参考链接

    一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

闭包

  • 闭包的概念

        函数执行时形成的私有上下文 EC(FN),正常情况下,代码执行完会出栈后释放;但是特殊情况下,如果当前私有上下文中的某个东西被上下文以外的事物占用了,则上下文不会出栈释放,从而形成不销毁的上下文。 函数执行函数执行过程中,会形成一个全新的私有上下文,可能会被释放,可能不会被释放,不论释放与否,他的作用是:
    
    1. 保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);

    2. 保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;

    我们把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包。

    闭包是指有权访问另一个函数作用域中的变量的函数--《JavaScript 高级程序设计》

  • 闭包的特性:

    • 内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。

      • 闭包是密闭的容器,,类似于 set、map 容器,存储数据的

      • 闭包是一个对象,存放数据的格式为 key-value 形式

    • 函数嵌套函数

    • 本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除

  • 闭包形成的条件:

    • 函数的嵌套

    • 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期

  • 闭包的用途:

    • 模仿块级作用域

    • 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)

    • 封装私有化变量

    • 创建模块

  • 闭包应用场景

      闭包的两个场景,闭包的两大作用:保存/保护。 在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送 ajax 请求成功|失败的回调;setTimeout 的延时回调;或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。
    
  • 闭包的优点:

      延长局部变量的生命周期
    
  • 闭包缺点:

      会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏
    

JS 中 this 的五种情况

  • 作为普通函数执行时,this 指向 window。

  • 当函数作为对象的方法被调用时,this 就会指向该对象。

  • 构造器调用,this 指向返回的这个对象。

  • 箭头函数 箭头函数的 this 绑定看的是 this 所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则 this 绑定到最近的一层对象上。

  • 基于 Function.prototype 上的 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply 接收参数的是数组,call 接受参数列表,``bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用new`时会被改变,其他情况下都不会改变。若为空默认是指向全局对象 window。

原型 && 原型链

  • 原型关系:

    • 每个 class 都有显示原型 prototype
    • 每个实例都有隐式原型 _ proto_
    • 实例的proto指向对应 class 的 prototype
  • 原型:

      在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个 prototype  属性,这个属性指向函数的原型对象。
    
  • 原型链:

      函数的原型链对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针**proto**,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用**proto**一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.** proto** = null 表示原型链顶端。如此形成了 js 的原型链继承。同时所有的 js 对象都有 Object 的基本防范
    
  • 特点:

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

new 运算符的实现机制

  • 首先创建了一个新的空对象
  • 设置原型,将对象的原型设置为函数的 prototype 对象。
  • 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  • 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

EventLoop 事件循环

    JS 是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,Promise.thenMutationObserver,宏任务的话就是 setImmediate setTimeout setInterval

    JS 运行的环境。一般为浏览器或者 Node。 在浏览器环境中,有 JS 引擎线程和渲染线程,且两个线程互斥。 Node 环境中,只有 JS 线程。 不同环境执行机制有差异,不同任务进入不同 Event Queue 队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。

事件循环可以简单的描述为以下四个步骤

  1. 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs,接着执行同步任务,直到 Stack 为空;

  2. 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)

  3. 执行栈为空时,Event Loop 把微任务队列执行清空;

  4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复 4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务

浏览器中的任务源(task)

  • 宏任务(macrotask):

    宿主环境提供的,比如浏览器  ajax、setTimeoutsetIntervalsetTmmediate(只兼容 ie)、script、requestAnimationFrame、messageChannel、UI 渲染、一些浏览器 api
    
  • 微任务(microtask):

    语言本身提供的,比如 promise.thenqueueMicrotask(基于 then)、mutationObserver(浏览器提供)、messageChannel 、mutationObersve
    

setTimeout、Promise、Async/Await 的区别

  • setTimeout

      settimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行。
    
  • Promise

        Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行。
    
    console.log("script start");
    let promise1 = new Promise(function (resolve) {
      console.log("promise1");
      resolve();
      console.log("promise1 end");
    }).then(function () {
      console.log("promise2");
    });
    setTimeout(function () {
      console.log("settimeout");
    });
    console.log("script end");
    // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
    
  • async/await

      async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
    
    async function async1() {
      console.log("async1 start");
      await async2();
      console.log("async1 end");
    }
    async function async2() {
      console.log("async2");
    }
    
    console.log("script start");
    async1();
    console.log("script end");
    
    // 输出顺序:script start->async1 start->async2->script end->async1 end
    

异步

实现一个 call/apply

实现一个 bind

手写一个 Promise

正常的 promise

var promise = new Promise((resolve, reject) => {
  if (操作成功) {
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then(
  function (value) {
    // success
  },
  function (value) {
    // failure
  }
);

来实现一个简易版本

function myPromise(constructor) {
  let self = this;
  self.status = "pending"; //定义状态改变前的初始状态
  self.value = undefined; //定义状态为resolved的时候的状态
  self.reason = undefined; //定义状态为rejected的时候的状态
  function resolve(value) {
    //两个==="pending",保证了状态的改变是不可逆的
    if (self.status === "pending") {
      self.value = value;
      self.status = "resolved";
    }
  }
  function reject(reason) {
    //两个==="pending",保证了状态的改变是不可逆的
    if (self.status === "pending") {
      self.reason = reason;
      self.status = "rejected";
    }
  }
  //捕获构造异常
  try {
    constructor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}
// 在myPromise的原型上定义链式调用的then方法
myPromise.prototype.then = function (onFullfilled, onRejected) {
  let self = this;
  switch (self.status) {
    case "resolved":
      onFullfilled(self.value);
      break;
    case "rejected":
      onRejected(self.reason);
      break;
    default:
  }
};

写一个 JS 深拷贝

var newObj = JSON.parse(JSON.stringify(someObj));
function deepCopy(obj) {
  //判断是否是简单数据类型,
  if (typeof obj == "object") {
    //复杂数据类型
    var result = obj.constructor == Array ? [] : {};
    for (let i in obj) {
      result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
    }
  } else {
    //简单数据类型 直接 == 赋值
    var result = obj;
  }
  return result;
}

实现一个 JSON.stringify

ES6

  • 解构赋值

    从对象 obj 中取值

    const obj = {
      a:1,
      b:2,
      c:3,
      d:4,
      e:5,
    }
    

    解构赋值

    const { a, b, c, d, e } = obj;
    const f = a + d;
    const g = c + e;
    

    补充: ES6 的解构赋值虽然好用。但是要注意解构的对象不能为 undefined、null。否则会报错,故要给被解构的对象一个默认值。

    const { a, b, c, d, e } = obj || {};
    
  • 扩展运算符 ...

    传统使用 concat()合并两个数组,使用 assign()合并两个对象。

    const a = [1, 2, 3];
    const b = [1, 5, 6];
    const c = a.concat(b); //合并数组 [1,2,3,1,5,6]
    
    const obj1 = {
      a: 1,
    };
    const obj2 = {
      b: 1,
    };
    const obj = Object.assign({}, obj1, obj2); //合并对象 {a:1,b:1}
    

    使用扩展运算符

    const a = [1, 2, 3];
    const b = [1, 5, 6];
    const c = [...new Set([...a, ...b])]; // [1,2,3,5,6]
    
    const obj1 = {
      a: 1,
    };
    const obj2 = {
      b: 1,
    };
    const obj = { ...obj1, ...obj2 }; // {a:1,b:1}
    
  • 字符串模板

    const name = "小明";
    const score = 59;
    const result = `${name}${score > 60 ? "的考试成绩及格" : "的考试成绩不及格"}`;
    
  • includes

    返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。

    const arr1 = ["a", "b", "c", "d", NaN];
    
    console.log(arr1.includes("c")); // true
    console.log(arr1.includes("z")); // false
    console.log(arr1.includes(NaN)); // true
    

    indexOf()无法判断 NAN

    const condition = [1, 2, 3, 4];
    
    if (condition.includes(type)) {
      //...
    }
    
  • find()

    filter 过滤搜索会遍历所有

    const a = [1, 2, 3, 4, 5];
    const result = a.find((item) => {
      return item === 3;
    });
    

    find 方法中找到符合条件的项,就不会继续遍历数组。

    const a = [1, 2, 3, 4, 5];
    const result = a.find((item) => {
      return item === 3;
    });
    
  • Object.values flat()

    const deps = {
      采购部: [1, 2, 3],
      人事部: [5, 8, 12],
      行政部: [5, 14, 79],
      运输部: [3, 64, 105],
    };
    let member = Object.values(deps).flat(Infinity);
    

    其中使用 Infinity 作为 flat 的参数,使得无需知道被扁平化的数组的维度

  • 可选链操作符 ?.

    const name = obj && obj.name; // 传统方式
    
    const name = obj?.name;
    
  • 空值合并运算符 ??

    if (value !== null && value !== undefined && value !== "") {
      // 传统方式
    }
    
    if ((value ?? "") !== "") {
      //...
    }
    
  • async / await

    回调地狱式

    const fn1 = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(1);
        }, 300);
      });
    };
    const fn2 = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(2);
        }, 600);
      });
    };
    const fn = () => {
      fn1().then((res1) => {
        console.log(res1); // 1
        fn2().then((res2) => {
          console.log(res2);
        });
      });
    };
    
    const fn = async () => {
      const res1 = await fn1();
      const res2 = await fn2();
      console.log(res1); // 1
      console.log(res2); // 2
    };
    

    要做并发请求时,还是要用到 Promise.all()。

    const fn = () => {
      Promise.all([fn1(), fn2()]).then((res) => {
        console.log(res); // [1,2]
      });
    };
    

MVVM

  • 什么是 MVVM?

    视图模型双向绑定,是 Model-View-ViewModel 的缩写,也就是把 MVC 中的 Controller 演变成 ViewModel。Model 层代表数据模型,View 代表 UI 组件,ViewModel 是 View 和 Model 层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中,视图变化的时候会通知 viewModel 层更新数据。以前是操作 DOM 结构更新视图,现在是数据驱动视图。
    
  • MVVM 的优点:

    1. 低耦合。视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变化,当 Model 变化的时候 View 也可以不变;

    2. 可重用性。你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑。

    3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。

    4. 可测试。

VUE

VUE2.x

  • v-show 与 v-if 区别

    v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

    v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换

    所以, v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

  • Class 与 Style 动态绑定

    • Class 可以通过对象语法和数组语法进行动态绑定

      // 对象语法
      <div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
      
      data: {
        isActive: true,
        hasError: false
      }
      
      // 数组语法
      <div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
      
      data: {
        activeClass: 'active',
        errorClass: 'text-danger'
      }
      
      
    • Style 也可以通过对象语法和数组语法进行动态绑定

      // 对象语法
      <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
      
      data: {
        activeColor: 'red',
        fontSize: 30
      }
      
      
      // 数组语法
      <div v-bind:style="[styleColor, styleSize]"></div>
      
      data: {
        styleColor: {
          color: 'red'
        },
        styleSize:{
          fontSize:'23px'
        }
      }
      
  • Vue 的单向数据流

      所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值,这意味着你不应该在一个子组件内部改变 prop
    
      子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
    
    • 有两种常见的试图改变一个 prop 的情形 :

      1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:

        props: ['initialCounter'],
        data: function () {
          return {
            counter: this.initialCounter
          }
        }
        
      2. 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性

        props: ['size'],
        computed: {
         normalizedSize: function () {
           return this.size.trim().toLowerCase()
         }
        }
        
  • computed 和 watch

    • 区别

      computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

      watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

    • 运用场景:

      当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

      当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用  watch  选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

  • 数组项赋值变动监测

    由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:

    1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

    2. 当你修改数组的长度时,例如:vm.items.length = newLength

      为了解决第一个问题,Vue 提供了以下操作方法:

      // Vue.set
      Vue.set(vm.items, indexOfItem, newValue);
      // vm.$set,Vue.set的一个别名
      vm.$set(vm.items, indexOfItem, newValue);
      // Array.prototype.splice
      vm.items.splice(indexOfItem, 1, newValue);
      

      为了解决第二个问题,Vue 提供了以下操作方法:

      // Array.prototype.splice
      vm.items.splice(newLength);
      
  • Vue 生命周期

    生命周期描述
    beforeCreate组件实例被创建之初,组件的属性生效之前
    created组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
    beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用
    mountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
    beforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
    update组件数据更新之后
    activitedkeep-alive 专属,组件被激活时调用
    deactivatedkeep-alive 专属,组件被销毁时调用
    beforeDestory组件销毁前调用
    destoryed组件销毁后调用

    生命周期使用常识:

    1. ajax 请求最好放在 created 里面,因为此时已经可以访问 this 了,请求到数据就可以直接放在 data 里面。

    2. 关于 dom 的操作要放在 mounted 里面,在 mounted 前面访问 dom 会是 undefined。

    3. 每次进入/离开组件都要做一些事情,用什么钩子:

      不缓存组件:

      进入的时候可以用 created 和 mounted 钩子,离开的时候用 beforeDestory 和 destroyed 钩子,beforeDestory 可以访问 this,destroyed 不可以访问 this

      缓存了组件:

      缓存了组件之后,再次进入组件不会触发 beforeCreate、created 、beforeMount、 mounted,如果你想每次进入组件都做一些事情的话,你可以放在 activated 进入缓存组件的钩子中。
      

      同理:离开缓存组件的时候,beforeDestroy 和 destroyed 并不会触发,可以使用 deactivated 离开缓存组件的钩子来代替。

  • 路由导航守卫

    有的时候,我们需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。
    为此我们有很多种方法可以植入路由的导航过程:全局的, 单个路由独享的, 或者组件级的

    • 全局守卫

      vue-router 全局有三个守卫:
      
      1. router.beforeEach 全局前置守卫 进入路由之前
      2. router.beforeResolve 全局解析守卫(2.5.0+) 在 beforeRouteEnter 调用之后调用
      3. router.afterEach 全局后置钩子 进入路由之后
      

      使用方法:

      // main.js 入口文件
      import router from "./router"; // 引入路由
      router.beforeEach((to, from, next) => {
        next();
      });
      router.beforeResolve((to, from, next) => {
        next();
      });
      router.afterEach((to, from) => {
        console.log("afterEach 全局后置钩子");
      });
      

      to,from,next 三个参数

      tofrom 是将要进入和将要离开的路由对象,路由对象指的是平时通过 this.$route 获取到的路由对象。next:Function 这个参数是个函数,且必须调用,否则不能进入路由(页面空白)。
      
      • next() 进入该路由。

      • next(false): 取消进入路由,url 地址重置为 from 路由地址(也就是将要离开的路由地址)。

      • next 跳转新路由,当前的导航被中断,重新开始一个新的导航。

        我们可以这样跳转:next('path地址')或者next({path:''})或者next({name:''})
        且允许设置诸如 replace: true、name: 'home' 之类的选项
        以及你用在router-link或router.push的对象选项。
        
  • 路由独享守卫

    如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫:

    // main.js 入口文件
    import router from "./router"; // 引入路由
    router.beforeEach((to, from, next) => {
      next();
    });
    router.beforeResolve((to, from, next) => {
      next();
    });
    router.afterEach((to, from) => {
      console.log("afterEach 全局后置钩子");
    });
    
  • 路由组件内的守卫

    1. beforeRouteEnter 进入路由前

    2. beforeRouteUpdate (2.2) 路由复用同一个组件时

    3. beforeRouteLeave 离开当前路由时

      beforeRouteEnter (to, from, next) {
      // 在路由独享守卫后调用 不!能!获取组件实例 `this`,组件实例还没被创建
      },
      beforeRouteUpdate (to, from, next) {
      // 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 `this`
      // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
      // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
      },
      beforeRouteLeave (to, from, next) {
      // 导航离开该组件的对应路由时调用,可以访问组件实例 `this`
      }
      
    • beforeRouteEnter

      因为钩子在组件实例还没被创建的时候调用,所以不能获取组件实例 this,可以通过传一个回调给 next 来访问组件实例 。
      但是回调的执行时机在 mounted 后面,所以在我看来这里对 this 的访问意义不太大,可以放在 created 或者 mounted 里面。

      beforeRouteEnter (to, from, next) {
        console.log('在路由独享守卫后调用');
        next(vm => {
          // 通过 `vm` 访问组件实例`this` 执行回调的时机在mounted后面,
        })
      }
      
    • beforeRouteLeave

      导航离开该组件的对应路由时调用,我们用它来禁止用户离开,比如还未保存草稿,或者在用户离开前,将 setInterval 销毁,防止离开之后,定时器还在调用。

      beforeRouteLeave (to, from , next) {
        if (文章保存) {
          next(); // 允许离开或者可以跳到别的路由 上面讲过了
        } else {
          next(false); // 取消离开
        }
      }
      
  • 触发钩子的完整顺序:

    将路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从 a 组件离开,第一次进入 b 组件:
    
    1. beforeRouteLeave: 路由组件的组件离开路由前钩子,可取消路由离开。
    2. beforeEach: 路由全局前置守卫,可用于登录验证、全局路由 loading 等。
    3. beforeEnter: 路由独享守卫
    4. beforeRouteEnter: 路由组件的组件进入路由前钩子。
    5. beforeResolve: 路由全局解析守卫
    6. afterEach: 路由全局后置钩子
    7. beforeCreate: 组件生命周期,不能访问 this。
    8. created: 组件生命周期,可以访问 this,不能访问 dom。
    9. beforeMount: 组件生命周期
    10. deactivated: 离开缓存组件 a,或者触发 a 的 beforeDestroy 和 destroyed 组件销毁钩子。
    11. mounted: 访问/操作 dom。
    12. activated: 进入缓存组件,进入 a 的嵌套子组件(如果有的话)。
    13. 执行 beforeRouteEnter 回调函数 next。
  • $Nexttick

  • 路由传参

    路由传参的三种基本方式

      先有如下场景 点击当前页的某个按钮跳转到另外一个页面去,并将某个值带过去
    
    <div class="examine" @click="insurance(2)">查看详情</div>
    
    1. 地址栏拼参(刷新页面传递数据不会丢失)

      methods:{
         insurance(id) {
           //直接调用$router.push 实现携带参数的跳转
           this.$router.push({
             path: `/particulars/${id}`,
           })
         }
      }
      

      路由需要提前配置

      {
         path: '/particulars/:id',
         name: 'particulars',
         component: particulars
      }
      

      获取地址栏参数如下

      this.$route.params.id;
      
    2. $router.push + params (页面刷新数据会丢失)

      通过路由属性中的 name 来确定匹配的路由,通过 params 来传递参数。

      methods:{
        insurance(id) {
            this.$router.push({
                name: 'particulars',
                params: {
                  id: id
                }
              })
        }
      }
      

      对应路由配置: 注意这里不能使用:/id 来传递参数了,因为组件中,已经使用 params 来携带参数了。

      {
      path: '/particulars',
      name: 'particulars',
      component: particulars
      }
      

      获取地址栏参数如下

      this.$route.params.id;
      
    3. $router.push + query

      通过路由属性中的 name 来确定匹配的路由,通过 params 来传递参数。

      methods:{
      insurance(id) {
        this.$router.push({
          path: '/particulars',
          query: {
            id: id
          }
        })
      }
      

      对应路由配置

      {
      path: '/particulars',
      name: 'particulars',
      component: particulars
      }
      

      获取地址栏参数如下

      this.$route.query.id;
      
  • slot 插槽

    <slot> 元素作为组件模板之中的内容分发插槽。<slot> 元素自身将被替换。
    
  • 动态组件

    有的时候,在不同组件之间进行动态切换是非常有用的;
    
    比如在一个多标签的界面里,可以通过 Vue 的 <component> 元素加一个特殊的 is attribute 来实现
    
    <button
      v-for="tab in tabs"
      v-bind:key="tab"
      v-bind:class="['tab-button', { active: currentTab === tab }]"
      v-on:click="currentTab = tab"
    >
      {{ tab }}
    </button>
    <!-- 组件会在 `currentTabComponent` 改变时改变 -->
    <component v-bind:is="currentTabComponent"></component>
    <!-- currentTabComponent 可以包括, 已注册组件的名字,或一个组件的选项对象 -->
    
    Vue.component("tab-home", {
      template: "<div>Home component</div>",
    });
    Vue.component("tab-posts", {
      template: "<div>Posts component</div>",
    });
    Vue.component("tab-archive", {
      template: "<div>Archive component</div>",
    });
    
    new Vue({
      el: "#dynamic-component-demo",
      data: {
        currentTab: "Home",
        tabs: ["Home", "Posts", "Archive"],
      },
      computed: {
        currentTabComponent: function () {
          return "tab-" + this.currentTab.toLowerCase();
        },
      },
    });
    
  • Keep-alive 组件

    用法:

    <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
    当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

    keep-alive 主要用于保留组件状态或避免重新渲染。

    <!-- 基本 -->
    <keep-alive>
      <component :is="view"></component>
    </keep-alive>
    
    <!-- 多个条件判断的子组件 -->
    <keep-alive>
      <comp-a v-if="a > 1"></comp-a>
      <comp-b v-else></comp-b>
    </keep-alive>
    
    <!-- 和 `<transition>` 一起使用 -->
    <transition>
      <keep-alive>
        <component :is="view"></component>
      </keep-alive>
    </transition>
    
  • transition 组件

    用法:

    <transition> 元素作为单个元素/组件的过渡效果。<transition> 只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。

    <!-- 简单元素 -->
    <transition>
      <div v-if="ok">toggled content</div>
    </transition>
    
    <!-- 动态组件 -->
    <transition name="fade" mode="out-in" appear>
      <component :is="view"></component>
    </transition>
    
    <!-- 事件钩子 -->
    <div id="transition-demo">
      <transition @after-enter="transitionComplete">
        <div v-show="ok">toggled content</div>
      </transition>
    </div>
    
  • diff 算法

          要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。
    
          我们先根据真实 DOM 生成一颗 virtual DOM,当 virtual DOM 某个节点的数据改变后会生成一个新的 Vnode,然后 Vnode 和 oldVnode 作对比,发现有不一样的地方就直接修改在真实的 DOM 上,然后使 oldVnode 的值为 Vnode。
    
          diff 的过程就是调用名为 patch 的函数,比较新旧节点,一边比较一边给真实的 DOM 打补丁。
    
    • virtual DOM 是将真实的 DOM 的数据抽取出来,以对象的形式模拟树形结构。比如 dom 是这样的:

      <div>
        <p>123</p>
      </div>
      

      对应的 virtual DOM(伪代码):

      var Vnode = {
        tag: "div",
        children: [{ tag: "p", text: "123" }],
      };
      
    • 在采取 diff 算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。

      <div>
        <p>123</p>
      </div>
      
      <div>
        <span>456</span>
      </div>
      

      上面的代码会分别比较同一层的两个 div 以及第二层的 p 和 span,但是不会拿 div 和 span 作比较。在别处看到的一张很形象的图

  • 特殊 attribute

    • key

      预期:number | string | boolean (2.4.2 新增) | symbol (2.5.12 新增)
      
      key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
      

      有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

      最常见的用例是结合 v-for:

      <ul>
        <li v-for="item in items" :key="item.id">...</li>
      </ul>
      

      它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:

      • 完整地触发组件的生命周期钩子
      • 触发过渡
        例如:
      <transition>
        <span :key="text">{{ text }}</span>
      </transition>
      // 当 text 发生改变时,总是会被替换而不是被修改,因此会触发过渡。
      
    • ref

      预期:string
      
      ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:
      
      <p ref="p">hello</p>
      <child-component ref="child"></child-component>
      

      当 v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。

      关于 ref 注册时间的重要说明:因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs 也不是响应式的,因此你不应该试图用它在模板中做数据绑定。

    • is

      预期:string | Object (组件的选项对象)
      
      用于动态组件且基于 DOM 内模板的限制来工作。
      
      <!-- 当 `currentView` 改变时,组件也跟着改变 -->
      <component v-bind:is="currentView"></component>
      
      <!-- 这样做是有必要的,因为 `<my-row>` 放在一个 -->
      <!-- `<table>` 内可能无效且被放置到外面 -->
      <table>
        <tr is="my-row"></tr>
      </table>
      
  • 与 Jquery 的区别

    jQuery 是使用选择器($)选取 DOM 对象,对其进行赋值、取值、事件绑定等操作,其实和原生的 HTML 的区别只在于可以更方便的选取和操作 DOM 对象
    
    Vue它通过双向数据绑定把 View 层和 Model 层连接了起来,通过对数据的操作就可以完成对页面视图的渲染使用简单,快速
    

VUE3.x

  • setup

    setup 中接受的第一个参数 props 是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式

    export default defineComponent({
      setup(props, context) {},
    });
    

    第二个参数 context,我们前面说了 setup 中不能访问 Vue2 中最常用的 this 对象,所以 context 中就提供了 this 中最常用的三个属性:attrs、slot 和 emit {attrs,solt,emit}

  • 生命周期钩子

    • beforeCreate --> setup
    • created --> setup
    • beforeMount --> onBeforeMount
    • mounted --> onMounted
    • beforeUpdate --> onBeforeUpdate
    • updated --> onUpdated
    • beforeDestroy --> onBeforeUnmount
    • destroyed --> onUnmounted

    avatar

  • ref

    接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

    const count = ref(0);
    console.log(count.value); // 0
    
    count.value++;
    console.log(count.value); // 1
    
    return {
      count,
    };
    

    可能需要为 ref 的内部值指定复杂类型。我们可以通过在调用 ref 来覆盖默认推断时传递一个泛型参数

    const foo = ref<string | number>("foo"); // foo's type: Ref<string | number>
    
    foo.value = 123; // ok!
    
    return {
      foo,
    };
    
  • reactive

    返回对象的响应式副本

    const obj = reactive({
      count: 0,
      name: "John",
    });
    
    return {
      obj,
      // toRefs(obj)  // 变成响应式并且解构,view中调用不在obj.count或obj.name,直接{{count}} {{name}}
    };
    
  • readonly

    获取一个对象 (响应式或纯对象) 或 ref 并返回原始 proxy 的只读 proxy。只读 proxy 是深层的:访问的任何嵌套 property 也是只读的。

    const original = reactive({ count: 0 });
    
    const copy = readonly(original);
    
    watchEffect(() => {
      // 适用于响应性追踪
      console.log(copy.count);
    });
    
    // 变更original 会触发侦听器依赖副本
    original.count++;
    
    // 变更副本将失败并导致警告
    copy.count++; // 警告!
    
  • Computed

    可以传入一个函数,默认该函数就是 getter,不管 getter 返回值为一个 ref 响应式数据还是一个普通变量,数据都是只读不能改变。

    import { ref, computed } from "vue";
    
    export default {
      name: "test",
      setup() {
        let name = ref("蛙人");
        let test = computed(() => name.value);
        test.value = "123"; // 修改无效,只能只读
      },
    };
    

    传入一个对象 set 和 get 函数方法,这样就可以修改

    import { ref, computed } from "vue";
    export default {
      name: "test",
      setup() {
        let name = ref("蛙人");
        let test = computed({
          get() {
            return name.value;
          },
          set(val) {
            return (name.value = val);
          },
        });
        test.value = "123";
      },
    };
    

    在 Vue3.x 中移除过滤器 filters,不在支持, 建议使用 computed 去替代

    <template>
      <h1>Bank Account Balance</h1>
      <p>{{ accountInUSD }}</p>
    </template>
    
    <script>
      export default {
        props: {
          accountBalance: {
            type: Number,
            required: true
          }
        },
        computed: {
          accountInUSD() {
            return '$' + this.accountBalance
          }
        }
      }
    </script>
    
    
  • watchEffect

    在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。

    const state = reactive({
      nickname: "Xiao fang",
      age: 20,
      weight: 5
    });
    
    const born = ref(1995)
    
    // watchEffect 会先执行一次用来自动收集依赖,无法获取到变化前的值, 只能获取变化后的值
    watchEffect(() => {
      console.log(state.weight);
      console.log(born.value);
    });
    
  • watch

    watch 需要监听特定数据,默认情况是懒执行,也就是只有当数据发生变化时才执行第二个参数函数
    监听单个值

    const obj = reactive({
      param: {
        Chinese: 90,
        English: 89,
      },
    });
    const year = ref(2000);
    const born = ref(1995);
    
    setTimeout(() => {
      state.age++;
      year.value++;
      obj.param.Chinese = 100;
    }, 2000);
    
    setInterval(() => {
      state.weight++;
      born.value++;
    }, 1000);
    
    // watch侦听 reactive 定义的数据
    watch(
      () => state.age,
      (curAge, preAge) => {
        console.log("age新值:", curAge, "age老值:", preAge);
      }
    );
    
    // watch侦听 ref 定义的数据
    watch(year, (newVal, oldVal) => {
      console.log("year新值:", newVal, "year老值:", oldVal);
    });
    
    // 同时侦听多个数据
    watch([() => state.age, year], ([curAge, preAge], [curYear, preYear]) => {
      console.log("age新值:", curAge, "age老值:", preAge);
      console.log("year新值:", curYear, "year老值:", preYear);
    });
    
    // immediate:true, deep:true
    // immediate:true,表示组件渲染时立即调用
    // deep:true,表示深度监听对象里面的子属性
    const stopWatchRoom = watch(
      () => obj.param,
      (curVal, preval) => {
        console.log("curVal", curVal, "preval", preval);
      },
      { deep: true }
    );
    setTimeout(() => {
      // 停止监听
      stopWatchRoom(); //如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值
    }, 3000);
    
  • Refs

    获取元素

    <template>
      <div class="test">
        <p ref="el">123</p>
      </div>
    </template>
    
    <script>
    import { ref, onMounted } from "vue"
    export default {
    name: 'test',
      setup() {
        let el = ref(null)
    
        onMounted(() => {
            console.log(el) // p标签元素
        })
    
        return { el }
      }
    }
    </script>
    
  • 自定义 Hooks

    约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分;例如:useCount.ts

  • 对比 vue2.x 与 vue3.x 响应式

    1. Object.defineProperty 只能劫持对象的属性, 而 Proxy 是直接代理对象

    2. 由于 Object.defineProperty 只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象, 不需要遍历操作

    3. Object.defineProperty 对新增属性需要手动进行 Observe

    4. 因为 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用 Object.defineProperty 进行劫持。
      也就是 Vue2.x 中给数组和对象新增属性时,需要使用set才能保证新增的属性也是响应式的,set 才能保证新增的属性也是响应式的, set 内部也是通过调用 Object.defineProperty 去处理的。

  • Teleport 传送门

    Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。

    示例:

    在 index.html 文件中定义一个供挂载的元素

    <body>
      <div id="app"></div>
      <div id="dialog"></div>
    </body>
    

    定义一个 Dialog 组件 Dialog.vue, 留意 to 属性, 与上面的 id 选择器一致

    <!-- Dialog.vue -->
    <template>
      <teleport to="#dialog">
        <div class="dialog">
          <div class="dialog_wrapper">
            <div class="dialog_header" v-if="title">
              <slot name="header">
                <span>{{ title }}</span>
              </slot>
            </div>
          </div>
          <div class="dialog_content">
            <slot></slot>
          </div>
          <div class="dialog_footer">
            <slot name="footer"></slot>
          </div>
        </div>
      </teleport>
    </template>
    

    最后在一个子组件 Header.vue 中使用 Dialog 组件

    <!-- Header.vue -->
    <div class="header">
      <navbar />
      <dialog v-if="dialogVisible"></dialog>
    </div>
    

    avatar

    渲染结果看,使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制.

  • isRef & isReactive

    isRef 用于检测该数据是不是 ref 响应式数据。返回一个 Boolean 类型。 isReactive 用于检测该数据是不是 reacttive 响应式数据。返回一个 Boolean 类型。

    let test = ref("Jf");
    console.log(isRef(test)); // true
    
    let test = reactive({ name: "哈哈" });
    console.log(isReactive(test)); // true
    
  • Suspense

    Suspense 只是一个带插槽的组件,只是它的插槽指定了 default 和 fallback 两种状态。 对应 Vue2.x 中应该经常遇到这样的场景如下:

    <template>
      <div>
        <div v-if="!loading">...</div>
        <div v-if="loading">加载中...</div>
      </div>
    </template>
    

    Vue3.x 新出的内置组件 Suspense, 它提供两个 template slot, 刚开始会渲染一个 fallback 状态下的内容, 直到到达某个条件后才会渲染 default 状态的正式内容

    <template>
      <Suspense>
        <template #default>
          <SuspenseAsyncChild></SuspenseAsyncChild>
        </template>
    
        <template #fallback>
          <div>
            <Loading></Loading>
          </div>
        </template>
      </Suspense>
    </template>
    
    <script>
    
    import { defineComponent } from "vue"
    import SuspenseAsyncChild from '../components/SuspenseAsyncChild.vue'
    import Loading from '../components/Loading.vue'
    
    export default defineComponent({
      components: {
        SuspenseAsyncChild,
        Loading
      }
    })
    </script>
    
  • Fragment

    在 vue3 的模板中,不再需要根标签,它内部有一个 fragment 的组件作为模板的根标签,可在 vue 调试台中查看到 Vue3.x 中将不在限制模板中只有一个根节点,根组件可以任意多个元素

    <template>
      <div class="app"></div>
      <p>标题</p>
    </template>
    
  • Tree-Shaking

    Vue3.x 在考虑到 tree-shaking 的基础上重构了全局和内部 API, 表现结果就是现在的全局 API 需要通过 ES Module 的引用方式进行具名引用

    // vue2.x
    import Vue from "vue"
    
    Vue.nextTick(()=>{
        ...
    })
    
    
    // vue3.x
    import { nextTick } from "vue"
    
    nextTick(() =>{
    ...
    })
    
    
  • solt

    //vue2.x 具名插槽
    
    //子组件
    <slot name="title"></slot>
    
    // 父组件
    <template slot="title">
      <h1>歌曲:成都</h1>
      <template> </template
    ></template>
    
    //vue2.x 作用域插槽,在 slot 上面绑定数据
    
    // 子组件
    <slot name="content" :data="data"></slot>
    export default {
      data(){
        return{ data:["走过来人来人往","不喜欢也得欣赏","陪伴是最长情的告白"] }
      }
    }
    
    // 父组件
    <template slot="content" slot-scope="scoped">
        <div v-for="item in scoped.data">{{item}}</div>
    <template>
    

    Vue3.0 中将 slotslot-scope 进行了合并同意使用。 Vue3.0 中 v-slot

    <!-- 父组件中使用 -->
    <template v-slot:content="scoped">
      <div v-for="item in scoped.data">{{item}}</div>
    </template>
    
    <!-- 也可以简写成: -->
    <template #content="{data}">
      <div v-for="item in data">{{item}}</div>
    </template>
    
  • 自定义指令

    // Vue 2.x, 注册一个全局自定义指令 `v-focus`
    
    Vue.directive("focus", {
      // 当被绑定的元素插入到 DOM 中时……
      inserted: function (el) {
        // 聚焦元素
        el.focus();
      },
    });
    

    Vue3 中, 可以这样来自定义指令

    const { createApp } from "vue"
    
    const app = createApp({})
    app.directive('focus', {
        mounted(el) {
            el.focus()
        }
    })
    
    <!-- 在模板中任何元素上使用新的 v-focus指令 -->
    <input v-focus />
    
  • v-model

    Vue 3 中又抛弃了.sync 写法, 统一使用 v-model

    <modal v-model:visible="isVisible" v-model:content="content"></modal>
    
    <!-- 相当于 -->
    <modal
      :visible="isVisible"
      :content="content"
      @update:visible="isVisible"
      @update:content="content"
    />
    
  • 异步组件

    Vue3 中 使用 defineAsyncComponent 定义异步组件,配置选项 component 替换为 loader ,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise,用法如下:

    <template>
      <!-- 异步组件的使用 -->
      <AsyncPage />
    </tempate>
    
    <script>
    import { defineAsyncComponent } from "vue";
    
    export default {
      components: {
        // 无配置项异步组件
        AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),
    
        // 有配置项异步组件
        AsyncPageWithOptions: defineAsyncComponent({
          loader: () => import(".NextPage.vue"),
          delay: 200,
          timeout: 3000,
          errorComponent: () => import("./ErrorComponent.vue"),
          loadingComponent: () => import("./LoadingComponent.vue"),
        })
      },
    }
    </script>
    
  • on,off,once 实例方法

    Vue3.x 中 onon,off 和 $once 实例方法已被移除,应用实例不再实现事件触发接口。

    created() {
      console.log(this.$on, this.$once, this.$off) // undefined undefined undefined
    }
    

VUEX

  • 为什么要使用 Vuex

    传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

    为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。

    如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择

  • 引入 Vuex

  • 下载 vuex

    npm install vuex --save
    
  • 在 main.js 添加:

    import Vuex from "vuex";
    
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      //待添加
    });
    
    new Vue({
      el: "#app",
      store,
      render: (h) => h(App),
    });
    
  • 核心概念

  • state

转载自(十分钟入门 Vuex)