面试题整理

201 阅读44分钟

1. HTTP知识:

1. TCP握手过程

本图是HTTPS的过程

http-request-over-tcp-tls@2x-292x300.png

2. 常见的请求头、响应头,分别的意思

请求头

accept,accept-encoding,content-type,cache-control,accept-language,origin,user-agent,Connection(HTTP1.1),host,cookie,referer,date,if-none-match,if-modified-since

响应头

Content-Length,Content-type,server,set-cookie,access-control-allow-credentials,access-control-allow-origin,date,etag,cache-control,Last-Modified

HTTP2的请求头

1.  :authority:united.bat100.net
2.  :method:POST
3.  :path:/api/login/doLogin
4.  :scheme:https
5.  sec-ch-ua:".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
6.  sec-ch-ua-mobile: ?0
7.  sec-ch-ua-platform:"Windows"
8.  sec-fetch-dest:empty
9.  sec-fetch-mode:cors
10.  sec-fetch-site:same-origin

3. HTTP1.1和HTTP2和HTTP3的区别,分别在什么情况下使用,目前广泛支持的最新版本是哪个版本?

HTTP1.1是目前默认使用的版本。 HTTP/1.1传输数据时,是以文本的方式,借助耗CPU的zip压缩的方式减少网络带宽,但是耗了前端和后端的CPU。

HTTP2需要指定开启,不会默认启用。

  • HTTP/2是一个二进制协议,增加了数据传输的效率。(HTTP1.1是基于文本的协议)
  • HTTP/2是可以在一个TCP链接中并发请求多个HTTP请求,移除了HTTP/1.1中的串行请求。
  • HTTP/2会压缩头,如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。这就是所谓的HPACK算法(参看RFC 7541 附录A)
  • HTTP/2允许服务端在客户端放cache,又叫服务端push,也就是说,你没有请求的东西,我服务端可以先送给你放在你的本地缓存中。比如,你请求X,我服务端知道X依赖于Y,虽然你没有的请求Y,但我把把Y跟着X的请求一起返回客户端。

HTTP3底层使用的是UDP。

4. 对TCP/IP协议的理解

IP协议在网络层;TCP协议在传输层,它构成了我们常用的应用层协议(HTTP、FTP、TELNET、SMTP、POP3等)的基础。

5. OSI7层模型、OSI5层模型分别是什么

物理层-数据链路层-网络层-传输层
两种模型的下四层都是以上
区别是:

  1. 7层模型:后三层是会话层-表现层-应用层
  2. 5层模型:最后是应用层

2021010409370574.gif

7. HTTP缓存分为几种,这几种分别怎么使用,需要前后端如何配合

强缓存和协商缓存

强缓存

HTTP1.1及以后,都是优先使用cache-control,其所包含的属性有:max-age(秒)publicprivatemust-revalidateno-cacheno-store
需要服务端配置这些响应头用来支持HTTP强缓存

max-age:单位为秒,设置数字即可使该资源被客户端缓存,在规定的时间内不会向服务器发起HTTP请求,客户端状态码是200,缓存在内存或硬盘中
public:可以被任何对象缓存
no-cache:要求客户端必须将请求提交至服务端进行验证资源的有效性
no-store:不使用任何缓存

HTTP1.0还包含一个请求头属性Pragma其优先级最高,只有一个值:no-cache,但目前已废弃,不建议使用。 如果命中强缓存则不会发出实际请求,会直接从内存或硬盘中读出结果。返回的是status:200(from disk)(from memony) 该属性放在请求头中和放在响应头中的实际效果不同。建议放在响应头中。

协商缓存

两组,分别是:etagif-none-matchif-modified-sincelast-modified 协商缓存一定会请求服务器,服务器会根据请求头带有的信息返回缓存(304 not modified)或新的资源(200 ok)

etag可以在服务中进行配置,使用默认的etag(/w开头)或自定义etag的值(例如文件的MD5值),配置后,响应中就会带着这个值,后续一旦再次发生请求这个资源(if-none-match的值为之前的etag的值)的行为,服务器会根据该资源是否被修改过返回304或200;

if-modified-sincelast-modified指的是以该资源在服务器上的最后修改时间为依据来判断该资源是否使用缓存。服务端进行资源的last-modified值得配置,跟随资源的响应返回给客户端,下次客户端再去请求该资源的时候,会在请求头带有if-modified-since,服务器会根据该资源的最后修改时间进行判断,如果在请求中所带有的最后修改时间后没有任何操作就返回304,一旦在这时间后有任何变动,就会返回200

8. DNS系统的意义,寻址过程是什么

  1. 全称:domain name system域名系统
  2. 寻址过程:先从本机hosts文件中查询,如果没有,就从本地DNS服务器查询,如果没有结果就向根域名服务器请求,然后依次是顶级域名服务、权威域名服务器,得到结果后,会返回该域名对应的ip,知道ip地址后,直接访问该IP地址。

9. SSL证书和TSL证书和HTTPS的关系是什么;HTTP和HTTPS的关系是什么

