写点什么

前端面试中小型公司都考些什么

作者:loveX001
  • 2022-11-08
    浙江
  • 本文字数:21548 字

    阅读完需:约 71 分钟

两栏布局的实现

一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应,两栏布局的具体实现:


  • 利用浮动,将左边元素宽度设置为 200px,并且设置向左浮动。将右边元素的 margin-left 设置为 200px,宽度设置为 auto(默认为 auto,撑满整个父元素)。


.outer {  height: 100px;}.left {  float: left;  width: 200px;  background: tomato;}.right {  margin-left: 200px;  width: auto;  background: gold;}
复制代码


  • 利用浮动,左侧元素设置固定大小,并左浮动,右侧元素设置 overflow: hidden; 这样右边就触发了 BFC,BFC 的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。


.left{     width: 100px;     height: 200px;     background: red;     float: left; } .right{     height: 300px;     background: blue;     overflow: hidden; }
复制代码


  • 利用 flex 布局,将左边元素设置为固定宽度 200px,将右边的元素设置为 flex:1。


.outer {  display: flex;  height: 100px;}.left {  width: 200px;  background: tomato;}.right {  flex: 1;  background: gold;}
复制代码


  • 利用绝对定位,将父级元素设置为相对定位。左边元素设置为 absolute 定位,并且宽度设置为 200px。将右边元素的 margin-left 的值设置为 200px。


.outer {  position: relative;  height: 100px;}.left {  position: absolute;  width: 200px;  height: 100px;  background: tomato;}.right {  margin-left: 200px;  background: gold;}
复制代码


  • 利用绝对定位,将父级元素设置为相对定位。左边元素宽度设置为 200px,右边元素设置为绝对定位,左边定位为 200px,其余方向定位为 0。


.outer {  position: relative;  height: 100px;}.left {  width: 200px;  background: tomato;}.right {  position: absolute;  top: 0;  right: 0;  bottom: 0;  left: 200px;  background: gold;}
复制代码

React 17 带来了哪些改变

最重要的是以下三点:


  • 新的 JSX 转换逻辑

  • 事件系统重构

  • Lane 模型的引入


1. 重构 JSX 转换逻辑


在过去,如果我们在 React 项目中写入下面这样的代码:


function MyComponent() {  return <p>这是我的组件</p>}
复制代码


React 是会报错的,原因是 React 中对 JSX 代码的转换依赖的是 React.createElement 这个函数。因此但凡我们在代码中包含了 JSX,那么就必须在文件中引入 React,像下面这样:


import React from 'react';function MyComponent() {  return <p>这是我的组件</p>}
复制代码


React 17 则允许我们在不引入 React 的情况下直接使用 JSX。这是因为在 React 17 中,编译器会自动帮我们引入 JSX 的解析器,也就是说像下面这样一段逻辑:


function MyComponent() {  return <p>这是我的组件</p>}
复制代码


会被编译器转换成这个样子:


import {jsx as _jsx} from 'react/jsx-runtime';function MyComponent() {  return _jsx('p', { children: '这是我的组件' });}
复制代码


react/jsx-runtime 中的 JSX 解析器将取代 React.createElement 完成 JSX 的编译工作,这个过程对开发者而言是自动化、无感知的。因此,新的 JSX 转换逻辑带来的最显著的改变就是降低了开发者的学习成本。


react/jsx-runtime 中的 JSX 解析器看上去似乎在调用姿势上和 React.createElement 区别不大,那么它是否只是 React.createElement 换了个马甲呢?当然不是,它在内部实现了 React.createElement 无法做到的性能优化和简化。在一定情况下,它可能会略微改善编译输出内容的大小


2. 事件系统重构


事件系统在 React 17 中的重构要从以下两个方面来看:


  • 卸掉历史包袱

  • 拥抱新的潮流


2.1 卸掉历史包袱:放弃利用 document 来做事件的中心化管控


React 16.13.x 版本中的事件系统会通过将所有事件冒泡到 document 来实现对事件的中心化管控


这样的做法虽然看上去已经足够巧妙,但仍然有它不聪明的地方——document 是整个文档树的根节点,操作 document 带来的影响范围实在是太大了,这将会使事情变得更加不可控


在 React 17 中,React 团队终于正面解决了这个问题:事件的中心化管控不会再全部依赖 document,管控相关的逻辑被转移到了每个 React 组件自己的容器 DOM 节点中。比如说我们在 ID 为 root 的 DOM 节点下挂载了一个 React 组件,像下面代码这样:


const rootElement = document.getElementById("root");ReactDOM.render(<App />, rootElement);
复制代码


那么事件管控相关的逻辑就会被安装到 root 节点上去。这样一来, React 组件就能够自己玩自己的,再也无法对全局的事件流构成威胁了


2.2 拥抱新的潮流:放弃事件池


在 React 17 之前,合成事件对象会被放进一个叫作“事件池”的地方统一管理。这样做的目的是能够实现事件对象的复用,进而提高性能:每当事件处理函数执行完毕后,其对应的合成事件对象内部的所有属性都会被置空,意在为下一次被复用做准备。这也就意味着事件逻辑一旦执行完毕,我们就拿不到事件对象了,React 官方给出的这个例子就很能说明问题,请看下面这个代码


