简答题
HTML
Doctype作用?标准模式与兼容模式各有什么区别?
> 声明位于HTML文档中的第一行,处于html标签之前,告知浏览器的解析器用什么文档标准解析这个文档。DOCTYPE不存在或格式不正确会导致文档以兼容模式呈现。
标准模式的排版和js运作模式都是以该浏览器支持的最高标准运行。在兼容模式中,页面以宽松的向后兼容的方式显示,模拟老式浏览器以防止站点无法工作过。
介绍一下你对浏览器内核的理解
> 主要分成两部分:渲染引擎(layout engine 或 rendering engine)和js引擎
渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入css等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网格内容的应用程序都需要内核
js引擎:解析和执行javascript来实现网页的动态效果
最开始渲染引擎和js引擎并没有区分很明确,后来js引擎越来越独立
- 浏览器组成:
从原理构成上分为七个模块,分别是 User Interface(用户界面)、 Browser engine(浏览器引擎) 、 Rendering engine(渲染引擎) 、 Networking(网络) 、 JavaScript Interpreter(js解释器) 、 UI Backend(UI后端) 、Date Persistence(数据持久化存储)
其中,最重要的是渲染引擎(内核)和 JavaScript 解释器(JavaScript引擎)
浏览器内核主要负责 HTML 、CSS 的解析,页面布局、渲染与复合层合成; JavaScript 引擎负责 JavaScript 代码的解释与执行
常见的浏览器内核有哪些?
- Trident内核:IE、MaxThon、TT、The World、360、搜狗浏览器等。[又称MSHTML]
- Gecko内核:Netscape6及以上版本、FF、MozillaSuite、SeaMonkey等
- Presto内核:Opera7及以上。[Opera内核原为Presto,现为Blink]
- Webkit内核:Safari、Chrome等。[Chrome的Blink(webkit的分支)]
h5新特性有哪些?
- 增强了图形渲染(canvas)
- 影音(video,audio)
- 数据存储(sessionStorage/localStorage)
- 语义化更好的元素,比如article、footer、header、nav、section
- 表单控件,calendar、date、time、email、url、search
- 新的技术,webworker(专用线程)、websocket(全双工通信)、Geolocation地理定位
如何处理html5新标签的兼容性
> IE6/7/8支持通过document.createElement方法产生的标签,可以利用这一特性让这些浏览器支持HTML5新标签,浏览器支持新标签后,还需要添加标签默认的样式,最好的方式是直接使用成熟的框架,使用最多的是html5shiv框架 <!--[if it IE 9]><script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
简述一下你对HTML语义化的理解
- 用正确的标签做正确的事情
- html语义化让页面的内容结构化,结构更清晰,便于对浏览器、搜索引擎解析
- 即使在没有样式css情况下也以一种文档格式显示,并且是容易阅读的
- 搜索引擎的爬虫也依赖于HTML标记来确定上下文和各个关键字的权重,利于SEO
- 使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解
请描述一下cookie、sessionStorage和localStorage的区别
- 相同点:都存储在客户端
- 不同点:
- 存储大小:
- cookie数据大小不能超过4k
- sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大的多,可以达到5M或更大,就是为了解决cookie存储空间不足而诞生的
- 有限时间:
- localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
- sessionStorage数据在当前浏览器窗口关闭后自动删除
- cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
- 数据域服务器之间的交互方式
- cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端
- sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存
iframe有哪些缺点
- iframe会阻塞主页面的onload事件
- 搜索引擎的检索程序无法解读这种页面,不利于seo
- iframe和主页面共享连接池,而浏览器对相同域的链接有限制,所以会影响页面的并行加载
> 使用iframe之前需要考虑这些缺点,如果需要使用iframe,最好通过JavaScript动态给iframe添加src属性值
网页验证码是干嘛的,是为了解决什么安全问题?
> 是为了防止一些人使用软件恶意注册、发帖等行为而设的,它的存在是为了确保登录网站的是一个坐在电脑面前的真人,而不是一个自动登录的软件
如何判断浏览器的userAgent
- 如果userAgent中包含MSIE,说明一定是IE浏览器
- 如果userAgent中包含Trident却不包含MSIE,说明一定是IE11浏览器
- 如果userAgent中包含Firefox,说明一定是Firefox浏览器
- 如果userAgent中包含OPR,说明一定是Opera浏览器
- 如果userAgent中包含Edge,说明一定是Edge浏览器
- 如果userAgent中包含Edge,说明一定是Chrome浏览器
- 如果userAgent中包含Edge,说明一定是Edge浏览器
请简述js的垃圾回收机制
垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,
并释放掉它们所指向的内存; 主要为了以防内存泄漏,
(内存泄漏: 当已经不需要某块内存时这块内存还存在着),
JS有两种变量: 全局变量和在函数中产生的局部变量。
局部变量的生命周期在函数执行过后就结束了,
此时便可将它引用的内存释放(即垃圾回收);
但全局变量生命周期会持续到浏览器关闭页面。
JS执行环境中的垃圾回收器有两种方式:
标记清除(mark and sweep)、
引用计数(reference counting)。
标记清除: 垃圾收集器给内存中的所有变量都加上标记,
然后去掉环境中的变量以及被环境中的变量引用的变量的标记。
在此之后再被加上的标记的变量即为需要回收的变量,
因为环境中的变量已经无法访问到这些变量。
引用计数(reference counting): 这种方式常常会引起内存泄漏,
低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,
当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,
当这个变量指向其他一个时该值的引用次数便减一。
当该值引用次数为0时就会被回收。
什么是渐进式渲染(progressive rendering)?
> 渐进式渲染是用于提高网页性能(尤其是提高用户感知的加载速度),以尽快呈现页面的技术。
在以前互联网带宽较小的时期,这种技术更为普遍。如今,移动终端的盛行,而移动网络往往不稳定,渐进式渲染在现代前端开发中仍然有用武之地。
一些举例:
- 图片懒加载——页面上的图片不会一次性全部加载。当用户滚动页面到图片部分时,JavaScript 将加载并显示图像。
确定显示内容的优先级(分层次渲染)——为了尽快将页面呈现给用户,页面只包含基本的最少量的 CSS、脚本和内容,然后可以使- 用延迟加载脚本或监听DOMContentLoaded/load事件加载其他资源和内容。
- 异步加载 HTML 片段——当页面通过后台渲染时,把 HTML 拆分,通过异步请求,分块发送给浏览器。更多相关细节可以在这里找到。
viewport是什么?
> Viewport :字面意思为视图窗口,在移动web开发中使用。表示将设备浏览器宽度虚拟成一个特定的值(或计算得出),这样利于移动web站点跨设备显示效果基本一致。移动版的 Safari 浏览器最新引进了 viewport 这个 meta tag,让网页开发者来控制 viewport 的大小和缩放,其他手机浏览器也基本支持。
在移动端浏览器当中,存在着两种视口,一种是可见视口(也就是我们说的设备大小),另一种是视窗视口(网页的宽度是多少)。举个例子:如果我们的屏幕是320像素 * 480像素的大小(iPhone4),假设在浏览器中,320像素的屏幕宽度能够展示980像素宽度的内容。那么320像素的宽度就是可见视口的宽度,而能够显示的980像素的宽度就是视窗视口的宽度。
为了显示更多的内容,大多数的浏览器会把自己的视窗视口扩大,简易的理解,就是让原本320像素的屏幕宽度能够容下980像素甚至更宽的内容(将网页等比例缩小)。
- Viewport属性值
- width 设置layout viewport 的宽度,为一个正整数,或字符串"width-device"
- initial-scale 设置页面的初始缩放值,为一个数字,可以带小数
- minimum-scale 允许用户的最小缩放值,为一个数字,可以带小数
- maximum-scale 允许用户的最大缩放值,为一个数字,可以带小数
- height 设置layout viewport 的高度,这个属性对我们并不重要,很少使用
- user-scalable 是否允许用户进行缩放,值为"no"或"yes", no 代表不允许,yes代表允许这些属性可以同时使用,也可以单独使用或混合使用,多个属性同时使用时用逗号隔开就行了。
img中的alt和元素的title属性作用
- img的alt属性
如果无法显示图像,浏览器将显示alt指定的内容
- 元素title属性
在鼠标移到元素上时显示title的内容
href和src区别
- href
href标识超文本引用,用在link和a等元素上,href是引用和页面关联,是在当前元素和引用资源之间建立联系
若在文档中添加href ,浏览器会识别该文档为 CSS 文件,就会并行下载资源并且不会停止对当前文档的处理。这也是为什么建议使用 link 方式加载 CSS,而不是使用 @import 方式。
- src
src表示引用资源,替换当前元素,用在img,script,iframe上,src是页面内容不可缺少的一部分。
当浏览器解析到src ,会暂停其他资源的下载和处理(图片不会暂停其他资源下载),直到将该资源加载、编译、执行完毕,类似于将所指向资源应用到当前内容。这也是为什么建议把 js 脚本放在底部而不是头部的原因。
行内元素和块级元素有哪些
- 行内元素
一个行内元素只占据它对应标签的边框所包含的空间
一般情况下,行内元素只能包含数据和其他行内元素
b, big, i, small, tt
abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var
a, bdo, br, img, map, object, q, script, span, sub, sup
button, input, label, select, textarea
- 块级元素
占据一整行,高度、行高、内边距和外边距都可以改变,可以容纳块级标签和其他行内标签
header,form,ul,ol,table,article,div,hr,aside,figure,canvas,video,audio,footer
label标签有什么作用
> label 标签通常是写在表单内,它关联一个控件,使用 label 可以实现点击文字选取对应的控件。
<input type="checkbox" id="test">
<label for="test" >test</label>
HTML5的form如何关闭自动完成功能
> 将不想要自动完成的 form 或 input 设置为 autocomplete=off
DOM和BOM有什么区别
- DOM:
Document Object Model,文档对象模型
DOM 是为了操作文档出现的 API,document 是其的一个对象
DOM和文档有关,这里的文档指的是网页,也就是html文档。DOM和浏览器无关,他关注的是网页本身的内容。
- BOM:
Browser Object Model,浏览器对象模型
BOM 是为了操作浏览器出现的 API,window 是其的一个对象
window 对象既为 javascript 访问浏览器提供API,同时在 ECMAScript 中充当 Global 对象
为什么最好把 CSS 的<link>标签放在<head></head>之间?为什么最好把 JS 的<script>标签恰好放在</body>之前,有例外情况吗?
- 把<link>放在<head>中
把<link>标签放在<head></head>之间是规范要求的内容。此外,这种做法可以让页面逐步呈现,提高了用户体验。将样式表放在文档底部附近,会使许多浏览器(包括 Internet Explorer)不能逐步呈现页面。一些浏览器会阻止渲染,以避免在页面样式发生变化时,重新绘制页面中的元素。这种做法可以防止呈现给用户空白的页面或没有样式的内容。
- 把<script>标签恰好放在</body>之前
脚本在下载和执行期间会阻止 HTML 解析。把<script>标签放在底部,保证 HTML 首先完成解析,将页面尽早呈现给用户。
例外情况是当你的脚本里包含document.write()时。但是现在,document.write()不推荐使用。同时,将<script>标签放在底部,意味着浏览器不能开始下载脚本,直到整个文档(document)被解析。也许,对此比较好的做法是,<script>使用defer属性,放在<head>中。
从前端角度出发, 谈谈做好网站seo需要考虑什么?
- 语义化html标签;
- 合理的title, description, keywords;
- 重要的html代码放前面;
- 少用iframe, 搜索引擎不会抓取iframe中的内容
- 图片加上alt
网络和安全
计算机网络体系结构有哪些
学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。
#### 应用层
应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。我们把应用层交互的数据单元称为报文。
**域名系统**
- 域名系统(Domain Name System 缩写 DNS,Domain Name 被译为域名)是因特网的一项核心服务,它作为可以将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。(百度百科)例如:一个公司的 Web 网站可看作是它在网上的门户,而域名就相当于其门牌地址,通常域名都使用该公司的名称或简称。例如上面提到的微软公司的域名,类似的还有:IBM 公司的域名是 www.ibm.com、Oracle 公司的域名是 www.oracle.com、Cisco 公司的域名是 www.cisco.com 等。
**HTTP 协议**
- 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW(万维网) 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。(百度百科)
#### 运输层
运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。
**运输层主要使用以下两种协议**
- 传输控制协议 TCP(Transmisson Control Protocol)--提供面向连接的,可靠的数据传输服务。
- 用户数据协议 UDP(User Datagram Protocol)--提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
**UDP 的主要特点**
- UDP 是无连接的;
- UDP 使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态(这里面有许多参数);
- UDP 是面向报文的;
- UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 直播,实时视频会议等);
- UDP 支持一对一、一对多、多对一和多对多的交互通信;
- UDP 的首部开销小,只有 8 个字节,比 TCP 的 20 个字节的首部要短。
**TCP 的主要特点**
- TCP 是面向连接的。(就好像打电话一样,通话前需要先拨号建立连接,通话结束后要挂机释放连接);
- 每一条 TCP 连接只能有两个端点,每一条 TCP 连接只能是点对点的(一对一);
- TCP 提供可靠交付的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复、并且按序到达;
- TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据;
- 面向字节流。TCP 中的“流”(Stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和 TCP 的交互是一次一个数据块(大小不等),但 TCP 把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。
#### 网络层
在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报。
这里要注意:不要把运输层的“用户数据报 UDP ”和网络层的“ IP 数据报”弄混。另外,无论是哪一层的数据单元,都可笼统地用“分组”来表示。
这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称.
互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Prococol)和许多路由选择协议,因此互联网的网络层也叫做网际层或 IP 层。
#### 数据链路层
数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装程帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。
在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提出数据部分,上交给网络层。 控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。
#### 物理层
在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的 TCP/IP 并不一定单指 TCP 和 IP 这两个具体的协议,而往往表示互联网所使用的整个 TCP/IP 协议族。
TCP 协议如何来保证传输的可靠性
TCP 提供一种面向连接的、可靠的字节流服务。其中,面向连接意味着两个使用 TCP 的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个 TCP 连接。在一个 TCP 连接中,仅有两方进行彼此通信;而字节流服务意味着两个应用程序通过 TCP 链接交换 8bit 字节构成的字节流,TCP 不在字节流中插入记录标识符。
对于可靠性,TCP 通过以下方式进行保证:
- 数据包校验:目的是检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给出响应,这时 TCP 发送数据端超时后会重发数据;
- 对失序数据包重排序:既然 TCP 报文段作为 IP 数据报来传输,而 IP 数据报的到达可能会失序,因此 TCP 报文段的到达也可能会失序。TCP 将对失序数据进行重新排序,然后才交给应用层;
- 丢弃重复数据:对于重复数据,能够丢弃重复数据;
- 应答机制:当 TCP 收到发自 TCP 连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒;
- 超时重发:当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段;
- 流量控制:TCP 连接的每一方都有固定大小的缓冲空间。TCP 的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这可以防止较快主机致使较慢主机的缓冲区溢出,这就是流量控制。TCP 使用的流量控制协议是可变大小的滑动窗口协议。
查找域名对应 IP 地址
这一步包括 DNS 具体的查找过程,包括:浏览器缓存->系统缓存->路由器缓存...
- 浏览器搜索自己的 DNS 缓存(维护一张域名与 IP 地址的对应表);
- 搜索操作系统中的 DNS 缓存(维护一张域名与 IP 地址的对应表);
- 搜索操作系统的 hosts 文件( Windows 环境下,维护一张域名与 IP 地址的对应表);
- 操作系统将域名发送至 LDNS(本地区域名服务器),LDNS 查询 自己的 DNS 缓存(一般查找成功率在 80% 左右),查找成功则返回结果,失败则发起一个迭代 DNS 解析请求:
- LDNS 向 Root Name Server (根域名服务器,如 com、net、org 等的解析的顶级域名服务器的地址)发起请求,此处,Root Name Server 返回 com 域的顶级域名服务器的地址;
- LDNS 向 com 域的顶级域名服务器发起请求,返回 baidu.com 域名服务器地址;
- LDNS 向 baidu.com 域名服务器发起请求,得到 www.baidu.com 的 IP 地址;
- LDNS 将得到的 IP 地址返回给操作系统,同时自己也将 IP 地址缓存起来;
- 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来;
127.0.0.1 与 192.168.0.1 有什么区别
首先明确二者没有区别!两个 IP 地址的角度不一样,127.0.0.1 是从 IETF(因特尔工程任务组)规定看,是保留给本机使用的 IP 地址,所有的计算机默认都是相同的。而 192.168.0.1 其实只是 IETF 在 c 类网址中,专门留出给专用网络用的一个网段中的一个 IP 而已,该网段包含了 192.168.0.1 到 192.168.255.255 中所有的 IP 地址。
### 五类 ip 地址的范围
IP 地址分为 A,B,C,D,E 五类。
网络号:用于识别主机所在的网络;
主机号:用于识别该网络中的主机。
其中 A 类分配给政府机关使用,B 类地址给大中型企业使用,C 类地址给个人使用。这三种是主要的。
IP 地址分为五类,A 类保留给政府机构,B 类分配给中等规模的公司,C 类分配给任何需要的人,D 类用于组播,E 类用于实验,各类可容纳的地址数目不同。
其中 A 类、B 类、和 C 类这三类地址用于 TCP/IP 节点,其它两类 D 类和 E 类被用于特殊用途。
A、B、C 三类 IP 地址的特征:当将 IP 地址写成二进制形式时,A 类地址的第一位总是 O,B 类地址的前两位总是 10,C 类地址的前三位总是 110。
A 类地址
- ⑴ A 类地址第 1 字节为网络地址,其它 3 个字节为主机地址。
- ⑵ A 类地址范围:1.0.0.1—126.155.255.254
- ⑶ A 类地址中的私有地址和保留地址:
- ① 10.X.X.X 是私有地址(所谓的私有地址就是在互联网上不使用,而被用在局域网络中的地址)。
- ② 127.X.X.X 是保留地址,用做循环测试用的。
B 类地址
- ⑴ B 类地址第 1 字节和第 2 字节为网络地址,其它 2 个字节为主机地址。
- ⑵ B 类地址范围:128.0.0.1—191.255.255.254。
- ⑶ B 类地址的私有地址和保留地址
- ① 172.16.0.0—172.31.255.255 是私有地址
- ② 169.254.X.X 是保留地址。如果你的 IP 地址是自动获取 IP 地址,而你在网络上又没有找到可用的 DHCP 服务器。就会得到其中一个 IP。
C 类地址
- ⑴ C 类地址第 1 字节、第 2 字节和第 3 个字节为网络地址,第 4 个个字节为主机地址。另外第 1 个字节的前三位固定为 110。
- ⑵ C 类地址范围:192.0.0.1—223.255.255.254。
- ⑶ C 类地址中的私有地址:
- 192.168.X.X 是私有地址。
D 类地址
- ⑴ D 类地址不分网络地址和主机地址,它的第 1 个字节的前四位固定为 1110。
- ⑵ D 类地址范围:224.0.0.1—239.255.255.254
E 类地址
- ⑴ E 类地址也不分网络地址和主机地址,它的第 1 个字节的前五位固定为 11110。
- ⑵ E 类地址范围:240.0.0.1—255.255.255.254
HTTP 长连接、短连接
在 HTTP/1.0 中默认使用短连接。也就是说,客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(如 JavaScript 文件、图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会重新建立一个 HTTP 会话。
而从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。使用长连接的 HTTP 协议,会在响应头加入这行代码:Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接
http(tcp) 报文结构
> 例如一个 100kb 的 HTML 文档需要传送到另外一台计算机,并不会整个文档直接传送过去,可能会切割成几个部分,比如四个分别为 25kb 的数据段。而每个数据段再加上一个 TCP 首部,就组成了 TCP 报文。
TCP 报文 (Segment),包括首部和数据部分。
首部:
- 源端口 source port
- 目的端口 destination port
- 序号 sequence number
- 确认号 acknowledgment number
- 数据偏移 offset
- 保留 reserved
- 标志位 tcp flags
- 窗口大小 window size
- 检验和 checksum
- 紧急指针 urgent pointer
- 选项 tcp options
如何实现浏览器多个标签页之间的通信?
- cookie+setInterval
- localStorage
- webSocket
- SharedWorker
一个页面从输入url到页面加载显示完成,发生了什么?
1.浏览器通过DNS将url地址解析为ip(如果有缓存直接返回缓存,否则递归解析)
2.通过DNS解析得到了目标服务器的ip地址后,与服务器建立TCP连接。
- ip协议:选择传输路线,负责找到
- tcp协议:三次握手,分片,可靠传输,重新发送的机制
3.浏览器通过http协议发送请求(增加http的报文信息)头 体 行
4.服务器接收请求后,查库,读文件,拼接好返回的http响应
5.浏览器收到html,开始渲染
6.解析html为dom,解析css为css-tree,最终生成render-tree阻塞渲染
7.遍历渲染树开始布局,计算每个节点的位置大小信息
8.将渲染树每个节点绘制到屏幕
9.加载js文件,运行js脚本
10.relow(样式)和repaint(位置)
简述同步和异步的区别?
- 在通信中,同步和异步强调的是消息的通信机制;而在计算机操作系统中,复用了通信里的概念,同步(Synchronous)是指在发生一个调用时,在没有得到结果之前,该调用不返回;异步(Asynchronous)是指在发生一个调用时,立即返回。
请简述常用的跨域方法(9种)
- cors
服务器端对于CORS的支持,主要就是通过设置Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问。
- jsonp
var script = document.createElement('script');
script.src = "http://aa.xx.com/js/*.js";
document.body.appendChild(script);
- postMessage
window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。
- window.name
- location.hash
- http-proxy
- nginx
- websocket
- iframe
基于iframe实现的跨域要求两个域具有aa.xx.com,bb.xx.com这种特点,也就是两个页面必须属于同一个顶级基础域(例如都是xxx.com,或是xxx.com.cn),使用同一协议(例如都是 http)和同一端口(例如都是80),这样在两个页面中同时添加document.domain,就可以实现父页面调用子页面的函数
http常见的状态?
- 200("OK")一切正常。实体主体中的文档(若存在的话)是某资源的表示。
- 400("Bad Request")客户端方面的问题。实体主题中的文档(若存在的话)是一个错误消息。希望客户端能够理解此错误消息,并改正问题。
- 500("Internal Server Error")服务期方面的问题。实体主体中的文档(如果存在的话)是一个错误消息。该错误消息通常无济于事,因为客户端无法修复服务器方面的问题。
- 301("Moved Permanently")当客户端触发的动作引起了资源URI的变化时发送此响应代码。另外,当客户端向一个资源的旧URI发送请求时,也发送此响应代码。
- 404用于服务器端不知道客户端要请求哪个资源的情况;
- 410用于服务器端知道客户端所请求的资源曾经存在,但现在已经不存在了的情况。
- 409("Conflict")当客户端试图执行一个”会导致一个或多个资源处于不一致状态“的操作时,发送此响应代码。
简述 http 1.1、http 1.0以及http2.x 的区别
- http1.0与1.1对比:
- http 1.0 对于每个连接都得建立一次连接, 一次只能传送一个请求和响应, 请求就会关闭, http1.0 没有 Host 字段
- 而 http1.1 在同一个连接中可以传送多个请求和响应, 多个请求可以重叠和同时进行, http1.1 必须有 host 字段
- http1.1 中引入了 ETag 头, 它的值 entity tag 可以用来唯一的描述一个资源. 请求消息中可以使用 If-None-Match 头域来匹配资源的 entitytag 是否有变化
- http1.1 新增了 Cache-Control 头域(消息请求和响应请求都可以使用), 它支持一个可扩展的指令子集
- http1.0 中只定义了 16 个状态响应码, 对错误或警告的提示不够具体. http1.1 引入了一个 Warning 头域, 增加对错误或警告信息的描述. 且新增了 24 个状态响应码
- http1.x与2.x对比:
- **二进制格式**:http1.x 是文本协议,而 http2.0 是二进制以帧为基本单位,是一个二进制协议,一帧中除了包含数据外同时还包含该帧的标识:Stream Identifier,即标识了该帧属于哪个 request,使得网络传输变得十分灵活。
- **多路复用**: 一个很大的改进,原先 http1.x 一个连接一个请求的情况有比较大的局限性,也引发了很多问题,如建立多个连接的消耗以及效率问题。
- http1.x 为了解决效率问题,可能会尽量多的发起并发的请求去加载资源,然而浏览器对于同一域名下的并发请求有限制,而优化的手段一般是将请求的资源放到不同的域名下来突破这种限制。
- 而 http2.0 支持的多路复用可以很好的解决这个问题,多个请求共用一个 TCP 连接,多个请求可以同时在这个 TCP 连接上并发,一个是解决了建立多个 TCP 连接的消耗问题,一个也解决了效率的问题。那么是什么原理支撑多个请求可以在一个 TCP 连接上并发呢?基本原理就是上面的二进制分帧,因为每一帧都有一个身份标识,所以多个请求的不同帧可以并发的无序发送出去,在服务端会根据每一帧的身份标识,将其整理到对应的 request 中。
- **header 头部压缩**:主要是通过压缩 header 来减少请求的大小,减少流量消耗,提高效率。因为之前存在一个问题是,每次请求都要带上 header,而这个 header 中的数据通常是一层不变的。
- 支持服务端推送
什么是流量控制
> 流量控制是对一条通信路径上的流量进行控制,就是发送方通过获取接收方的回馈来动态调整发送的速率,来达到控制流量的效果,其目的是保证发送者的发送速度不超过接收者的接收速度。
什么是拥塞控制
拥塞控制是对整个通信子网的流量进行控制,属于全局控制。
1. 慢开始+拥塞避免
一开始使用慢启动,即拥塞窗口设为 1,然后拥塞窗口指数增长到慢开始的门限值(ssthresh=16),则切换为拥塞避免,即加法增长,这样增长到一定程度,导致网络拥塞,则此时会把拥塞窗口重新降为 1,即重新慢开始,同时调整新的慢开始门限值为 12,之后以此类推。
2. 快重传+快恢复
- **快重传**:上面我们说的重传机制都是等到超时还未收到接收方的回复,才开始进行重传。而快重传的设计思路是:如果发送方收到 3 个重复的接收方的 ACK,就可以判断有报文段丢失,此时就可以立即重传丢失的报文段,而不用等到设置的超时时间到了才开始重传,提高了重传的效率。
- **快恢复**:上面的拥塞控制会在网络拥塞时将拥塞窗口降为 1,重新慢开始,这样存在的一个问题就是网络无法很快恢复到正常状态。快恢复就是来优化这个问题的,使用快恢复,则出现拥塞时,拥塞窗口只会降低到新的慢开始门阀值(即 12),而不会降为 1,然后直接开始进入拥塞避免加法增长
说一下 TCP 三次握手和四次挥手
- 建立 TCP 连接需要三次握手:三次握手: 首先 Client 端发送连接请求报文,Server 段接受连接后回复 ACK 报文,并为这次连接分配资源。Client 端接收到 ACK 报文后也向 Server 段发生 ACK 报文,并分配资源,这样 TCP 连接就建立了。
- 第一步: 客户机的 TCP 先向服务器的 TCP 发送一个连接请求报文. 这个特殊的报文中不含应用层数据, 其首部中的 SYN 标志位被置 1. 另外, 客户机会随机选择一个起始序号 seq=x(连接请求报文不携带数据,但要消耗掉一个序号)
- 第二步: 服务器端的 TCP 收到连接请求报文后, 若同意建立连接, 就向客户机发送请求, 并为该 TCP 连接分配 TCP 缓存和变量. 在确认报文中,SYN 和 ACK 位都被置为 1, 确认号字段的值为 x+1, 并且服务器随机产生起始序号 seq=y(确认报文不携带数据, 但也要消耗掉一个序号). 确认报文同样不包含应用层数据.
- 第三步: 当客户机收到确认报文后, 还要向服务器给出确认, 并且也要给该连接分配缓存和变量. 这个报文的 ACK 标志位被置为 1, 序号字段为 x+1, 确认号字段为 y+1
- 四次挥手
- 第一步: 客户机打算关闭连接,就向其 TCP 发送一个连接释放报文,并停止再发送数据,主动关闭 TCP 连接, 该报文的 FIN 标志位被置 1, seq=u, 它等于前面已经传送过的数据的最后一个字节的序号加 1(FIN 报文即使不携带数据,也要消耗掉一个序号)
- 第二步: 服务器接收连接释放报文后即发出确认, 确认号是 ack=u+1, 这个报文自己的序号是 v, 等于它前面已传送过的数据的最后一个自己的序号加 1. 此时, 从客户机到服务器这个方向的连接就释放了, TCP 连接处于半关闭状态. 但服务器若发送数据, 客户机仍要接收, 即从服务器到客户机的连接仍未关闭.
- 第三步: 若服务器已经没有了要向客户机发送的数据, 就通知 TCP 释放连接, 此时其发出 FIN=1 的连接释放报文
- 第四步: 客户机收到连接释放报文后, 必须发出确认. 在确认报文中, ACK 字段被置为 1, 确认号 ack=w+1, 序号 seq=u+1. 此时, TCP 连接还没有释放掉, 必须经过等待计时器设置的时间 2MSL 后, A 才进入到连接关闭状态.
对称加密与非对称加密
- 对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。
- 由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,它非常的慢,所以我们还是要用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。
请你简述下 SYN flood 攻击
> 攻击方伪造源地址发送 SYN 报文,服务端此时回复 syn+ack,但是真正的 IP 地址收到这个包之后,有可能直接回复了 RST 包,但是如果不回复 RST 包,那就更严重了,可能服务端会在几十秒后才关闭这个 socket 链接(时间根据每个系统不一样)
TCP可以快速握手吗?
可以
1. 客户端发送一个SYN包,头部包含Fast Open选项的Cookie长度为0
2. 服务端根据客户端IP生成cookie,放在SYN+ACK包中一同发回客户端
3. 客户端收到Cookie以后缓存在自己的本地内存
4. 客户端再次访问服务端时,在SYN包携带数据,并在头部包含上次缓存在本地的TCP cookie
5. 如果服务端校验Cookie合法,则在客户端回复ACK前就可以直接发送数据。如果Cookie不合法,则按照正常三次握手进行
TCP 链接和 UDP 的区别,什么时候选择使用 UDP 链接?
> Udp接收到应用协议的某个消息(已编码为二进制)后,不会分包,但是要记录当前数据包的大小(消息大小加上8个字节头部),然后交给IP层。而TCP接收到应用层协议的消息(已编码为二进制)后,然后参考该计算机连接的网络数据链路层MTU(最大传输单元)确定是否要分包,然后交给IP层。
UDP是非面向连接的,即发送数据之前不需要建立连接,而TCP则是面向连接的,也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达。也就是说UDP管发不管到,而TCP管发管到。因此,在安全性方面来说,TCP更具有优越性。
Udp适用场合:如果需要做实时性很高且消息小(比如小于以太网的MTU1500Byte)的通信程序,UDP就很适合。关键消息重复发,不用等确认都可以,当然最终还是需要确认。如果是内网环境,网络非常稳定,UDP几乎不会出问题,如果极度要求性能,可以考虑。如果你公网网络资源有限,而用户之间需要传递大量数据,可以考虑用UDP做NAT穿透。
Chrome 打开一个页面需要启动多少进程?分别有哪些进程?
打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。
- 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
如何保证页面文件能被完整送达浏览器?
> 互联网中的数据是通过数据包来传输的。数据包要在互联网上进行传输,就要符合网际协议(IP),互联网上不同的在线设备都有唯一的地址,地址只是一个数字,只要知道这个具体的地址,就可以往这里发送信息。
如果要想把一个数据包从主机 A 发送给主机 B,那么在传输之前,数据包上会被附加上主机 B 的 IP 地址信息,这样在传输过程中才能正确寻址。额外地,数据包上还会附加上主机 A 本身的 IP 地址,有了这些信息主机 B 才可以回复信息给主机 A。这些附加的信息会被装进一个叫 IP 头的数据结构里。IP 头是 IP 数据包开头的信息,包含 IP 版本、源 IP 地址、目标 IP 地址、生存时间等信息。
IP 是非常底层的协议,只负责把数据包传送到对方电脑,但是对方电脑并不知道把数据包交给哪个程序,是交给浏览器还是交给王者荣耀?因此,需要基于 IP 之上开发能和应用打交道的协议,最常见的是用户数据包协议(User Datagram Protocol),简称UDP和传输控制协议(Transmission Control Protocol),简称TCP.
基本传输过程为:
- 上层将数据包交给传输层
- 传输层会在数据包前面附加上UDP 头,组成新的 UDP 数据包,再将新的 UDP 数据包交给网络层
- 网络层再将 IP 头附加到数据包上,组成新的 IP 数据包,并交给底层
- 数据包被传输到主机 B 的网络层,在这里主机 B 拆开 IP 头信息,并将拆开来的数据部分交给传输层
- 在传输层,数据包中的 UDP 头会被拆开,并根据 UDP 中所提供的端口号,把数据部分交给上层的应用程序
- 最终,数据包就发送到了主机 B 上层应用程序这里。
为什么很多站点第二次打开速度会很快?
- DNS缓存
主要就是在浏览器本地把对应的 IP 和域名关联起来,这样在进行DNS解析的时候就很快。
- MemoryCache
是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。
- 浏览器缓存
浏览器缓存,也称Http缓存,分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。
- 强缓存:强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。
实现强缓存,过去我们一直用expires。当服务器返回响应时,在 Response Headers 中将过期时间写入 expires 字段。像这样
expires: Wed, 12 Sep 2019 06:12:18 GMT
可以看到,expires 是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,那么就直接去缓存中取这个资源。
从这样的描述中大家也不难猜测,expires 是有问题的,它最大的问题在于对“本地时间”的依赖。如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。
考虑到 expires 的局限性,HTTP1.1 新增了Cache-Control字段来完成 expires 的任务。expires 能做的事情,Cache-Control 都能做;expires 完成不了的事情,Cache-Control 也能做。因此,Cache-Control 可以视作是 expires 的完全替代方案。在当下的前端实践里,我们继续使用 expires 的唯一目的就是向下兼容。
cache-control: max-age=31536000
在 Cache-Control 中,我们通过max-age来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度。在本例中,max-age 是 31536000 秒,它意味着该资源在 31536000 秒以内都是有效的,完美地规避了时间戳带来的潜在问题。
Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。
- 协商缓存:协商缓存依赖于服务端与浏览器之间的通信。协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304。
协商缓存的实现,从 Last-Modified 到 Etag,Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:
Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:
If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。
使用 Last-Modified 存在一些弊端,这其中最常见的就是这样两个场景:
我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。
当我们修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了。
这两个场景其实指向了同一个 bug——服务器并没有正确感知文件的变化。为了解决这样的问题,Etag 作为 Last-Modified 的补充出现了。
Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串可以是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化。
Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。因此启用 Etag 需要我们审时度势。正如我们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充和强化存在。
Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
- Service Worker Cache
Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。
Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件.
- Push Cache
Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。这块的知识比较新,应用也还处于萌芽阶段,应用范围有限不代表不重要——HTTP2 是趋势、是未来。在它还未被推而广之的此时此刻,我仍希望大家能对 Push Cache 的关键特性有所了解:
- Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
- Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
- 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
Ajax和Fetch区别
- ajax是使用XMLHttpRequest对象发起的,但是用起来很麻烦,所以ES6新规范就有了fetch,fetch发一个请求不用像ajax那样写一大堆代码。
- 使用fetch无法取消一个请求,这是因为fetch基于Promise,而Promise无法做到这一点。
- 在默认情况下,fetch不会接受或者发送cookies
- fetch没有办法原生监测请求的进度,而XMLHttpRequest可以
- fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
- fetch由于是ES6规范,兼容性上比不上XMLHttpRequest
如何做到修改url参数页面不刷新
HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。
let stateObj = {
foo: "bar",
};
history.pushState(stateObj, "page 2", "bar.html");
假设当前页面为 foo.html,执行上述代码后会变为 bar.html,点击浏览器后退,会变为 foo.html,但浏览器并不会刷新。 pushState() 需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个 URL. 让我们来解释下这三个参数详细内容:
- 状态对象 — 状态对象 state 是一个 JavaScript 对象,通过 pushState () 创建新的历史记录条目。无论什么时候用户导航到新的状态,popstate 事件就会被触发,且该事件的 state 属性包含该历史记录条目状态对象的副本。状态对象可以是能被序列化的任何东西。原因在于 Firefox 将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有640k的大小限制。如果你给 pushState() 方法传了一个序列化后大于 640k 的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage 以及 localStorage.
- 标题 — Firefox 目前忽略这个参数,但未来可能会用到。传递一个空字符串在这里是安全的,而在将来这是不安全的。二选一的话,你可以为跳转的 state 传递一个短标题。
- URL — 该参数定义了新的历史URL记录。注意,调用 pushState() 后浏览器并不会立即加载这个 URL,但可能会在稍后某些情况下加载这个 URL,比如在用户重新打开浏览器时。新URL不必须为绝对路径。如果新URL是相对路径,那么它将被作为相对于当前 URL 处理。新 URL 必须与当前URL同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前 URL。
RESTful是什么?
REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。
- GET
get方法在Rest中主要用于获取资源,能够发送参数,不过有限制,且参数都会以?开头的形 式附加在URL尾部。规范的get方法处理器应该是幂等的,也就是说对一个资源不论发送多少次get请求都不会更改数据或造成破坏。
- POST
post方法在Rest请求中主要用于添加资源,参数信息存放在请求报文的消息体中相对安全,且可发送较大信息
- PUT
put方法在Rest中主要用于更新资源,因为大多数浏览器不支持put和delete,会自动将put和delete请求转化为get和post. 因此为了使用put和delete方法, 需要以post发送请求,在表单中使用隐藏域发送真正的请求。put方法的参数是同post一样是存放在消息中的,同样具有安全性,可发送较大信息。put方法是幂等的,对同一URL资源做出的同一数据的任意次put请求其对数据的改变都是一致的。
- DELETE
Delete在Rest请求中主要用于删除资源,因为大多数浏览器不支持put和delete,会自动将put和delete请求转化为get和post。因此为了使用put和delete方法,需要以post发送请求,在表单中使用隐藏域发送真正的请求。Delete方法的参数同post一样存放在消息体中,具有安全性,可发送较大信息 Delete方法是幂等的,不论对同一个资源进行多少次delete请求都不会破坏数据
GET和POST的区别
- GET产生一个TCP数据包;POST产生两个TCP数据包。
- GET在浏览器回退时是无害的,而POST会再次提交请求。
- GET产生的URL地址可以被Bookmark,而POST不可以。
- GET请求会被浏览器主动cache,而POST不会,除非手动设置。
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求在URL中传送的参数是有长度限制的,而POST么有。
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
- GET参数通过URL传递,POST放在Request body中。
Accept和Content-Type
Accept 请求头用来告知客户端可以处理的内容类型,这种内容类型用MIME类型来表示。服务器使用 Content-Type 应答头通知客户端它的选择。
Accept: text/html
Accept: image/*
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
- Accept属于请求头, Content-Type属于实体头。
Http报头分为通用报头,请求报头,响应报头和实体报头。
请求方的http报头结构:通用报头|请求报头|实体报头
响应方的http报头结构:通用报头|响应报头|实体报头
- Accept代表发送端(客户端)希望接受的数据类型。
比如:Accept:text/xml;
代表客户端希望接受的数据类型是xml类型
Content-Type代表发送端(客户端|服务器)发送的实体数据的数据类型。
比如:Content-Type:text/html;
代表发送端发送的数据格式是html。
二者合起来,
Accept:text/xml;
Content-Type:text/html
即代表希望接受的数据类型是xml格式,本次请求发送的数据的数据格式是html。
Http与Https的区别
- HTTP 的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头
- HTTP 是不安全的,而 HTTPS 是安全的
- HTTP 标准端口是80 ,而 HTTPS 的标准端口是443
- 在OSI 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
- HTTP 无法加密,而HTTPS 对传输的数据进行加密
- HTTP无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书
什么是Http协议无状态协议?怎么解决Http协议无状态协议?
> 无状态协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息也就是说,
当客户端一次HTTP请求完成以后,客户端再发送一次HTTP请求,HTTP并不知道当前客户端是一个”老用户“。
可以使用Cookie来解决无状态的问题,Cookie就相当于一个通行证,第一次访问的时候给客户端发送一个Cookie,
当客户端再次来的时候,拿着Cookie(通行证),那么服务器就知道这个是”老用户“。
Socket 连接与 HTTP 连接的联系与区别
> 由于通常情况下 Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。
而 HTTP 连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。
很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是 Socket 连接,服务器就可以直接将数据传送给客户端;若双方建立的是 HTTP 连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。
http 报文大小限制
> 如前所述,一个 HTTP 报文包含起始行,头域和消息体,HTTP 协议本身并没有对报文中任一部分的长度做限制,也就是说,理论上一个请求 URI 可以无限长,头域可以无限多,请求体可以无限大。但在实际场景下,请求 URI 的长度会受到浏览器的限制,如果在浏览器中输入过长的 URL,那么浏览器会自动进行截断。而服务器出于安全性和效率的考虑,也会对头域和消息体的大小作出一定的限制。
常用的HTTP方法有哪些
- GET 方法:发送一个请求来取得服务器上的某一资源
- POST 方法:向 URL 指定的资源提交数据或附加新的数据
- PUT 方法:跟 POST 方法很像,也是想服务器提交数据。但是,它们之间有不同。PUT 指定了资源在服务器上的位置,而 POST 没有
- HEAD 方法:只请求页面的首部
- DELETE 方法:删除服务器上的某资源
- OPTIONS 方法:它用于获取当前 URL 所支持的方法。如果请求成功,会有一个 Allow 的头包含类似“GET,POST”这样的信息
- TRACE 方法:TRACE 方法被用于激发一个远程的,应用层的请求消息回路
- CONNECT 方法:把请求连接转换到透明的 TCP/IP 通道
一次完整的HTTP请求所经历的7个步骤
HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤:
- 建立TCP连接
在HTTP工作开始之前,Web浏览器首先要通过网络与Web服务器建立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建 Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。HTTP是比TCP更高层次的应用层协议,根据规则, 只有低层协议建立之后才能,才能进行更层协议的连接,因此,首先要建立TCP连接,一般TCP连接的端口号是80。
- Web浏览器向Web服务器发送请求行
一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令。例如:GET /sample/hello.jsp HTTP/1.1。
- Web浏览器发送请求头
浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。
- Web服务器应答
客户机向服务器发出请求后,服务器会客户机回送应答, HTTP/1.1 200 OK ,应答的第一部分是协议的版本号和应答状态码。
- Web服务器发送应答头
正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。
- Web服务器向浏览器发送数据
Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据。
- Web服务器关闭TCP连接
一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码:
Connection:keep-alive
TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。
建立TCP连接->发送请求行->发送请求头->(到达服务器)发送状态行->发送响应头->发送响应数据->断TCP连接
关于建站安全防护
要做好防XSS、CSRF、SQL注入攻击.DDOS攻击。
- XSS概念:
译为跨站脚本攻击,具体是指攻击者在Web页面里插入恶意Script脚本,当用户浏览该网页时,Script代码会被执行,从而进行恶意攻击。
- XSS预防:
关键cookie字段设置httpOnly
输入检查,特殊字符 < > / &等,对其进行转义后存储
- CSRF概念:
本质上讲,是黑客将一个http接口中需要传递的所有参数都预测出来,然后不管以什么方式,他都可以根据他的目的来任意调用你的接口,对服务器实现CURD。
- CSRF 预防:
使用验证码,更高级用图灵测试
- SQL概念:
通常没有任何过滤,直接把参数存放到了SQL语句当中
- SQL预防:
根本上防止SQL注入的方法,就是参数化查询或者做词法分析。
- DDOS概念:
利用木桶原理,寻找利用系统应用的瓶颈;阻塞和耗尽;当前问题:用户的带宽小于攻击的规模,噪声访问带宽成为木桶的短板。
- DDOS预防:用软硬件结合的方式来防御是最有效的
为什么利用多个域名来提供网站资源会更有效?
1.CDN缓存更方便
2.突破浏览器并发限制
(一般每个域名建立的链接不超过6个)
3.Cookieless,节省带宽,
尤其是上行带宽一般比下行要慢;
4.对于UGC的内容和主站隔离,
防止不必要的安全问题
(上传js窃取主站cookie之类的)。
正是这个原因要求用户内容的域名
必须不是自己主站的子域名,
而是一个完全独立的第三方域名。
5.数据做了划分,
甚至切到了不同的物理集群,
通过子域名来分流比较省事。
什么是CDN, CDN对于网站有什么意义, 它有什么样的缺点?
CDN又称为内容分发网络; 本意在于
尽可能避开互联网上有可能影响数据
传输速度和稳定性的瓶颈和环节,
使内容传输的更快、更稳定。
主要目的:
解决因分布、带宽、服务器性能带来的访问延迟问题,
适用于站点加速、点播、直播等场景。
使用户可就近取得所需内容,
解决 Internet网络拥挤的状况,
提高用户访问网站的响应速度和成功率。
缺点:
- 实施复杂 , 投资大;
- 目前大部分的CDN还只是对静态内容加速,
对动态加速效果不好;
而双线对动态加速的效果跟静态是一样的。
什么是HTTP持久化和管线化
- HTTP 最初的版本中,每进行一次 HTTP 通信,就要断开一次 TCP 连接(无连接)
HTTP/1.1 增加了持久连接(HTTP Persistent Connections )的方法,其特点是,只要一方未明确提出断开连接,则另一方保持 TCP 连接状态
- 管线化是指将多个 HTTP 请求整批发送,在发送过程中不用等待对方响应
管线化是在持久连接的基础上实现的,管线化的实现,能够同时并行发送多个请求,而不需要一个接一个的等待响应
用户就是要上传 10 个 G 的文件,服务器存储允许的情况下,你会怎么处理保证整体架构顺畅,不影响其他用户?
> 我会准备两个服务器上传接口,前端或者原生客户端上传文件可以拿到文件大小,根据文件大小,分发不同的对应服务器接口处理上传,大文件可以进行断点续传,原理是 md5 生成唯一的 hash 值,将分片的 hash 数组先上传到后端,然后将文件分片上传,对比 hash 值,相同的则丢弃。不一致的话,根据数组内容进行 buffer 拼接生成文件。
断点续传最核心的内容就是把文件“切片”然后再一片一片的传给服务器,但是这看似简单的上传过程却有着无数的坑。
首先是文件的识别,一个文件被分成了若干份之后如何告诉服务器你切了多少块,以及最终服务器应该如何把你上传上去的文件进行合并,这都是要考虑的。
因此在文件开始上传之前,我们和服务器要有一个“握手”的过程,告诉服务器文件信息,然后和服务器约定切片的大小,当和服务器达成共识之后就可以开始后续的文件传输了。
前台要把每一块的文件传给后台,成功之后前端和后端都要标识一下,以便后续的断点。
当文件传输中断之后用户再次选择文件就可以通过标识来判断文件是否已经上传了一部分,如果是的话,那么我们可以接着上次的进度继续传文件,以达到续传的功能。有了HTML5 的 File api之后切割文件比想想的要简单的多的多。
只要用slice 方法就可以了
var packet = file.slice(start, end);
参数start是开始切片的位置,end是切片结束的位置 单位都是字节。通过控制start和end 就可以是实现文件的分块
如
file.slice(0,1000);
file.slice(1000,2000);
file.slice(2000,3000);
// ......
在把文件切成片之后,接下来要做的事情就是把这些碎片传到服务器上。如果中间掉线了,下次再传的时候就得先从服务器获取上一次上传文件的位置,然后以这个位置开始上传接下来的文件内容。
JavaScript
不声明第三个变量,变换两个变量中的值,共有几种方法
- 方案一:```a+=b;b=a-b;a-=b;```
- 方案二:```a^=b;b^=a;a^=b;```
- 方案三:```a=[a,b];b=a[0];a=a[1];```
- 方案四:```a=[b,b=a][0];```
- 方案五:```a={a:b,b:a};b=a.b;a=a.a;```
- 方案六:```[a,b]=[b,a];```
es6的新特性有哪儿些?
- let 和 const
- Set 和 Map数据结构
- Class
- 模板字符串
- 箭头函数
- Itertor 和 for of 遍历索引数组和类数组对象
- ... 参数增强和打散数组
- 解构 数组/对象/参数
- Promise
- Symbol 基本类型
- Reflect
- Proxy
- Decorator 装饰器
- es6 module es6模块
谈谈你对闭包的理解
- 闭包是函数和声明该函数的词法环境的组合。(MDN定义)
A clousure is the combination of a function and the lexical environment within which that function was declared.
注:离散数学中是对集合间关系的一种描述
- ECMAScript支持闭包,因而js中的闭包表现为:外层函数调用后,外层函数变量被内层函数对象的[[scope]]引用着而导致外层函数的作用域对象AO无法释放(垃圾回收));
- js中闭包常见作用:<1>.实现共有变量,如:函数累加器;<2>.可以做缓存(存储结构);<3>.属性私有化;<4>.模块化开发,防止污染全局变量
- js闭包的缺点:比普通函数占有更多内存(多的是外层函数作用域对象AO始终存在),容易造成内存泄漏
谈谈原型链继承的理解?
> 什么是原型链:只要是对象就有原型, 并且原型也是对象, 因此只要定义了一个对象, 那么就可以找到他的原型, 如此反复, 就可以构成一个对象的序列, 这个结构就被称为原型链
所有的实例有一个内部指针(prototype),指向它的原型对象,并且可以访问原型对象上的所有属性和方法。
Map和Set的区别
> Set 对象类似于数组,且成员的值都是唯一的。Map 对象是键值对集合,和 JSON 对象类似,但是 key 不仅可以是字符串还可以是对象
请简述js继承的六种方式
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承
function object(o){
function F(){};
F.prototype=o;
return new F();
}
- 寄生式继承
- 寄生组合式继承
请简述js创建对象的七种方式
- 工厂模式
- 构造函数模式
- 原型模式
- 动态原型模式
- 寄生构造函数模式
- 稳妥构造函数模式
请简述typeof可以判断的数据类型
- number(NaN)
- boolean
- string
- object(Null)
- undefined
- function
- symbol
请简述'=='和'==='的区别
- '==='会首先进行类型判断,屏蔽了自动类型转换;而'=='会先进行自动类型转换为数字后再比较。'==='是屏蔽了自动类型转换的'=='
请简述null和undefined的区别
- js底层undefined会被自动翻译成null;undefined可看做未赋值,null指曾赋过值,但是目前没有值;null是一个特殊关键字,不是标识符,不能当做变量来使用和赋值,而undefined却是一个标识符,可当作变量来使用和赋值
请简述Number(null)、Number(undefined)、Number(NaN)会返回的值
- Number(null)返回0,Number(undefined)返回NaN,Number(NaN)返回NaN,但是Number(undefined)==Number(NaN)返回false
如何判断NaN
- 只能通过isNaN判断
如何判断对象和数组
- isArray() es6语法
- Object.prototype.toString.call() 无兼容性问题
请简述this绑定规则
优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
- 默认绑定 非严格模式下默认绑定指向全局对象,严格模式下默认绑定指向undefined
- 隐式绑定 存在隐式丢失现象,且对象属性引用链中只有最顶层会影响调用位置
- 显式绑定 硬绑定和api调用上下文可以解决隐式丢失问题
- new绑定
请简述new操作符执行过程
- 创建(或者说构造)一个全新的对象
- 这个对象会被执行[[prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
请简述JavaScript中会更改原数组的操作有哪些?
- pop() 返回pop出来的元素
- push() 返回新数组的长度
- shift() 返回头部的元素
- unshift() 返回新数组的长度
- splice() 返回被删除元素的数组
- sort() 返回新数组
- reverse() 返回新数组
- fill() 返回新数组
- copyWithin() 返回新数组
JavaScript中会返回false的哪些值?
- undefined
- null
- false
- +0、-0和NaN
- ""
什么是JSON安全值?
> undefined、function、symbol和包含循环引用的对象都不符合JSON的结构标准,JSON.stringify在对象中遇到undefined、function和symbol时会自动将其忽略,在数组中则会返回null
请简述ToString的转化规则
> 基本类型值的字符串化规则为null转换为"null",undefined转换为"undefined",true转换为"true"。数字的字符串转换遵循通用规则,极小和极大的数字使用指数形式。对普通对象来说,如无自行定义,toString()返回内部属性[[class]]的值
谈谈toString和String的区别?
- toString()方法;数值、字符串、对象、布尔;都有toString方法;这个方法唯一能做的就是返回相应的字符串;其中null和undefined没有toString()方法;
- String()属于强制转换, null转换的结果为null;undefined转换的结果为undefined;其余的如果有toString()方法,即调用该方法,返回相应的结果;
请简述ToNumber的转化规则
> true转化为1,false转化为0,undefined转化为NaN,null转化为0。对象会首先被转化为相应的基本类型,如果返回非数字的基本类型,则再遵循以上规则将其强制转换为数字
请简述ToBoolean的转化规则
> JavaScript中的值可以分为可以被强制类型转为false的值和其他,其中可以转换为假的值包括:undefined;null;false;+0、-0和NaN;"",其余均为真值
请简述ToPrimitive抽象操作
> 为了将值转换为相应的基本类型值,抽象操作ToPrimitive会首先通过内部操作DefaultValue,检查该值是否有valueOf()方法。如果有返回基本类型值,使用该值进行强制类型转换;如果没有就使用toString()的返回值(如果存在)来进行强制类型转换;如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误。
let,var和const有什么区别?
- const定义的变量不可以修改,而且必须初始化
- var定义的变量可以修改,如果不初始化会输出undefined,不会报错。
- let是块级作用域,函数内部使用let定义后,对函数外部无影响。
解释一下变量提升?
> 在进入一个执行上下文后,先把 var 和 function 声明的变量前置,再去顺序执行代码。
PS:作用域分为全局作用域和函数作用域,用var声明的变量,只在自己所在的所用域有效。
箭头函数和普通函数有什么区别?
主要区别在this指向问题
- 普通函数的this 指向调用它的那个对象,例如 obj.func ,那么func中的this就是obj
- 箭头函数不能作为构造函数,不能使用new,没有this,arguments箭头函数,箭头函数的this永远指向其上下文的 this ,任何方法都改变不了其指向,如 call() , bind() , apply()(或者说箭头函数中的this指向的是定义时的this,而不是执行时的this)
如何理解事件委托?
> 也可以称之为事件代理,给父元素绑定事件,用来监听子元素的冒泡事件,并找到是哪个子元素的事件。将事件委托给另外的元素,利用事件冒泡的特性,将里层的事件委托给外层事件,将事件绑定到目标元素的父节点,根据event对象的属性进行事件委托,改善性能。事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。
求数组交集?并集?
- 直接使用 filter、concat 来计算
var a = [1,2,3,4,5]
var b = [2,4,6,8,10]
//交集
var c = a.filter(function(v){ return b.indexOf(v) > -1 })
//差集
var d = a.filter(function(v){ return b.indexOf(v) == -1 })
//补集
var e = a.filter(function(v){ return !(b.indexOf(v) > -1) })
.concat(b.filter(function(v){ return !(a.indexOf(v) > -1)}))
//并集
var f = a.concat(b.filter(function(v){ return !(a.indexOf(v) > -1)}));
console.log("数组a:", a);
console.log("数组b:", b);
console.log("a与b的交集:", c);
console.log("a与b的差集:", d);
console.log("a与b的补集:", e);
console.log("a与b的并集:", f);
- 借助扩展运算符(...)以及 Set 的特性实现相关计算,代码也会更加简单些
var b = [2,4,6,8,10]
console.log("数组a:", a);
console.log("数组b:", b);
var sa = new Set(a);
var sb = new Set(b);
// 交集
let intersect = a.filter(x => sb.has(x));
// 差集
let minus = a.filter(x => !sb.has(x));
// 补集
let complement = [...a.filter(x => !sb.has(x)), ...b.filter(x => !sa.has(x))];
// 并集
let unionSet = Array.from(new Set([...a, ...b]));
console.log("a与b的交集:", intersect);
console.log("a与b的差集:", minus);
console.log("a与b的补集:", complement);
console.log("a与b的并集:", unionSet);
将 Symbol 引入ES6 的目的是什么?
> Symbol 是一种新的、特殊的对象,可以用作对象中惟一的属性名。使用 Symbol 替换string 可以避免不同的模块属性的冲突。还可以将Symbol设置为私有,以便尚无直接访问Symbol权限的任何人都不能访问它们的属性。
Symbol 是JS新的基本数据类型。与number、string和boolean 原始类型一样,Symbol 也有一个用于创建它们的函数。与其他原始类型不同,Symbol没有字面量语法。创建它们的唯一方法是使用以下方法中的Symbol构造函数
let symbol = Symbol();
解释一下原型设计模式(Prototype Pattern)
> 原型模式会创建新的对象,而不是创建未初始化的对象,它会返回使用从原型或样本对象复制的值进行初始化的对象。原型模式也称为属性模式。
原型模式有用的一个例子是使用与数据库中的默认值匹配的值初始化业务对象。原型对象保留默认值,这些默认值将被复制到新创建的业务对象中。
传统语言很少使用原型模式,但是JavaScript作为一种原型语言,在构建新对象及其原型时使用这种模式。
ES6 中的临时死区是什么
> 在 ES6 中,let 和const 跟 var、class和function一样也会被提升,只是在进入作用域和被声明之间有一段时间不能访问它们,这段时间是临时死区(TDZ)。
ES6 中的 WeakMap的实际用途是什么?
> WeakMaps 提供了一种从外部扩展对象而不影响垃圾收集的方法。当咱们想要扩展一个对象,但是因为它是封闭的或者来自外部源而不能扩展时,可以应用WeakMap。
WeakMap只适用于 ES6 或以上版本。WeakMap是键和值对的集合,其中键必须是对象。
WeakMaps的有趣之处在于,它包含了对map内部键的弱引用。弱引用意味着如果对象被销毁,垃圾收集器将从WeakMap中删除整个条目,从而释放内存。
> 和Map的区别:
当它们的键/值引用的对象被删除时,它们的行为都不同,以下面的代码为例:
var map=new Map();
var weakmap=new WeakMap();
(funciton(){
var a={
x:12
};
var b={
y:12
};
map.set(a,1);
weakmap.set(b,2);
})()
执行上面的 IIFE,就无法再引用{x:12}和{y:12}。垃圾收集器继续运行,并从 WeakMa中删除键b指针,还从内存中删除了{y:12}。
但在使用 Map的情况下,垃圾收集器不会从Map中删除指针,也不会从内存中删除{x:12}。
WeakMap 允许垃圾收集器执行其回收任务,但Map不允许。对于手动编写的 Map,数组将保留对键对象的引用,以防止被垃圾回收。但在WeakMap中,对键对象的引用被“弱”保留,这意味着在没有其他对象引用的情况下,它们不会阻止垃圾回收。
举一个柯里化函数的例子,并说明柯里化的好处?
> 柯里化是一种模式,其中一个具有多个参数的函数被分解成多个函数,当被串联调用时,这些函数将一次累加一个所需的所有参数。这种技术有助于使用函数式编写的代码更容易阅读和编写。需要注意的是,要实现一个函数,它需要从一个函数开始,然后分解成一系列函数,每个函数接受一个参数。
function curry(fn){
if(fn.length===0){
return fn;
}
function _curried(depth,args){
return function(newArgument){
if(depth-1===0){
return fn(...args,newArgument)
}
return _curried(depth-1,[...args,newArgument]);
}
}
return _curried(fn.length,[]);
}
function add(a,b){
return a+b;
}
var curriedAdd=curry(add);
var addFive=curried(5);
var result=[0,1,2,3,4,5].map(addFive);
什么是域名收敛?
> PC 时代为了突破浏览器的域名并发限制。有了域名发散。
浏览器有并发限制,是为了防止DDOS攻击。
域名收敛:就是将静态资源放在一个域名下。减少DNS解析的开销。
域名发散:是将静态资源放在多个子域名下,就可以多线程下载,提高并行度,使客户端加载静态资源更加迅速。
域名发散是pc端为了利用浏览器的多线程并行下载能力。而域名收敛多用与移动端,提高性能,因为dns解析是是从后向前迭代解析,如果域名过多性能会下降,增加DNS的解析开销。
target和currentTarget区别
- event.target
返回触发事件的元素
- event.currentTarget
返回绑定事件的元素
让异步编程更像同步编程,有几种方法?
- 回调函数实现
- 事件监听
- 发布订阅
- Promise/A+ 和生成器函数
- async/await
如何编写高性能的 JavaScript?
- 遵循严格模式:"use strict";
- 将 js 脚本放在页面底部,加快渲染页面
- 将 js 脚本将脚本成组打包,减少请求
- 使用非阻塞方式下载 js 脚本
- 尽量使用局部变量来保存全局变量
- 尽量减少使用闭包
- 使用 window 对象属性方法时,省略 window
- 尽量减少对象成员嵌套
- 缓存 DOM 节点的访问
- 通过避免使用 eval() 和 Function() 构造器
- 给 setTimeout() 和 setInterval() 传递函数而不是字符串作为参数
- 尽量使用直接量创建对象和数组
- 最小化重绘(repaint)和回流(reflow)
DOM 元素 e 的 e.getAttribute(propName)和 e.propName 有什么区别和联系
- e.getAttribute(),是标准 DOM 操作文档元素属性的方法,具有通用性可在任意文档上使用,返回元素在源文件中设置的属性
- e.propName 通常是在 HTML 文档中访问特定元素的特性,浏览器解析元素后生成对应对象(如 a 标签生成 HTMLAnchorElement),这些对象的特性会根据特定规则结合属性设置得到,对于没有对应特性的属性,只能使用 getAttribute 进行访问
- e.getAttribute()返回值是源文件中设置的值,类型是字符串或者 null(有的实现返回"")
- e.propName 返回值可能是字符串、布尔值、对象、undefined 等
- 大部分 attribute 与 property 是一一对应关系,修改其中一个会影响另一个,如 id,title 等属性
- 一些布尔属性`<input hidden/>`的检测设置需要 hasAttribute 和 removeAttribute 来完成,或者设置对应 property
- 像`<a href="../index.html">link</a>`中 href 属性,转换成 property 的时候需要通过转换得到完整 URL
- 一些 attribute 和 property 不是一一对应如:form 控件中`<input value="hello"/>`对应的是 defaultValue,修改或设置 value property 修改的是控件当前值,setAttribute 修改 value 属性不会改变 value property
offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别
- offsetWidth/offsetHeight 返回值包含 content + padding + border,效果与 e.getBoundingClientRect()相同
- clientWidth/clientHeight 返回值只包含 content + padding,如果有滚动条,也不包含滚动条
- scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸
script 的位置是否会影响首屏显示时间?
> 在解析 HTML 生成 DOM 过程中,js 文件的下载是并行的,不需要 DOM 处理到 script 节点。因此,script 的位置不影响首屏显示的开始时间。浏览器解析 HTML 是自上而下的线性过程,script 作为 HTML 的一部分同样遵循这个原则。因此,script 会延迟 DomContentLoad,只显示其上部分首屏内容,从而影响首屏显示的完成时间
web 开发中会话跟踪的方法有哪些
- cookie
- session
- url 重写
- 隐藏 input
- ip 地址
document.write 和 innerHTML 的区别
- document.write 只能重绘整个页面
- innerHTML 可以重绘页面的一部分
如何删除一个 cookie
- 将时间设为当前时间往前一点
var date = new Date();
date.setDate(date.getDate() - 1);//真正的删除
setDate()方法用于设置一个月的某一天
-expires 的设置
document.cookie = 'user='+ encodeURIComponent('name') + ';expires = ' + new Date(0)
attribute 和 property 的区别是什么?
- attribute 是 dom 元素在文档中作为 html 标签拥有的属性;
- property 就是 dom 元素在 js 中作为对象拥有的属性。
- 对于 html 的标准属性来说,attribute 和 property 是同步的,是会自动更新的
- 但是对于自定义的属性来说,他们是不同步的
在 javascript 中,1 与 Number(1)有什么区别
var a = Number(1) // 1
var b = new Number(1) // Number {[[PrimitiveValue]]: 1}
typeof (a) // number
typeof (b) // object
a == b // true
- var a = 1 是一个常量,而 Number(1)是一个函数
- new Number(1)返回的是一个对象
- a==b 为 true 是因为所以在求值过程中,总是会强制转为原始数据类型而非对象,例如下面的代码:
typeof 123 // "number"
typeof new Number(123) // "object"
123 instanceof Number // false
(new Number(123)) instanceof Number // true
123 === new Number(123) // false
为什么 JS 是单线程,而不是多线程 [常考]
- 单线程是指 JavaScript 在执行的时候,有且只有一个主线程来处理所有的任务。
- 目的是为了实现与浏览器交互。
- 我们设想一下,如果 JavaScript 是多线程的,现在我们在浏览器中同时操作一个 DOM,一个线程要求浏览器在这个 DOM 中添加节点,而另一个线程却要求浏览器删掉这个 DOM 节点,那这个时候浏览器就会很郁闷,他不知道应该以哪个线程为准。所以为了避免此类现象的发生,降低复杂度,JavaScript 选择只用一个主线程来执行代码,以此来保证程序执行的一致性。
介绍一下rAF(requestAnimationFrame)
> 专门用来做动画,不卡顿,用法和setTimeout一样。对 rAF 的阐述 MDN 资料
定时器一直是 js 动画的核心技术,但它们不够精准,因为定时器时间参数是指将执行代码放入 UI 线程队列中等待的时间,如果前面有其他任务队列执行时间过长,则会导致动画延迟,效果不精确等问题。
所以处理动画循环的关键是知道延迟多长时间合适:时间要足够短,才能让动画看起来比较柔滑平顺,避免多余性能损耗;时间要足够长,才能让浏览器准备好变化渲染。这个时候 rAF 就出现了,采用系统时间间隔(大多浏览器刷新频率是 60Hz,相当于 1000ms/60≈16.6ms),保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制。并且 rAF 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成
CSS
水平垂直居中的方法有哪些?
- 绝对定位水平垂直居中
<div style="position: absolute;
width: 500px;
height: 300px;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: green;">水平垂直居中</div>
- 水平垂直居中
<div style="position: relative;
width:400px;
height:200px;
top: 50%;
left: 50%;
margin: -100px 0 0 -200px;
background-color: red;">水平垂直居中</div>
- 水平垂直居中
<div style="position: absolute;
width:300px;
height:200px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: blue;">水平垂直居中</div>
- flex 布局居中
<div style="display: flex;align-items: center;justify-content: center;">
<div style="width: 100px;height: 100px;background-color: gray;">flex 布局</div>
</div>
请写出你所知道的display属性的值
- display:none 此元素不会被显示
- display:block 此元素将显示为块级元素,此元素前后会带有换行符
- display:inline 此元素会被显示为内联元素,元素前后没有换行符
- display:inline-block 行内块元素,css2.1新增
- display:list-item 此元素会作为列表显示
- display:run-in 此元素会根据上下文作为块级元素或内两元素显示
- display:compact css2.1废除
- display:marker css2.1废除
- display:table 此元素会作为块级表格来显示,表格前后带有换行符
- display:inline-table 此元素会作为内联表格来显示,表格前后没有换行符
- display:table-row-group 此元素会作为一个或多个行的分组来显示(类似<tbody>)
- display:table-header-group 此元素会作为一个或多个行的分组来显示(类似<thead>)
- display:table-footer-group 此元素会作为一个或多个行的分组来显示(类似<tfoot>)
- display:table-row 此元素会作为一个表格行来显示(类似<tr>)
- display:table-column-group 此元素会作为一个或多个列的分组来显示(类似<colgroup>)
- display:table-column 此元素会作为一个单元格列显示(类似<col>)
- display:table-cell 此元素会作为一个表格单元格来显示(类似<td>和<th>)
- display:table-caption 此元素会作为一个表格标题来显示(类似<caption>)
- display:inherit 规定应该从父元素继承display属性的值
谈一谈CSS重绘与回流/重排?
会触发重绘或回流/重排的操作
- 添加、删除元素(回流+重绘)
- 隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)
- 移动元素,如改变top、left或移动元素到另外1个父元素中(重绘+回流)
- 改变浏览器大小(回流+重绘)
- 改变浏览器的字体大小(回流+重绘)
- 改变元素的padding、border、margin(回流+重绘)
- 改变浏览器的字体颜色(只重绘,不回流)
- 改变元素的背景颜色(只重绘,不回流)
优化:
- 用transform 代替 top,left ,margin-top, margin-left... 这些位移属性
- opacity 加上 transform: translateZ/3d 这个属性之后便不会发生回流和重绘了
- 不要使用 js 代码对dom 元素设置多条样式,选择用一个 className 代替之。
- 如果确实需要用 js 对 dom 设置多条样式那么可以将这个dom 先隐藏,然后再对其设置
- 不要使用table 布局,因为table 的每一个行甚至每一个单元格的样式更新都会导致整个table 重新布局
- 对于频繁变化的元素应该为其加一个 transform 属性,对于视频使用video 标签
px、em、rem的区别?
- 三者都是制作web网页时用到的单位
- px是实际像素大小,em是相对于父元素的像素大小,rem是相对于跟玉原石的像素大小
- IE6~8不支持em和rem属性
css有哪些选择器?权重?
- 选择器类型:
- ID #id
- class .class
- 标签 p
- 通用 *
- 属性 [type="text"]
- 伪类 :hover
- 伪元素 ::first-line
- 子选择器、相邻选择器
- 权重计算规则:
- 第一等:代表内联样式,如: style=””,权值为1000。
- 第二等:代表ID选择器,如:#content,权值为0100。
- 第三等:代表类,伪类和属性选择器,如.content,权值为0010。
- 第四等:代表类型选择器和伪元素选择器,如div p,权值为0001。
- 通配符、子选择器、相邻选择器等的。如*、>、+,权值为0000。
- 继承的样式没有权值。
说说flex布局?
> Flex(Flexible Box),也就是”弹性布局”,它可以很灵活地实现垂直居中、多列布局等自适应问题。而任何一个容器都可以指定为Flex布局。设为Flex布局以后,子元素的float、clear和vertical-align属性将失效
介绍一下盒模型
- 盒模型:内容(content)、填充(padding)、边界(margin)、 边框(border)
- 类型: IE 盒子模型、标准 W3C 盒子模型;
- 两种盒模型的主要区别是:标准盒模型的宽高是值内容宽高(content)
- 而IE盒模型的宽高是指content+padding+border。
- 设置盒模型的方式是:设置box-sizing box-sizing:content-box 标准盒模型, box-sizing:border-box IE盒模型
重置(resetting)CSS 和 标准化(normalizing)CSS 的区别是什么?你会选择哪种方式,为什么?
- 重置(Resetting):重置意味着除去所有的浏览器默认样式。对于页面所有的元素,像margin、padding、font-size这些样式全部置成一样。你将必须重新定义各种元素的样式。
- 标准化(Normalizing):标准化没有去掉所有的默认样式,而是保留了有用的一部分,同时还纠正了一些常见错误。
当需要实现非常个性化的网页设计时,我会选择重置的方式,因为我要写很多自定义的样式以满足设计需求,这时候就不再需要标准化的默认样式了。
请阐述Float定位的工作原理。
> 浮动(float)是 CSS 定位属性。浮动元素从网页的正常流动中移出,但是保持了部分的流动性,会影响其他元素的定位(比如文字会围绕着浮动元素)。这一点与绝对定位不同,绝对定位的元素完全从文档流中脱离。
CSS 的clear属性通过使用left、right、both,让该元素向下移动(清除浮动)到浮动元素下面。
如果父元素只包含浮动元素,那么该父元素的高度将塌缩为 0。我们可以通过清除(clear)从浮动元素后到父元素关闭前之间的浮动来修复这个问题。
有一种 hack 的方法,是自定义一个.clearfix类,利用伪元素选择器::after清除浮动。另外还有一些方法,比如添加空的<div></div>和设置浮动元素父元素的overflow属性。与这些方法不同的是,clearfix方法,只需要给父元素添加一个类,定义如下:
.clearfix::after {
content: '';
display: block;
clear: both;
}
值得一提的是,把父元素属性设置为overflow: auto或overflow: hidden,会使其内部的子元素形成块格式化上下文(Block Formatting Context),并且父元素会扩张自己,使其能够包围它的子元素。
请阐述z-index属性,并说明如何形成层叠上下文(stacking context)。
> CSS 中的z-index属性控制重叠元素的垂直叠加顺序。z-index只能影响position值不是static的元素。
没有定义z-index的值时,元素按照它们出现在 DOM 中的顺序堆叠(层级越低,出现位置越靠上)。非静态定位的元素(及其子元素)将始终覆盖静态定位(static)的元素,而不管 HTML 层次结构如何。
层叠上下文是包含一组图层的元素。在一组层叠上下文中,其子元素的z-index值是相对于该父元素而不是 document root 设置的。每个层叠上下文完全独立于它的兄弟元素。如果元素 B 位于元素 A 之上,则即使元素 A 的子元素 C 具有比元素 B 更高的z-index值,元素 C 也永远不会在元素 B 之上.
每个层叠上下文是自包含的:当元素的内容发生层叠后,整个该元素将会在父层叠上下文中按顺序进行层叠。少数 CSS 属性会触发一个新的层叠上下文,例如opacity小于 1,filter不是none,transform不是none。
请阐述块格式化上下文(Block Formatting Context)及其工作原理。
块格式上下文(BFC)是 Web 页面的可视化 CSS 渲染的部分,是块级盒布局发生的区域,也是浮动元素与其他元素交互的区域。
一个 HTML 盒(Box)满足以下任意一条,会创建块格式化上下文:
- float的值不是none.
- position的值不是static或relative.
- display的值是table-cell、table-caption、inline-block、flex、或inline-flex。
- overflow的值不是visible。
在 BFC 中,每个盒的左外边缘都与其包含的块的左边缘相接。
两个相邻的块级盒在垂直方向上的边距会发生合并(collapse)。更多内容请参考边距合并(margin collapsing)。
有哪些清除浮动的技术,都适用哪些情况?
- 空div方法:<div style="clear:both;"></div>。
- Clearfix 方法:上文使用.clearfix类已经提到。
- overflow: auto或overflow: hidden方法:上文已经提到。
在大型项目中,我会使用 Clearfix 方法,在需要的地方使用.clearfix。设置overflow: hidden的方法可能使其子元素显示不完整,当子元素的高度大于父元素时。
请解释什么是雪碧图(css sprites),以及如何实现?
雪碧图是把多张图片整合到一张上的图片。它被运用在众多使用了很多小图标的网站上(Gmail 在使用)。实现方法:
- 使用生成器将多张图片打包成一张雪碧图,并为其生成合适的 CSS。
- 每张图片都有相应的 CSS 类,该类定义了background-image、background-position和background-size属性。
- 使用图片时,将相应的类添加到你的元素中。
好处:
- 减少加载多张图片的 HTTP 请求数(一张雪碧图只需要一个请求)。但是对于 HTTP2 而言,加载多张图片不再是问题。
- 提前加载资源,防止在需要时才在开始下载引发的问题,比如只出现在:hover伪类中的图片,不会出现闪烁。
如何解决不同浏览器的样式兼容性问题?
- 在确定问题原因和有问题的浏览器后,使用单独的样式表,仅供出现问题的浏览器加载。这种方法需要使用服务器端渲染。
- 使用已经处理好此类问题的库,比如 Bootstrap。
- 使用 autoprefixer 自动生成 CSS 属性前缀。
- 使用 Reset CSS 或 Normalize.css。
如何为功能受限的浏览器提供页面?使用什么样的技术和流程?
- 优雅的降级:为现代浏览器构建应用,同时确保它在旧版浏览器中正常运行。
- Progressive enhancement - The practice of building an application for a base level of user experience, but adding functional enhancements when a browser supports it.
- 渐进式增强:构建基于用户体验的应用,但在浏览器支持时添加新增功能。
- 利用 caniuse.com 检查特性支持。
- 使用 autoprefixer 自动生成 CSS 属性前缀。
- 使用 Modernizr进行特性检测。
有什么不同的方式可以隐藏内容(使其仅适用于屏幕阅读器)?
这些方法与可访问性(a11y)有关。
- visibility: hidden:元素仍然在页面流中,并占用空间。
- width: 0; height: 0:使元素不占用屏幕上的任何空间,导致不显示它。
- position: absolute; left: -99999px:将它置于屏幕之外。
- text-indent: -9999px:这只适用于block元素中的文本。
- Metadata:例如通过使用 Schema.org,RDF 和 JSON-LD。
- WAI-ARIA:如何增加网页可访问性的 W3C 技术规范。
即使 WAI-ARIA 是理想的解决方案,我也会采用绝对定位方法,因为它具有最少的注意事项,适用于大多数元素,而且使用起来非常简单。
除了screen,你还能说出一个 @media 属性的例子吗?
- all
适用于所有设备。
- print
为了加载合适的文档到当前使用的可视窗口. 需要提前咨询 paged media(媒体屏幕尺寸), 以满足个别设备网页尺寸不匹配等问题。
- screen
主要适用于彩色的电脑屏幕
- speech
解析speech这个合成器. 注意: CSS2已经有一个相似的媒体类型叫aural.
编写高效的 CSS 应该注意什么?
> 首先,浏览器从最右边的选择器,即关键选择器(key selector),向左依次匹配。根据关键选择器,浏览器从 DOM 中筛选出元素,然后向上遍历被选元素的父元素,判断是否匹配。选择器匹配语句链越短,浏览器的匹配速度越快。避免使用标签和通用选择器作为关键选择器,因为它们会匹配大量的元素,浏览器必须要进行大量的工作,去判断这些元素的父元素们是否匹配。
BEM (Block Element Modifier) methodology recommends that everything has a single class, and, where you need hierarchy, that gets baked into the name of the class as well, this naturally makes the selector efficient and easy to override. BEM (Block Element Modifier)原则上建议为独立的 CSS 类命名,并且在需要层级关系时,将关系也体现在命名中,这自然会使选择器高效且易于覆盖。
搞清楚哪些 CSS 属性会触发重新布局(reflow)、重绘(repaint)和合成(compositing)。在写样式时,避免触发重新布局的可能。
使用 CSS 预处理的优缺点分别是什么?
- 优点:
- 提高 CSS 可维护性。
- 易于编写嵌套选择器。
- 引入变量,增添主题功能。可以在不同的项目中共享主题文件。
- 通过混合(Mixins)生成重复的 CSS。
- Splitting your code into multiple files. CSS files can be split up too but doing so will require a HTTP request to download each CSS file.
- 将代码分割成多个文件。不进行预处理的 CSS,虽然也可以分割成多个文件,但需要建立多个 HTTP 请求加载这些文件。
- 缺点:
- 需要预处理工具。
- 重新编译的时间可能会很慢
对于你使用过的 CSS 预处理,说说喜欢和不喜欢的地方?
- 喜欢:
- 绝大部分优点上题以及提过。
- Less 用 JavaScript 实现,与 NodeJS 高度结合。
- 不喜欢:
- 我通过node-sass使用 Sass,它用 C ++ 编写的 LibSass 绑定。在 Node 版本切换时,我必须经常重新编译。
- Less 中,变量名称以@作为前缀,容易与 CSS 关键字混淆,如@media、@import和@font-face。
如何实现一个使用非标准字体的网页设计?
> 使用@font-face并为不同的font-weight定义font-family。
解释浏览器如何确定哪些元素与 CSS 选择器匹配。
> 这部分与上面关于编写高效的 CSS 有关。浏览器从最右边的选择器(关键选择器)根据关键选择器,浏览器从 DOM 中筛选出元素,然后向上遍历被选元素的父元素,判断是否匹配。选择器匹配语句链越短,浏览器的匹配速度越快。
例如,对于形如p span的选择器,浏览器首先找到所有<span>元素,并遍历它的父元素直到根元素以找到<p>元素。对于特定的<span>,只要找到一个<p>,就知道'`已经匹配并停止继续匹配。
描述伪元素及其用途。
CSS 伪元素是添加到选择器的关键字,去选择元素的特定部分。它们可以用于装饰(:first-line,:first-letter)或将元素添加到标记中(与 content:...组合),而不必修改标记(:before,:after)。
- :first-line和:first-letter可以用来修饰文字。
- 上面提到的.clearfix方法中,使用clear: both来添加不占空间的元素。
- 使用:before和after展示提示中的三角箭头。鼓励关注点分离,因为三角被视为样式的一部分,而不是真正的 DOM。如果不使用额外的 HTML 元素,只用 CSS 样式绘制三角形是不太可能的。
inline和inline-block有什么区别?
- block:
- 大小:填充其父容器的宽度。
- 定位:从新的一行开始,并且不允许旁边有 HTML 元素(除非是float)
- 能否设置width和height:能
- 可以使用vertical-align对齐:不可以
- 边距(margin)和填充(padding): 各个方向都存在
- 浮动(float):-
- inline-block:
- 大小:取决于内容。
- 定位:与其他内容一起流动,并允许旁边有其他元素。
- 能否设置width和height:能
- 可以使用vertical-align对齐:可以
- 边距(margin)和填充(padding):各个方向都存在
- 浮动(float):-
- inline:
- 大小:取决于内容。
- 定位:与其他内容一起流动,并允许旁边有其他元素。
- 能否设置width和height:不能。设置会被忽略。
- 可以使用vertical-align对齐:可以
- 边距(margin)和填充(padding):只有水平方向存在。垂直方向会被忽略。尽管border和padding在content周围,但垂直方向上的空间取决于'line-height'
- 浮动(float):就像一个block元素,可以设置垂直边距和填充。
relative、fixed、absolute和static四种定位有什么区别?
经过定位的元素,其position属性值必然是relative、absolute、fixed或sticky。
- static:默认定位属性值。该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置。此时 top, right, bottom, left 和 z-index 属性无效。
- relative:该关键字下,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)。
- absolute:不为元素预留空间,通过指定元素相对于最近的非 static 定位祖先元素的偏移,来确定元素位置。绝对定位的元素可以设置外边距(margins),且不会与其他边距合并。
- fixed:不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。打印时,元素会出现在的每页的固定位置。fixed 属性会创建新的层叠上下文。当元素祖先的 transform 属性非 none 时,容器由视口改为该祖先。
- sticky:盒位置根据正常流计算(这称为正常流动中的位置),然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。在所有情况下(即便被定位元素为 table 时),该元素定位均不对后续元素造成影响。当元素 B 被粘性定位时,后续元素的位置仍按照 B 未定位时的位置来确定。position: sticky 对 table 元素的效果与 position: relative 相同。
你使用过哪些现有的 CSS 框架?你是如何改进它们的?
- Bootstrap:更新周期缓慢。Bootstrap 4 已经处于 alpha 版本将近两年了。添加了在页面中广泛使用的微调按钮组件。
- Semantic UI:源代码结构使得自定义主题很难理解。非常规主题系统的使用体验很差。外部库的路径需要硬编码(hard code)配置。变量重新赋值没有 Bootstrap 设计得好。
- Bulma:需要很多非语义的类和标记,显得很多余。不向后兼容,以至于升级版本后,会破坏应用的正常运行。
你了解 CSS Flex 和 Grid 吗?
- Flex:
flex容器中存在两条轴, 横轴和纵轴, 容器中的每个单元称为flex item。
在容器上可以设置6个属性: flex-direction flex-wrap flex-flow justify-content align-items align-content
注意:当设置 flex 布局之后,子元素的 float、clear、vertical-align 的属性将会失效。
Flex 项目属性
有六种属性可运用在 item 项目上: 1. order 2. flex-basis 3. flex-grow 4. flex-shrink 5. flex 6. align-self
- Grid:
CSS网格布局用于将页面分割成数个主要区域,或者用来定义组件内部元素间大小、位置和图层之间的关系。
像表格一样,网格布局让我们能够按行或列来对齐元素。但是,使用CSS网格可能还是比CSS表格更容易布局。例如,网格容器的子元素可以自己定位,以便它们像CSS定位的元素一样,真正的有重叠和层次。
响应式设计与自适应设计有何不同?
> 响应式设计和自适应设计都以提高不同设备间的用户体验为目标,根据视窗大小、分辨率、使用环境和控制方式等参数进行优化调整。
响应式设计的适应性原则:网站应该凭借一份代码,在各种设备上都有良好的显示和使用效果。响应式网站通过使用媒体查询,自适应栅格和响应式图片,基于多种因素进行变化,创造出优良的用户体验。就像一个球通过膨胀和收缩,来适应不同大小的篮圈。
自适应设计更像是渐进式增强的现代解释。与响应式设计单一地去适配不同,自适应设计通过检测设备和其他特征,从早已定义好的一系列视窗大小和其他特性中,选出最恰当的功能和布局。与使用一个球去穿过各种的篮筐不同,自适应设计允许使用多个球,然后根据不同的篮筐大小,去选择最合适的一个。
你有没有使用过视网膜分辨率的图形?当中使用什么技术?
> 我倾向于使用更高分辨率的图形(显示尺寸的两倍)来处理视网膜显示。更好的方法是使用媒体查询,像@media only screen and (min-device-pixel-ratio: 2) { ... },然后改变background-image。
对于图标类的图形,我会尽可能使用 svg 和图标字体,因为它们在任何分辨率下,都能被渲染得十分清晰。
还有一种方法是,在检查了window.devicePixelRatio的值后,利用 JavaScript 将<img>的src属性修改,用更高分辨率的版本进行替换。
什么情况下,用translate()而不用绝对定位?什么时候,情况相反。
> translate()是transform的一个值。改变transform或opacity不会触发浏览器重新布局(reflow)或重绘(repaint),只会触发复合(compositions)。而改变绝对定位会触发重新布局,进而触发重绘和复合。transform使浏览器为元素创建一个 GPU 图层,但改变绝对定位会使用到 CPU。因此translate()更高效,可以缩短平滑动画的绘制时间。
当使用translate()时,元素仍然占据其原始空间(有点像position:relative),这与改变绝对定位不同。
display:none、visibile:hidden、opacity:0的区别
- display:none:
- 是否隐藏:是
- 是否在文档中占用空间:否
- 是否会触发事件:否
- visible:hidden:
- 是否隐藏:是
- 是否在文档中占用空间:是
- 是否会触发事件:否
- opacity:0:
- 是否隐藏:是
- 是否在文档中占用空间:是
- 是否会触发事件:是
页面导入样式时,使用link和@import有什么区别?
- 用途:
- @import只能引入css文件
- link既能引入css,又能引入其它文件,比如:Vue脚手架唯一index.html中引入.ico图标
- 加载顺序:
- 加载页面时,link标签引入的css被同时加载;
- @import引入的css将在页面加载完毕后被加载
- 优先级:
- @import中的样式虽然比link引入的css内容晚加载,但是,优先级却低于当前link引入的css内的其余样式
- link支持使用DOM动态添加和改变,而@import用DOM操作不了
css hack是什么
> 由于不同的浏览器,比如Internet Explorer 6,Internet Explorer 7,Mozilla Firefox等,对CSS的解析认识不一样,因此会导致生成的页面效果不一样,得不到我们所需要的页面效果。
这个时候我们就需要针对不同的浏览器去写不同的CSS,让它能够同时兼容不同的浏览器,能在不同的浏览器中也能得到我们想要的页面效果。
这个针对不同的浏览器写不同的CSS code的过程,就叫CSS hack,也叫写CSS hack。
过渡与动画的区别是什么
- transition
可以在一定的时间内实现元素的状态过渡为最终状态,用于模拟以一种过渡动画效果,但是功能有限,只能用于制作简单的动画效果而动画属性
- animation
可以制作类似Flash动画,通过关键帧控制动画的每一步,控制更为精确,从而可以制作更为复杂的动画。
什么是外边距合并
> 外边距合并指的是,当两个垂直外边距相遇时,它们将形成一个外边距。
合并后的外边距的高度等于两个发生合并的外边距的高度中的较大者。
去除inline-block元素间间距的方法
- 移除空格
- 使用margin负值
- 使用font-size:0
- letter-spacing
- word-spacing
CSS 去掉inline-block元素间隙的几种方法?
间隙是怎么来的:
间隙是由换行或者回车导致的;
只要把标签写成一行或者
标签直接没有空格,就不会出现间隙;
怎么去除?
方法1:
元素间的间隙出现的原因
是元素标签之间的空格,
把空格去掉间隙自然就会消失。
<div class="itlike">
<span>撩课itlike</span><span>撩课itlike</span>
</div>
方法2:
利用HTML注释标签
<div class="demo">
<span>撩课itlike</span>
<!-- -->
<span>撩课itlike</span>
</div>
方法3:
取消标签闭合
<div class="demo">
<span>撩课itlike
<span>撩课itlike
<span>撩课itlike
<span>撩课itlike
</div>
方法4:
在父容器上使用font-size:0;可以消除间隙
<div class="demo">
<span>撩课itlike</span>
<span>撩课itlike</span>
<span>撩课itlike</span>
<span>撩课itlike</span>
</div>
.demo {font-size: 0;}
怎么让 Chrome 支持小于 12px 的文字?
.shrink{
-webkit-transform:scale(0.8);
-o-transform:scale(1);
display:inline-block;
}
让页面里的字体变清晰,变细用 CSS 怎么做?(IOS 手机浏览器字体齿轮设置)
-webkit-font-smoothing: antialiased;
Node及服务器
请简述Node.js为什么处理异步IO快?
> Node 底层采用线程池的原理管理异步 IO,所以我们通常所的 单线程是指 Node 中 JavaScript 的执行是单线程的,但 Node 本身是多线程的。Node.js 中异步 IO 是通过事件循环的方式实现的,异步 IO 事件主要来源于网络请求和文件 IO。但是正因为如此,Node.js 处理很多计算密集型的任务,就比较吃力,当然有多进程方式可以解决这个问题。
Node.js 有 cluster、fork 两种模式多进程,那么这两种情况下,主进程负责 TCP 通信,怎样才可以让子进程共享用户的 Socket 对象?
> cluster 模式,多实例、自动共享端口链接、自动实现负载均衡。fork 模式实现的多进程,单实例、多进程,可以通过手动分发 socket 对象给不同子进程进行定制化处理、实现负载均衡
请简述Node.js 多进程维护,以及通信方式
> 原生的 cluster 和 fork 模式都有 API 封装好的进行通信。如果是 execfile 这样形式调起第三方插件形式,想要与第三方插件进行通信,可以自己封装一个类似 promisyfy 形式进行通信,维护这块,子进程可以监听到异常,一旦发现异常,立刻通知主进程,杀死这个异常的子进程,然后重新开启一个子进程~
请简述Koa洋葱模型
> 洋葱圈的实现,有点类似 Promise 中的 then 实现,每次通过 use 方法定义中间件函数时候,就会把这个函数存入一个队列中,全局维护一个 ctx 对象,每次调用 next(),就会调用队列的下一个任务函数。
use (fn) {
// this.fn = fn 改成:
this.middlewares.push(fn) // 每次use,把当前回调函数存进数组
}
compose(middlewares, ctx){ // 简化版的compose,接收中间件数组、ctx对象作为参数
function dispatch(index){ // 利用递归函数将各中间件串联起来依次调用
if(index === middlewares.length) return // 最后一次next不能执行,不然会报错
let middleware = middlewares[index] // 取当前应该被调用的函数
middleware(ctx, () => dispatch(index + 1)) // 调用并传入ctx和下一个将被调用的函数,用户next()时执行该函数
}
dispatch(0)
}
Node.js 的消息队列应用场景是什么?原理是什么?
> 我们公司之前用的 kafka,消息队列的核心概念,异步,提供者,消费者。例如 IM 应用,每天都会有高峰期,但是我们不可能为了高峰期配置那么多服务器,那样就是浪费,所以使用消息队列,在多长时间内流量达到多少,就控制消费频率,例如客户端是流的提供者,有一个中间件消费队列,我们的服务器是消费者,每次消费一个任务就回复一个 ACK 给消费队列,消费频率由我们控制,这样任务不会丢失,服务器也不会挂。 还有一个异步问题,一个用户下单购买一件商品,可能要更新库存,已购数量,支付,下单等任务。不可能同步进行,这时候需要异步并行,事务方式处理。这样既不耽误时间,也能确保所有的任务成功才算成功,不然没有支付成功,但是已购数量增长了就有问题。
怎样绑定node程序到80端口?
- sudo
- apache/nginx代理
- 用操作系统的firewall iptables进行端口重定向
两个node程序之间怎样交互?
用fork嘛,上面讲过了.原理是子程序用process.on, process.send,父程序里用child.on,child.send进行交互.
代码演示
1) fork-parent.js
var cp = require('child_process');
var child = cp.fork('./fork-child.js');
child.on('message', function(msg){
console.log('老爸从儿子接受到数据:', msg);
});
child.send('我是你爸爸,送关怀来了!');
2) fork-child.js
process.on('message', function(msg){
console.log("儿子从老爸接收到的数据:", msg);
process.send("我不要关怀,我要银民币!");
});
实现一个简单的命令行交互程序?
那就用spawn吧.
var cp = require('child_process');
var child = cp.spawn('echo', ['你好', "钩子"]); // 执行命令
child.stdout.pipe(process.stdout); // child.stdout是输入流,process.stdout是输出流
// 这句的意思是将子进程的输出作为当前程序的输入流,然后重定向到当前程序的标准输出,即控制台
为什么需要child-process?
> node是异步非阻塞的,这对高并发非常有效.可是我们还有其它一些常用需求,比如和操作系统shell命令交互,调用可执行文件,创建子进程进行阻塞式访问或高CPU计算等,child-process就是为满足这些需求而生的.child-process顾名思义,就是把node阻塞的工作交给子进程去做.
exec,execFile,spawn和fork都是做什么用的?
> exec可以用操作系统原生的方式执行各种命令,如管道 cat ab.txt | grep hello; execFile是执行一个文件; spawn是流式和操作系统进行交互; fork是两个node程序(javascript)之间时行交互.
node是怎样支持https,tls的?
> 主要实现以下几个步骤即可: 1) openssl生成公钥私钥 2) 服务器或客户端使用https替代http 3) 服务器或客户端加载公钥私钥证书
读写一个文件有多少种方法?
> 总体来说有四种: 1) POSIX式低层读写 2) 流式读写 3) 同步文件读写 4) 异步文件读写
fs.watch和fs.watchFile有什么区别,怎么应用?
> 二者主要用来监听文件变动.fs.watch利用操作系统原生机制来监听,可能不适用网络文件系统; fs.watchFile则是定期检查文件状态变更,适用于网络文件系统,但是相比fs.watch有些慢,因为不是实时机制.
内置的fs模块架构是什么样子的?
> fs模块主要由下面几部分组成: 1) POSIX文件Wrapper,对应于操作系统的原生文件操作 2) 文件流 fs.createReadStream和fs.createWriteStream 3) 同步文件读写,fs.readFileSync和fs.writeFileSync 4) 异步文件读写, fs.readFile和fs.writeFile
有哪些常用Stream,分别什么时候使用?
> Readable为可被读流,在作为输入数据源时使用;Writable为可被写流,在作为输出源时使用;Duplex为可读写流,它作为输出源接受被写入,同时又作为输入源被后面的流读出.Transform机制和Duplex一样,都是双向流,区别时Transfrom只需要实现一个函数_transfrom(chunk, encoding, callback);而Duplex需要分别实现_read(size)函数和_write(chunk, encoding, callback)函数.
请解释一下什么是Nginx?
> Nginx是一个web服务器和方向代理服务器,用于HTTP、HTTPS、SMTP、POP3和IMAP协议。
请列举Nginx的一些特性。
- 反向代理/L7负载均衡器
- 嵌入式Perl解释器
- 动态二进制升级
- 可用于重新编写URL,具有非常好的PCRE支持
请解释Nginx如何处理HTTP请求。
> Nginx使用反应器模式。主事件循环等待操作系统发出准备事件的信号,这样数据就可以从套接字读取,在该实例中读取到缓冲区并进行处理。单个线程可以提供数万个并发连接。
Nginx正向代理与反向代理
- 正向代理:
正向代理也是大家最常接触的到的代理模式,那究竟什么是正向代理呢?我们都知道Google在国内是无法正常访问的,但是某些时候我们由于技术问题需要去访问Google时,我们会先找到一个可以访问Google的代理服务器,我们将请求发送到代理服务器,代理服务器去访问Google,然后将访问到的数据返回给我们,这样的过程就是正向代理。正向代理最大的特点是客户端需要明确知道要访问的服务器地址,Google服务器只清楚请求来自哪个代理服务器,而不清楚来自哪个具体的客户端,正向代理可以隐藏真实客户端的具体信息。
客户端必须设置正向代理服务器,而且需要知道正向代理服务器的IP地址以及代理程序的端口。一句话来概括就是正向代理代理的是客户端,是一个位于客户端和Google服务器之间的服务器,为了从Google服务器取得数据,客户端向代理服务器发送一个请求并指定目标(Google服务器),然后代理向原始服务器转交请求并将获得的数据返回给客户端。总结正向代理的几个作用:
- 访问国外无法访问的网站做缓存,加速访问资源
- 对客户端访问授权,上网进行认证代理
- 可以记录用户访问记录(上网行为管理),对外隐藏用户信息
- 反向代理:
多个客户端给服务器发送的请求,Nginx服务器接收到请求以后,按照一定的规则转发到不同的服务器进行业务逻辑处理,也就是我们刚才讲到的负载均衡的策略。此时请求来源于哪个客户端是确定的,但是请求由哪台服务器处理的并不明确,Nginx扮演的就是一个反向代理角色。可以这样来理解,反向代理对外都是透明的,访问者并不知道自己访问的是一个代理。反向代理代理的是服务端,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息。总结下反向代理的两个作用:
- 保证内网的安全,通常将反向代理作为公网访问地址,Web服务器是内网
- 负载均衡,通过反向代理服务器来优化网站的负载
- 区别:
- 在正向代理中,隐藏了请求来源的客户端信息;
- 在反向代理中,隐藏了请求具体处理的服务端信息;
请列举Nginx服务器的最佳用途。
> Nginx服务器的最佳用法是在网络上部署动态HTTP内容,使用SCGI、WSGI应用程序服务器、用于脚本的FastCGI处理程序。它还可以作为负载均衡器。
Nginx如何实现高并发
> 这个问题出来可能懂一点Nginx的朋友们都是浮现出5个字:异步非阻塞。实际上Nginx就是异步非阻塞,使用了epoll模型并对底层代码进行大幅度优化。之前其实有讲过Nginx是采用1个master进程,多个worker进程的模式,每次接收到一个请求,master会将请求按照一定策略分发给一个worker进程去进行处理请求。worker进程数一般设置为和CPU核心数一致,异步非阻塞模式就会使得worker线程在等待请求callback的空闲时间可以接收处理新的请求,当接收到旧请求的callback时再回去继续处理该请求,这样就完成了少数几个worker进程却实现了高并发的问题。
Nginx为何不使用多线程?
> 众所周知,没创建一个新的线程,都需要为其分配cpu和内存。当然,创建进程也是一样,但是由于线程过多会导致内存消耗过多。所以Nginx采用单线程异步处理用户请求,这样不需要不断地为新的线程分配cpu和内存,减轻服务器内存消耗,所以使得Nginx性能方面更为高效。
Nginx如何处理请求?
> Nginx启动后,首先进行配置文件的解析,解析成功会得到虚拟服务器的ip和端口号,在主进程master进程中创建socket,对addrreuse选项进行设置,并将socket绑定到对应的ip地址和端口并进行监听。然后创建子进程worker进程,当客户端和Nginx进行三次握手,则可以创建成功与Nginx的连接。当有新的请求进入时,空闲的worker进程会竞争,当某一个worker进程竞争成功,则会得到这个已经成功建立连接的socket,然后创建ngx_connection_t结构体,接下来设置读写事件处理函数并添加读写事件用来与客户端进行数据交换。当请求结束Nginx或者客户端主动关闭连接,此时一个请求处理完毕。
为什么要做动静分离?
> 在日常开发中,前端请求静态文件比如图片资源是不需要经过后端服务器的,但是调用API这些类型的就需要后端进行处理请求,所以为了提高对资源文件的响应速度,我们应该使用动静分离的策略去做架构。我们可以将静态文件放到Nginx中,将动态资源的请求转发到后端服务器去进行进一步的处理。
Nginx负载均衡的几种常用方式?
轮询方式:默认情况下Nginx使用轮询的方式实现负载均衡,每个新的请求按照时间顺序逐一分配到不同的后端服务器去进行处理,如果后端服务器宕机,则Nginx的健康检查功能会将这个后端服务器剔除。但是轮询方式是显而易见的:可靠性低而且负载分配不平衡,所以轮询方式更适用于图片服务器或者静态资源服务器。
- weight:可以对不同的后端服务器设置不同的权重比例,这样可以改变不同后端服务器处理请求的比例。可以给性能更优的后端服务器配置更高的权重。
- ip_hash:这种方式会根据请求的ip地址的hash结果分配后端服务器来处理请求,这样每个用户发起的请求固定只会由同一个后端服务器处理,这样可以解决session问题。
- fail:这种方式有点类似于轮询方式,主要是根据后端服务器的响应时间来分配请求,响应时间短的后端服务器优先分配请求。
- url_hash:这种方式是按照请求url的hash结果来将不同请求分配到不同服务器,使用这种方式每个url的请求都会由同一个后端服务器进行处理,后端服务器为缓存时效率会更高。
Session不同步如何处理?
> 上面其实提过了解决方案,负载均衡方式使用ip_hash方式,如果用户已经访问过某个后端器,则再次访问时会将这个请求的ip地址进行哈希算法转换,自动定位到该服务器。当然也可以通过redis缓存用户session,一样可以处理session不同步的问题。
Nginx常用优化配置
- 调整worker_processes指定Nginx需要创建的worker进程数量,刚才有提到worker进程数一般设置为和CPU核心数一致。
- 调整worker_connections设置Nginx最多可以同时服务的客户端数。结合worker_processes配置可以获得每秒可以服务的最大客户端数。
- 启动gzip压缩,可以对文件大小进行压缩,减少了客户端http的传输带宽,可以大幅度提高页面的加载速度。
- 启用缓存,如果请求静态资源,启用缓存是可以大幅度提升性能的。关于启用缓存可以观看Nginx缓存这篇文章:Nginx缓存原理及机制
Vue
MVVM和MVC的区别?
- MVC:MVC模式可以这样理解,将html看成view;js看成controller,处理用户与应用的交互,响应对view的操作(对事件的监听),调用Model对数据进行操作,完成model与view的同步(根据model的改变,通过选择器对view进行操作);将js的ajax当做Model,从服务器获取数据,MVC是单向的。
- MVVM:它实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再自己手动操作Dom元素,来改变View的显示,而是改变属性后该属性对应View层显示会自动改变,MVVM是双向的。
Model–View–ViewModel (MVVM) 是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表
MVVM 源自于经典的 Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。
(1)View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
(2)Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
(3)ViewModel 层
ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。
MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:
(1)View 层
<div id="app">
<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>
</div>
(2)ViewModel 层
var app = new Vue({
el: '#app',
data: { // 用于描述视图状态
message: 'Hello Vue!',
},
methods: { // 用于描述视图行为
showMessage(){
let vm = this;
alert(vm.message);
}
},
created(){
let vm = this;
// Ajax 获取 Model 层的数据
ajax({
url: '/your/server/data/api',
success(res){
vm.message = res;
}
});
}
})
(3) Model 层
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "IoveC",
"domain": "www.cnblogs.com"
}
}
谈一谈vue的虚拟DOM是什么回事?
> Vue.js通过编译将模版转换成渲染函数(render),执行渲染函数就可以得到一个虚拟DOM
简单点讲,在Vue的实现上,Vue讲模版编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应用到DOM操作上。
优点:
保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点:
无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
vue的双向数据绑定原理?
> 实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。
其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
vue组件通信?
- 父组件向子组件传值 --Props传递数据
在父组件中使用儿子组件
<template>
<div>
父组件:{{money}}
<Son1 :money="money"><Son1>
</div>
</template>
<script>
import Son1 from ''./Son1";
export default{
components:{
Son1
},
data(){
return { money: 100};
}
};
</script>
子组件接受数据
props:{
value:{
type:Number,
default:1
}
}
如果是数组
props:{
value:{
type:Array,
default: ()=>[]
}
}
- 子组件通信父组件 $emit使用
<template>
<div>
父组件:{{money}}
<Son1 :money="money" @input="change"><Son1>
</div>
</template>
<script>
import Son1 from ''./Son1";
export default{
methods:{
change(data){
this.money = data
}
},
components:{
Son1
},
data(){
return { money: 100};
}
};
</script>
子组件触发绑定自己身上的方法
<template>
<div>
子组件1:{{money}}
<button @click="$emit('input',200)">修改父组件的值<Son1>
</div>
</template>
<script>
export default{
props:{
money:{
type:Number
}
}
};
</script>
- $parent、$children(多层级传递)
<Grandson1 :value="value"></Grandson1>
<template>
<div>
孙子1:{{value}}
<---调用父组件的input事件-->
<button @click="$parent.$emit('input',200)">更改<Son1>
</div>
</template>
<script>
export default{
props:{
value:{
type:Number
}
}
};
</script>
- $attrs、 $listeners:
$attrs批量向下传入属性:
<Son2 name="小明" age="18"></Son2>
<--可以在son2组件中使用$attrs,可以将属性继续向下传递-->
<div>
儿子2:{{ $attrs.name }}
<Grandson2 v-bind="$attrs"></Grandson2>
</div>
<tempalte>
<div>孙子:{{$attrs}}</div>
</template>
$listeners批量向下传入方法:
<Son2 name="小明" age="18" @click=“()=>{this.money =500}”></Son2>
<--可以在son2组件中使用$attrs,可以将属性继续向下传递-->
<Grandson2 v-bind="$attrs" v-on="$listeners"></Grandson2>
<button @click="$listeners.click()">更改<Son1>
- Provide&Inject
Provide 在父级中注入数据
provide(){
return {parentMsg:'父亲'};
}
Inject
在任意子组件中可以注入父级数据
inject:['parentMsg']//会将数据挂载在当前实例上
- ref使用
<Grandson2 name="花花" ref="grand2"></Grandson2>
mounted(){
console.log(this.$refs.grand2.name);
}
- EventBus:用于跨组件通知
Vue.prototype.$bus = new Vue();
Son2组件和Grandson1互相通信
mounted() {
//父亲组件注册
this.$bus.$on('my',data=>{
console.log(data)
})
}
mounted(){
//侄子组件调用
this.$nextTick(()=>{
this.$bus.$emit('my',"我是小红”);
})
}
父组件可以监听到子组件的生命周期吗?
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
vue中method,computed和watch三者的区别
> computed--适用于重新计算比较费时不用重复数据计算的环境。所有 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例。如果一个数据依赖于其他数据,那么把这个数据设计为computed
watch--像是一个 data 的数据监听回调,当依赖的 data 的数据变化,执行回调,在方法中会传入 newVal 和 oldVal。可以提供输入值无效,提供中间值 特场景。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。如果你需要在某个数据变化时做一些事情,使用watch。
method-- 跟前面的都不一样,我们通常在这里面写入方法,只要调用就会重新执行一次
vue-router是怎么实现的?
vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
- hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':
hash 路由模式的实现主要是基于下面几个特性:
URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。
- history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
history 路由模式的实现主要基于存在下面几个特性:
pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。
- abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
为什么 Vue 的 nextTick 不稳定?
> 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。原因是什么呢,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中
优雅降级:首选 promise.then;然后是 setImmediate;然后是一个浏览器目前支持不好的 API
;最后是 setTimeout。dom 真正更新渲染好的时间,不能真正确定,不论是框架还是原生,都存在这个问题。所以用 nextTick 并不能保证拿到最新的 dom。
vue中$set有什么用?
> 在我们使用vue进行开发的过程中,可能会遇到一种情况:当生成vue实例后,当再次给数据赋值时,有时候并不会自动更新到视图上去,因此可以使用$set。
initTableData() {
this.tableData.forEach(element => {
this.$set(element, 'edit', false)
})
}
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
Vue 框架怎么实现对象和数组的监听?
> 如果被问到 Vue 怎么实现数据双向绑定,大家肯定都会回答 通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 Vue 框架中都知道,Vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // observe 功能为监测数据的变化
}
}
/**
* 对属性进行递归遍历
*/
let childOb = !shallow && observe(val) // observe 功能为监测数据的变化
通过以上 Vue 源码部分查看,我们就能知道 Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。
vue生命周期的理解?
Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
生命周期中有多个事件钩子,如下:
- beforeCreate(创建前) 在数据观测和初始化事件还未开始
- created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
- beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
- mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
- beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
- updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
- beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
- destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
Vue 的父组件和子组件生命周期钩子函数执行顺序?
Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
- 加载渲染过程:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
- 子组件更新过程:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程:
父 beforeUpdate -> 父 updated
- 销毁过程:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
v-if和v-show的区别
> v-if是通过控制dom节点的存在与否来控制元素的显隐;v-show是通过设置DOM元素的display样式,block为显示,none为隐藏;
基于以上区别,因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
父组件:
<ModelChild v-model="message"></ModelChild>
子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
$route和$router的区别
- $router是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性。
- $route对象表示当前的路由信息,包含了当前 URL 解析得到的信息
**1.$route.path** 字符串,对应当前路由的路径,总是解析为绝对路径,如 "/foo/bar"。
**2.$route.params** 一个 key/value 对象,包含了 动态片段 和 全匹配片段, 如果没有路由参数,就是一个空对象。
**3.$route.query** 一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 $route.query.user == 1, 如果没有查询参数,则是个空对象。
**4.$route.hash** 当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。锚点
**5.$route.fullPath** 完成解析后的 URL,包含查询参数和 hash 的完整路径。
**6.$route.matched** 数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
**7.$route.name 当前路径名字**
**8.$route.meta 路由元信息
vue组件data为什么必须是函数?
> 如果data是一个函数的话,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。
// data
data() {
return {
message: "子组件",
childName:this.name
}
}
// new Vue
new Vue({
el: '#app',
router,
template: '<App/>',
components: {App}
})
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
vue中怎么自定义指令?
- 定义全局的自定义变量
Vue.directive('color',{
inserted(el){
// 各单位注意,这里的el获取的是标签元素,说白了就是可以直接操作DOM console.log(el)
el.style.color = "red"
}
})
<div >前端伪大叔</div>
<div v-color>前端伪大叔</div>
- 组件内指令-只有自己组件可以使用
// template
<div >前端伪大叔</div>
<div v-color>前端伪大叔</div>
// script
directives:{
color:{
inserted(el){
el.style.color = 'cyan'
}
}
}
自定义指令(v-check,v-focus)的方法有哪些?它由哪些钩子函数?还有哪些钩子参数?
- 全局定义指令:在vue对象的directive方法里面有两个参数,一个是指令名称,另一个是函数
- 组件内定义指令:directives
- 钩子函数:bind(绑定事件触发)、inserted(节点插入时候触发)、update(组件内相关更新)
- 钩子函数参数:el、binding
对keep-aerlive的了解?
> 通过设置了keep-alive,可以简单理解为从页面1跳转到页面2后,然后后退到页面1,只会加载缓存中之前已经渲染好的页面1,而不会再次重新加载页面1,及不会再触发页面一种的created等类似的钩子函数,除非自己重新刷新该页面1。
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中key的作用?
> key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。
所以 Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
强制替换元素,从而可以触发组件的生命周期钩子或者触发过渡。因为当key改变时,Vue认为一个新的元素产生了,从而会新插入一个元素来替换掉原有的元素。
<transition> <span :key="text">{{text}}</span> </transition>、
--这里如果text发生改变,整个<span>元素会发生更新,因为当text改变时,这个元素的key属性就发生了改变,在渲染更新时,Vue会认为这里新产生了一个元素,而老的元素由于key不存在了,所以会被删除,从而触发了过渡。
同理,key属性被用在组件上时,当key改变时会引起新组件的创建和原有组件的删除,此时组件的生命周期钩子就会被触发。
vue中常用的路由钩子函数有哪一些?
- 全局守卫:
- router.beforeEach 全局前置守卫 进入路由之前
- router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
- router.afterEach 全局后置钩子 进入路由之后
- 路由组件内的守卫:
- beforeRouteEnter 进入路由前
- beforeRouteUpdate (2.2) 路由复用同一个组件时
- beforeRouteLeave 离开当前路由时
vue中常用的修饰符?
- .stop //组织单击事件冒泡
- .prevent //提交事件不再重新加载页面
- .capture //添加事件侦听器时使用事件捕获模式
- .self //只当事件在该元素本身时触发回调(在其子元素上不触发)
- .once //只触发一次事件
vuex中的成员?对应的作用?
- state => 基本数据
- getters => 从基本数据派生的数据
- mutations => 提交更改数据的方法,同步!
- actions => 像一个装饰器,包裹mutations,使之可以异步。
- modules => 模块化Vuex
使用过 Vue SSR 吗?说说 SSR?
> Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
即:SSR大致的意思就是vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。
服务端渲染 SSR 的优缺点如下:
(1)服务端渲染的优点:
更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
(2) 服务端渲染的缺点:
更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。
说说你对 SPA 单页面的理解,它的优缺点分别是什么?
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
- 优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
- 缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
Proxy 与 Object.defineProperty 优劣对比
- Proxy 的优势如下:
- Proxy 可以直接监听对象而非属性;
- Proxy 可以直接监听数组的变化;
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
- Object.defineProperty 的优势如下:
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
你有对 Vue 项目进行哪些优化?
- 代码层面的优化:
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 长列表性能优化
- 事件的销毁
- 图片资源懒加载
- 路由懒加载
- 第三方插件的按需引入
- 优化无限列表性能
- 服务端渲染 SSR or 预渲染
- Webpack 层面的优化:
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
- 基础的 Web 技术的优化:
- 开启 gzip 压缩
- 浏览器缓存
- CDN的使用
- 使用Chrome Performance 查找性能瓶颈
对于即将到来的 vue3.0 特性你有什么了解的吗?
Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:
(1)监测机制的改变
3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
只能监测属性,不能监测对象
检测属性的添加和删除;
检测数组索引和长度的变更;
支持 Map、Set、WeakMap 和 WeakSet。
新的 observer 还提供了以下特性:
用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。
(2)模板
模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
(3)对象式的组件声明方式
vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。
此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。
(4)其它方面的更改
vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:
支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
基于 treeshaking 优化,提供了更多的内置功能。
Vue项目中如何解决首屏加载过慢?
- 配置打包工具,将组件分别打包到不同的js代码块中
build/webpack.base.conf.js
output:{
path: config.build.assetsRoot,
filename:’[name].js’,
//新增
chunkFilename:”[name].js”,
publicPath: process.env.NODE_ENV===”production”
?config.build.assetsPublicPath
:config.dev.assetsPublicPath
}
- 当路由请求到该组件时,才动态加载组件的内容
const Index=()=>import(‘@/views/Index.vue’) // 当用户在vue中请求当前组件对应的路由地址时,由vue-router自动调用加载函数,动态请求Index.vue组件对象
- 服务端渲染SSR
React
什么是虚拟DOM?
> 虚拟 DOM (VDOM)是真实 DOM 在内存中的表示。UI 的表示形式保存在内存中,并与实际的 DOM 同步。这是一个发生在渲染函数被调用和元素在屏幕上显示之间的步骤,整个过程被称为调和。
类组件和函数组件之间的区别是啥?
> 类组件可以使用其他特性,如状态 state 和生命周期钩子;当组件只是接收 props 渲染到页面时,就是无状态组件,就属于函数组件,也被称为哑组件或展示组件。函数组件和类组件当然是有区别的,而且函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件。
区别:
- 是否有this:函数组件没有,类组件有
- 是否有生命周期:函数组件没有,类组件有
- 是否有状态state:函数组件没有,类组件有
解释 React 中 render() 的目的。
> 它被视为普通函数,但 render() 函数必须返回某些值,无论值是否为空。调用组件文件时默认会调用 render() 方法,因为组件需要显示 HTML 标记,或者我们可以说 JSX 语法。每个 React 组件必须有一个 render() 函数,它返回单个 React 元素,该元素代表原生 DOM 组件。如果需要渲染多个 HTML 元素,则必须将它们分组在一个封闭的标签内,如 <form>、<group>和 <div> 等。此函数必须保持纯净,就是说它在每次调用时必须返回相同的结果。
React 中 refs 干嘛用的?
> Refs 提供了一种访问在render方法中创建的 DOM 节点或者 React 元素的方法。在典型的数据流中,props 是父子组件交互的唯一方式,想要修改子组件,需要使用新的pros重新渲染它。凡事有例外,某些情况下咱们需要在典型数据流外,强制修改子代,这个时候可以使用 Refs。
咱们可以在组件添加一个 ref 属性来使用,该属性的值是一个回调函数,接收作为其第一个参数的底层 DOM 元素或组件的挂载实例。
class UnControlledForm extends Component{
handleSubmit=()=>{
console.log('Input Value:',this.input.value)
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
ref={(input)=>this.input=input} />
<button type='submit'>Submit</button>
</form>
)
}
}
请注意,input 元素有一个ref属性,它的值是一个函数。该函数接收输入的实际 DOM 元素,然后将其放在实例上,这样就可以在 handleSubmit 函数内部访问它。
经常被误解的只有在类组件中才能使用 refs,但是refs也可以通过利用 JS 中的闭包与函数组件一起使用。
function CustomForm({handleSubmit}){
let inputElement
return (
<form onSubmit={()=>handleSubmit(inputElement.value)}>
<input
type='text'
ref={(input)=>inputElement=input} />
<button type='submit'>Submit</button>
</form>
)
}
在 React 中如何处理事件
> 为了解决跨浏览器的兼容性问题,SyntheticEvent 实例将被传递给你的事件处理函数,SyntheticEvent是 React 跨浏览器的浏览器原生事件包装器,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。
比较有趣的是,React 实际上并不将事件附加到子节点本身。React 使用单个事件侦听器侦听顶层的所有事件。这对性能有好处,也意味着 React 在更新 DOM 时不需要跟踪事件监听器。
state 和 props 区别是啥?
> props和state是普通的 JS 对象。虽然它们都包含影响渲染输出的信息,但是它们在组件方面的功能是不同的。即
- state 是组件自己管理数据,控制自己的状态,可变;
- props 是外部传入的数据参数,不可变;
- 没有state的叫做无状态组件,有state的叫做有状态组件;
- 多用 props,少用 state,也就是多写无状态组件。
有状态和无状态组件的区别。
- 有状态组件:
- 在内存中存储组件状态更改的信息
- 有权更改状态
- 包含过去、现在和可能的未来状态更改的信息
- 无安装组件爱你通知它们关于状态更改的需求,然后它们将props传递给前者
- 无状态组件:
- 计算组件的内部状态
- 无权更改状态
- 没有包含关于状态更改的信息
- 它们从有状态组件接收props,将其视为回调函数
如何创建 refs
> Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
class MyComponent extends React.Component{
constructor(props){
super(props);
this.myRef=React.createRef();
}
render(){
return <div ref={this.myRef} />
}
}
或者这样用:
class UserForm extends Component{
handleSubmit=()=>{
console.log('Input Value is:',this.input.value)
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
ref={(input)=>this.input=input} />
<button type='submit'>Submit</button>
</form>
)
}
}
什么是高阶组件?
> 高阶组件(HOC)是接受一个组件并返回一个新组件的函数。基本上,这是一个模式,是从 React 的组合特性中衍生出来的,称其为纯组件,因为它们可以接受任何动态提供的子组件,但不会修改或复制输入组件中的任何行为。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
HOC 可以用于以下许多用例:
- 代码重用、逻辑和引导抽象
- 渲染劫持
- state 抽象和操作
- props 处理
在构造函数调用 super
并将 props
作为参数传入的作用是啥?
> 在调用 super() 方法之前,子类构造函数无法使用this引用,ES6 子类也是如此。将 props 参数传递给 super() 调用的主要原因是在子构造函数中能够通过this.props来获取传入的 props。
传递props:
class MyComponent extends React.Component{
constructor(props){
super(props);
console.log(this.props)
}
}
没传递props:
class MyComponent extends React.Component{
constructor(props){
super();
console.log(this.props); // undefined
// 但是 Props 参数仍然可用
console.log(props); // Prints {name:'sudheer',age:30}
}
render(){
// 构造函数外部不受影响
console.log(this.props) // {name:'sudheer',age:30}
}
}
上面示例揭示了一点。props 的行为只有在构造函数中是不同的,在构造函数之外也是一样的。
什么是控制组件?
> 在 HTML 中,表单元素如 <input>、<textarea>和<select>通常维护自己的状态,并根据用户输入进行更新。当用户提交表单时,来自上述元素的值将随表单一起发送。
而 React 的工作方式则不同。包含表单的组件将跟踪其状态中的输入值,并在每次回调函数(例如onChange)触发时重新渲染组件,因为状态被更新。以这种方式由 React 控制其值的输入表单元素称为受控组件。
如何 React.createElement ?
const element = (
<h1 className="greeting">
Hello, world!
</h1>
)
上述代码如何使用 React.createElement 来实现:
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
讲讲什么是 JSX ?
> 当 Facebook 第一次发布 React 时,他们还引入了一种新的 JS 方言 JSX,将原始 HTML 模板嵌入到 JS 代码中。JSX 代码本身不能被浏览器读取,必须使用Babel和webpack等工具将其转换为传统的JS。很多开发人员就能无意识使用 JSX,因为它已经与 React 结合在一直了。
class MyComponent extends React.Component{
render(){
let props=this.props;
return (
<div className='my-component'>
<a href={props.url}>{props.name}</a>
</div>
)
}
}
为什么不直接更新 state
呢 ?
> 如果试图直接更新 state ,则不会重新渲染组件。
// 错误
This.state.message = 'Hello world';
需要使用setState()方法来更新 state。它调度对组件state对象的更新。当state改变时,组件通过重新渲染来响应:
// 正确做法
This.setState({message: ‘Hello World’});
React 组件生命周期有哪些不同阶段?
在组件生命周期中有四个不同的阶段:
- Initialization:在这个阶段,组件准备设置初始化状态和默认属性。
- Mounting:react 组件已经准备好挂载到浏览器 DOM 中。这个阶段包括componentWillMount和componentDidMount生命周期方法。
- Updating:在这个阶段,组件以两种方式更新,发送新的 props 和 state 状态。此阶段包括shouldComponentUpdate、componentWillUpdate和componentDidUpdate生命周期方法。
- Unmounting:在这个阶段,组件已经不再被需要了,它从浏览器 DOM 中卸载下来。这个阶段包含 componentWillUnmount 生命周期方法。
除以上四个常用生命周期外,还有一个错误处理的阶段:
Error Handling:在这个阶段,不论在渲染的过程中,还是在生命周期方法中或是在任何子组件的构造函数中发生错误,该组件都会被调用。这个阶段包含了 componentDidCatch 生命周期方法。
React 的生命周期方法有哪些?
- componentWillMount:在渲染之前执行,用于根组件中的 App 级配置。
- componentDidMount:在第一次渲染之后执行,可以在这里做AJAX请求,DOM 的操作或状态更新以及设置事件监听器。
- componentWillReceiveProps:在初始化render的时候不会执行,它会在组件接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染
- shouldComponentUpdate:确定是否更新组件。默认情况下,它返回true。如果确定在 state 或 props 更新后组件不需要在重新渲染,则可以返回false,这是一个提高性能的方法。
- componentWillUpdate:在shouldComponentUpdate返回 true 确定要更新组件之前件之前执行。
- componentDidUpdate:它主要用于更新DOM以响应props或state更改。
- componentWillUnmount:它用于取消任何的网络请求,或删除与组件关联的所有事件监听器。
这三个点(…)在 React 干嘛用的?
> ... 在React(使用JSX)代码中做什么?它叫什么?
<Modal {...this.props} title='Modal heading' animation={false}/>
这个叫扩展操作符号或者展开操作符,例如,如果this.props包含a:1和b:2,则
<Modal {...this.props} title='Modal heading' animation={false}>
等价于下面内容:
<Modal a={this.props.a} b={this.props.b} title='Modal heading' animation={false}>
扩展符号不仅适用于该用例,而且对于创建具有现有对象的大多数(或全部)属性的新对象非常方便,在更新state 咱们就经常这么做:
this.setState(prevState => {
return {foo: {...prevState.foo, a: "updated"}};
});
使用 React Hooks 好处是啥?
> 首先,Hooks 通常支持提取和重用跨多个组件通用的有状态逻辑,而无需承担高阶组件或渲染 props 的负担。Hooks 可以轻松地操作函数组件的状态,而不需要将它们转换为类组件。
Hooks 在类中不起作用,通过使用它们,咱们可以完全避免使用生命周期方法,例如 componentDidMount、componentDidUpdate、componentWillUnmount。相反,使用像useEffect这样的内置钩子。
什么是 React Hooks?
> Hooks是 React 16.8 中的新添加内容。它们允许在不编写类的情况下使用state和其他 React 特性。使用 Hooks,可以从组件中提取有状态逻辑,这样就可以独立地测试和重用它。Hooks 允许咱们在不改变组件层次结构的情况下重用有状态逻辑,这样在许多组件之间或与社区共享 Hooks 变得很容易。
React 中的 useState()
是什么?
const [count,setCounter]=useState(0);
const [moreStuff,setMoreStuff]=useState(...);
const setCount=()=>{
setCounter(count+1);
setMoreStuff(...);
}
useState 是一个内置的 React Hook。useState(0) 返回一个元组,其中第一个参数count是计数器的当前状态,setCounter 提供更新计数器状态的方法。
咱们可以在任何地方使用setCounter方法更新计数状态-在这种情况下,咱们在setCount函数内部使用它可以做更多的事情,使用 Hooks,能够使咱们的代码保持更多功能,还可以避免过多使用基于类的组件。
React 中的StrictMode(严格模式)是什么?
> React 的StrictMode是一种辅助组件,可以帮助咱们编写更好的 react 组件,可以使用<StrictMode />包装一组组件,并且可以帮咱们以下检查:
- 验证内部组件是否遵循某些推荐做法,如果没有,会在控制台给出警告。
- 验证是否使用的已经废弃的方法,如果有,会在控制台给出警告。
- 通过识别潜在的风险预防一些副作用。
为什么类方法需要绑定到类实例?
> 在 JS 中,this 值会根据当前上下文变化。在 React 类组件方法中,开发人员通常希望 this 引用组件的当前实例,因此有必要将这些方法绑定到实例。通常这是在构造函数中完成的:
class SubmitButton extends React.Component{
constructor(props){
super(props);
this.state={
isFormSubmitted:false
};
this.handleSubmit=this.handleSubmit.bind(this);
}
handleSubmit(){
this.setState({
isFormSubmitted:true
})
}
render(){
return (
<button onClick={this.handleSubmit}>Submit</button>
)
}
}
什么是 prop drilling,如何避免?
> 在构建 React 应用程序时,在多层嵌套组件来使用另一个嵌套组件提供的数据。最简单的方法是将一个 prop 从每个组件一层层的传递下去,从源组件传递到深层嵌套组件,这叫做prop drilling。
prop drilling的主要缺点是原本不需要数据的组件变得不必要地复杂,并且难以维护。
为了避免prop drilling,一种常用的方法是使用React Context。通过定义提供数据的Provider组件,并允许嵌套的组件通过Consumer组件或useContext Hook 使用上下文数据。
描述 Flux 与 MVC?
> 传统的 MVC 模式在分离数据(Model)、UI(View和逻辑(Controller)方面工作得很好,但是 MVC 架构经常遇到两个主要问题:
- 数据流不够清晰:跨视图发生的级联更新常常会导致混乱的事件网络,难于调试。
- 缺乏数据完整性:模型数据可以在任何地方发生突变,从而在整个UI中产生不可预测的结果。
使用 Flux 模式的复杂用户界面不再遭受级联更新,任何给定的React 组件都能够根据 store 提供的数据重建其状态。Flux 模式还通过限制对共享数据的直接访问来加强数据完整性。
受控组件和非受控组件区别是啥?
- 受控组件是 React 控制中的组件,并且是表单数据真实的唯一来源。
- 非受控组件是由 DOM 处理表单数据的地方,而不是在 React 组件中。
尽管非受控组件通常更易于实现,因为只需使用refs即可从 DOM 中获取值,但通常建议优先选择受控制的组件,而不是非受控制的组件。
这样做的主要原因是受控组件支持即时字段验证,允许有条件地禁用/启用按钮,强制输入格式。
什么是 React Context?
> Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
什么是 React Fiber?
> Fiber 是 React 16 中新的协调引擎或重新实现核心算法。它的主要目标是支持虚拟DOM的增量渲染。React Fiber 的目标是提高其在动画、布局、手势、暂停、中止或重用等方面的适用性,并为不同类型的更新分配优先级,以及新的并发原语。
React Fiber 的目标是增强其在动画、布局和手势等领域的适用性。它的主要特性是增量渲染:能够将渲染工作分割成块,并将其分散到多个帧中。
如何在 ReactJS 的 Props上应用验证?
> 当应用程序在开发模式下运行时,React 将自动检查咱们在组件上设置的所有 props,以确保它们具有正确的数据类型。对于不正确的类型,开发模式下会在控制台中生成警告消息,而在生产模式中由于性能影响而禁用它。强制的 props 用 isRequired定义的。
下面是一组预定义的 prop 类型
- React.PropTypes.string
- React.PropTypes.number
- React.PropTypes.func
- React.PropTypes.node
- React.PropTypes.bool
例如,咱们为用户组件定义了如下的propTypes
import PropTypes from 'prop-types';
class User extends React.Component{
render(){
return (
<h1>Welcome,{this.props.name}</h1>
<h2>Age,{this.props.age}</h2>
)
}
}
User.propTypes={
name:PropTypes.string.isRequired,
age:PropTypes.number.isRequired
}
在 React 中使用构造函数和 getInitialState 有什么区别?
> 构造函数和getInitialState之间的区别就是ES6和ES5本身的区别。在使用ES6类时,应该在构造函数中初始化state,并在使用React.createClass时定义getInitialState方法。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { /* initial state */ };
}
}
等价于
var MyComponent = React.createClass({
getInitialState() {
return { /* initial state */ };
},
});
如何有条件地向 React 组件添加属性?
> 对于某些属性,React 非常聪明,如果传递给它的值是虚值,可以省略该属性。例如:
var InputComponent=React.createClass({
render:function(){
var required=true;
var disabled=false;
return (
<input type='text' disabled={disabled}
required={required} />
)
}
})
渲染结果:
<input type="text" required>
另一种可能的方法是:
var condition = true;
var component = (
<div
value="foo"
{ ...( condition && { disabled: true } ) } />
);
Hooks会取代 render props
和高阶组件吗?
> 通常,render props和高阶组件仅渲染一个子组件。React团队认为,Hooks 是服务此用例的更简单方法。
这两种模式仍然有一席之地(例如,一个虚拟的 scroller 组件可能有一个 renderItem prop,或者一个可视化的容器组件可能有它自己的 DOM 结构)。但在大多数情况下,Hooks 就足够了,可以帮助减少树中的嵌套。
如何避免组件的重新渲染?
> React 中最常见的问题之一是组件不必要地重新渲染。React 提供了两个方法,在这些情况下非常有用:
- React.memo():这可以防止不必要地重新渲染函数组件
- PureComponent:这可以防止不必要地重新渲染类组件
这两种方法都依赖于对传递给组件的props的浅比较,如果 props 没有改变,那么组件将不会重新渲染。虽然这两种工具都非常有用,但是浅比较会带来额外的性能损失,因此如果使用不当,这两种方法都会对性能产生负面影响。
通过使用 React Profiler,可以在使用这些方法前后对性能进行测量,从而确保通过进行给定的更改来实际改进性能。
什么是纯函数?
> 纯函数是不依赖并且不会在其作用域之外修改变量状态的函数。本质上,纯函数始终在给定相同参数的情况下返回相同结果。
当调用setState
时,React render
是如何工作的?
> 咱们可以将"render"分为两个步骤:
- 虚拟 DOM 渲染:当render方法被调用时,它返回一个新的组件的虚拟 DOM 结构。当调用setState()时,render会被再次调用,因为默认情况下shouldComponentUpdate总是返回true,所以默认情况下 React 是没有优化的。
- 原生 DOM 渲染:React 只会在虚拟DOM中修改真实DOM节点,而且修改的次数非常少——这是很棒的React特性,它优化了真实DOM的变化,使React变得更快。
如何避免在React重新绑定实例?
有几种常用方法可以避免在 React 中绑定方法:
- 将事件处理程序定义为内联箭头函数
class SubmitButton extends React.Component{
constructor(props){
super(props);
this.state={
isFormSubmitted:false
};
}
render(){
return (
<button onClick={()=>{
this.setState({isFormSubmitted:true})
}}>Submit</button>
)
}
}
- 使用箭头函数来定义方法
class SubmitButton extends React.Component{
state={
isFormSubmitted:false
}
handleSubmit=()=>{
this.setState({
isFormSubmitted:true
});
}
render(){
return (
<button onClick={this.handleSubmit}>Submit</button>
)
}
}
- 使用带有 Hooks 的函数组件
const SubmitButton=()=>{
const [isFormSubmitted,setIsFormSubmitted]=useState(false);
return (
<button onClick={()=>{
setIsFormSubmitted(true);
}}>Submit</button>
)
}
如何理解“单一可信源”?
> 单一可信源(SSOT)是构造信息模型和相关数据模式的实践,其中每个数据元素都只能在一个地方掌握(或编辑)
Redux 使用“存储”将应用程序的整个状态存储在一个位置。因此,组件的所有状态都存储在存储中,并且存储本身会接收更新。单一状态树使我们能更容易地跟踪历史更改,更方便地调试或检查应用程序。
列出 Redux 的组件。
- 动作——这是一个描述发生了什么的对象。
- Reducer——确定状态如何变化的地方。
- 存储——整个应用程序的状态 / 对象树保存在存储中。
- 视图——仅显示存储提供的数据。
在 Redux 中如何定义动作?
> React 中的动作必须具有 type 属性,该属性指示正在执行的 ACTION 的类型。必须将它们定义为字符串常量,你也可以为其添加更多属性。在 Redux 中使用称为“动作创建者”的函数来创建动作。以下是动作和动作创建者的示例:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
说明 Reducer 的作用。
> Reducer 是用于指示 ACTION 反应中应用程序状态变化的简单功能。它接收先前的状态和动作,然后返回新的状态。它根据动作类型确定需要哪种更新,然后返回新值。如果没有要完成的工作,它将按原样返回先前状态。
在 Redux 中存储的用途是什么?
> 存储是一个 JavaScript 对象,可以保存应用程序的状态,并提供一些辅助方法来访问状态、调度动作并记录侦听器。应用程序的整个状态 / 对象树存储在单个存储中。因此 Redux 非常容易理解且可预测。我们可以将中间件转移到存储,以管理数据处理任务,并维护更改存储状态的各种活动的日志。通过 Reducer,所有活动都返回新的状态。
Redux 与 Flux 有何不同?
- Flux
- 存储包括状态和更改逻辑
- 有多个存储
- 所有存储不互通,是平行的
- 有单个调度器
- React组件订阅到存储
- 状态是可变的
- Redux
- 存储和更改逻辑是分离的
- 只有一个存储
- 带有分层Reducer的单个存储
- 没有调度器的概念
- 容器组件是有联系的
- 状态是不可变的
Redux 有哪些优势?
Redux 的优点如下:
- 结果的可预测性——由于总是有单一可信源,比如存储,因此当前状态与动作及应用程序的其他部分同步时不会出现混乱。
- 可维护性——代码易于维护,具有可预测的结果和严格的结构。
- 服务端渲染——你只需将在服务器上创建的存储传递给客户端即可。这对于初始渲染非常有用,并优化了应用程序性能,提供了更好的用户体验。
- 开发人员工具——从动作到状态更改,开发人员可以利用这些工具实时跟踪应用程序中发生的所有事情。
- 社区和生态系统——Redux 背后拥有巨大的社区,用起来更加便利。大批优秀的开发者为库的发展做出了贡献,并开发了很多应用程序。
- 易于测试——Redux 的代码主要是较小的、纯净的和孤立的函数。这使代码可测试且独立。
- 组织——Redux 精确地规定了代码的组织方式,这使得团队合作时代码更加一致,更容易理解。
什么是 React Router?
> React Router 是建立在 React 之上的功能强大的路由库。它使 URL 与网页上显示的数据保持同步。它保持标准化的结构和行为,可用于开发单页 Web 应用程序。React Router 有一个简单的 API。React Router 提供了一种方法,只会显示你的应用中路由匹配你的定义的那些组件。
为什么我们在 React 中需要一个路由器?
> 路由器用于定义多个路由,并且当用户键入特定的 URL 时,如果该 URL 与路由器内部定义的任何“路由”的路径匹配,则该用户将被重定向到该路由。因此我们需要在应用程序中添加一个路由器库,以允许创建多个路由,每个路由都为我们指向一个独特的视图。
从 React Router 包导入的组件有两个属性,一个是将用户引导到指定路径的 path,另一个是用于定义所述路径中内容的 component。
列出 React Router 的优点。
- 就像 React 基于组件的理念一样,在 React Router v4 中 API 是“完全组件化的”。路由器可以可视化为单个根组件(<BrowserRouter>),其中包含特定的子路由(<route>)。
- 无需手动设置历史值:在 React Router v4 中,我们要做的就是将路由包装在<BrowserRouter>组件中。
- 包是拆分的:三个包分别用于 Web、Native 和 Core。这使我们的应用更加紧凑。它们的编码样式类似,所以很容易来回切换。
React Router 与传统路由有何不同?
- 传统路由:
- 参与的页面:每个视图对应一个新页面
- URL更改:向服务器发送一个HTTP请求并接收对应的HTML页面
- 体验:用户其实是在每个视图的不同页面间切换
- React路由:
- 参与的页面:只涉及单个HTML页面
- URL更改:只有历史属性被更改
- 体验:用户以为自己正在不同的页面间切换
小程序/Hybrid
简述微信小程序原理
> 微信小程序采用 JavaScript、WXML、WXSS 三种技术进行开发,本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口
微信的架构,是数据驱动的架构模式,它的 UI 和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现
小程序分为两个部分 webview 和 appService 。其中 webview 主要用来展现 UI ,appService 有来处理业务逻辑、数据及接口调用。它们在两个进程中运行,通过系统层 JSBridge 实现通信,实现 UI 的渲染、事件的处理
小程序的数据绑定和vue哪里不一样
小程序直接 this.data 的属性是不可以同步到视图的,必须调用
this.setData({
// 这里设置
})
小程序的wxss和css有哪些不一样的地方
WXSS 和 CSS 类似,不过在 CSS 的基础上做了一些补充和修改
尺寸单位 rpx
rpx 是响应式像素,可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。如在 iPhone6 上,屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素
使用 @import 标识符来导入外联样式。@import 后跟需要导入的外联样式表的相对路径,用;表示语句结束
/** index.wxss **/
@import './base.wxss';
.container{
color: red;
}
请谈谈wxml与标准的html的异同?
- 都是用来描述页面的结构;
- 都由标签、属性等构成;
- 标签名字不一样,且小程序标签更少,单一标签更多;
- 多了一些 wx:if 这样的属性以及 {{ }} 这样的表达式
- WXML仅能在微信小程序开发者工具中预览,而HTML可以在浏览器内预览
- 组件封装不同, WXML对组件进行了重新封装,
- 小程序运行在JS Core内,没有DOM树和window对象,小程序中无法使用window对象和document对象。
小程序页面间有哪些传递数据的方法
- 使用全局变量实现数据传递 在 app.js 文件中定义全局变量 globalData, 将需要存储的信息存放在里面
// app.js
App({
// 全局变量
globalData: {
userInfo: null
}
})
使用的时候,直接使用 getApp() 拿到存储的信息
- 使用 wx.navigateTo 与 wx.redirectTo 的时候,可以将部分数据放在 url 里面,并在新页面 onLoad 的时候初始化
//pageA.js
wx.navigateTo({
url: '../pageD/pageD?name=raymond&gender=male',
})
wx.redirectTo({
url: '../pageD/pageD?name=raymond&gender=male',
})
// pageB.js
...
Page({
onLoad: function(option){
console.log(option.name + 'is' + option.gender)
this.setData({
option: option
})
}
})
需要注意的问题:
wx.navigateTo 和 wx.redirectTo 不允许跳转到 tab 所包含的页面;onLoad 只执行一次
- 使用本地缓存 Storage 相关
小程序的生命周期函数
- onLoad 页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数
- onShow() 页面显示/切入前台时触发
- onReady() 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互
- onHide() 页面隐藏/切入后台时触发。 如 navigateTo 或底部 tab 切换到其他页面,小程序切入后台等
- onUnload() 页面卸载时触发。如 redirectTo 或 navigateBack 到其他页面时
哪些方法可以用来提高微信小程序的应用速度
- 提高页面加载速度
- 用户行为预测
- 减少默认 data 的大小
- 组件化方案
简述下 wx.navigateTo()
, wx.redirectTo()
, wx.switchTab()
, wx.navigateBack()
, wx.reLaunch()
的区别
- wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面
- wx.redirectTo():关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面
- wx.switchTab():跳转到 abBar 页面,并关闭其他所有非 tabBar 页面
- wx.navigateBack()关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层
- wx.reLaunch():关闭所有页面,打开到应用内的某个页面
bindtap和catchtap的区别是什么
- 相同点:首先他们都是作为点击事件函数,就是点击时触发。在这个作用上他们是一样的,可以不做区分
- 不同点:他们的不同点主要是bindtap是不会阻止冒泡事件的,catchtap是阻值冒泡的
微信小程序与H5的区别
- 第一条是运行环境的不同
传统的HTML5的运行环境是浏览器,包括webview,而微信小程序的运行环境并非完整的浏览器,是微信开发团队基于浏览器内核完全重构的一个内置解析器,针对小程序专门做了优化,配合自己定义的开发语言标准,提升了小程序的性能。
- 第二条是开发成本的不同
只在微信中运行,所以不用再去顾虑浏览器兼容性,不用担心生产环境中出现不可预料的奇妙BUG
- 第三条是获取系统级权限的不同
系统级权限都可以和微信小程序无缝衔接
- 第四条便是应用在生产环境的运行流畅度
长久以来,当HTML5应用面对复杂的业务逻辑或者丰富的页面交互时,它的体验总是不尽人意,需要不断的对项目优化来提升用户体验。但是由于微信小程序运行环境独立
webview中的页面怎么跳回小程序中?
- 首先在外部网页的HTML要引入最新版的jweixin-1.3.2.js:
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>
- 然后
wx.miniProgram.navigateTo({ //如果跳转到tabBar中的页面,必须换成switchTo
url: '/pages/login/login'+'$params'
})
使用webview直接加载要注意哪些事项?
- 必须要在小程序后台使用管理员添加业务域名;
- h5页面跳转至小程序的脚本必须是1.3.1以上;
- 微信分享只可以都是小程序的主名称了,如果要自定义分享的内容,需小程序版本在1.7.1以上;
- h5的支付不可以是微信公众号的appid,必须是小程序的appid,而且用户的openid也必须是用户和小程序的。
看你的技术栈对 Electron 比较熟悉,有使用过 React-native,请你谈谈使用的感受?
> React-native 的坑还是比较多,但是目前也算拥有成熟的生态了,开发简单的 APP 可以使用它。但是复杂的应用还是原生比较好,Electron 目前非常受欢迎,它基本上可以完成桌面应用的大部分需求,重型应用开发也是完全没问题的,可以配合大量 C# C++ 插件等。
hybrid是什么,为何用hybrid?
- hybrid是客户端与前端的混合开发
- hybrid存在的核心意义在意快速迭代,无需审核
- hybrid实现流程,以及webview和file协议
介绍一下hybrid更新和上线的流程?
- 服务端的版本和zip包维护
- 更新zip包之前,先对比版本号
- zip下载解压和覆盖
hybrid和h5的主要区别?
- 优点: 体验好,可快速迭代
- 缺点: 开发成本高,运维成本高
- 适合的场景: hybrid适合产品型,H5适合运营型
前端JS和客户端如何通讯?
- 通讯的基本形式: 前端调用能力,传递参数,监听回调
- 对schema协议的理解和使用:
- 定义了前端与客户端的约定;
- 可以通过ifream使用
- 调用schema代码的封装
放在客户端内置上线的好处: 更快,更安全
Flutter 是什么?
> Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。
Flutter中,Widget 和 element 和 RenderObject 之间的关系?
- Widget是用户界面的一部分,并且是不可变的。
- Element是在树中特定位置Widget的实例。
- RenderObject是渲染树中的一个对象,它的层次结构是渲染库的核心。
Widget会被inflate(填充)到Element,并由Element管理底层渲染树。Widget并不会直接管理状态及渲染,而是通过State这个对象来管理状态。Flutter创建Element的可见树,相对于Widget来说,是可变的,通常界面开发中,我们不用直接操作Element,而是由框架层实现内部逻辑。就如一个UI视图树中,可能包含有多个TextWidget(Widget被使用多次),但是放在内部视图树的视角,这些TextWidget都是填充到一个个独立的Element中。Element会持有renderObject和widget的实例。记住,Widget 只是一个配置,RenderObject 负责管理布局、绘制等操作。
在第一次创建 Widget 的时候,会对应创建一个 Element, 然后将该元素插入树中。如果之后 Widget 发生了变化,则将其与旧的 Widget 进行比较,并且相应地更新 Element。重要的是,Element 不会被重建,只是更新而已。
Flutter中,mixin extends implement 之间的关系?
继承(关键字 extends)、混入 mixins (关键字 with)、接口实现(关键字 implements)。这三者可以同时存在,前后顺序是extends -> mixins -> implements。
Flutter中的继承是单继承,子类重写超类的方法要用@Override,子类调用超类的方法要用super。
在Flutter中,Mixins是一种在多个类层次结构中复用类代码的方法。mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件。
Flutter中的Widget、State、Context 的核心概念?是为了解决什么问题?
- Widget: 在Flutter中,几乎所有东西都是Widget。将一个Widget想象为一个可视化的组件(或与应用可视化方面交互的组件),当你需要构建与布局直接或间接相关的任何内容时,你正在使用Widget。
- Widget树: Widget以树结构进行组织。包含其他Widget的widget被称为父Widget(或widget容器)。包含在父widget中的widget被称为子Widget。
- Context: 仅仅是已创建的所有Widget树结构中的某个Widget的位置引用。简而言之,将context作为widget树的一部分,其中context所对应的widget被添加到此树中。一个context只从属于一个widget,它和widget一样是链接在一起的,并且会形成一个context树。
- State: 定义了StatefulWidget实例的行为,它包含了用于”交互/干预“Widget信息的行为和布局。应用于State的任何更改都会强制重建Widget。
这些状态的引入,主要是为了解决多个部件之间的交互和部件自身状态的维护。
工程化
介绍一下webpack基本的属性?
const path = require('path')
module.exports = {
entry: { // main是默认入口,也可以是多入口
main: './src/main.js'
},
// 出口
output: {
filename: './build.js', // 指定js路径
path: path.join(__dirname, '..', '', 'dist') // 最好是绝对路径
// 代表上一级的dist
},
module: {
// 一样的功能rules: webpack2.xx新加的
loaders: [ // require('./a.css||./a.js')
{
test: /\.css$/,
loader: 'style-loader!css=loader',
//多个loader用!分割
//顺序是反过来的 2!1 多个loader
},
{
test: /\.(jpg|svg)$/,
loaderL 'url-loader?limit=4096&name=[name].[ext]',
// limit=4096&name=[name].[ext]' 多个参数之间用&符号分割
//[name].[ext]内置提供的
options: {
limit: 4096,
name: '[name].[ext]'
}
}
]
},
plugins: [
// 插件的执行顺序是依次执行的,和loader是反过来的
new htmlWebpackPlugin({
template: './src/index.html',
})
// 将src下的template属性描述的文件根据当前配置的output.path,将文件移动到该目录。
// 在插件的执行过程中,它本身可以去拿当前所设置的webpack选项,便于对webpack选项的复用,
]
}
减少页面加载时间的方法
- 重复的HTTP请求数量应尽量减少
- 压缩Javascript、CSS代码
- 在文件头部放置css样式的定义
- 在文件末尾放Javascript脚本
- css、javascript改由外部调用,避免重复调用
- 尽可能减少DOM元素
- 避免使用CSS Expressions
- 添加文件过期或缓存头
- 使用CDN(Content Delivery Network)网络加速
- 服务器启用gzip压缩功能
- Ajax采用缓存调用
- 如果可以,Ajax调用尽量采用GET方法调用(其他方法会发送两次请求,一次option,一次为正常请求)
- 缩减iframe的使用,如无必要,尽量不要使用
- 合理使用Flush(后端)
- 避免采用301、302转向
- 优化图片文件
- 采用分页或翻页后展示
- 使用多域名负载网页内的多个文件、图片
web前端性能优化
- 减少http请求,合理设置http缓存
- 使用浏览器缓存
- 启用压缩
- css sprites
- lazyload images
- css放最上部,js放最下面
- 异步请求callback
- 减少cookie传输
- JavaScript代码优化:
- dom:html colleciton、重绘
- 慎用with
- 避免使用eval和Function
- 减少作用域链查找
- 数据访问
- 字符串拼接
- css选择符优化
- cdn加速
- 反向代理
babel转换es6语法工作原理?
> babel是一个转译器,感觉相对于编译器compiler,叫转译器transpiler更准确,因为它只是把同种语言的高版本规则翻译成低版本规则,而不像编译器那样,输出的是另一种更低级的语言代码。
但是和编译器类似,babel的转译过程也分为三个阶段:parsing、transforming、generating,以ES6代码转译为ES5代码为例,babel转译的具体过程如下:
ES6代码输入 ==》 babylon进行解析 ==》 得到AST ==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树 ==》 用babel-generator通过AST树生成ES5代码
webpack本地开发怎么解决跨域的
- 下载 webpack-dev-server 插件
- 配置 webpack.config.js 文件
// webpack.config.js
var WebpackDevServer = require("webpack-dev-server");
module.exports = {
...
devServer: {
...
port: '8088', //设置端口号
// 代理设置
proxy: {
'/api': {
target: 'http://localhost:80/index.php', // 目标代理
pathRewrite: {'^/api' : ''}, // 重写路径
secure: false, // 是否接受运行在 HTTPS 上
}
}
}
}
webpack与grunt、gulp的不同
> 三者都是前端构建工具
grunt 和 gulp 是基于任务和流的。找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程
webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能
webpack 与前者最大的不同就是支持代码分割,模块化(AMD,CommonJ,ES2015),全局分析
有哪些常见的Loader?他们是解决什么问题的
- css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
- style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
- slint-loader:通过 SLint 检查 JavaScript 代码
- babel-loader:把 ES6 转换成 ES5
- file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
- url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
有哪些常见的Plugin?他们是解决什么问题的
- define-plugin:定义环境变量
- commons-chunk-plugin:提取公共代码
Loader和Plugin的不同
- loader 加载器
Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件. Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力
在 module.rules 中配置,也就是说他作为模块的解析规则而存在,类型为数组
- Plugin 插件
扩展 webpack 的功能,让 webpack 具有更多的灵活性
在 plugins 中单独配置。类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入
webpack的构建流程是什么
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
- 确定入口:根据配置中的 entry 找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
是否写过Loader和Plugin?描述一下编写loader或plugin的思路
编写 Loader 时要遵循单一原则,每个 Loader 只做一种"转义"工作。 每个 Loader 的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用 this.callback() 方法,将内容返回给 webpack 。 还可以通过 this.async() 生成一个 callback 函数,再用这个 `callback`` 将处理后的内容输出出去
相对于 Loader 而言,Plugin 的编写就灵活了许多。 webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果
webpack的热更新是如何做到的?说明其原理
- 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
- 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
- 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
- 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
- webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
- HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
- 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
- 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。
如何利用webpack来优化前端性能
- 压缩代码。删除多余的代码、注释、简化代码的写法等等方式
- 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径
- 删除死代码 Tree Shaking)。将代码中永远不会走到的片段删除掉
- 优化图片,对于小图可以使用 base64 的方式写入文件中
- 按照路由拆分代码,实现按需加载,提取公共代码
- 给打包出来的文件名添加哈希,实现浏览器缓存文件
如何提高webpack的构建速度
- 多入口情况下,使用CommonsChunkPlugin来提取公共代码
- 通过externals配置来提取常用库
- 利用DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。
- 使用Happypack 实现多线程加速编译
- 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度
- 使用Tree-shaking和Scope Hoisting来剔除多余代码
怎么配置单页应用?怎么配置多页应用
- 单页应用可以理解为 webpack 的标准模式,直接在 entry 中指定单页应用的入口即可
- 多页应用的话,可以使用 webpack 的 AutoWebPlugin 来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范
什么是bundle,什么是chunk,什么是module
> bundle 是由 webpack 打包出来的文件,chunk 是指 webpack 在进行模块的依赖分析的时候,代码分割出来的代码块。module是开发中的单个模块
Git和SVN有什么区别?
- Git:
- Git是一个分布式的版本控制工具
- 它属于第3代版本控制工具
- 客户端可以在其本地系统上克隆整个存储库
- 即使离线也可以提交
- Push/pull 操作更快
- 工程可以用 commit 自动共享
- SVN:
- SVN 是集中版本控制工具
- 它属于第2代版本控制工具
- 版本历史记录存储在服务器端存储库中
- 只允许在线提交
- Push/pull 操作较慢
- 没有任何东西自动共享
什么是Git?
- Git 是分布式版本控制系统(DVCS)。它可以跟踪文件的更改,并允许你恢复到任何特定版本的更改。
- 与 SVN 等其他版本控制系统(VCS)相比,其分布式架构具有许多优势,一个主要优点是它不依赖于中央服务器来存储项目文件的所有版本。
- 每个开发人员都可以“克隆”我在图中用“Local repository”标注的存储库的副本,并且在他的硬盘驱动器上具有项目的完整历史记录,因此当服务器中断时,你需要的所有恢复数据都在你队友的本地 Git 存储库中。
- 还有一个中央云存储库,开发人员可以向其提交更改,并与其他团队成员进行共享,如图所示,所有协作者都在提交更改“远程存储库”。
什么是 Git 中的“裸存储库”?
> Git 中的 “裸” 存储库只包含版本控制信息而没有工作文件(没有工作树),并且它不包含特殊的 .git 子目录。相反,它直接在主目录本身包含 .git 子目录中的所有内容,其中工作目录包括:
一个 .git 子目录,其中包含你的仓库所有相关的 Git 修订历史记录。
工作树,或签出的项目文件的副本。
Git 是用什么语言编写的?
> Git使用 C 语言编写。 GIT 很快,C 语言通过减少运行时的开销来做到这一点。
列举工作中常用的几个git命令?
- 新增文件的命令:git add file或者git add .
- 提交文件的命令:git commit –m或者git commit –a
- 查看工作区状况:git status –s
- 拉取合并远程分支的操作:git fetch/git merge或者git pull
- 查看提交记录命令:git reflog
提交时发生冲突,你能解释冲突是如何产生的吗?你是如何解决的?
> 开发过程中,我们都有自己的特性分支,所以冲突发生的并不多,但也碰到过。诸如公共类的公共方法,我和别人同时修改同一个文件,他提交后我再提交就会报冲突的错误。
发生冲突,在IDE里面一般都是对比本地文件和远程分支的文件,然后把远程分支上文件的内容手工修改到本地文件,然后再提交冲突的文件使其保证与远程分支的文件一致,这样才会消除冲突,然后再提交自己修改的部分。特别要注意下,修改本地冲突文件使其与远程仓库的文件保持一致后,需要提交后才能消除冲突,否则无法继续提交。必要时可与同事交流,消除冲突。
发生冲突,也可以使用命令。
- 通过git stash命令,把工作区的修改提交到栈区,目的是保存工作区的修改;
- 通过git pull命令,拉取远程分支上的代码并合并到本地分支,目的是消除冲突;
- 通过git stash pop命令,把保存在栈区的修改部分合并到最新的工作空间中;
如果本次提交误操作,如何撤销?
> 如果想撤销提交到索引区的文件,可以通过git reset HEAD file;如果想撤销提交到本地仓库的文件,可以通过git reset –soft HEAD^n恢复当前分支的版本库至上一次提交的状态,索引区和工作空间不变更;可以通过git reset –mixed HEAD^n恢复当前分支的版本库和索引区至上一次提交的状态,工作区不变更;可以通过git reset –hard HEAD^n恢复当前分支的版本库、索引区和工作空间至上一次提交的状态。
如果我想修改提交的历史信息,应该用什么命令?
如果修改最近一次提交的历史记录,就可以用git commit –amend命令;vim编辑的方式;
如果修改之前提交的历史记录,就需要按照下面的步骤:
第一步:首先查看前三次的提交历史记录:
第二步:执行命令git rebase –i HEAD~3,会把前3次的提交记录按照倒叙列出来;这里把第一行的‘pick’修改为‘edit’,然后esc + :wq退出vim编辑器;
第三步:根据提示,执行git commit –amend命令,进入vim编辑器并修改提交信息。
第四步:然后执行git rebase –continue命令
你使用过git stash命令吗?你一般什么情况下会使用它?
命令git stash是把工作区修改的内容存储在栈区。
以下几种情况会使用到它:
- 解决冲突文件时,会先执行git stash,然后解决冲突;
- 遇到紧急开发任务但目前任务不能提交时,会先执行git stash,然后进行紧急任务的开发,然后通过git stash pop取出栈区的内容继续开发;
- 切换分支时,当前工作空间内容不能提交时,会先执行git stash再进行分支切换;
什么是git stash drop?
> git stash drop 命令用于删除隐藏的项目。默认情况下,它将删除最后添加的存储项,如果提供参数的话,它还可以删除特定项。
下面举个例子。
如果要从隐藏项目列表中删除特定的存储项目,可以使用以下命令:
git stash list:它将显示隐藏项目列表,如:
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert “added file_size”
stash@{2}: WIP on master: 21d80a5 added number to log
如果要删除名为 stash@{0} 的项目,请使用命令 git stash drop stash@{0}。
如何查看分支提交的历史记录?查看某个文件的历史记录呢?
查看分支的提交历史记录:
- 命令git log –number:表示查看当前分支前number个详细的提交历史记录;
- 命令git log –number –pretty=oneline:在上个命令的基础上进行简化,只显示sha-1码和提交信息;
- 命令git reflog –number: 表示查看所有分支前number个简化的提交历史记录;
- 命令git reflog –number –pretty=oneline:显示简化的信息历史信息;
如果要查看某文件的提交历史记录,直接在上面命令后面加上文件名即可。
注意:如果没有number则显示全部提交次数
如何找到特定提交中已更改的文件列表?
> 要获取特定提交中已更改的列表文件,请使用以下命令:
git diff-tree -r {hash}
给定提交哈希,这将列出在该提交中更改或添加的所有文件。 -r 标志使命令列出单个文件,而不是仅将它们折叠到根目录名称中。
你还可以包括下面提到的内容,虽然它是可选的,但有助于给面试官留下深刻印象。
输出还将包含一些额外信息,可以通过包含两个标志把它们轻松的屏蔽掉:
git diff-tree –no-commit-id –name-only -r {hash}
这里 -no-commit-id 将禁止提交哈希值出现在输出中,而 -name-only 只会打印文件名而不是它们的路径。
git config 的功能是什么?
git 使用你的用户名将提交与身份相关联。 git config 命令可用来更改你的 git 配置,包括你的用户名。
下面用一个例子来解释。
假设你要提供用户名和电子邮件 ID 用来将提交与身份相关联,以便你可以知道是谁进行了特定提交。为此,我将使用:
git config –global user.name "Your Name": 此命令将添加用户名。
git config –global user.email "Your E-mail Address": 此命令将添加电子邮件ID。
如果分支是否已合并为master,你可以通过什么手段知道?
- git branch –merged 它列出了已合并到当前分支的分支。
- git branch –no-merged 它列出了尚未合并的分支。
提交对象包含什么?
Commit 对象包含以下组件,你应该提到以下这三点:
- 一组文件,表示给定时间点的项目状态
- 引用父提交对象
- SHAI 名称,一个40个字符的字符串,提交对象的唯一标识。
怎样将 N 次提交压缩成一次提交?
将N个提交压缩到单个提交中有两种方式:
- 如果要从头开始编写新的提交消息,请使用以下命令:
git reset –soft HEAD~N &&
git commit
- 如果你想在新的提交消息中串联现有的提交消息,那么需要提取这些消息并将它们传给 git commit,可以这样:
git reset –soft HEAD~N &&
git commit –edit -m"$(git log –format=%B –reverse .HEAD@{N})"
什么是 Git bisect?如何使用它来确定(回归)错误的来源?
Git bisect 用于查找使用二进制搜索引入错误的提交。 Git bisect的命令是
git bisect <subcommand> <options>
既然你已经提到过上面的命令,那就解释一下这个命令会做什么。
此命令用了二进制搜索算法来查找项目历史记录中的哪个提交引入了错误。你可以通过告诉它已知包含该错误的“错误”提交以及在引入错误之前已知的“良好”提交来使用它。然后 git bisect 在这两个端点之间选择一个提交,并询问你所选的提交是“好”还是“坏”。它继续缩小范围,直到找到引入更改的确切提交。
描述一下你所使用的分支策略?
- 功能分支(Feature branching)
要素分支模型将特定要素的所有更改保留在分支内。当通过自动化测试对功能进行全面测试和验证时,该分支将合并到主服务器中。
- 任务分支(Task branching)
在此模型中,每个任务都在其自己的分支上实现,任务键包含在分支名称中。很容易看出哪个代码实现了哪个任务,只需在分支名称中查找任务键。
- 发布分支(Release branching)
一旦开发分支获得了足够的发布功能,你就可以克隆该分支来形成发布分支。创建该分支将会启动下一个发布周期,所以在此之后不能再添加任何新功能,只有错误修复,文档生成和其他面向发布的任务应该包含在此分支中。一旦准备好发布,该版本将合并到主服务器并标记版本号。此外,它还应该再将自发布以来已经取得的进展合并回开发分支。
什么是SubGit?
> SubGit 是将 SVN 到 Git迁移的工具。它创建了一个可写的本地或远程 Subversion 存储库的 Git 镜像,并且只要你愿意,可以随意使用 Subversion 和 Git。
这样做有很多优点,比如你可以从 Subversion 快速一次性导入到 Git 或者在 Atlassian Bitbucket Server 中使用SubGit。我们可以用 SubGit 创建现有 Subversion 存储库的双向 Git-SVN 镜像。你可以在方便时 push 到 Git 或提交 Subversion。同步由 SubGit 完成。
能不能说一下git fetch和git pull命令之间的区别?
> 简单来说:git fetch branch是把名为branch的远程分支拉取到本地;而git pull branch是在fetch的基础上,把branch分支与当前分支进行merge;因此pull = fetch + merge。
使用过git merge和git rebase吗?它们之间有什么区别?
> 简单的说,git merge和git rebase都是合并分支的命令。
git merge branch会把branch分支的差异内容pull到本地,然后与本地分支的内容一并形成一个committer对象提交到主分支上,合并后的分支与主分支一致;
git rebase branch会把branch分支优先合并到主分支,然后把本地分支的commit放到主分支后面,合并后的分支就好像从合并后主分支又拉了一个分支一样,本地分支本身不会保留提交历史。
能说一下git系统中HEAD、工作树和索引之间的区别吗?
> HEAD文件包含当前分支的引用(指针);
工作树是把当前分支检出到工作空间后形成的目录树,一般的开发工作都会基于工作树进行;
索引index文件是对工作树进行代码修改后,通过add命令更新索引文件;GIT系统通过索引index文件生成tree对象;
之前项目中是使用的GitFlow工作流程吗?它有什么好处?
GitFlow可以用来管理分支。GitFlow工作流中常用的分支有下面几类:
- master分支:最为稳定功能比较完整的随时可发布的代码,即代码开发完成,经过测试,没有明显的bug,才能合并到 master 中。请注意永远不要在 master 分支上直接开发和提交代码,以确保 master 上的代码一直可用;
- develop分支;用作平时开发的主分支,并一直存在,永远是功能最新最全的分支,包含所有要发布 到下一个 release 的代码,主要用于合并其他分支,比如 feature 分支; 如果修改代码,新建 feature 分支修改完再合并到 develop 分支。所有的 feature、release 分支都是从 develop 分支上拉的。
- feature分支;这个分支主要是用来开发新的功能,一旦开发完成,通过测试没问题(这个测试,测试新功能没问题),我们合并回develop 分支进入下一个 release
- release分支;用于发布准备的专门分支。当开发进行到一定程度,或者说快到了既定的发布日,可以发布时,建立一个 release 分支并指定版本号(可以在 finish 的时候添加)。开发人员可以对 release 分支上的代码进行集中测试和修改bug。(这个测试,测试新功能与已有的功能是否有冲突,兼容性)全部完成经过测试没有问题后,将 release 分支上的代码合并到 master 分支和 develop 分支
- hotfix分支;用于修复线上代码的bug。**从 master 分支上拉。**完成 hotfix 后,打上 tag 我们合并回 master 和 develop 分支。
GitFlow主要工作流程
- 1.初始化项目为gitflow , 默认创建master分支 , 然后从master拉取第一个develop分支
- 2.从develop拉取feature分支进行编码开发(多个开发人员拉取多个feature同时进行并行开发 , 互不影响)
- 3.feature分支完成后 , 合并到develop(不推送 , feature功能完成还未提测 , 推送后会影响其他功能分支的开发);合并feature到develop , 可以选择删除当前feature , 也可以不删除。但当前feature就不可更改了,必须从release分支继续编码修改
4.从develop拉取release分支进行提测 , 提测过程中在release分支上修改BUG
5.release分支上线后 , 合并release分支到develop/master并推送;合并之后,可选删除当前release分支,若不删除,则当前release不可修改。线上有问题也必须从master拉取hotfix分支进行修改;
6.上线之后若发现线上BUG , 从master拉取hotfix进行BUG修改;
7.hotfix通过测试上线后,合并hotfix分支到develop/master并推送;合并之后,可选删除当前hotfix ,若不删除,则当前hotfix不可修改,若补丁未修复,需要从master拉取新的hotfix继续修改;
8.当进行一个feature时 , 若develop分支有变动 , 如其他开发人员完成功能并上线 , 则需要将完成的功能合并到自己分支上,即合并develop到当前feature分支;
9.当进行一个release分支时 , 若develop分支有变动 , 如其他开发人员完成功能并上线 , 则需要将完成的功能合并到自己分支上,即合并develop到当前release分支 (!!! 因为当前release分支通过测试后会发布到线上 , 如果不合并最新的develop分支 , 就会发生丢代码的情况);
GitFlow的好处
为不同的分支分配一个明确的角色,并定义分支之间如何交互以及什么时间交互;可以帮助大型项目理清分支之间的关系,简化分支的复杂度。
使用过git cherry-pick,有什么作用?
命令git cherry-pick可以把branch A的commit复制到branch B上。
在branch B上进行命令操作:
- 复制单个提交:git cherry-pick commitId
- 复制多个提交:git cherry-pick commitId1…commitId3
注意:复制多个提交的命令不包含commitId1.
git跟其他版本控制器有啥区别?
> GIT是分布式版本控制系统,其他类似于SVN是集中式版本控制系统。
分布式区别于集中式在于:每个节点的地位都是平等,拥有自己的版本库,在没有网络的情况下,对工作空间内代码的修改可以提交到本地仓库,此时的本地仓库相当于集中式的远程仓库,可以基于本地仓库进行提交、撤销等常规操作,从而方便日常开发。
我们在本地工程常会修改一些配置文件,这些文件不需要被提交,而我们又不想每次执行git status时都让这些文件显示出来,我们该如何操作?
- 首先利用命令touch .gitignore新建文件
$ touch .gitignore
- 然后往文件中添加需要忽略哪些文件夹下的什么类型的文件
$ vim .gitignore
$ cat .gitignore
/target/class
.settings
.imp
*.ini
如何把本地仓库的内容推向一个空的远程仓库?
- 首先确保本地仓库与远程之间是连同的。如果提交失败,则需要进行下面的命令进行连通:
git remote add origin XXXX
- 如果是第一次推送,则进行下面命令:
git push -u origin master // -u 是指定origin为默认主分支
- 之后的提交,只需要下面的命令:
git push origin master
你有提到白屏时间,有什么办法可以减少吗?都是什么原理?
> GZIP,SSR 同构、PWA 应用、预渲染、localStorage 缓存 js 文件等。
下面就是细分拆解答案,无限的连带问题,这里非常耗时,这些内容大都网上能搜到,我这里就不详细说
其中有问到 PWA 的原理,我的回答是:
Service Worker 有一套自己的声明周期,当安装并且处于激活状态时候,网站在 https 或者 localhost 的协议时候,可以拦截过滤发出的请求,会先把请求克隆一份(请求是流,消费就没有了),然后判断请求的资源是否在 Service Worker 缓存中,如果存在那么可以直接从 Service Worker 缓存中取出,如果不存在,那么就真正的发出这个请求。
介绍下你会用的自动化构建方式
1)Jenkins 自动化构建
2)自己搭建 Node.js 服务器,实现 Jenkins
3)Docker 配合 Travis CI 实现自动化构建
Jenkins 自动化构建:
配置,自动同步某个分支代码,打包构建。
自己搭建 Node.js 服务器,实现 Jenkins:
自己搭建 Node.js 的服务器,在 GitLab 上指定 webhook 地址,分支代码更新触发事件,服务器接受到 post 请求,里面附带分支的信息,执行自己的 shell 脚本命令,指定文件夹,构建打包。
服务器上使用 Docker-compose 指定镜像,每次代码推送到 gitHub,通过自己编写的 yml 和 dockerfile 文件构建打包,服务器自动拉取最新镜像并且发布到正式环境。
谈谈你对前端、客户端架构的认识?
> 前端的架构,首先明确项目的兼容性,面向浏览器编程,是否做成 PC、移动端的响应式布局。根据项目规模、后期可能迭代的需求制定技术方案,如果比较重型的应用应该选用原生开发,尽量少使用第三方库。
客户端架构:是否跨平台,明确兼容系统,例如是否兼容 XP ,如果兼容 XP 就选择 nw.js,再然后根据项目复杂度招聘相应技术梯度人员,安排系统学习相关内容,招聘人员或者购买定制开发相关原生插件内容。
虽然说只是谈谈,但是感觉面试的职位越高级、轮数越往后,越考验你的架构能力,前面考察基础,后面考察你的技术广度以及逻辑思维,能否在复杂的应用中保持清醒头脑,定位性能这类型的细节能力。很多人基础面试面得很好,但是拿不到 offer,原因就是没有这种架构能力,只能自己写代码,不能带领大家学习、写代码。这也是我在面试时偶然听到某个大公司 HR 之间的对话,原话是:他面试还可以,看起来是很老实(某个之前的面试者),但是他对之前项目整体流程并不是那么清楚,连自己做的项目,前后端流程都不清楚,感觉不合适。
谈谈你对微前端的看法,以及实践:
> 将 Vue 和 React 一起开发,其实一点都不难,只要自己能造出 Redux 这样的轮子,熟悉两个框架原理,就能一起开发,难的是将这些在一个合适的场景中使用。之前看到网上有微前端的实践,但是并不是那么完美,当然,类似 Electron 这样的应用,混合开发很正常,微前端并不是只单单多个框架混合开发,更多是多个框架引入后解决了什么问题、带来的问题怎么解决?毕竟 5G 还没完全普及,数据传输还是不那么快。过大的包容易带来客户端的过长白屏时间(自己给自己挖坑)
什么是jenkins?
Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。
Jenkins功能包括:
- 持续的软件版本发布/测试项目。
- 监控外部调用执行的工作
如何将 Jenkins 从一台服务器迁移或者复制到另一台服务器?
我会通过将 jobs 目录从旧服务器复制到新服务器的方式来完成这个事情。有很多种方法可以做到这一点:
- 只需复制相应的 job 目录,即可将 job 从一个 Jenkins 服务器移动到另一个。
- 通过使用其它名称克隆 job 目录来制作现有 job 的副本。
- 通过重命名目录来重命名现有 job。请注意,如果你更改了 job 名称,则需要更改尝试调用该重命名 job 的所有 job 。
如何在 Jenkins 中创建备份和复制文件?
> 定期备份 JENKINS_HOME 目录。这包含所有构建 job 配置,从属节点配置和构建历史记录。要创建 Jenkins 的备份,只需复制此目录即可,你还可以复制 job 目录或重命名目录。
如何配置 Jenkins 的 job?
关于这个答案的解决方法是首先提一下如何创建 job:转到 Jenkins 首页,选择“New Job”,然后选择“Build a free-style software project”。然后你可以设置这个自由式 job 的元素:
- 可选的 SCM,例如源代码所在的 CVS 或 Subversion。
- 用于控制 Jenkins 何时执行构建的触发器。
- 某种构建脚本,用于执行实际工作的构建(ant,maven,shell 脚本,批处理文件等)。
- 从构建中收集信息的可选步骤,例如归档制品、记录 javadoc 和测试结果。
- 配置构建结果通知其他人/系统的步骤,例如发送电子邮件、即时消息、更新问题跟踪器等。
列举 Jenkins 中一些有用的插件
- Maven 2 project
- Amazon EC2
- HTML publisher
- Copy artifact
- Join
- Green Balls
如何保证 Jenkins 的安全?
- 确保 global security 配置项已经打开。
- 确保用适当的插件将 Jenkins 与企业员工目录进行集成。
- 确保启用项目矩阵的权限访问设置。
- 通过自定义版本控制的脚本来自动化 Jenkins 中设置权限/特权的过程。
- 限制对 Jenkins 数据/文件夹的物理访问。
- 定期对其进行安全审核。
什么是docker?
- Docker是一个容器化平台,它将应用程序及其所有依赖项以容器的形式打包在一起,以确保应用程序在任何环境(无论是开发环境、测试环境还是生产环境)中无缝运行。
- Docker容器,将一个软件包在一个完整的文件系统中,其中包含运行所需的一切:代码、运行时、系统工具、系统库等任何可以安装在服务器上的东西。
- 它都将始终运行相同的程序,无论软件的环境如何。
什么是Docker镜像?
> Docker镜像是Docker容器的源代码。换句话说,Docker镜像用于创建容器。使用build命令创建镜像,并且在使用run启动时它们将生成容器。镜像存储在Docker注册表中,registry.hub.docker.com因为它们可能变得非常大,镜像被设计为由其他镜像层组成,允许在通过网络传输镜像时发送最少量的数据。
什么是Docker容器?
> Docker容器包括应用程序及其所有依赖项,但与其他容器共享内核,在主机操作系统的用户空间中作为独立进程运行。Docker容器不依赖于任何特定的基础架构:它们可以在任何计算机,任何基础架构和任何云中运行。
什么是Docker Hub?
> Docker hub是一个基于云的注册表服务,允许您链接到代码存储库,构建映像并测试它们,存储手动推送的镜像以及指向Docker云的链接,以便您可以将镜像部署到主机。它为整个开发流程中的容器发现,分发和变更管理,用户和团队协作以及工作流自动化提供了集中资源。
Dockerfile中最常见的指令是什么?
- FROM:我们使用FROM为后续指令设置基本镜像。在每个有效的Dockerfile中,FROM是第一条指令。
- LABEL:我们使用LABEL根据项目,模块,许可等组织我们的镜像。我们也可以使用LABEL来帮助实现自动化。在LABEL中,我们指定一个键值对,以后可用于以编程方式处理Dockerfile。
- RUN:我们使用RUN命令在当前图像之上的新图层中执行任何指令。使用每个RUN命令,我们在图像上添加一些内容,并在Dockerfile的后续步骤中使用它。
- CMD:我们使用CMD命令提供执行容器的默认值。在Dockerfile中,如果我们包含多个CMD命令,则只使用最后一条指令。
什么类型的应用程序 - 无状态或有状态更适合Docker容器?
> 最好为Docker Container创建无状态应用程序。我们可以从应用程序中创建一个容器,并从应用程序中取出可配置的状态参数。现在我们可以在生产环境和具有不同参数的QA环境中运行相同的容器。这有助于在不同场景中重用相同的镜像。另外,无状态应用程序比有状态应用程序更容易使用Docker容器进行扩展。
解释基本的Docker使用工作流程
- 一切都从Dockerfile开始。Dockerfile是镜像的源代码。
- 创建Dockerfile后,您可以构建它以创建容器的镜像。图像只是“源代码”的“编译版本”,即Dockerfile。
- 获得容器的镜像后,应使用注册表重新分发容器。注册表就像一个git存储库 - 你可以推送和拉取镜像。
- 接下来,您可以使用该图像来运行容器。在许多方面,正在运行的容器与虚拟机(但没有虚拟机管理程序)非常相似。
什么是虚拟化?
在其构想的形式中,虚拟化被认为是逻辑上划分大型机以允许多个应用程序同时运行的方法。但是,当公司和开源社区能够以某种方式提供处理特权指令的方法,并允许在单个基于x86的系统上同时运行多个操作系统时,情况发生了巨大变化。
实际效果是虚拟化允许您在同一硬件上运行两个完全不同的操作系统。每个客户操作系统都经历了引导,加载内核等所有过程。您可以拥有非常严格的安全性,例如,客户操作系统无法完全访问主机操作系统或其他客户,从而完全混乱。
可以基于虚拟化方法如何模仿客户操作系统的硬件并模拟客户操作环境来对虚拟化方法进行分类。主要有三种类型的虚拟化:
- 仿真
- 半虚拟化
- 基于容器的虚拟化
什么是管理程序?
> 管理程序处理创建用户虚拟机运行的虚拟环境。它监督用户系统,并确保在必要时为客户分配资源。虚拟机管理程序位于物理机和虚拟机之间,并为虚拟机提供虚拟化服务。为了实现它,它拦截虚拟机上的客户操作系统操作,并模拟主机操作系统上的操作。
虚拟化技术的快速发展(主要是在云端),通过允许在一个物理服务器上创建多个虚拟服务器,借助于管理程序(如Xen、VMware Player、KVM等),以及在商品处理器(如Intel VT AN)中集成硬件支持,进一步推动了虚拟化的使用。例如Intel VT和AMD-V。
什么是Docker Swarm?
> Docker Swarm是Docker的群集管理工具。它将Docker主机池转变为一个虚拟Docker主机。Docker Swarm提供标准的Docker API,任何已经与Docker守护进程通信的工具都可以使用Swarm扩展到多个主机。
您将如何监控生产中的Docker?
> Docker提供docker stats和docker events等工具来监控生产中的Docker。我们可以使用这些命令获取重要统计数据的报告。
Docker stats:当我们使用容器ID调用docker stats时,我们获得容器的CPU,内存使用情况等。它类似于Linux中的top命令。
Docker events:Docker events是一个命令,用于查看Docker守护程序中正在进行的任务。
一些常见的Docker事件是:attach,commit,die,detach,rename,destroy等。我们还可以使用各种选项来限制或过滤我们感兴趣的事件。
数据库
数据库事务的四个特征及含义
- 原子性:要么全部完成,要么不完成,若发生错误会进行回滚操作;
- 一致性:开始到结束后,数据库完整性约束没收到破坏;(实体完整性,参照完整性,用户定义的完整性)
- 隔离性:事务与事务之间相隔离,串行化执行;
- 持久性:事务完成对数据的影响是永久的;
数据库范式
- 1NF:每一列都是不可分割的基本数据项,同一列无二值;无重复的域;
- 2NF:实例依赖于主键部分;
- 3NF:属性不依赖于其他非主属性;
首先要明确的是:满足着第三范式,那么就一定满足第二范式、满足着第二范式就一定满足第一范式
第一范式:字段是最小的的单元不可再分
学生信息组成学生信息表,有年龄、性别、学号等信息组成。这些字段都不可再分,所以它是满足第一范式的
第二范式:满足第一范式,表中的字段必须完全依赖于全部主键而非部分主键。
其他字段组成的这行记录和主键表示的是同一个东西,而主键是唯一的,它们只需要依赖于主键,也就成了唯一的
学号为1的同学,姓名是damao,年龄是100岁。姓名和年龄字段都依赖着学号主键。
第三范式:满足第二范式,非主键外的所有字段必须互不依赖
就是数据只在一个地方存储,不重复出现在多张表中,可以认为就是消除传递依赖
比如,我们大学分了很多系(中文系、英语系、计算机系……),这个系别管理表信息有以下字段组成:系编号,系主任,系简介,系架构。那我们能不能在学生信息表添加系编号,系主任,系简介,系架构字段呢?不行的,因为这样就冗余了,非主键外的字段形成了依赖关系(依赖到学生信息表了)!正确的做法是:学生表就只能增加一个系编号字段。
MySQL存储过程与触发器的区别
- 分表 :真正的分表,每张表对应三个文件;提高MYSQL的并发能力;
- 分区 :表中的数据分成多个区块;突破磁盘的读写能力;
乐观锁和悲观锁
- 乐观锁:假定不会发生并发冲突,只在提交时检查,若有其他数据更新了数据,则回滚;使用数据版本标示数据(时间戳,版本号)
- 悲观锁:假定会发生并发冲突,屏蔽一切破坏数据库一致性的操作,主要用于数据争用激烈的环境,以及锁成本低于回滚成本时;排他锁;
实践中如何优化MySQL
- SQL语句及索引的优化
- 数据库表结构的优化
- 系统配置的优化
- 硬件的优化
优化MySQL数据库的方法
- 选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置NOTNULL,例如'省份'、'性别'最好适用ENUM
- 使用连接(JOIN)来代替子查询
- 适用联合(UNION)来代替手动创建的临时表
- 事务处理
- 锁定表、优化事务处理
- 适用外键,优化锁定表
- 建立索引
- 优化查询语句
简单描述mysql中,索引,主键,唯一索引,联合索引的区别,对数据库的性能有什么影响(从读写两方面)
> 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
普通索引(由关键字KEY或INDEX定义的索引)的唯一任务是加快对数据的访问速度。
普通索引允许被索引的数据列包含重复的值。如果能确定某个数据列将只包含彼此各不相同的值,在为这个数据列创建索引的时候就应该用关键字UNIQUE把它定义为一个唯一索引。也就是说,唯一索引可以保证数据记录的唯一性。
主键,是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字 PRIMARY KEY 来创建。
索引可以覆盖多个数据列,如像INDEX(columnA, columnB)索引,这就是联合索引。
索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件。
什么是NoSQL数据库?NoSQL和RDBMS有什么区别?在哪些情况下使用和不使用NoSQL数据库?
> NoSQL是非关系型数据库,NoSQL = Not Only SQL。
关系型数据库采用的结构化的数据,NoSQL采用的是键值对的方式存储数据。
在处理非结构化/半结构化的大数据时;在水平方向上进行扩展时;随时应对动态增加的数据项时可以优先考虑使用NoSQL数据库。
在考虑数据库的成熟度;支持;分析和商业智能;管理及专业性等问题时,应优先考虑关系型数据库。
如何理解MongoDB中的GridFS机制,MongoDB为何使用GridFS来存储文件?
> GridFS是一种将大型文件存储在MongoDB中的文件规范。使用GridFS可以将大文件分隔成多个小文档存放,这样我们能够有效的保存大文档,而且解决了BSON对象有限制的问题。
MongoDB如何执行事务/加锁?
> MongoDB没有使用传统的锁或者复杂的带回滚的事务,因为它设计的宗旨是轻量,快速以及可预计的高性能。可以把它类比成MySQL MylSAM的自动提交模式。通过精简对事务的支持,性能得到了提升,特别是在一个可能会穿过多个服务器的系统里。
数据在什么时候才会扩展到多个分片(shard)里?
> MongoDB 分片是基于区域(range)的。所以一个集合(collection)中的所有的对象都被存放到一个块(chunk)中。只有当存在多余一个块的时候,才会有多个分片获取数据的选项。现在,每个默认块的大小是 64Mb,所以你需要至少 64 Mb 空间才可以实施一个迁移。
当我试图更新一个正在被迁移的块(chunk)上的文档时会发生什么?
> 更新操作会立即发生在旧的分片(shard)上,然后更改才会在所有权转移(ownership transfers)前复制到新的分片上。
介绍一下 Redis,为什么快,怎么做持久化存储?
> Redis 将数据存储在内存中,key-value 形式存储,所以获取也快。支持的 key 格式相对于 memorycache 更多,而且支持 RDB 快照形式、AOF。
> RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是 fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。RDB 是 Redis 默认的持久化方式,会在对应的目录下生产一个 dump.rdb 文件,重启会通过加载 dump.rdb 文件恢复数据。
优点:
1)只有一个文件 dump.rdb,方便持久化;
2)容灾性好,一个文件可以保存到安全的磁盘;
3)性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化(使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能) ;
4)如果数据集偏大,RDB 的启动效率会比 AOF 更高。
缺点:
1)数据安全性低。(RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不是特别严格的时候)
2)由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是 1 秒钟。
> AOF 持久化是以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,文件中可以看到详细的操作记录。她的出现是为了弥补 RDB 的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
优点:
1)数据安全性更高,AOF 持久化可以配置 appendfsync 属性,其中 always,每进行一次命令操作就记录到 AOF 文件中一次。
2)通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
3)AOF 机制的 rewrite 模式。(AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
缺点
1)AOF 文件比 RDB 文件大,且恢复速度慢;数据集大的时候,比 RDB 启动效率低。
2)根据同步策略的不同,AOF 在运行效率上往往会慢于 RDB。
为什么要用 redis /为什么要用缓存
- 高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
- 高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
为什么要用 redis 而不用 map/guava 做缓存?
> 缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
redis 和 memcached 的区别
- redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
- 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
- Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
redis 常见数据结构以及使用场景分析
- String:常用命令: set,get,decr,incr,mget 等。
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。
- Hash:常用命令: hget,hset,hgetall 等。
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等
- List:常用命令: lpush,rpush,lpop,rpop,lrange等
list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
- Set:常用命令: sadd,spop,smembers,sunion 等
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
- Sorted Set:常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
redis 设置过期时间
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?
定期删除+惰性删除。
通过名字大概就能猜出这两个删除方式的意思了。
定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢?
redis 内存淘汰机制。。
redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)
redis 提供 6种数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的).
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
redis 事务
> Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
介绍下缓存击穿和穿透
- 缓存穿透:是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果 key 不存在或者 key 已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。
- 缓存击穿:是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
- 缓存雪崩
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法:
- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
- 事后:利用 redis 持久化机制保存的数据尽快恢复缓存
- 缓存穿透
简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
如何解决 Redis 的并发竞争 Key 问题
> 所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
如何保证缓存与数据库双写时的数据一致性?
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。
这种情况不存在并发问题么?
不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。其次,采用异步延时删除策略,保证读请求完成以后,再进行删除操作。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。