HTTPS和HTTP的关系是,HTTPS加了一层安全机制。 证书由权威CA机构颁发。颁发时会将申请公司的一系列信息包含进去,通过将该证书的一系列信息用哈希算法生成数据摘要,使用当前证书的私钥加密后得到该证书的数字签名。权威CA机构的公钥都内置在浏览器中。因此当网站开启了HTTPS后,服务器会将证书内容和这个签名一起返回给客户端,客户端使用公钥进行解密,得到数字摘要;同时将证书内容用和CA相同的哈希算法生成数字摘要后,将两个结果进行对比,如果相同就认为是可信的,服务端的身份值得信赖;此时,客户端随机生成一个会话秘钥(对称加密的秘钥),使用公钥进行加密后,发给服务端,服务端使用私钥解密该秘钥,之后的一切请求和响应都会使用该会话秘钥确认双方身份。

10. keep alive的作用机制是什么?是否稳定?HTTP1.1的keep-alive和HTTP2的TCP连接可复用有什么区别?

是请求头和响应头中的connection属性,值为keep-alive,请求中包含它,意思就是告诉服务器请保持这个TCP连接以便后续复用,可以节省服务器的资源,不需要每次都创建TCP连接。

区别:

  • HTTP/1.x 虽然引入了 keep-alive 长连接,但它每次请求必须等待上一次响应之后才能发起,
  • 所以,在 HTTP/1.1 中提出了管道机制(默认不开启),下一次的请求不需要等待上一个响应来之后再发送,但这要求服务端必须按照请求发送的顺序返回响应,当顺序请求多个文件时,其中一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,这就是队头阻塞 (Head-Of-Line Blocking)
  • HTTP/2 是基于二进制流的,它就可以把 HTTP 消息分解为独立的帧,交错发送,然后在另一端通过帧中的标识重新组装,这就是多路复用。
  • 这就实现了在同一个TCP连接中,同一时刻可以发送多个请求和响应,且不用按照顺序一一对应,即使某个请求任务耗时严重,也不会影响到其它连接的正常执行

总结

  • HTTP/1.x 是基于文本的,只能整体去传;HTTP/2 是基于二进制流的,可以分解为独立的帧,交错发送
  • HTTP/1.x keep-alive 必须按照请求发送的顺序返回响应;HTTP/2 多路复用不按序响应
  • HTTP/1.x keep-alive 为了解决队头阻塞,将同一个页面的资源分散到不同域名下,开启了多个 TCP 连接;HTTP/2 同域名下所有通信都在单个连接上完成
  • HTTP/1.x keep-alive 单个 TCP 连接在同一时刻只能处理一个请求(两个请求的生命周期不能重叠);HTTP/2 单个 TCP 同一时刻可以发送多个请求和响应

11. 客户端和服务端如何协商升级HTTP协议版本?或连接方式?(web socket)

将普通的HTTP1.1版本协议升级为web socket时,服务端会返回状态码101(connection upgrade)来告知客户端当前协议需要升级为ws

12. cookie有哪些属性

服务器使用 Set-Cookie 响应头部向用户代理(一般是浏览器)发送 Cookie 信息

Secure和httpOnly

secure:标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端

httpOnly:JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie;此类 Cookie 仅作用于服务器。

其他属性:Expires、Max-Age、Path、SameSite(none、Strict、Lax)

2. 其他工作中会用到的知识

4. 对称加密和非对称加密、散列函数有哪些,用途分别是什么?

1. 对称加密

对称加密:同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。 常见的有AES、DES

2. 非对称加密

非对称加密:需要两个密钥来进行加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。一般使用的时候,私钥需要被妥善保管,而公钥一般放在客户端。 常见的有RSA、DSA、ECDSA

3. 散列函数:

是把任意长度的输入(又叫做预映射pre-image)通过散列算法(函数)变换成固定长度的输出,该输出就是散列值。 常见的有md4、md5、SHA-1、SHA256

4. 区别:

散列函数是用来做摘要的,并不是用来做加密使用;不能与加密算法混为一谈。
摘要主要是对内容做唯一性或者完整性的校验,而不是针对内容做加密使用的,本质上的目的就是不同的。
一个内容通过某种加密方式进行加密后,是有合法方式可以对其进行解密得到原始内容的,散列算法是无法通过合法方式得到原始值的,我们常见的彩虹表便是穷举了收集得到的原始内容和散列后的值进行对应。

5. 你了解哪些请求方法,分别有哪些作用和不同

HTTP PUT 请求方法使用请求中的负载创建或者替换目标资源。 PUT 与 POST 方法的区别在于,PUT 方法是幂等的:调用一次与连续调用多次是等价的(即没有副作用),而连续调用多次 POST 方法可能会有副作用,比如将一个订单重复提交多次。

HTTP 的 OPTIONS 方法 用于获取目的资源所支持的通信选项。客户端可以对特定的 URL 使用 OPTIONS 方法,也可以对整站(通过将 URL 设置为“*”)使用该方法。

DELETE HTTP DELETE 请求方法用于删除指定的资源。