function handleChange(e) {  // This won't work because the event object gets reused.  setTimeout(() => {    console.log(e.target.value); // Too late!  }, 100);}
复制代码


异步执行的 setTimeout 回调会在 handleChange 这个事件处理函数执行完毕后执行,因此它拿不到想要的那个事件对象 e


要想拿到目标事件对象,必须显式地告诉 React——我永远需要它,也就是调用 e.persist() 函数,像下面这样:


function handleChange(e) {  // Prevents React from resetting its properties:  e.persist();  setTimeout(() => {    console.log(e.target.value); // Works  }, 100);}
复制代码


在 React 17 中,我们不需要 e.persist(),也可以随时随地访问我们想要的事件对象。


3. Lane 模型的引入


初学 React 源码的同学由此可能会很自然地认为:优先级就应该是用 Lane 来处理的。但事实上,React 16 中处理优先级采用的是 expirationTime 模型


expirationTime 模型使用 expirationTime(一个时间长度) 来描述任务的优先级;而 Lane 模型则使用二进制数来表示任务的优先级


lane 模型通过将不同优先级赋值给一个位,通过 31 位的位运算来操作优先级。


Lane 模型提供了一个新的优先级排序的思路,相对于 expirationTime 来说,它对优先级的处理会更细腻,能够覆盖更多的边界条件。

变量提升

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。


b() // call bconsole.log(a) // undefined
var a = 'Hello world'
function b() { console.log('call b')}
复制代码


想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用


  • 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升


b() // call b second
function b() { console.log('call b fist')}function b() { console.log('call b second')}var b = 'Hello world'
复制代码


var 会产生很多错误,所以在 ES6 中引入了 letlet不能在声明前使用,但是这并不是常说的 let 不会提升,let提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用

OSI 七层模型

ISO为了更好的使网络应用更为普及,推出了OSI参考模型。

(1)应用层

OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTPHTTPSFTPPOP3SMTP等。


  • 在客户端与服务器中经常会有数据的请求,这个时候就是会用到http(hyper text transfer protocol)(超文本传输协议)或者https.在后端设计数据接口时,我们常常使用到这个协议。

  • FTP是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,在一些资源网站,比如百度网盘``迅雷应该是基于此协议的。

  • SMTPsimple mail transfer protocol(简单邮件传输协议)。在一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。

(2)表示层

表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。


在项目开发中,为了方便数据传输,可以使用base64对数据进行编解码。如果按功能来划分,base64应该是工作在表示层。

(3)会话层

会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

(4)传输层

传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

(5)网络层

本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。我们可以这样理解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。

(6)数据链路层

将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用 MAC 地址)来访问介质,并进行差错检测。网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了差错控制的功能。

(7)物理层

实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。


OSI 七层模型通信特点:对等通信 对等通信,为了使数据分组从源传送到目的地,源端 OSI 模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。


参考:前端进阶面试题详细解答

== 操作符的强制类型转换规则?

对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 xy 是否相同,就会进行如下判断流程:


  1. 首先会判断两者类型是否相同,相同的话就比较两者的大小;

  2. 类型不相同的话,就会进行类型转换;

  3. 会先判断是否在对比 nullundefined,是的话就会返回 true

  4. 判断两者类型是否为 stringnumber,是的话就会将字符串转换为 number


1 == '1'1 ==  1
复制代码


  1. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断


'1' == true'1' ==  1 1  ==  1
复制代码


  1. 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol,是的话就会把 object 转为原始类型再进行判断


'1' == { name: 'js' }        ↓'1' == '[object Object]'
复制代码

Ajax

它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。



面试手写(原生):


//1:创建Ajax对象var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本//2:配置 Ajax请求地址xhr.open('get','index.xml',true);//3:发送请求xhr.send(null); // 严谨写法//4:监听请求,接受响应xhr.onreadysatechange=function(){     if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )          console.log(xhr.responsetXML)}
复制代码


jQuery 写法


$.ajax({  type:'post',  url:'',  async:ture,//async 异步  sync  同步  data:data,//针对post请求  dataType:'jsonp',  success:function (msg) {
}, error:function (error) {
}})
复制代码


promise 封装实现:


// promise 封装实现:
function getJSON(url) { // 创建一个 promise 对象 let promise = new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest();
// 新建一个 http 请求 xhr.open("GET", url, true);
// 设置状态的监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态 if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } };
// 设置错误监听函数 xhr.onerror = function() { reject(new Error(this.statusText)); };
// 设置响应的数据类型 xhr.responseType = "json";
// 设置请求头信息 xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求 xhr.send(null); });
return promise;}
复制代码

数字证书是什么?

现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。


首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。


这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。

介绍一下 Tree Shaking

对 tree-shaking 的了解


作用:


它表示在打包的时候会去除一些无用的代码


原理


  • ES6的模块引入是静态分析的,所以在编译时能正确判断到底加载了哪些模块

  • 分析程序流,判断哪些变量未被使用、引用,进而删除此代码


