面试题:
自我介绍,说说你做的项目和亮点,以及难点
- 工作经历
- 技术栈
- 为什么要跳槽
介绍:
- 在两家公司工作过,都是外企,都是采用的敏捷开发模式。前一家是外包,现在这家是自研。
- 第一家公司主要是做安防系统,美国911报警系统,window桌面开发后端服务,用到的.net相关的技术栈,wpf,wcf,.net core等,中间自愿跨项目帮忙,在此期间跨项目做过两个月的小程序开发。
- 在第二家公司,主要是做iot的智能工厂项目的,做过一段时间API发开,数据库使用的是elasticsearch,后来因为iot数据源都在国外,不好调试,武汉这边所有后端同事都转型到前端开发了,然后研究react的技术栈,采用typescript开发前端业务和组件。然后这三年参与两个solution的, 大部分是表单业务,echart图表,机器状态,传感器实时监控websocket实现,邮件订阅模块,还有一个adminsite的单点登录后来管理和公共组件库的维护。
- classcomponent转化成functioncompoennt
- 构建一套基于mui的自定义主题组件库,参与整个过程,包括组件分包,rollup打包,ci/cd发版过程中changelog采集,version控制,release tag等,都是用的node脚本写的。
- 中间随着业务组件的体量增大,solution想自定义布局等,钻研过一段时间的微前端,写过demo。
业余时间的话,我也会研究一些技术:java,java的三大框架还有springboot,我自己的站点就是采用这些做的,python做一些爬虫采集数据,做一些分析啊,数据库方面方面比较熟悉的mysql,sqlserver,elasticsearch。
至于为啥跳槽,目前这家公司业务都很熟了,然后也没什么技术难度,想走出舒适区。
你做过哪些网站重构,及性能提升方面的优化相关工作
代码重构:
之前react用的class component,现在全部转成function component了
之前都是javascript,现在全部转成typescript
ajax调用之前都写在具体文件,现在都抽离在对应的api文件
对所有的Exception都加上了handle的逻辑
websocket多并发造成页面卡死
script脚本简化log日志
性能优化:
- 做icon库的时候,做了一个公共的icon,然后属性传参的方式加载icon。随着icon的增加,很多其它的solution也加入了图标,solution里面使用icon,很多都没有用到,也被打包进去了。然后我做了两件事:1)icon按分类打包,比如菜单类型,机器类型(业务相关的图),这样你在solution里面使用,只import你需要的,携带对应的属性,这样只会打包同一类的。2)要求team里面尽量import具体的Icon图标。修改之后经过treeshaking打包,体积小了1M。
- React开发时,首页加载很慢,React使用Suspense 用于包裹路由组件,然后进行lazyload去加载。这样每个lazyLoad会被分拆成不同的bundle,在不同的页面加载的时候开始下载。
npm i-D webpack-bundle-analyzer
在node_module的react-script的config文件夹中找到webpack.config.js追加以下代码就可以在每次build的时候分析bundle大小了了。const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin({ analyzerPort:8888 // 默认监听8888端口 }) ] }
项目相关的问题,怎么做的,架构为什么这么做,有没有改进的空间?
模块化加载方案有哪些?ES6模块话相对与CommonJS有啥区别?
没有ES6之前,模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。CommonJS 模块就是对象,输入时必须查找对象属性,这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。ES6模块可以执行类型检测等静态分析能力。编译打包时按需获取,缩小体积。类似Python和java 的import package。
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用
require()
和module.exports
,ES6 模块使用import
和export
。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。CommonJS 加载的是一个对象(即
module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。由于 ES6 输入的模块变量是只读的,对它进行重新赋值会报错。
export
通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。commonjs
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; // main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3
ES6 module
// lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4
JS异步加载defer
与async
的区别?
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
对web的安全性方面的了解?比如xss,xsrf/csrf
跨站脚本攻击(也称为XSS(cross-site scripting 的缩写),为了和 CSS 区分,故叫它XSS)
指利用网站漏洞从用户那里恶意盗取信息。它允许恶意web用户将代码植入到提供给其它用户使用的页面中。比如这些代码包括HTML代码、CSS样式和客户端脚本js。
- 反射型(Reflected XSS)发出请求时,XSS代码出现在url中,作为输入提交到服务器端,服务器端解析后响应,XSS代码随响应内容一起传回给浏览器,最后浏览器解析执行XSS代码。这个过程像一次反射,所以叫反射型XSS。
- 存储型 Stored XSS和Reflected XSS的差别就在于,具有攻击性的脚本被保存到了服务器端(数据库,内存,文件系统)并且可以被普通用户完整的从服务的取得并执行,从而获得了在网络上传播的能力。
- DOM型(DOM-based or local XSS)即基于DOM或本地的XSS攻击:其实是一种特殊类型的反射型XSS,它是基于DOM文档对象模型的一种漏洞。可以通过DOM来动态修改页面内容,从客户端获取DOM中的数据并在本地执行。基于这个特性,就可以利用JS脚本来实现XSS漏洞的利用。
举例:
在文章中发表评论的时候偷偷插入一段< script>…< /script>代码,然后别人在访问这个文章的时候就会执行这段代码了,如果攻击代码中为获取cookie发送到攻击者服务器,那么攻击者就能拿到这个数据了。这就是恶意获取别人的信息。
发布博客,在博客中插入脚本代码,别人在查看博客内容时就会执行这段脚本代码,这种不正常操作的脚本代码就会影响别人的网站。
1. 通过注入的标签事件触发<body onload="alert('xss')"></body> <p onclick="alert('1')" style="postion:fixed;width:100%;heith:100%"> </p> <input onfocus="alert('1')" autofocus/>2. 通过注入的带有src属性的标签触发 <iframe src="http://www.xss.com/xss.html"></iframe> <iframe src="data:text/html,<script>alert('1')</script>"></iframe> <iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnMScpPC9zY3JpcHQ+"></iframe>
防范:
- 对用户提交的内容进行过滤,前端对关键符号进行转义,如script标签的尖括号,’<’ 替换为 ‘
<
’ ,’>’ 替换为 ‘>
’ 等再提交给后台- 过滤用户的事件属性(如onclick等)、过滤不安全的style标签、script标签、iframe标签等
跨站点请求伪造(也称为XSRF或CSRF(Cross-site request forgery))
指的是黑客诱导用户点击链接,打开黑客的网站,然后黑客利用用户目前的登录状态发起跨站请求。
攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
举例:
- 你已经登录过一个购物网站,正在浏览商品
- 该网站的付费接口是 xxx.com/pay?money=100&toSb=XXX 但是付费时没有任何验证(当然这只是假设,现在很多电商网站验证性都做得比较安全了)
- 然后你收到了一封邮件,邮件有一个图片你点击了,图片地址隐藏为< img src=“xxx.com/pay?money=100&toSb=黑客” />
- 当你点开邮件图片的时候,就已经悄悄付款给黑客了,因为token没过期,连接也正常。
防范:
- 不要相信不知来源的外链,当你点击了网站A的时候不要乱点击其他外链来跳转到网页A。
- 网站上增加验证流程(如输入指纹,密码,短信验证)
- token验证、referer验证、隐藏令牌(百度学习下)
let,const的暂时性死区?
let和const都是块级作用域,不存在变量前提
const是声明常量,不允许改变。但是const定义的是一个对象,keep的仅仅是对象的地址,对象内的属性依旧可以被改变。
var作用域为函数作用域
暂时性死区指的是在被let或const定义的变量,在该变量被声明之前无法被访问,会报错
对MutationObserver的理解及应用
MutationObserver
接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分。
disconnect()
阻止 MutationObserver
实例继续接收的通知,直到再次调用其observe()
方法,该观察者对象包含的回调函数都不会再被调用。observe()
配置MutationObserver
在DOM更改匹配给定选项时,通过其回调函数开始接收通知。takeRecords()
从MutationObserver的通知队列中删除所有待处理的通知,并将它们返回到MutationRecord
对象的新Array
中。
大文件上传断点续传
大文件上传
- 前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片
- 服务端接收切片并存储,收到合并请求后使用流将切片合并到最终文件
- 原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听
- 使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度
断点续传
- 使用 spark-md5 根据文件内容算出文件 hash
- 通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)
- 计算
hash
耗时的问题,不仅可以通过web-workder
,还可以参考React
的FFiber
架构,通过requestIdleCallback
来利用浏览器的空闲时间计算,也不会卡死主线程- 通过 XMLHttpRequest 的 abort 方法暂停切片的上传
- 上传前服务端返回已经上传的切片名,前端跳过这些切片的上传
AST是什么
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
babel-cli开始读取我们的参数(源文件test1.js、输出文件test1.babel.js、配置文件.babelrc)
babel-core根据babel-cli的参数开始编译
Babel Parser 把我们传入的源码解析成ast对象
Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点(也就是结合我们传入的插件把es6转换成es5的一个过程)
Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。
HTTP建立连接的过程
HTTP 连接建立过程
我们来看一下在浏览器输入 URL 后获取 HTML 页面的过程。
- 先通过域名系统(Domain Name System,DNS)查询将域名转换为 IP 地址。即将
test.com
转换为221.239.100.30
这一过程。- 通过三次握手(稍后会讲)建立 TCP 连接。
- 发起 HTTP 请求。
- 目标服务器接收到 HTTP 请求并处理。
- 目标服务器往浏览器发回 HTTP 响应。
- 浏览器解析并渲染页面。
三次握手建立连接:
TCP 标准规定,ACK 报文段可以携带数据,但不携带数据就不用消耗序号。
- 客户端发送一个不包含应用层数据的 TCP 报文段,首部的 SYN 置为 1,ACK为0,随机选择一个初始序号X(一般为 0)放在 TCP 报文段的序号字段中。(SYN 为 1 的时候,不能携带数据,但要消耗掉一个序号)
- TCP 报文段到达服务器主机后,服务器提取报文段,并为该 TCP 连接分配缓存和变量。然后向客户端发送允许连接的 ACK 报文段(不包含应用层数据)。这个报文段的首部包含 4 个信息:SYN 置为 1,ACK 置 为 1;确认号字段置为客户端的序号X + 1;随机选择自己的初始序号Y(一般为 0)。
- 收到服务器的 TCP 响应报文段后,客户端也要为该 TCP 连接分配缓存和变量,并向服务器发送一个 ACK 报文段。这个报文段将服务器端的序号Y + 1 放置在确认号字段中,用来对服务器允许连接的报文段进行响应,顺序号为X+1,因为连接已经建立,所以 SYN 置为 0。然后建立连接。
TCP 四次挥手拆除连接:
FIN 报文段即使不携带数据,也要消耗序号。
- 客户端发送一个 FIN 置为 1 的报文段。
- 服务器回送一个确认报文段。
- 服务器发送 FIN 置为 1 的报文段。
- 客户端回送一个确认报文段。
TCP 为什么是四次挥手,而不是三次?
- 当 A 给 B 发送 FIN 报文时,代表 A 不再发送报文,但仍可以接收报文。
- B 可能还有数据需要发送,因此先发送 ACK 报文,告知 A “我知道你想断开连接的请求了”。这样 A 便不会因为没有收到应答而继续发送断开连接的请求(即 FIN 报文)。
- B 在处理完数据后,就向 A 发送一个 FIN 报文,然后进入 LAST_ACK 阶段(超时等待)。
- A 向 B 发送 ACK 报文,双方都断开连接。
对HTTP1,2,3的理解
HTTP 版本发布时间表
- HTTP 0.9 — Time Berners-Lee 于 1991 年发布了 HTTP 的第一个文档版本。它由一行包含 GET 方法和所请求文档的路径组成。响应同样简单,返回一个没有标题或任何其他元数据的超文本文档。
- HTTP 1.0 — 1.0 版于 1996 年获得官方认可,恰逢 HTML 规范和“网络浏览器”的快速发展。主要增加的是“请求标头”和“响应标头”。此外,新的响应标头允许多种文件类型,例如 HTML、纯文本、图像等。
- HTTP 1.1 — 1.1 版于 1997 年发布并成为 Internet 标准。这个版本增加了许多性能增强,例如保持连接、缓存机制、请求管道、传输编码和字节范围请求。这个新版本更好,并消除了 HTTP/1.0 中的许多歧义。
- HTTP 2.0 — 由 Internet 工程任务组 (IETF) 于 2015 年 2 月发布,专注于提高 HTTP 的性能。本文更详细地介绍了该版本的主要变化。
- HTTP 3 — 基于 QUIC 协议的新 HTTP/3 预计将于 2019 年底发布。我将在本文末尾简要讨论 HTTP/3。
HTTP/1.1 的问题:
- HTTP (HOL) 线头阻塞问题
- 未实现请求多路复用的请求流水线
- 打开多个 TCP 连接以请求多个资源
- 数据传输的文本性质
- 长 HTTP 标头
- 克服上述 HTTP 问题的多种解决方法(域分片、Spriting 等)
- 网页加载速度慢
- 对于同一个域名,默认允许同时建立 6 个 TCP持久连接,在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态
由于报文Header一般会携带"User Agent""Cookie""Accept""Server"等许多固定的头字段(如下图),多达几百字节甚至上千字节,但Body却经常只有几十字节,一是增加传输成本,二是大量重复。 所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。- 不支持服务端推送
HTTP/2 的特点
- 它是一个二进制协议而不是文本协议,因此消除了与 HTTP1.x 的文本性质相关的安全问题,例如响应拆分攻击
- HTTP/2 完全复用
- 它使用头压缩 HPACK 来减少开销大小
- 它允许服务器主动将响应“推送”到客户端缓存中,而不是等待每个资源的新请求
- 它减少了 额外的往返时间 (RTT),使您的网站加载速度更快,无需任何优化。
- HTTP/2 支持响应优先级、流量控制和 TLS 的有效处理
- 解析数据的低开销 — HTTP/2 与 HTTP1 中的关键价值主张。
- 减少网络延迟并提高吞吐量
- 更不容易出错和更轻的网络占用空间
- 它被浏览器广泛支持。作为一项基本的互联网技术,当前版本的浏览器必须支持 HTTP/2 协议才能正常工作
- 1).在应用层使用二进制分帧方式传输,服务器单位时间接收到的请求数变多,可以提高并发数,支持了多路复用;2)多路复用,就是在一个 TCP 连接中可以存在多个流(流由帧组成),允许在一个连接上无限制并发流,因为请求在一个通道上,TCP 效率更高;3)高优先级的请求会被优先处理。
第一次请求发送所有所需头部信息,后面每次请求和响应只发送差异头部,一般可以达到 50%~90% 的高压缩率- 支持使用 HTTPS 进行加密传输。
- 支持在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟,这被称为"服务器推送"
问题:
1.TCP 以及 TCP+TLS 建立连接的延时;2.TCP 的队头阻塞并没有彻底解决;3.多路复用导致服务器压力上升;4.多路复用容易 Timeout
HTTP/3的特点
不使用 TCP,而是使用 Google 的 QUIC 协议。HTTP/3 最初被称为 HTTP-over-QUIC。HTTP/3 还包括 TLS 1.3 加密,因此不需要像今天那样将安全性固定在协议上的单独 HTTPS。
QUIC 最初代表“快速UDP互联网连接”。此协议旨在比 TCP 更快且延迟更低。QUIC 在建立连接时提供更少的开销,并通过连接更快地传输数据。与 TCP 不同的是,一条数据在此过程中丢失之类的错误不会导致连接停止并等待问题得到修复。在问题解决期间,QUIC 将继续传输其他数据。
1.由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现 0-RTT 或者 1-RTT 来建立连接,可以大大提升首次打开页面的速度;2.集成了 TLS 1.3 加密,在完全握手情况下,需要 1-RTT 建立连接。 TLS1.3 恢复会话可以直接发送加密后的应用数据,不需要额外的 TLS 握手,也就是 0-RTT。3.QUIC 是为多路复用从头设计的,携带个别流的的数据的包丢失时,通常只影响该流。4.TCP 是按照四要素(客户端 IP、端口, 服务器 IP、端口)确定一个连接的。而 QUIC 则是让客户端生成一个 Connection ID (64 位)来区别不同连接。只要 Connection ID 不变,连接就不需要重新建立,即便是客户端的网络发生变化1.改进的拥塞控制、可靠传输,应用程序层面就能实现不同的拥塞控制算法;2.单调递增的 Packet Number — 使用 Packet Number 代替了 TCP 的 seq,避免重传引起的二义性;3.不允许 Reneging — 一个 Packet 只要被 Ack,就认为它一定被正确接收;4.前向纠错(FEC);5.更多的 Ack 块和增加 Ack Delay 时间,在丢包率比较高的网络下,可以提升网络的恢复速度,减少重传量。
对package.json里面的dependencies, devDependencies, peerDependencies的理解?
dependencies:
项目要跑起来必须依赖的包
devDependencies:
开发过程中用到的辅助包,比如插件,babel等
peerDependencies:
是用来防止多次引入相同的库,如果在package1和 package2中都加上dependencies的packageBase,那么package1和package2会再下载一遍packgeBase
├── Project │ └── node_modules │ ├── packageBase │ ├── package1 │ │ └── nodule_modules │ │ └── packageBase │ └── package2 │ │ └── nodule_modules │ │ └── packageBase
当我们在package1和 package2中都加上peerDependencies的packageBase,效果如下
├── Project │ └── node_modules │ ├── packageBase │ ├── package1 │ └── package2
线程,进程,协程
进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。线程的引入进一步提高了操作系统的并发性,线程能并发执行。同一个进程的多个线程共享进程的资源。如果线程使用不当,会引起竞争,死锁问题。
协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。例如react中的fiber结构。
并发,并行,单核,多核
并发: 当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
并行: 当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
一个进程的线程都是串行(用户态多线程中),不同的进程可以并行(多核处理器中)。在用户态多线程中同一个进程下的多个线程不可以并行运行,不管多少核处理器,它的线程只能交替顺序运行。你可以把一个用户进程看做是一个人,线程是他要处理的事,cpu核心看做是办事处窗口,一个人再nb也不可能同一刻时间到两个以上窗口办理不同的事
TCP,UDP,HTTP
TCP是一种面向连接的、可靠的传输层协议;
TCP协议建立在不可靠的网络层 IP 协议之上,IP协议并不能提供任何可靠性机制,TCP的可靠性完全由自己实现;
TCP采用的最基本的可靠性技术是:确认与超时重传机制、流量控制机制;UDP是一种无连接的、不可靠的传输层协议;
提供了有限的差错检验功能;
目的是希望以最小的开销来达到网络环境中的进程通信目的;HTTP(超文本传输协议)是利用TCP在两台电脑(通常是Web服务器和客户端)之间传输信息的协议。客户端使用Web浏览器发起HTTP请求给Web服务器,Web服务器发送被请求的信息给客户端。
对Promise的理解
Promise是一种异步编程的解决方案,解决了之前旧时代嵌套回调,造成的回调地狱问题。promise采用链式调用的方法来代替嵌套调用。
Promise里面有三种状态,pending状态也就是初始状态,fullfiled状态表示成功状态,rejected状态表示失败状态。
Promise对象的构造函数需要传入一个执行器方法,这个执行器的参数又是两个函数,分别对应着resolve和reject。当Promise的构造函数被调用时,会立即执行这个执行器,并且将执行器的两个参数分别绑定到内部定义好的两个触发器resolve和reject函数上。也就是说现在这个执行器的两个参数变成了触发器,一旦调用会触发内部代码的执行。
Promise的then方法也有两个参数,可以说时两个回调函数,一个是onFullfilled,一个是onRejected,当then方法被调用时,会将它们的回调函数放在回调队列里面。当resolve被触发时,将状态改为fullfilled,并且循环回调队列调用callback里面的onFullfilled。那么如何实现的链式调用呢?then每次都返回一个新的Promise。
整体上来说,Promise这种思想时基于发布-订阅模式的一种实现。
Object.defineProperty和Proxy有什么区别
Object.defineProperty(obj, prop, descriptor)
参数
obj: 要在其上定义属性的对象。
prop: 要定义或修改的属性的名称。
descriptor: 将被定义或修改的属性的描述符。
第三个参数descriptor内的属性描述:
1、简单点就是 设置属性的值value,
2、是否可操作属性值 writable,
3、是否可修改配置属性configurable,如果值为false,则descriptor内的所有属性都不可操作)
4、是否可枚举的属性enumerable
5、另外descriptor还有一对核心方法setter,getter,vue2.0也正是利用这对方法实现了数据的双向绑定。当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。
var obj = {}, value = null;Object.defineProperty(obj, "num", { enumerable:true,//是否可枚举 writable:true,//是否可写 configurable:true,//是否可配置 get: function(){ console.log('执行了 get 操作') return value; }, set: function(newValue) { console.log('执行了 set 操作') value = newValue; } }) obj.value = 1 // 执行了 set 操作 console.log(obj.value); // 执行了 get 操作 // 1
let proxy = new Proxy(target, handler);
Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)
let obj = {a : 1} let proxyObj = new Proxy(obj,{ get: function (target,prop) { return prop in target ? target[prop] : 0 }, set: function (target,prop,value) { target[prop] = 888; }}) console.log(proxyObj.a); // 1 console.log(proxyObj.b); // 0 proxyObj.a = 666; console.log(proxyObj.a) // 888
defineProperty和Proxy对比
从实现效果上讲Object.definety和Proper对数组和对象的表现是一致的,那么它和 Proxy 对比存在哪些缺点呢?
Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。Object.defineProperty对新增属性需要手动进行Observe。
由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象(改变属性不会自动触发setter),对其新增属性再使用 Object.defineProperty 进行劫持。
也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
3.defineProperty会污染原对象(关键区别)
proxy去代理了ob,他会返回一个新的代理对象不会对原对象ob进行改动,而defineproperty是去修改元对象,修改元对象的属性,而proxy只是对元对象进行代理并给出一个新的代理对象。
event对象target和currentTarget的区别
event.target返回触发事件的元素
event.currentTarget返回绑定事件的元素
比如ul>li,在ul上绑定点击事件,但是点击li是,触发事件的是li,绑定事件的是ul
浏览器线程有哪些:
浏览器的渲染进程是多线程的。js是阻塞单线程的。
浏览器包含有以下线程:
1.GUI渲染线程
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。2.JS引擎线程
也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
JS引擎线程负责解析Javascript脚本,运行代码。
JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。3.事件触发线程
归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)4.定时触发器线程
传说中的setInterval与setTimeout所在线程
浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。5.异步http请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
js 编译原理,介绍下 AST
将源代码作为纯文本解析为 抽象语法树(abstract syntax tree, AST) 的数据结构。
AST 不仅以结构化的方式显示源代码,而且在语义分析中扮演着重要角色。在语义分析中,编译器验证程序和语言元素的语法使用是否正确。之后,使用 AST 来生成实际的字节码或者机器码。
抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。
AST 不仅仅是用于语言解释器和编译器,在计算机世界中,它们还有多种应用。使用它们最常见的方法之一是进行静态代码分析。静态分析器不执行输入的代码,但是,他们仍然需要理解代码的结构。
事件循环机制,宏任务,微任务
node 是单线程,为什么能处理高并发
- 每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。
- 主线程之外,还维护了一个"事件队列"(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
- 主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
- 主线程不断重复上面的第三步。
单线程的好处:
- 多线程占用内存高
- 多线程间切换使得CPU开销大
- 多线程由内存同步开销
- 编写单线程程序简单
- 线程安全
单线程的劣势:
- CPU密集型任务占用CPU时间长(可通过cluster方式解决)
- 无法利用CPU的多核(可通过cluster方式解决)
- 单线程抛出异常使得程序停止(可通过try catch方式或自动重启机制解决)
跨域的解决方式,当然主要的就是 jsonp cors 这两种了
浏览器session,token,cookie,localStoage,sessionStorage,indexDB,Mainfest,PWA, ServiceWroker, Web Worker, MessageChannel, Background Service(Background Fetch, Background Sync, Notificaitions, Reporting API, Push Message)
session: 当浏览器第一次访问服务器时,服务器创建一个session对象(该对象有一个唯一的id,一般称之为sessionId),服务器会将sessionId以cookie的方式发送给浏览器。当浏览器再次访问服务器时,会将sessionId发送过来,服务器依据sessionId就可以找到对应的session对象(session一般会被持久化到数据库,避免服务端重启导致session消失)。Session对象包括用户的一些信息,也包括过期时间(tomcat默认30分钟),用户每次操作会传入sessionid,后端更新session过期时间。假如30分钟都没有任何操作,服务器会清除session。此后用户再请求时,找不到sessionid,就会把你踢到登录页面。
cookie: 存储在客户端的,记录用户信息,由key=value存放,cookie的数据4k左右, cookie中还含有max-age的过期时间,cookie由后端set-cookie设置的。默认cookie存放在内存,Cookie在浏览器关闭后就会消失,如果设置maxAge,则会存放到硬盘,关闭浏览器也不会消失。
token: 服务端生成的类似session的一种令牌,session的验证需要查数据库。而token是一种更高明的方式,token在服务端生成之后可以不用存储,发给客户端即可。客户端拿着token,发给服务器,服务器自然可以辨别真伪。这里采用了对称加密算法,先将JWT生成的对象(包括一些用户基本字段)进行hash摘要,再使用只存在服务器的密钥进行加密,这样就生成了一个签名(token)。前端拿到这个token,发给后端,后端用密钥能解出来就认为是ok的。
localStorage:客户端本地一个5M大小的持久化存储空间,通过localStorage.setItem/ getItem设置获值。比如token就可以放在localStorage,用户的信息等。还有react的redux等。受同源策略限制,只在当前域名,IP,端口共享。
sessionStorage:大小为 5M 左右,sessionStorage保存的数据用于浏览器的一次会话,只在当前窗口有效,当会话结束(通常是该窗 口关闭),无法像localStorage一样在同域名的不同窗口下共享,通常可以保存刷新页面也不想清空的东西,比如表单信息不丢失。
indexedDB: 是一种低级 API,用于客户端存储大量结构化数据(包括文件和 blobs),是一个运行 在浏览器上的非关系型数据库,数据以"键值对"的形式保存, 每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出 一个错误。
manifest:首先manifest是一个后缀名为minifest的文件,在文件中定义那些需要缓存的文件,支持manifest的浏览器,会将按照manifest文件的规则,像文件保存在本地,从而在没有网络链接的情况下,也能访问页面。
- 离线浏览: 用户可以在离线状态下浏览网站内容。
- 更快的速度: 因为数据被存储在本地,所以速度会更快.
- 减轻服务器的负载: 浏览器只会下载在服务器上发生改变的资源。
PWA:
PWA(Progressive Web App) 架构,它旨在不丢失 Web 开放特性的前提下,让 Web 应用能够以渐进的形式撕掉浏览器的标签,最终抹平与原生应用的差异。其主要特点有:
- 可通过 Manifest 配置文件实现将应用添加到主屏幕,以解决应用入口依赖浏览器的问题。
- 借助 Service Worker、离线存储、后台同步等技术来提供离线处理能力。
- 通过推送通知、蓝牙、支付等新接口来突破浏览器限制,从而达到集成底层系统功能。
Service Worker:
Service Worker 是一个 基于HTML5 API ,也是PWA技术栈中最重要的特性, 它在 Web Worker 的基础上加上了持久离线缓存和网络代理能力,结合Cache API面向提供了JavaScript来操作浏览器缓存的能力,这使得Service Worker和PWA密不可分。
- 一个独立的执行线程,单独的作用域范围,单独的运行环境,有自己独立的context上下文。
- 一旦被 install,就永远存在,除非被手动 unregister。即使Chrome(浏览器)关闭也会在后台运行。利用这个特性可以实现离线消息推送功能。
- 处于安全性考虑,必须在 HTTPS 环境下才能工作。当然在本地调试时,使用localhost则不受HTTPS限制。
- 提供拦截浏览器请求的接口,可以控制打开的作用域范围下所有的页面请求。需要注意的是一旦请求被Service Worker接管,意味着任何请求都由你来控制,一定要做好容错机制,保证页面的正常运行。
- 由于是独立线程,Service Worker不能直接操作页面 DOM。但可以通过事件机制来处理。例如使用postMessage。
Web Worker:
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用
XMLHttpRequest
执行 I/O,WebSockets,IndexedDB (尽管responseXML
和channel
属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码。workers和主线程间的数据传递通过这样的消息机制进行——双方都使用postMessage()方法发送各自的消息,使用onmessage事件处理函数来响应消息(消息被包含在Message
事件的data属性中)。这个过程中数据并不是被共享而是被复制。
- 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
- JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
- JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
用法:
main.js
myWorker = new Worker(./worker.js)
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
}
input.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
}worker.js
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
self.postMessage(workerResult);
}MessageChannel:
多个web worker并想要在两个web worker之间实现通信的时候,MessageChannel就可以派上用场.
var w1 = new Worker("worker1.js");
var w2 = new Worker("worker2.js");
var ch = new MessageChannel();
w1.postMessage("initial port",[ch.port1]);
w2.postMessage("initial port",[ch.port2]);
w2.onmessage = function(e){
console.log(e.data);
}iframe兄弟间通
var iframe1 = document.getElementById('iframe1');
iframe1.postMessage(message, '*');MessageChannel端口通信
const channel = new MessageChannel();
let port1 = channel.port1;
let port2 = channel.port2;
port1.onmessage = function (event) {
console.log("port1收到来自port2的数据:" + event.data);
};
port2.onmessage = function (event) {
console.log("port2收到来自port1的数据:" + event.data);
};
port1.postMessage("发送给port2");
port2.postMessage("发送给port1");
缓存机制,主要是问 304 和 强缓存, 协商缓存
- cache-control:max-age="xxxx"算是强缓存的一个标志。强缓存(max-age="设置缓存的时间")。意思就是只要时间没超时,就会在请求的时使用缓存的文件,不会重新请求资源。浏览器先根据这个资源的http头信息来判断是否命中强缓存。如果命中则直接加在缓存中的资源,并不会将请求发送到服务器。(强缓存)
- 若未命中强缓存,则浏览器会将请求发送至服务器。服务器根据http头信息中的Last-Modify/If-Modify-Since或Etag/If-None-Match来判断是否命中协商缓存。如果命中,则http返回码为304,浏览器从缓存中加载资源(协商缓存)
- 如果未命中协商缓存,则服务器会将完整的资源返回给浏览器,浏览器加载新资源,并更新缓存。(新的请求)
协商缓存:它的触发条件有两点、
第一点是 Cache-Control 的值为 no-cache,则会促发协商缓存。
第二点是 max-age 过期了,触发协商缓存。
后端需要怎么设置
以nodejs为例,如果需要浏览器强缓存,可以这样设置:
res.setHeader('Cache-Control', 'public, max-age=xxx');如果需要协商缓存,则可以这样设置:
res.setHeader('Cache-Control', 'public, max-age=0'); res.setHeader('Last-Modified', xxx); res.setHeader('ETag', xxx);