PATCH

在 HTTP 协议中,请求方法 PATCH 用于对资源进行部分修改。 在 HTTP 协议中, PUT 方法已经被用来表示对资源进行整体覆盖, 而 POST 方法则没有对标准的补丁格式的提供支持。不同于 PUT 方法,而与 POST 方法类似,PATCH 方法是非幂等的,这就意味着连续多个的相同请求会产生不同的效果。 要判断一台服务器是否支持 PATCH 方法,那么就看它是否将其添加到了响应首部 Allow 或者 Access-Control-Allow-Methods(在跨域访问的场合,CORS)的方法列表中 。 另外一个支持 PATCH 方法的隐含迹象是 Accept-Patch 首部的出现,这个首部明确了服务器端可以接受的补丁文件的格式。

6. 浏览器的进程和线程是什么样子的?

浏览器中每个tab都是一个单独的进程

进程

浏览器包含以下进程:浏览器主进程(只有一个,无论打开几个tab或几个弹窗)、渲染进程、网络进程、GPU进程(只有一个)、音频进程

线程

浏览器渲染进程所包含的线程有以下:GUI线程(负责绘制)(有且只有一个)、js线程(有且只有一个)、异步HTTP请求线程、事件触发器线程、定时器线程,其中:GUI线程和JS引擎线程互斥

其他

JS执行引擎是单线程的,就是说,每一个浏览器内核(Render进程中),有且只有一个JS执行引擎(web worker另谈),为什么只能有一个?这和JS的用途有关,JS作为浏览器的脚本语言,主要用处是与用户进行交互,以及操作DOM的,如果JS是多线程,当在不同线程进行不同的DOM操作,会造成更复杂的一些问题,所以从一开始,JS就是单线程的。 默认情况下会为每一个标签页配置一个渲染进程,一个Tab页中无论什么时候都只有一个JS线程在运行JS程序

v2-c6958ffcc3279e39e490d0f68dee9c8b_r.jpg

3. 前端

1. JS的数据类型有哪些?新出的分别用在什么场景下?聊一下弱引用

基本数据类型:StringNumberBooleanundefinednullSymbolBigint
引用数据类型:Object

2. 前端上传大文件时如何处理?

前端分片上传,使用file的API:slice结合分片时的索引,后端将文件数据拼起来。

3. web worker主要应用于什么情况下?浏览器对于渲染、执行JS等其他是怎么一种处理方式?

是另外开启一个线程用来执行worker中的内容。 一般是新建一个文件worker.js,在需要使用的位置new worker(worker file path) 方法包含 postMessage()terminate()service worker不支持该操作。

事件有:errormessagemessageerror

4. js运行在浏览器中是个什么过程

js是浏览器的渲染进程(其他还有主进程、GPU进程、网络进程等)中的一个线程,js线程的运行GUI线程是相互冲突的,同一时刻,只有一个能运行(所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞)。
js遵从event loop的原则:首先执行微任务,其次才会执行宏任务,微任务包含Promise和一些同步代码,宏任务包含从事件触发线程中拿到的一些事件(例如点击事件、双击事件等)和定时触发器线程中拿到的定时器(包含setTimeout和setInterval)和messageChannel等,js线程周期性的按照以上顺序来执行。

5. 常用的鉴权方式和维持登录状态方式

HTTP是无状态的协议;常用cookie和token来做登录状态的识别。 cookie由登录的相应来设置:set-Cookie来告诉浏览器当前域名后续发起的请求携带cookie。cookie的属性包括:securehttpOnlymax-agecredentialsSameSiteexpires
token是由服务端签发的一个令牌,由服务端指定包含什么内容。
常用的jwt是一个自包含的加密内容。
更建议大家去搜一下这个内容,看更详细的解读(我这里只是记住一个大体思路,以便我自己看的时候串起来)

6. 箭头函数和普通函数有什么区别?

this的指向不同,箭头函数没有自己的this,箭头函数的this永远指向自己的外层的this,若没有,那么则是全局对象(浏览器中是Window)。

7. PromiseA+规范的实现

首先Promise有三个状态:PendingFulfilledRejected 其次需要有Promise的实例需要有then方法和catch方法,Promise的静态方法需要包含.resolve方法和.reject方法。

8. 函数柯里化是什么?有什么作用?一般在什么情况下使用?函数式编程和传统的面向对象编程有什么区别?

9. 跨域的解决办法

  1. 本地开发有很多方式,例如配置webpack的代理
  2. 线上可以使用反向代理,例如配置Nginx来代理真实的服务所在
  3. 服务端可以配置cors,指定客户端发起访问时的白名单
  4. 还有一些其他方式目前不建议使用了。

10. 为了避免浏览器重绘或回流,需要怎么处理?

CSS的处理

  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolutefixed的元素上。
  • 避免使用CSS表达式(例如:calc())。

JavaScript的处理

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
    以上参考了作者:腰花的文章
    链接:juejin.cn/post/684490…

11. CSS选择器的使用技巧

(待补充)

12. package.json 与 package-lock.json