特点:


  • 在生产模式下它是默认开启的,但是由于经过babel编译全部模块被封装成IIFE,它存在副作用无法被tree-shaking

  • 可以在package.json中配置sideEffects来指定哪些文件是有副作用的。它有两种值,一个是布尔类型,如果是false则表示所有文件都没有副作用;如果是一个数组的话,数组里的文件路径表示改文件有副作用

  • rollupwebpack中对tree-shaking的层度不同,例如对babel转译后的class,如果babel的转译是宽松模式下的话(也就是loosetrue),webpack依旧会认为它有副作用不会tree-shaking掉,而rollup会。这是因为rollup有程序流分析的功能,可以更好的判断代码是否真正会产生副作用。


原理


  • ES6 Module 引入进行静态分析,故而编译的时候正确判断到底加载了那些模块

  • 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码


依赖于import/export


通过导入所有的包后再进行条件获取。如下:


import foo from "foo";import bar from "bar";
if(condition) { // foo.xxxx} else { // bar.xxx}
复制代码


ES6 的 import 语法完美可以使用 tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码


CommonJS 的动态特性模块意味着 tree shaking 不适用 。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在 ES6 中,进入了完全静态的导入语法:import。这也意味着下面的导入是不可行的:


// 不可行,ES6 的import是完全静态的if(condition) {    myDynamicModule = require("foo");} else {    myDynamicModule = require("bar");}
复制代码

setState 原理分析

1. setState 异步更新


  • 我们都知道,React通过this.state来访问state,通过this.setState()方法来更新state。当this.setState()方法被调用的时候,React会重新调用render方法来重新渲染UI

  • 首先如果直接在setState后面获取state的值是获取不到的。在React内部机制能检测到的地方, setState就是异步的;在React检测不到的地方,例如setInterval,setTimeoutsetState就是同步更新的



因为setState是可以接受两个参数的,一个state,一个回调函数。因此我们可以在回调函数里面获取值



  • setState方法通过一个队列机制实现state更新,当执行setState的时候,会将需要更新的state合并之后放入状态队列,而不会立即更新this.state

  • 如果我们不使用setState而是使用this.state.key来修改,将不会触发组件的re-render

  • 如果将this.state赋值给一个新的对象引用,那么其他不在对象上的state将不会被放入状态队列中,当下次调用setState并对状态队列进行合并时,直接造成了state丢失


1.1 setState 批量更新的过程


react生命周期和合成事件执行前后都有相应的钩子,分别是pre钩子和post钩子,pre钩子会调用batchedUpdate方法将isBatchingUpdates变量置为true,开启批量更新,而post钩子会将isBatchingUpdates置为false


  • isBatchingUpdates变量置为true,则会走批量更新分支,setState的更新会被存入队列中,待同步代码执行完后,再执行队列中的state更新。 isBatchingUpdatestrue,则把当前组件(即调用了 setState的组件)放入 dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新

  • 而在原生事件和异步操作中,不会执行pre钩子,或者生命周期的中的异步操作之前执行了pre钩子,但是pos钩子也在异步操作之前执行完了,isBatchingUpdates必定为false,也就不会进行批量更新



enqueueUpdate包含了React避免重复render的逻辑。mountComponentupdateComponent方法在执行的最开始,会调用到batchedUpdates进行批处理更新,此时会将isBatchingUpdates设置为true,也就是将状态标记为现在正处于更新阶段了。 isBatchingUpdatestrue,则把当前组件(即调用了 setState 的组件)放入dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新


1.2 为什么直接修改 this.state 无效


  • 要知道setState本质是通过一个队列机制实现state更新的。 执行setState时,会将需要更新的 state 合并后放入状态队列,而不会立刻更新state,队列机制可以批量更新state

  • 如果不通过setState而直接修改this.state,那么这个state不会放入状态队列中,下次调用setState时对状态队列进行合并时,会忽略之前直接被修改的state,这样我们就无法合并了,而且实际也没有把你想要的state更新上去


1.3 什么是批量更新 Batch Update


在一些mv*框架中,,就是将一段时间内对model的修改批量更新到view的机制。比如那前端比较火的ReactvuenextTick机制,视图的更新以及实现)


1.4 setState 之后发生的事情


  • setState操作并不保证是同步的,也可以认为是异步的

  • ReactsetState之后,会经对state进行diff,判断是否有改变,然后去diff dom决定是否要更新UI。如果这一系列过程立刻发生在每一个setState之后,就可能会有性能问题

  • 在短时间内频繁setStateReact会将state的改变压入栈中,在合适的时机,批量更新state和视图,达到提高性能的效果


1.5 如何知道 state 已经被更新


传入回调函数


setState({    index: 1}}, function(){    console.log(this.state.index);})
复制代码


在钩子函数中体现


componentDidUpdate(){    console.log(this.state.index);}
复制代码


2. setState 循环调用风险


  • 当调用setState时,实际上会执行enqueueSetState方法,并对partialState以及_pending-StateQueue更新队列进行合并操作,最终通过enqueueUpdate执行state更新

  • performUpdateIfNecessary方法会获取_pendingElement,_pendingStateQueue_pending-ForceUpdate,并调用receiveComponentupdateComponent方法进行组件更新

  • 如果在shouldComponentUpdate或者componentWillUpdate方法中调用setState,此时this._pending-StateQueue != null,就会造成循环调用,使得浏览器内存占满后崩溃


3 事务


  • 事务就是将需要执行的方法使用wrapper封装起来,再通过事务提供的perform方法执行,先执行wrapper中的initialize方法,执行完perform之后,在执行所有的close方法,一组initializeclose方法称为一个wrapper

  • 那么事务和setState方法的不同表现有什么关系,首先我们把4setState 简单归类,前两次属于一类,因为它们在同一调用栈中执行,setTimeout中的两次setState属于另一类

  • setState调用之前,已经处在batchedUpdates执行的事务中了。那么这次batchedUpdates方法是谁调用的呢,原来是ReactMount.js中的_renderNewRootComponent方法。也就是说,整个将React组件渲染到DOM中的过程就是处于一个大的事务中。而在componentDidMount中调用setState时,batchingStrategyisBatchingUpdates已经被设为了true,所以两次setState的结果没有立即生效

  • 再反观setTimeout中的两次setState,因为没有前置的batchedUpdates调用,所以导致了新的state马上生效


4. 总结


  • 通过setState去更新this.state,不要直接操作this.state,请把它当成不可变的

  • 调用setState更新this.state不是马上生效的,它是异步的,所以不要天真以为执行完setStatethis.state就是最新的值了

  • 多个顺序执行的setState不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理

absolute 与 fixed 共同点与不同点

共同点:


  • 改变行内元素的呈现方式,将 display 置为 inline-block  

  • 使元素脱离普通文档流,不再占据文档物理空间

  • 覆盖非定位文档元素


不同点:


  • abuselute 与 fixed 的根元素不同,abuselute 的根元素可以设置,fixed 根元素是浏览器。

  • 在有滚动条的页面中,absolute 会跟着父元素进行移动,fixed 固定在页面的具体位置。

async/await 对比 Promise 的优势

  • 代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是 then 的链式调⽤也会带来额外的阅读负担

  • Promise 传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅

  • 错误处理友好,async/await 可以⽤成熟的 try/catch,Promise 的错误捕获⾮常冗余

  • 调试友好,Promise 的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then 代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then 代码块,因为调试器只能跟踪同步代码的每⼀步。

性能优化

DNS 预解析

  • DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP


<link rel="dns-prefetch" href="//blog.poetries.top">
复制代码

缓存

  • 缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度

  • 通常浏览器缓存策略分为两种:强缓存和协商缓存


强缓存


实现强缓存可以通过两种响应头实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code200


Expires: Wed, 22 Oct 2018 08:41:00 GMT
复制代码


ExpiresHTTP / 1.0 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效


Cache-control: max-age=30
复制代码


Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires 。该属性表示资源会在 30 秒后过期,需要再次请求


协商缓存


  • 如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304

  • 协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式


Last-ModifiedIf-Modified-Since


  • Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来

  • 但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag


ETagIf-None-Match


  • ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified


选择合适的缓存策略


对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略


  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存

  • 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。

  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件

使用 HTTP / 2.0

  • 因为浏览器会有并发请求限制,在 HTTP / 1.1 时代,每个请求都需要建立和断开,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间

  • HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小

预加载

  • 在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载

  • 预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载


<link rel="preload" href="http://example.com">
复制代码


预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好

预渲染

可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染


<link rel="prerender" href="http://poetries.com">
复制代码


  • 预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染


总结


  • deferasync在网络读取的过程中都是异步解析

  • defer是有顺序依赖的,async只要脚本加载完后就会执行

  • preload 可以对当前页面所需的脚本、样式等资源进行预加载

  • prefetch 加载的资源一般不是用于当前页面的,是未来很可能用到的这样一些资源

懒执行与懒加载

懒执行


  • 懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒


懒加载


  • 懒加载就是将不关键的资源延后加载


懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载


  • 懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等

文件优化

图片优化


对于如何优化图片,有 2 个思路


  • 减少像素点

  • 减少每个像素点能够显示的颜色


图片加载优化


  • 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。

  • 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片

  • 小图使用 base64格式

  • 将多个图标文件整合到一张图片中(雪碧图)

  • 选择正确的图片格式:

  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好

  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替

  • 照片使用 JPEG


其他文件优化


  • CSS文件放在 head

  • 服务端开启文件压缩功能

  • script 标签放在 body 底部,因为 JS 文件执行会阻塞渲染。当然也可以把 script 标签放在任意位置然后加上 defer ,表示该文件会并行下载,但是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS文件可以加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。 执行 JS代码过长会卡住渲染,对于需要很多时间计算的代码

  • 可以考虑使用 WebworkerWebworker可以让我们另开一个线程执行脚本而不影响渲染。


CDN


静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie

其他

使用 Webpack 优化项目


  • 对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩

  • 使用 ES6 模块来开启 tree shaking,这个技术可以移除没有使用的代码

  • 优化图片,对于小图可以使用 base64 的方式写入文件中

  • 按照路由拆分代码,实现按需加载

  • 给打包出来的文件名添加哈希,实现浏览器缓存文件


监控


对于代码运行错误,通常的办法是使用 window.onerror 拦截报错。该方法能拦截到大部分的详细报错信息,但是也有例外


  • 对于跨域的代码运行错误会显示 Script error. 对于这种情况我们需要给 script 标签添加 crossorigin 属性

  • 对于某些浏览器可能不会显示调用栈信息,这种情况可以通过 arguments.callee.caller 来做栈递归

  • 对于异步代码来说,可以使用 catch 的方式捕获错误。比如 Promise 可以直接使用 catch 函数,async await 可以使用 try catch

  • 但是要注意线上运行的代码都是压缩过的,需要在打包时生成 sourceMap 文件便于 debug

  • 对于捕获的错误需要上传给服务器,通常可以通过 img 标签的 src发起一个请求