package-lock文件是提供了一个当前工程可运行的依赖库的各种版本。 因为package.json中很多依赖都是使用^和~来进行了很宽松的版本控制,因此会存在版本不兼容而无法运行的情况,所以在创建工程时,该文件会保存当前可运行的各种依赖的版本,以便在协作者的电脑上也能运行. 参考:package.json 与 package-lock.json 的关系 - 掘金 (juejin.cn)

13. 对闭包的理解

可参考这篇分享:juejin.cn/post/684490…
可以理解为上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。

14. 手写拍平数组的方法

(……待补充)

15. 对防抖和节流的理解及实现方式

防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
节流(throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
实现
//防抖
function debounce(fun,time){
  let timer
  return function(){
    clearTimeout(timer)
    let args = arguments
    timer=setTimeout(()=>{
      fun.apply(this,args)
    },time)
  }
}

//节流
function throttle(fun,time){
  let timeStart=0 //初始时间
  return function(){
    let timeNow=new Date() //当前时间
    if(timeNow-timeStart>time){
      fun.apply(this,arguments)
      timeStart=timeNow
    }
  }
}

16. 开一个新项目时,前端搭项目的流程

以SPA为例

  1. 确认使用的框架或库,按照官方给出的脚手架安装并明确出使用的包管理工具(npm、yarn、pnpm等)和nodejs版本等支撑内容的版本并形成文档。
  2. 针对一些webpack或其他构建工具的个性化设置,例如配置sass的解析工具,配置打包后的输出文件目录,配置是否生成.map文件,打包文件后是否带有hash等
  3. 安装router库、安装UI框架并根据实际文档中的内容配置使用模式和一些主题色一类的配置。
  4. 将UI框架和router挂载到使用的主框架中
  5. 确定静态资源的路径和使用规范
  6. 确定页面主要布局方式,创建routerView的出口
  7. 确定要使用的请求库并进行封装,主要封装常用HTTP请求方法,身份验证需要携带的参数和内容,所有的API路径和请求该API后得到的响应,以及发生异常时抛出来的错误码和友好提示,根据需要,在请求拦截器和相应拦截器中进行相关操作(例如根据响应的数据结构进行初步判断本次响应是否正确或进行一些值的初步处理等)。
  8. 针对一些常用的UI组件进行进一步的封装,例如封装表格的分页请求、表格的查询条件、权限组件等等。
  9. 根据公司内部开发流程来对应配置不用环境中的配置文件,例如.env.dev.env.test等。

17. 前端并发模型和事件循环模型

事件循环模型

js是单线程运行的,因此在同时发生了很多事情(包括但不限于发起异步请求,dom中某个元素经某事件触发后需要进行的处理,定时器)需要处理的时候,是按照事件循环模型进行处理的,事件循环模型的执行顺序是:本次事件循环周期内的微任务-本次事件循环周内的宏任务。 微任务包含PromiseProcess.nextTick(nodejs)、mutationObserverObject.observe 宏任务包含:setTimeoutsetIntervalsetImmediatepostMessagemessageChannel和一些dom中发生的事件,如点击事件。

并发模型

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其它事情,比如用户输入。

18. js的严格模式

开启严格模式的方式:

function fn() { 
    'use strict' 
    ... 
}

具体请参考这篇文章:juejin.cn/post/705666…

19. js的class是什么样子的,本质是类吗

(……待补充)

20. js如何判断相等

js判断相等有3种方式:=====is
在比较两件事情时,双等号将执行类型转换; 三等号将进行相同的比较,而不进行类型转换 (如果类型不同,只是总会返回 false ); 而 Object.is 的行为方式与三等号相同,但是对于 NaN 和-0 和 +0 进行特殊处理,所以最后两个不相同,而 Object.is(NaN,NaN)将为 true。(通常使用双等号或三等号将 NaN 与 NaN 进行比较,结果为 false,因为 IEEE 754 如是说.) 请注意,所有这些之间的区别都与其处理原语有关; 这三个运算符的原语中,没有一个会比较两个变量是否结构上概念类似。对于任意两个不同的非原始对象,即便他们有相同的结构,以上三个运算符都会计算得到 false。 更详细的示例可以查看:developer.mozilla.org/zh-CN/docs/…

21. async和await的使用,他们和generate函数有什么关系?

async和await是generate函数的语法糖,主要用来实现顺序执行异步操作,在函数声明时添加async关键字,函数体内部就可以使用await关键字来等待一个Promise 对象(如果等待的不是 Promise 对象,则返回该值本身)。await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理 (fulfilled),其回调的 resolve 函数参数作为 await 表达式的值,继续执行 async function。 若 Promise 处理异常 (rejected),await 表达式会把 Promise 的异常原因抛出。 示例:

async () => {
    await Promise.resolve('结果')
}

22. 说说MessageChannel

经由MessageChannel产生的消息是宏任务。 使用MessageChannle可以创建一个消息通道,这个消息通道通过它的两个MessagePort 属性发送数据:MessageChannel.port1MessageChannel.port2 使用的时候通过创建一个MessageChannle的实例:

let channel = new MessageChannel();

23. 定时器的使用和销毁

包含setTimeoutsetInterval,使用时这两个方法都会返回一个timeid,当页面或组件被销毁时,需要清除掉这个timeId

24. XHR和fetch的关系和区别是什么?

都可以发起异步请求; XHR使用时是创建一个实例,按照指定的步骤依次进行。 fetch,返回值是一个Promise,得到的结果使用Response(是一个Response 流)的各种method来处理,例如:arrayBufferblobformDatajsontext

区别:

  1. fetch是直接使用fetch()来发起请求,xhr需要创建实例后调用实例方法进行使用

25. Map在使用上和Object有什么不同?

Map是es6新增的一种标准内置对象,Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。 使用时需要创建一个实例进行使用,例如:

const map1 = new Map();

map1.set('a', 1)
map1.set('b', 2)
map1.get('a')  //1
map1.size  //2
map1.delete('b')
map1.has('b')  //false
map1.size  //1
map1.clear()
map1.size  //0

相对使用Object保存键值对的行为,Map可以更好地解决以下问题

  1. 键值重复的问题(一个 Object 有一个原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。)
  2. Map 的键可以是任意值,包括函数、对象或任意基本类型,但是Object只允许String和Symbol,此外的其他类型都会被转为这两种类型作为键
  3. Map 中的键是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。虽然 Object 的键目前是有序的,但并不总是这样,而且这个顺序是复杂的。因此,最好不要依赖属性的顺序。
  4. 关于键值对的数量:Map 的键值对个数可以轻易地通过 size 属性获取。但是Object中键值对的数量需要通过一些办法获得。
  5. 使用友好程度:在频繁增删键值对的场景下表现更好,例如提供了clear方法可以清除所有内容,Object需要手动delete处理内容。
  6. 其他:
与数组的关系
const kvArray = [["key1", "value1"], ["key2", "value2"]];

// 使用常规的 Map 构造函数可以将一个二维键值对数组转换成一个 Map 对象
const myMap = new Map(kvArray);

myMap.get("key1"); // 返回值为 "value1"

// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组
console.log(Array.from(myMap)); // 输出和 kvArray 相同的数组

// 更简洁的方法来做如上同样的事情,使用展开运算符
console.log([...myMap]);

// 或者在键或者值的迭代器上使用 Array.from,进而得到只含有键或者值的数组
console.log(Array.from(myMap.keys())); // 输出 ["key1", "key2"]

也可以合并
const first = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

const second = new Map([
  [1, 'uno'],
  [2, 'dos']
]);

// 合并两个 Map 对象时,如果有重复的键值,则后面的会覆盖前面的。
// 展开运算符本质上是将 Map 对象转换成数组。
const merged = new Map([...first, ...second]);

console.log(merged.get(1)); // uno
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three

26. 手写深拷贝

github.com/axuebin/art…

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in source){ // 遍历目标
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

27. 计算两数之和,大数之和

这个大家可以自行去搜索解决办法,在这里只是用作问题参考。

28. options请求方法有什么用

可以参考developer.mozilla.org/zh-CN/docs/… HTTP 的 OPTIONS 方法 用于获取目的资源所支持的通信选项。可以使用 OPTIONS 方法对服务器发起请求,以检测服务器支持哪些 HTTP 方法,响应报文包含一个 Allow 首部字段,该字段的值表明了服务器支持的所有 HTTP 方法。
在 CORS 中,可以使用 OPTIONS 方法发起一个预检请求,以检测实际请求是否可以被服务器所接受。预检请求报文中的 Access-Control-Request-Method 首部字段告知服务器实际请求所使用的 HTTP 方法;Access-Control-Request-Headers 首部字段告知服务器实际请求所携带的自定义首部字段。服务器基于从预检请求获得的信息来判断,是否接受接下来的实际请求。
服务器所返回的 Access-Control-Allow-Methods 首部字段将所有允许的请求方法告知客户端。该首部字段与 Allow 类似,但只能用于涉及到 CORS 的场景中。

29. es6 module和commonjs有什么不同?

(……待补充)

30. 数组方法中有哪些方法会改变原数组

splice shift unshift push pop reverse fill sort

31. es6常用的新增方法有哪些?

数组

map reduce filter find findIndex findLast findLastIndex isArray every flat flatMap fill some includes reduceRight of

对象

assign freeze seal is hasOwn keys values entries

其他

BigInt(可以用在一个整数字面量后面加 `n` 的方式定义一个 `BigInt` ,如:`10n`)
扩展运算符 模板字符串 解构赋值 空值合并运算符(??)
Set Map 
let const
Worker
fetch
async await Generator
Promise
箭头函数
for in

32. 暂时性死区是什么?

developer.mozilla.org/zh-CN/docs/…

33. 实际使用过程中,对api和请求的统一封装和处理

juejin.cn/post/712457…

34. package.js中,各种依赖版本前面的^~符号分别代表什么意思?

版本号的确定规则:

  1. 主版本号:当你做了不兼容的 API 修改
  2. 次版本号:当你做了向下兼容的功能性新增
  3. 修订号:当你做了向下兼容的问题修正 其他有关版本号的内容参阅semver.org/lang/zh-CN/

~:比如~1.2.2,表示安装1.2.x的最新版本(不低于1.2.2),但是不安装1.3.x,也就是说安装时不改变大版本号和次要版本号。
^:比如ˆ1.2.2,表示安装1.x.x的最新版本(不低于1.2.2),但是不安装2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
没有任何符号时,直接安装指定版本。

35. 闭包是什么?

闭包是由函数以及声明该函数的此法环境组合而成的,该环境包含了这个闭包创建时作用域内的任何局部变量。 闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

4. 性能优化

参考文章 juejin.cn/post/684490…

思路主要就是以下几个方面:

浏览器

  1. 按照使用的框架,针对dom结构优化(template),例如展示用的空元素,考虑使用伪元素实现,针对重复的样式部分,考虑使用一些css属性实现,比如box-shadow,可以写多组属性,合理使用可以减少dom节点。
  2. 合理使用浏览器缓存,例如sessionStorage,localStorage存储变动频率低的数据。
  3. 图片上,合理使用雪碧图、字体图标来替代直接使用高清图片的情况,展示缩略图和展示大图(根据业务场景来)时,合理使用文件服务的添加不同参数展示不同质量的图片的功能,减少传输内容体积,加快资源下载速度,增强用户体验。图片在渲染前指定大小:因为img元素是内联元素,所以在加载图片后会改变宽高,严重的情况会导致整个页面重排,所以最好在渲染前就指定其大小,或者让其脱离文档流。
  4. 条件允许的情况下使用cdn来减少图片一类资源的延迟。
  5. 减少会导致浏览器重绘和回流的可能性(上文中有介绍哪些行为可能会导致该事件的发生)。

其他

  1. 善用工程打包工具的插件,例如分包插件,tree-sharking工具,关闭一些不需要的配置,例如webpack生成map的配置(productionSourceMap)。
  2. 打包时配置开启gzip
  3. 打包后生成的文件,在服务器中对应的HTTP服务器内开启gzip,并开启HTTP2,在协议层面服用TCP连接,节省资源

5. Vue

1. vue的所有生命周期包含哪些?

vue2.xvue3含义
beforeCreatebeforeCreate(组合式API中没有,以setup替代)创建完实例之初,初始化各种事件和生命周期
createdcreated(组合式API中没有,以setup替代)初始化完各项响应式数据,methods
beforeMountbeforeMount生成虚拟dom,但并未挂载到真实dom中,相关的 render 函数首次被调用
mountedmounted已经挂载到真实的DOM中,此时可以访问$el
beforeUpdatemounted组件数据发生变化,更新之前
updatedmounted组件数据发生变化,更新后
beforeDestorybeforeUnmount组件销毁之前
destoriedunmounted组件被完全销毁

2. nextTick是用来做什么的?

Vue的响应式并不是只数据发生变化之后,DOM就立刻发生变化,而是按照一定的策略进行DOM的更新。这样的好处是可以避免一些对DOM不必要的操作,提高渲染性能。
官网介绍:

Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。

nextTick方法将回调延迟到下次DOM更新循环之后执行

3. v-if和v-show的区别?分别适合什么时候用?

v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景
v-show 适用于需要非常频繁切换条件的场景

4. v-for中key的作用是什么?

  1. key的作用主要是为了更高效的更新虚拟DOM。
  2. vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。

5. v-model用在表单上的时候,本质上和什么语法是相同的?

v-model 本质上不过是语法糖,它负责监听用户的输入事件以更新数据。 在表单元素上他等于

//大部分情况下如此,根据element的不同,部分事件不同
<el-input v-model="foo" /> 
<el-input :value="foo" @input="foo = $event" />

但是在组件上使用,如果 v-model 绑定的是响应式对象上某个不存在的属性,那么 vue 会悄悄地增加这个属性,并让它响应式。

6. 如何让你开发的组件支持 v-model

请点击此处查看完整链内容
此处是搬运了 摸鱼的春哥 的文章
在定义 vue 组件时,你可以提供一个 model 属性,用来定义该组件以何种方式支持 v-model
model 属性本身是有默认值的,如下:

// 默认的 model 属性 
export default { 
    model: { 
        prop: 'value', 
        event: 'input' 
    } 
}

也就是说,如果你不定义 model 属性,或者你按照当前方法定义属性,当其他人使用你的自定义组件时,v-model="foo" 就完全等价于 :value="foo" 加上 @input="foo = $event"
如果把 model 属性进行一些改装,如下:

// 默认的 model 属性
export default {
  model: {
    prop: 'ame',
    event: 'zard'
  }
}

那么,v-model="foo" 就等价于 :ame="foo" 加上 @zard="foo = $event"
没错,就是这么容易,让我们看个例子。
先定义一个自定义组件:

<template>
    <div>
      我们是TI{{ ame }}冠军
      <el-button @click="playDota2(1)"></el-button>
      <el-button @click="playDota2(-1)"></el-button>
    </div>
</template>
<script>
export default {
  props: {
    ame: {
      type: Number,
      default: 8
    }
  },
  model: { // 自定义v-model的格式
    prop: 'ame', // 代表 v-model 绑定的prop名
    event: 'zard' // 代码 v-model 通知父组件更新属性的事件名
  },
  methods: {
    playDota2(step) {
      const newYear = this.ame + step
      this.$emit('zard', newYear)
    }
  }
}
</script>

然后我们在父组件中使用该组件:就可以看到对应的效果了。

// template中
<dota v-model="ti"></dota>
// script中
export default {
  data() {
    return {
      ti: 8
    }
  }
}

7. 不同组件之间数据传递的方式有哪些

  1. 父子组件:数据从父组件流向子组件,事件由子组件告知父组件。数据上通过props传递数据,事件则由$emit来触发,子组件可以通过$parent来获取父组件的各种响应式数据和方法,父组件可以通过ref来获取子组件中的响应式数据和方法等,$attrs
  2. 兄弟组件:访问父组件$parent,访问根组件$root【节制地使用 $parent 和 $children - 它们的主要目的是作为访问组件的应急方法。更推荐用 props 和 events 实现父子组件通信】
  3. 一些全局状态(包含兄弟组件,没有直接关系的组件等)可以通过各种状态管理库来统一管理,例如vuex、pinia。
  4. 通过localStorage放到客户端的本地存储来统一管理。
  5. 事件总线
  6. provide/inject

8. Vue实例挂载的过程中发生了什么?

  1. 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化建立更新机制
  2. 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
  3. 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。

9. computed和watch的区别是什么,分别适合什么情况下使用?

computed

计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 响应式依赖 还没有发生改变,多次访问 它对应的 计算属性会立即返回之前的计算结果,而不必再次执行函数。使用时,需要明确返回一个计算后的值。

watch

当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。例如搜索输入框,当用户输入内容超过某一个时间段没有更新时,去发起一个请求来获取对应的搜索结果,这种是computed无法做到的,就更适合使用watch来实现。

分别适合的情况

  1. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性。
  2. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deep(请谨慎使用,可能存在性能问题)、immediate等选项。比如需要立即执行的,可以使用以下方式来实现
watch:{
    current:{
        handler(newValue,oldValue){
            //do something
        },
        immediate: true //在侦听器创建时立即触发回调
    }
}

其他

watch 和 watchEffect有什么差异?
watchEffect()立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。第一个参数就是要运行的副作用函数,这个副作用函数的参数也是一个函数,用来注册清理回调;第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖;返回值是一个用来停止该副作用的函数。默认情况下,侦听器将在组件渲染之前执行。设置 flush: 'post'(默认为pre) 将会使侦听器延迟到组件渲染之后再执行。也可以设置为sync不过应该谨慎使用。
与 watchEffect() 相比,watch() 使我们可以:

  • 懒执行副作用;
  • 更加明确是应该由哪个状态触发侦听器重新执行;
  • 可以访问所侦听状态的前一个值和当前值。(immediate:第一次调用时旧值是 undefined)

watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

10. 对vue 响应式数据的理解

数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制

  1. MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
  2. 以vue为例说明,通过数据响应式加上虚拟DOM和patch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。
  3. vue2中的数据响应式会根据数据类型来做不同处理,如果是对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题。
  4. 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6的Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了。

11. 对虚拟dom的理解

虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构。

  1. 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
  • 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
  • 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流。
  1. vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。 过程
  2. 挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图。

12. Vue 子组件和父组件创建和挂载顺序

  1. 创建过程自上而下,挂载过程自下而上;即:
  • parent created
  • child created
  • child mounted
  • parent mounted
  1. 之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

13. Vue 修饰符相关问题

事件修饰符

  1. .stop 阻止单击事件继续传播
  2. .prevent 提交事件不再重载页面
  3. .capture 添加事件监听器时使用事件捕获模式
  4. .self 只当在 event.target 是当前元素自身时触发处理函数
  5. .once 点击事件将只会触发一次
  6. .passive 不想阻止事件的默认行为

需要按照顺序进行使用,相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。

按键修饰符

  1. .enter
  2. .tab
  3. .delete (捕获“删除”和“退格”键)
  4. .esc
  5. .space
  6. .up
  7. .down
  8. .left
  9. .right

系统修饰键

  1. .ctrl
  2. .alt
  3. .shift
  4. .meta

.exact 修饰符

控制由精确的系统修饰符组合触发的事件,也就是说控制仅由某个指定的按键被按下时才会触发。

鼠标按钮修饰符

  1. .left
  2. .right
  3. .middle

这些修饰符会限制处理函数仅响应特定的鼠标按钮。

14. VUE router中的两种路由模式分别是什么?区别是什么?路由守卫有哪些?

createWebHistory() 创建一个 HTML5 历史模式
createWebHashHistory() 创建一个 hash 历史模式

概述:

hash模式

  1. hash 模式是一种把前端路由的路径用井号 # 拼接在真实 url 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 onhashchange 事件。- hash变化会触发网页跳转,即浏览器的前进和后退。
  2. hash 可以改变 url ,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url
  3. hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。
  4. hash 永远不会提交到 server 端(可以理解为只在前端自生自灭)。

history模式

  1. 新的 url 可以是与当前 url 同源的任意 url ,也可以是与当前 url 一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中。
  2. 通过 history.state ,添加任意类型的数据到记录中。
  3. 可以额外设置 title 属性,以便后续使用。
  4. 通过 pushStatereplaceState 来实现无刷新跳转的功能。

但同样的,history模式也带了一些问题,比如 对于 history 来说,确实解决了不少 hash 存在的问题,但是也带来了新的问题。具体如下:

  • 使用 history 模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。
  • 而对于 hash 模式来说, 它虽然看着是改变了 url ,但不会被包括在 http 请求中。所以,它算是被用来指导浏览器的动作,并不影响服务器端。因此,改变 hash 并没有真正地改变 url ,所以页面路径还是之前的路径, nginx 也就不会拦截。
  • 因此,在使用 history 模式时,需要通过服务端来允许地址可访问,如果没有设置,就很容易导致出现 404 的局面。

关于路由的两者描述,来源于作者:星期一研究室
链接:juejin.cn/post/699384…

区别

  1. createWebHistory()模式应用程序必须通过 http 协议被提供服务,注重SEO的用这种方式;而createWebHashHistory()不一定需要(可以用file协议)
  2. createWebHashHistory()模式路径中包含#符号。
  3. 本质依赖的API不同:
  • createWebHashHistory():依赖hashchange事件来解析不同路由
  • createWebHistory():依赖History的API来解析不同路由
  1. 其他:参考以上概述中的内容

路由守卫

全局的(每次导航时都会触发)
  1. 全局前置守卫:router.beforeEach
  2. 全局解析守卫:router.beforeResolve
  3. 全局后置钩子:afterEach
单个路由独享的
  1. 路由独享的守卫:beforeEnter
组件级的
  1. beforeRouteEnter
  2. beforeRouteUpdate
  3. beforeRouteLeave
其他

组合式api的守卫:beforeRouteLeave beforeRouteUpdate(使用时需要添加前缀on

顺序和流程

微信图片_20221009170123.png

15. vue router中的动态路由是什么?如何使用?

很多时候,我们需要将给定匹配模式的路由映射到同一个组件。例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。

{ path: '/users/:id', component: User }
//此时, `/users/johnny` 和 `/users/jolyne` 这样的 URL 都会映射到同一个路由

路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。 同一个路由中设置有多个 路径参数,它们会映射到 $route.params 上的相应字段。 需要注意的是,使用动态路由时,同一组件在参数不同的情况下,是不会销毁当前组件实例并再次创建的,因此组件的生命周期也不会再次被调用,要处理这种情况,我们应该要对同一个组件中参数的变化做出响应的话,你可以简单地 watch $route 对象上的任意属性。

watch:{
    '$route.params':{
        handler(toParams, previousParam){
            //do sth
        }
    }
}

也可以使用组件内的导航守卫beforeRouteUpdate来处理。

16. 实现一个路由的思路

  1. 定义一个函数用来返回路由的实例
  2. 这个函数接受一系列的配置项(参考vue router的routerOptions),根据不同的配置项实现具体的功能。
  3. 根据选择的路由形式(hash,history)的不同,监听url的变化并根据url中对应的path进行components的切换(创建、销毁、更新等)
  4. 实现路由的导航守卫,例如:全局守卫、组件内的守卫、单个路由级别的守卫
  5. 实现路由容器组件(如router-view,router-link)
  6. 如果以vue插件形式提供路由,则需要实现install方法(用于Vue.use方法)

17. Vue2.x和Vue3的底层对响应式的实现方式有什么区别?

1. 2.x

底层的实现方式是使用Object.deineProperty对数据进行get和set,如果数据试一个对象,那么就需要层层遍历所有的属性,进行get和set,对一些类似于数组的方法还存在一些使用上的限制,比如通过下标修改数组中的某一个值,是无法被检测到的。

2. 3

使用Proxy来代理某一个对象,这样就不存在2.x中讯在的一些问题,类似于数组处理、新增需要监听的对象时需要手动$set的情况。

18. 使用Vue过程中可以做的性能优化?

  1. keep-alive
  2. 路由懒加载
  3. Preload / Prefetch
  4. key
  5. 响应式依赖扁平化
  6. 减少非必要的响应式依赖
  7. Object.freeze
  8. DOM 滚动加载
  9. v-if / v-show
  10. computed / watch
  11. 事件销毁(防止内存泄漏)
  12. 组件按需引入
  13. 预渲染
  14. 按需 Polyfill
  15. 模板预编译
  16. 渲染函数
  17. 官方风格指南 + 官方 ESLint 插件

6. 其他规范类

1. git使用规范

(……待补充)