如何根据 chrome 的 timing 优化

性能优化 API


  • Performanceperformance.now()new Date()区别,它是高精度的,且是相对时间,相对于页面加载的那一刻。但是不一定适合单页面场景

  • window.addEventListener("load", ""); window.addEventListener("domContentLoaded", "");

  • Imgonload事件,监听首屏内的图片是否加载完成,判断首屏事件

  • RequestFrameAnmationRequestIdleCallback

  • IntersectionObserverMutationObserverPostMessage

  • Web Worker,耗时任务放在里面执行


检测工具


  • Chrome Dev Tools

  • Page Speed

  • Jspref


前端指标



window.onload = function(){    setTimeout(function(){        let t = performance.timing        console.log('DNS查询耗时 :' + (t.domainLookupEnd - t.domainLookupStart).toFixed(0))        console.log('TCP链接耗时 :' + (t.connectEnd - t.connectStart).toFixed(0))        console.log('request请求耗时 :' + (t.responseEnd - t.responseStart).toFixed(0))        console.log('解析dom树耗时 :' + (t.domComplete - t.domInteractive).toFixed(0))        console.log('白屏时间 :' + (t.responseStart - t.navigationStart).toFixed(0))        console.log('domready时间 :' + (t.domContentLoadedEventEnd - t.navigationStart).toFixed(0))        console.log('onload时间 :' + (t.loadEventEnd - t.navigationStart).toFixed(0))
if(t = performance.memory){ console.log('js内存使用占比 :' + (t.usedJSHeapSize / t.totalJSHeapSize * 100).toFixed(2) + '%') } })}
复制代码


DNS 预解析优化


dns 解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑 dns-prefetch 优化


DNS Prefetch 应该尽量的放在网页的前面,推荐放在 后面。具体使用方法如下:


<meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//www.zhix.net"><link rel="dns-prefetch" href="//api.share.zhix.net"><link rel="dns-prefetch" href="//bdimg.share.zhix.net">
复制代码


request 请求耗时


  • 不请求,用 cache(最好的方式就是尽量引用公共资源,同时设置缓存,不去重新请求资源,也可以运用 PWA 的离线缓存技术,可以帮助 wep 实现离线使用)

  • 前端打包时压缩

  • 服务器上的 zip 压缩

  • 图片压缩(比如 tiny),使用 webp 等高压缩比格式

  • 把过大的包,拆分成多个较少的包,防止单个资源耗时过大

  • 同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。如果资源来自于多个域下,可以增大并行请求和下载速度

  • 延迟、异步、预加载、懒加载

  • 对于非首屏的资源,可以使用 defer 或 async 的方式引入

  • 也可以按需加载,在逻辑中,只有执行到时才做请求

  • 对于多屏页面,滚动时才动态载入图片

移动端优化


1. 概述


  • PC优化手段在Mobile侧同样适用

  • Mobile侧我们提出三秒种渲染完成首屏指标

  • 基于第二点,首屏加载3秒完成或使用Loading

  • 基于联通 3G 网络平均338KB/s(2.71Mb/s),所以首屏资源不应超过1014KB

  • Mobile侧因手机配置原因,除加载外渲染速度也是优化重点

  • 基于第五点,要合理处理代码减少渲染损耗

  • 基于第二、第五点,所有影响首屏加载和渲染的代码应在处理逻辑中后置

  • 加载完成后用户交互使用时也需注意性能


2. 加载优化


加载过程是最为耗时的过程,可能会占到总耗时的80%时间,因此是优化的重点


2.1 缓存


使用缓存可以减少向服务器的请求数,节省加载时间,所以所有静态资源都要在服务器端设置缓存,并且尽量使用长Cache(长Cache资源的更新可使用时间戳)


2.2 压缩 HTML、CSS、JavaScript


减少资源大小可以加快网页显示速度,所以要对HTMLCSSJavaScript等进行代码压缩,并在服务器端设置GZip


  • a) 压缩(例如,多余的空格、换行符和缩进)

  • b) 启用GZip


2.3 无阻塞


写在HTML头部的JavaScript(无异步),和写在HTML标签中的Style会阻塞页面的渲染,因此CSS放在页面头部并使用Link方式引入,避免在HTML标签中写StyleJavaScript放在页面尾部或使用异步方式加载


2.4 使用首屏加载


首屏的快速显示,可以大大提升用户对页面速度的感知,因此应尽量针对首屏的快速显示做优化。


2.5 按需加载


将不影响首屏的资源和当前屏幕资源不用的资源放到用户需要时才加载,可以大大提升重要资源的显示速度和降低总体流量。


PS:按需加载会导致大量重绘,影响渲染性能


  • a) LazyLoad

  • b) 滚屏加载

  • c) 通过Media Query加载


2.6 预加载


大型重资源页面(如游戏)可使用增加Loading的方法,资源加载完成后再显示页面。但Loading时间过长,会造成用户流失。


对用户行为分析,可以在当前页加载下一页资源,提升速度。


  • a)可感知Loading

  • b)不可感知的Loading(如提前加载下一页)


2.7 压缩图片


图片是最占流量的资源,因此尽量避免使用他,使用时选择最合适的格式(实现需求的前提下,以大小判断),合适的大小,然后使用智图压缩,同时在代码中用Srcset来按需显示


PS:过度压缩图片大小影响图片显示效果


  • a)使用智图( http://zhitu.tencent.com/

  • b)使用其它方式代替图片(1. 使用CSS3 2. 使用SVG 3. 使用IconFont

  • c)使用Srcset

  • d)选择合适的图片(1. webP优于JPG2. PNG8优于GIF

  • e)选择合适的大小(1. 首次加载不大于1014KB 2. 不宽于640(基于手机屏幕一般宽度))


2.8 减少 Cookie


Cookie会影响加载速度,所以静态资源域名不使用Cookie


2.9 避免重定向


重定向会影响加载速度,所以在服务器正确设置避免重定向。


2.10 异步加载第三方资源


第三方资源不可控会影响页面的加载和显示,因此要异步加载第三方资源


2.11 减少 HTTP 请求


因为手机浏览器同时响应请求为 4 个请求(Android支持 4 个,iOS 5 后可支持 6 个),所以要尽量减少页面的请求数,首次加载同时请求数不能超过 4 个


  • a)合并CSSJavaScript

  • b)合并小图片,使用雪碧图


3. 三、脚本执行优化


脚本处理不当会阻塞页面加载、渲染,因此在使用时需当注意


  • CSS写在头部,JavaScript写在尾部或异步

  • 避免图片和iFrame等的空Src,空Src会重新加载当前页面,影响速度和效率。

  • 尽量避免重设图片大小

  • 重设图片大小是指在页面、CSS、JavaScript等中多次重置图片大小,多次重设图片大小会引发图片的多次重绘,影响性能

  • 图片尽量避免使用DataURLDataURL图片没有使用图片的压缩算法文件会变大,并且要解码后再渲染,加载慢耗时长


4. CSS 优化


尽量避免写在 HTML 标签中写Style属性


4.1 css3 过渡动画开启硬件加速


.translate3d{   -webkit-transform: translate3d(0, 0, 0);   -moz-transform: translate3d(0, 0, 0);   -ms-transform: translate3d(0, 0, 0);   transform: translate3d(0, 0, 0); }
复制代码


4.2 避免 CSS 表达式


CSS 表达式的执行需跳出 CSS 树的渲染,因此请避免 CSS 表达式。


4.3 不滥用 Float


Float 在渲染时计算量比较大,尽量减少使用


4.4 值为 0 时不需要任何单位


为了浏览器的兼容性和性能,值为0时不要带单位


5. JavaScript 执行优化


5.1 减少重绘和回流


  • 避免不必要的 Dom 操作

  • 尽量改变Class而不是Style,使用classList代替className

  • 避免使用document.write

  • 减少drawImage


5.2 TOUCH 事件优化


使用touchstarttouchend代替click,因快影响速度快。但应注意Touch响应过快,易引发误操作


6. 渲染优化


6.1 HTML 使用 Viewport


Viewport 可以加速页面的渲染,请使用以下代码


<meta name=”viewport” content=”width=device-width, initial-scale=1″>
复制代码


6.2 动画优化


  • 尽量使用CSS3动画

  • 合理使用requestAnimationFrame动画代替setTimeout

  • 适当使用Canvas动画 5个元素以内使用css动画,5个以上使用Canvas动画(iOS8可使用webGL


6.3 高频事件优化


TouchmoveScroll 事件可导致多次渲染


  • 使用requestAnimationFrame监听帧变化,使得在正确的时间进行渲染

  • 增加响应变化的时间间隔,减少重绘次数


6.4 GPU 加速


CSS中以下属性(CSS3 transitionsCSS3 3D transformsOpacityCanvasWebGLVideo)来触发GPU渲染,请合理使用

HTTPS 通信(握手)过程

HTTPS 的通信过程如下:


  1. 客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。

  2. 服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。

  3. 客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服 务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。

  4. 服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。

  5. 客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。

JavaScript 中如何进行隐式类型转换?

首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:


/*** @obj 需要转换的对象* @type 期望的结果类型*/ToPrimitive(obj,type)
复制代码


type的值为number或者string


(1)当typenumber时规则如下:


  • 调用objvalueOf方法,如果为原始值,则返回,否则下一步;

  • 调用objtoString方法,后续同上;

  • 抛出TypeError 异常。


(2)当typestring时规则如下:


  • 调用objtoString方法,如果为原始值,则返回,否则下一步;

  • 调用objvalueOf方法,后续同上;

  • 抛出TypeError 异常。


可以看出两者的主要区别在于调用toStringvalueOf的先后顺序。默认情况下:


  • 如果对象为 Date 对象,则type默认为string

  • 其他情况下,type默认为number


总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:


var objToNumber = value => Number(value.valueOf().toString())objToNumber([]) === 0objToNumber({}) === NaN
复制代码


而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。


以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):


  1. +操作符 +操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。


1 + '23' // '123' 1 + false // 1  1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number '1' + false // '1false' false + true // 1
复制代码


  1. -*\操作符


NaN也是一个数字


1 * '23' // 23 1 * false // 0 1 / 'aa' // NaN
复制代码


  1. 对于==操作符


操作符两边的值都尽量转成number


3 == true // false, 3 转为number为3,true转为number为1'0' == false //true, '0'转为number为0,false转为number为0'0' == 0 // '0'转为number为0
复制代码


  1. 对于<>比较符


如果两边都是字符串,则比较字母表顺序:


'ca' < 'bd' // false'a' < 'b' // true
复制代码


其他情况下,转换为数字再比较:


'12' < 13 // truefalse > -1 // true
复制代码


以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:


var a = {}a > 2 // false
复制代码


其对比过程如下:


a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步a.toString() // "[object Object]",现在是一个字符串了Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字NaN > 2 //false,得出比较结果
复制代码


又比如:


var a = {name:'Jack'}var b = {age: 18}a + b // "[object Object][object Object]"
复制代码


运算过程如下:


a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步a.toString() // "[object Object]"b.valueOf() // 同理b.toString() // "[object Object]"a + b // "[object Object][object Object]"
复制代码

HTTPS 的特点

HTTPS 的优点如下:


  • 使用 HTTPS 协议可以认证用户和服务器,确保数据发送到正确的客户端和服务器;

  • 使用 HTTPS 协议可以进行加密传输、身份认证,通信更加安全,防止数据在传输过程中被窃取、修改,确保数据安全性;

  • HTTPS 是现行架构下最安全的解决方案,虽然不是绝对的安全,但是大幅增加了中间人攻击的成本;


HTTPS 的缺点如下:


  • HTTPS 需要做服务器和客户端双方的加密个解密处理,耗费更多服务器资源,过程复杂;

  • HTTPS 协议握手阶段比较费时,增加页面的加载时间;

  • SSL 证书是收费的,功能越强大的证书费用越高;

  • HTTPS 连接服务器端资源占用高很多,支持访客稍多的网站需要投入更大的成本;

  • SSL 证书需要绑定 IP,不能再同一个 IP 上绑定多个域名。

HTTP 前生今世


  • HTTP 协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文

  • HTTP/0.9 是个简单的文本协议,只能获取文本资源;

  • HTTP/1.0 确立了大部分现在使用的技术,但它不是正式标准;

  • HTTP/1.1 是目前互联网上使用最广泛的协议,功能也非常完善;

  • HTTP/2 基于 Google 的 SPDY 协议,注重性能改善,但还未普及;

  • HTTP/3 基于 Google 的 QUIC 协议,是将来的发展方向

::before 和 :after 的双冒号和单冒号有什么区别?

(1)冒号(:)用于CSS3伪类,双冒号(::)用于CSS3伪元素。(2)::before就是以一个子元素的存在,定义在元素主体内容之前的一个伪元素。并不存在于dom之中,只存在在页面之中。


注意: :before:after 这两个伪元素,是在CSS2.1里新出现的。起初,伪元素的前缀使用的是单冒号语法,但随着Web的进化,在CSS3的规范里,伪元素的语法被修改成使用双冒号,成为::before::after

即时通讯的实现:短轮询、长轮询、SSE 和 WebSocket 间的区别?

短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。


短轮询的基本思路: 浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。


长轮询的基本思路: 首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。


SSE 的基本思想: 服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。


WebSocket 是 HTML5 定义的一个新协议议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。


上面的四个通信协议,前三个都是基于 HTTP 协议的。


对于这四种即使通信协议,从性能的角度来看: WebSocket > 长连接(SEE) > 长轮询 > 短轮询 但是,我们如果考虑浏览器的兼容性问题,顺序就恰恰相反了: 短轮询 > 长轮询 > 长连接(SEE) > WebSocket 所以,还是要根据具体的使用场景来判断使用哪种方式。

同样是重定向,307303302 的区别?

302 是 http1.0 的协议状态码,在 http1.1 版本的时候为了细化 302 状态码⼜出来了两个 303 和 307。 303 明确表示客户端应当采⽤get⽅法获取资源,他会把 POST 请求变为 GET 请求进⾏重定向。 307 会遵照浏览器标准,不会从 post 变为 get。

vue3 带来的新特性/亮点

1. 压缩包体积更小


当前最小化并被压缩的 Vue 运行时大小约为 20kB(2.6.10 版为 22.8kB)。Vue 3.0 捆绑包的大小大约会减少一半,即只有 10kB!


2. Object.defineProperty -> Proxy


  • Object.defineProperty是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小。将它替换为 es6 的Proxy,在目标对象之上架了一层拦截,代理的是对象而不是对象的属性。这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大。

  • javascript 引擎在解析的时候希望对象的结构越稳定越好,如果对象一直在变,可优化性降低,proxy 不需要对原始对象做太多操作。


3. Virtual DOM 重构


vdom 的本质是一个抽象层,用 javascript 描述界面渲染成什么样子。react 用 jsx,没办法检测出可以优化的动态代码,所以做时间分片,vue 中足够快的话可以不用时间分片


  • 传统 vdom 的性能瓶颈:

  • 虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vdom 树。

  • 传统 vdom 的性能跟模版大小正相关,跟动态节点的数量无关。在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费。

  • JSX 和手写的 render function 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足

  • 那为什么不直接抛弃 vdom 呢?

  • 高级场景下手写 render function 获得更强的表达力

  • 生成的代码更简洁

  • 兼容 2.x


vue 的特点是底层为 Virtual DOM,上层包含有大量静态信息的模版。为了兼容手写 render function,最大化利用模版静态信息,vue3.0采用了动静结合的解决方案,将 vdom 的操作颗粒度变小,每次触发更新不再以组件为单位进行遍历,主要更改如下


  • 将模版基于动态节点指令切割为嵌套的区块

  • 每个区块内部的节点结构是固定的

  • 每个区块只需要以一个 Array 追踪自身包含的动态节点


vue3.0 将 vdom 更新性能由与模版整体大小相关提升为与动态内容的数量相关


Vue 3.0 动静结合的 Dom diff


  • Vue3.0 提出动静结合的 DOM diff 思想,动静结合的 DOM diff 其实是在预编译阶段进行了优化。之所以能够做到预编译优化,是因为 Vue core 可以静态分析 template,在解析模版时,整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签和文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

  • 借助预编译过程,Vue 可以做到的预编译优化就很强大了。比如在预编译时标记出模版中可能变化的组件节点,再次进行渲染前 diff 时就可以跳过“永远不会变化的节点”,而只需要对比“可能会变化的动态节点”。这也就是动静结合的 DOM diff 将 diff 成本与模版大小正相关优化到与动态节点正相关的理论依据。


4. Performance


vue3 在性能方面比 vue2 快了 2 倍。


  • 重写了虚拟 DOM 的实现

  • 运行时编译

  • update 性能提高

  • SSR 速度提高


5. Tree-shaking support


vue3 中的核心 api 都支持了 tree-shaking,这些 api 都是通过包引入的方式而不是直接在实例化时就注入,只会对使用到的功能或特性进行打包(按需打包),这意味着更多的功能和更小的体积。


6. Composition API


vue2 中,我们一般会采用 mixin 来复用逻辑代码,用倒是挺好用的,不过也存在一些问题:例如代码来源不清晰、方法属性等冲突。基于此在 vue3 中引入了 Composition API(组合 API),使用纯函数分隔复用代码。和 React 中的hooks的概念很相似


  • 更好的逻辑复用和代码组织

  • 更好的类型推导


<template>    <div>X: {{ x }}</div>    <div>Y: {{ y }}</div></template>
<script>import { defineComponent, onMounted, onUnmounted, ref } from "vue";
const useMouseMove = () => { const x = ref(0); const y = ref(0);
function move(e) { x.value = e.clientX; y.value = e.clientY; }
onMounted(() => { window.addEventListener("mousemove", move); });
onUnmounted(() => { window.removeEventListener("mousemove", move); });
return { x, y };};
export default defineComponent({ setup() { const { x, y } = useMouseMove();
return { x, y }; }});</script>
复制代码


7. 新增的三个组件 Fragment、Teleport、Suspense


Fragment


在书写 vue2 时,由于组件必须只有一个根节点,很多时候会添加一些没有意义的节点用于包裹。Fragment 组件就是用于解决这个问题的(这和 React 中的 Fragment 组件是一样的)。


这意味着现在可以这样写组件了。


/* App.vue */<template>  <header>...</header>  <main v-bind="$attrs">...</main>  <footer>...</footer></template>
<script>export default {};</script>
复制代码


或者这样


// app.jsimport { defineComponent, h, Fragment } from 'vue';
export default defineComponent({ render() { return h(Fragment, {}, [ h('header', {}, ['...']), h('main', {}, ['...']), h('footer', {}, ['...']), ]); }});
复制代码


Teleport


Teleport 其实就是 React 中的 Portal。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。


一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。


/* App.vue */<template>    <div>123</div>    <Teleport to="#container">        Teleport    </Teleport></template>
<script>import { defineComponent } from "vue";
export default defineComponent({ setup() {}});</script>
/* index.html */<div id="app"></div><div id="container"></div>
复制代码



Suspense


同样的,这和 React 中的 Supense 是一样的。


Suspense 让你的组件在渲染之前进行“等待”,并在等待时显示 fallback 的内容


// App.vue<template>    <Suspense>        <template #default>            <AsyncComponent />        </template>        <template #fallback>            Loading...        </template>    </Suspense></template>
<script lang="ts">import { defineComponent } from "vue";import AsyncComponent from './AsyncComponent.vue';
export default defineComponent({ name: "App",
components: { AsyncComponent }});</script>
// AsyncComponent.vue<template> <div>Async Component</div></template>
<script lang="ts">import { defineComponent } from "vue";
const sleep = () => { return new Promise(resolve => setTimeout(resolve, 1000));};
export default defineComponent({ async setup() { await sleep(); }});</script>
复制代码


8. Better TypeScript support


在 vue2 中使用过 TypesScript 的童鞋应该有过体会,写起来实在是有点难受。vue3 则是使用 ts 进行了重写,开发者使用 vue3 时拥有更好的类型支持和更好的编写体验。


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
前端面试中小型公司都考些什么_JavaScript_loveX001_InfoQ写作社区