我是靠谱客的博主 义气可乐,这篇文章主要介绍前端面试总结三,现在分享给大家,希望可以做个参考。

1.深入理解Vue
Vue.js是什么?
Vue.js不是一个框架——它只聚焦视图层,是一个构建数据驱动的web界面的库。
特性:
1)轻量
体积小,不依赖其他基础库;
2) 数据绑定
3)指令
类似AngularJS 可以用一些内置的简单指令(v-*),也可以自定义指令,通过对应表达式的值的变化就可以修改对应的Dom;
4)插件化
Vue的核心不包含Router,Ajax表单验证,但可方便地加载对应的插件。

Vue是一套构建用户界面的渐进式框架,也可以理解为是一个视图模板引擎,强调的是状态到界面的映射。倘若用一句话来概括vue,那么我首先想到的便是官方文档中的一句话: Vue.js(读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。
在具有响应式系统的Vue实例中,DOM状态只是数据状态的一个映射,即 UI=VM(State) ,当等式右边State改变了,页面展示部分UI就会发生相应改变。很多人初次上手Vue时,觉得很好用,原因就是这个。不过,Vue的核心定位并不是一个框架,设计上也没有完全遵循MVVM模式,可以看到在图中只有State和View两部分, Vue的核心功能强调的是状态到界面的映射,对于代码的结构组织并不重视, 所以单纯只使用其核心功能时,它并不是一个框架,而更像一个视图模板引擎,这也是为什么Vue开发者把其命名成读音类似于view的原因。
Vue的核心的功能,是一个视图模板引擎,但这不是说Vue就不能成为一个框架。如下图所示,这里包含了Vue的所有部件,在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统、客户端路由、大规模状态管理来构建一个完整的框架。更重要的是,这些功能相互独立,你可以在核心功能的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念
在这里插入图片描述
Vue.js 自身不是一个全能框架——它只聚焦于视图层。因此它非常容易学习,非常容易与其它库或已有项目整合。另一方面,在与相关工具和支持库一起使用时,Vue.js 也能完美地驱动复杂的单页应用。

渐进式框架
每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式。渐进式代表的含义是:主张最少。
比如说,Angular,它两个版本都是强主张的,如果你用它,必须接受以下东西:- 必须使用它的模块机制- 必须使用它的依赖注入- 必须使用它的特殊形式定义组件(这一点每个视图框架都有,难以避免)所以Angular是带有比较强的排它性的,如果你的应用不是从头开始,而是要不断考虑是否跟其他东西集成,这些主张会带来一些困扰。
比如React,它也有一定程度的主张,它的主张主要是函数式编程的理念,比如说,你需要知道什么是副作用,什么是纯函数,如何隔离副作用。它的侵入性看似没有Angular那么强,主要因为它是软性侵入。
Vue可能有些方面是不如React,不如Angular,但它是渐进的,没有强主张,你可以在原有大系统的上面,把一两个组件改用它实现,当jQuery用;也可以整个用它全家桶开发,当Angular用;还可以用它的视图,搭配你自己设计的整个下层用。你可以在底层数据逻辑的地方用OO和设计模式的那套理念,也可以函数式,都可以,它只是个轻量视图而已,只做了自己该做的事,没有做不该做的事,仅此而已。渐进式的含义,我的理解是:没有多做职责之外的事。

声明式渲染

复制代码
1
2
<div id=app{{ message }}input v-model=inputValue/div>var app = new Vue({el: '#app',data: {message: 'Hello Vue!',inputValue}})

vue的这种声明式渲染,也算是响应式的核心原理是使用了js的一个函数 defineProperty ,这个函数可以将对象进行监控,setter和getter都可以进行其他的操作。就是进门出门要登记,这种中间就可以进行其他操作,比如对视图进行刷新,通知对应的watcher等等。
命令式渲染 : 命令我们的程序去做什么,程序就会跟着你的命令去一步一步执行。
声明式渲染 : 我们只需要告诉程序我们想要什么效果,其他的交给程序来做。

自底向上的开发模式
我的理解就是比如我们先写一个基础的页面,把基础的东西写好,再逐一去添加功能和效果,由简单到繁琐的这么一个过程。

组件化应用构建
组件化应用就是把一个应用或者页面进行拆分,各个部分处理各个部分的事情就可以了。

2.什么是webpack
webpack是一个打包模块化javascript的工具,在webpack里一切文件皆模块,通过loader转换文件,通过plugin注入钩子,最后输出由多个模块组合成的文件,webpack专注构建模块化项目。
WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。
在这里插入图片描述
几个常见的loader:

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件;
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去;
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试;
  • image-loader:加载并且压缩图片文件;
  • babel-loader:把 ES6 转换成 ES5;
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性;
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS;
  • eslint-loader:通过 ESLint 检查 JavaScript 代码。

几个常见的plugin:

  • define-plugin:定义环境变量;
  • terser-webpack-plugin:通过TerserPlugin压缩ES6代码;
  • html-webpack-plugin 为html文件中引入的外部资源,可以生成创建html入口文件;
  • mini-css-extract-plugin:分离css文件;
  • clean-webpack-plugin:删除打包文件;
  • happypack:实现多线程加速编译。

webpack与grunt、gulp的不同?:
Webpack与Gulp、Grunt没有什么可比性,它可以看作模块打包机,通过分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

他们的工作方式也有较大区别

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。
三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。

grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。

webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。

所以总结一下:

从构建思路来说
gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系
webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工
对于知识背景来说
gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

webpack有哪些优点:

  • 专注于处理模块化的项目,能做到开箱即用,一步到位;
  • 可通过plugin扩展,完整好用又不失灵活;
  • 使用场景不局限于web开发;
  • 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
  • 良好的开发体验。

webpack的缺点:
webpack的缺点是只能用于采用模块化开发的项目。

分别介绍bundle,chunk,module是什么:

  • bundle:是由webpack打包出来的文件;
  • chunk:代码块,一个chunk由多个模块组合而成,用于代码的合并和分割;
  • module:是开发中的单个模块,在webpack的世界,一切皆模块,一个模块对应一个文件,webpack会从配置的entry中递归开始找出所有依赖的模块。

分别介绍什么是loader?什么是plugin?:
loader:模块转换器,用于将模块的原内容按照需要转成你想要的内容
plugin:在webpack构建流程中的特定时机注入扩展逻辑,来改变构建结果,是用来自定义webpack打包过程的方式,一个插件是含有apply方法的一个对象,通过这个方法可以参与到整个webpack打包的各个流程(生命周期)。

模块热更新:
模块热更新是webpack的一个功能,他可以使得代码修改过后不用刷新浏览器就可以更新,是高级版的自动刷新浏览器。
devServer中通过hot属性可以控制模块的热替换
1,通过配置文件

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const webpack = require('webpack'); const path = require('path'); let env = process.env.NODE_ENV == "development" ? "development" : "production"; const config = { mode: env, devServer: { hot:true } } plugins: [ new webpack.HotModuleReplacementPlugin(), //热加载插件 ], module.exports = config;

2,通过命令行

复制代码
1
2
3
4
5
"scripts": { "test": "echo "Error: no test specified" && exit 1", "start": "NODE_ENV=development webpack-dev-server --config webpack.develop.config.js --hot", },

通过webpack处理长缓存:
浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升级或是更新,都需要浏览器去下载新的代码,最方便和简单的更新方式就是引入新的文件名称。在webpack中可以在output纵输出的文件指定chunkhash,并且分离经常更新的代码和框架代码。通过NameModulesPlugin或是HashedModuleIdsPlugin使再次打包文件名不变。

如何提高webpack的构建速度:
(1)通过externals配置来提取常用库;
(2)利用DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来;
(3)使用Happypack 实现多线程加速编译;

要注意的第一点是,它对file-loader和url-loader支持不好,所以这两个loader就不需要换成happypack了,其他loader可以类似地换一下

(4)使用Tree-shaking和Scope Hoisting来剔除多余代码;
(5)使用fast-sass-loader代替sass-loader;
(6)babel-loader开启缓存;
babel-loader在执行的时候,可能会产生一些运行期间重复的公共文件,造成代码体积大冗余,同时也会减慢编译效率
可以加上cacheDirectory参数或使用 transform-runtime 插件试试

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js use: [{ loader: 'babel-loader', options: { cacheDirectory: true }] // .bablerc { "presets": [ "env", "react" ], "plugins": ["transform-runtime"] }

(7)不需要打包编译的插件库换成全局"script"标签引入的方式;
比如jQuery插件,react, react-dom等,代码量是很多的,打包起来可能会很耗时
可以直接用标签引入,然后在webpack配置里使用 expose-loader 或 externals 或 ProvidePlugin 提供给模块内部使用相应的变量

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @1 use: [{ loader: 'expose-loader', options: '$' }, { loader: 'expose-loader', options: 'jQuery' }] // @2 externals: { jquery: 'jQuery' }, // @3 new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery' }),

(8)优化构建时的搜索路径。
在webpack打包时,会有各种各样的路径要去查询搜索,我们可以加上一些配置,让它搜索地更快
比如说,方便改成绝对路径的模块路径就改一下,以纯模块名来引入的可以加上一些目录路径
还可以善于用下resolve alias别名 这个字段来配置
还有exclude等的配置,避免多余查找的文件,比如使用babel别忘了剔除不需要遍历的

3.SGML、XML、HTML之间的区别与联系
SGML是指“标准通用标记语言”(Standard Generalized Markup Language), 是1986年出版发布的一个信息管理方面的国际标准(ISO 8879),它是国际上定义电子文件结构和内容描述的标准,是一种非常复杂的文档的结构,主要用于大量高度结构化数据的防卫区和其他各种工业领域,利于分类和索引。
SGML规定了在文档中嵌入描述标记的标准格式,指定了描述文档结构的标准方法,目前在WEB上使用的HTML格式便是使用固定标签集的一种SGML文档。由于SGML可以支持无数的文档结构类型,并且可以创建与特定的软硬件无关的文档,因此很容易与使用不同计算机系统的用户交换文档。同XML相比,定义的功能很强大,缺点是它不适用于Web数据描述,而且SGML软件价格非常价格昂贵。

HTML相信大家都比较熟悉,即“HyperText Markup Language” (超文本标识语言),它的优点是比较适合web 页面的开发。但它有一个缺点是标记相对少,只有固定的标记集如<p>.<strong>等。缺少SGML的柔性和适应性。不能支持特定领域的标记语言,如对数学、化学、音乐等领域的表示支持较少。

所谓的XML(eXtensible Markup Language), 翻译成中文就是“可扩展标识语言”,在国内很多人理解XML为HTML的简单扩展,这实际上是一种误解。尽管XML同HTML关系非常密切。它由万维网协会(W3C)创建,用来克服 HTML的局限。和 HTML 一样,XML 基于 SGML。XML 是为 Web 设计的。XML实际上是Web上表示结构化信息的一种标准文本格式,它没有复杂的语法和包罗万象的数据定义。

SGML是一种在Web发明之前就早已存在的用标记来描述文档资料的通用语言。但SGML十分庞大且难于学习和使用。鉴于此,人们提出了HTML语言。但近年来,随着Web应用的不断深入,HTML在需求广泛的应用中已显得捉襟见肘,有人建议直接使用SGML作为Web语言。但SGML太庞大了,学用两难尚且不说,就是全面实现SGML的浏览器也非常困难。于是Web标准化组织W3C建议使用一种精简的SGML版本——XML。XML与SGML一样,是一个用来定义其他语言的元语言。与SGML相比,XML规范不到SGML规范的1/10,简单易懂,是一门既无标签集也无语法的新一代标记语言。

4.CDN托管
大型Web应用对速度的追求并没有止步于仅仅利用浏览器缓存,因为浏览器缓存始终只是为了提升二次访问的速度,对于首次访问的加速,我们需要从网络层面进行优化,最常见的手段就是CDN(Content Delivery Network,内容分发网络)加速。通过将静态资源缓存到离用户很近的相同网络运营商的CDN节点上,不但能提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。
不同地区的用户会访问到离自己最近的相同网络线路上的CDN节点,当请求到达CDN节点后,节点会判断自己的内容缓存是否有效,如果有效,则立即响应缓存内容给用户,从而加快响应速度。如果CDN节点的缓存失效,它会根据服务配置去我们的内容源服务器获取最新的资源响应给用户,并将内容缓存下来以便响应给后续访问的用户。因此,一个地区内只要有一个用户先加载资源,在CDN中建立了缓存,该地区的其他后续用户都能因此而受益。
之所以不同地区的用户访问同一个域名却能得到不同CDN节点的IP地址,这要依赖于CDN服务商提供的智能域名解析服务,浏览器发起域名查询时,这种智能DNS服务会根据用户IP计算并返回离它最近的同网络CDN节点IP,引导浏览器与此节点建立连接以获取资源。
  
结合上述两点,为了使用CDN网络缓存,我们至少要对静态资源的部署做两项改变:
  1、将静态资源部署到不同网络线路的服务器中,以加速对应网络中CDN节点无缓存时溯源的速度。
  2、加载静态资源时使用与页面不同的域名,一方面是便于接入为CDN而设置的智能DNS解析服务,另一方面因为静态资源和主页面不同域,这样加载资源的HTTP请求就不会带上主页面中的Cookie等数据,减少了数据传输量,又进一步加快网络访问。

CDN服务基本上已经成为现代大型Web应用的标配,这项技术“几乎”是一种对开发透明的网络性能优化手段,使用它的理由很充分,但是这里既然强调了“几乎透明”而不是“完全透明”,是因为使用CDN服务所需要的两项改变对前端工程产生了一定的影响,就是前端工程必须引入非覆盖式发布的根本原因。

5.什么是 FOUC(无样式内容闪烁)?你如何来避免 FOUC?
如果使用import方法对css进行导入,会导致某些页面在Windows 下的Internet Explorer出现一些奇怪的现象: 以无样式显示页面内容的瞬间闪烁,这种现象称之为文档样式短暂失效(Flash of Unstyled Content),简称为FOUC。

原因:

  • 使用import方法导入样式表;
  • 将样式表放在页面底部;
  • 有几个样式表,放在html结构的不同位置。

原理:
当样式表晚于结构性html加载,当加载到此样式表时,页面将停止之前的渲染。
此样式表被下载和解析后,将重新渲染页面,也就出现了短暂的花屏现象。

解决方法:
使用link标签将样式表放在文档head中

6.JavaScript中Null和Undefined的区别
Null:
null是js中的关键字,表示空值,null可以看作是object的一个特殊的值,如果一个object值为空,表示这个对象不是有效对象。

Undefined:
undefined不是js中的关键字,是一个全局变量,是Global的一个属性,以下情况会返回undefined:
1)使用了一个未定义的变量 var i;
2)使用了已定义但未声明的变量;
3)使用了一个对象属性,但该属性不存在或者未赋值;
4)调用函数时,该提供的参数没有提供:

复制代码
1
2
3
4
5
function func(a){ console.log(a); } func();//undefined

5)函数没有返回值时,默认返回undefined

复制代码
1
2
3
var aa=func(); aa;//undefined

相同点:
都是原始类型的值,保存在栈中变量本地。
区别:
类型不一样:

复制代码
1
2
3
console.log(typeOf undefined);//undefined console.log(typeOf null);//object

转化为值时不一样:undefined为NaN ,null为0

复制代码
1
2
3
4
5
console.log(Number(undefined));//NaN console.log(Number(10+undefined));//NaN console.log(Number(null));//0 console.log(Number(10+null));//10

3.undefined=null;//false
undefined
null;//true

何时使用:
null当使用完一个比较大的对象时,需要对其进行释放内存时,设置为null;
var arr=[“aa”,“bb”,“cc”];
arr=null;//释放指向数组的引用

7.ts与js
JavaScript:
JavaScript是一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型。
JavaScript已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果。

  • 脚本语言:无需编译,是在程序的运行过程中逐行进行解释;
  • 基于对象:不仅可以创建对象,也能使用现有的对象。但是并不支持其它面向对象语言所具有的继承和重载功能;
  • 简单:采用弱类型的变量类型,对使用的数据类型未做出严格的要求,是基于Java基本语句和控制的脚本语言,其设计简单紧凑;
  • 动态性:不需要经过Web服务器就可以对用户的输入做出响应。在访问一个网页时,鼠标在网页中进行鼠标点击或上下移、窗口移动等操作JavaScript都可直接对这些事件给出相应的响应;
  • 跨平台性:不依赖于操作系统,仅需要浏览器的支持。因此一个JavaScript脚本在编写后可以带到任意机器上使用,前提上机器上的浏览器支持JavaScript脚本语言,目前JavaScript已被大多数的浏览器所支持。

TypeScript:
TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型基于类的面向对象编程
TypeScript 支持为已存在的 JavaScript 库添加类型信息的头文件,扩展了它对于流行的库如 jQuery,MongoDB,Node.js 和 D3.js 的好处。

  • 添加语言特性:类型批注和编译时类型检查、类、接口、模块、lambda函数;
  • 适用于大型应用的开发。

JS和TS主要差异:

  • TypeScript扩展了JavaScript的语法,任何现有的JavaScript程序可以不加改变的在TypeScript下工作;
  • TypeScript 中的数据要求带有明确的类型,JavaScript不要求;
  • TypeScript 引入了 JavaScript 中没有的“类”概念;
  • TypeScript 中引入了模块的概念,可以把声明、数据、函数和类封装在模块中;
  • TypeScript 为函数提供了缺省参数值;
  • TypeScript 通过类型注解提供编译时的静态类型检查。

TypeScript优势:
(1)静态输入
静态类型化是一种功能,可以在开发人员编写脚本时检测错误。查找并修复错误是当今开发团队的迫切需求。有了这项功能,就会允许开发人员编写更健壮的代码并对其进行维护,以便使得代码质量更好、更清晰。
(2)大型的开发项目
有时为了改进开发项目,需要对代码库进行小的增量更改。这些小小的变化可能会产生严重的、意想不到的后果,因此有必要撤销这些变化。使用TypeScript工具来进行重构更变的容易、快捷。
(3)更好的协作
当开发大型项目时,会有许多开发人员,此时乱码和错误的机也会增加。类型安全是一种在编码期间检测错误的功能,而不是在编译项目时检测错误。这为开发团队创建了一个更高效的编码和调试过程。
(4)更强的生产力
干净的 ECMAScript 6 代码,自动完成和动态输入等因素有助于提高开发人员的工作效率。这些功能也有助于编译器创建优化的代码。

参考:TypeScript & JavaScript

8.Async/Await
在很长一段时间里面,FE们不得不依靠回调来处理异步代码。使用回调的结果是,代码变得很纠结,不便于理解与维护,值得庆幸的是Promise带来了.then(),让代码变得井然有序,便于管理。于是我们大量使用,代替了原来的回调方式。但是不存在一种方法可以让当前的执行流程阻塞直到promise完成。JS里面,我们无法直接原地等promise完成,唯一可以用于提前计划promise完成后的执行逻辑的方式就是通过then附加回调函数。 现在随着Async/Await的增加,可以让接口按顺序异步获取数据,用更可读,可维护的方式处理回调。

Async/Await:
Async/Await是一个期待已久的JavaScript特性,让我们更好的理解使用异步函数。它建立在Promises上,并且与所有现有的基于Promise的API兼容。

(1)Async—声明一个异步函数(async function someName(){…})

  • 自动将常规函数转换成Promise,返回值也是一个Promise对象;
  • 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数;
  • 异步函数内部可以使用await。

(2)Await—暂停异步的功能执行(var result = await someAsyncCall()

  • 放置在Promise调用之前,await强制其他代码等待,直到Promise完成并返回结果;
  • 只能与Promise一起使用,不适用与回调;
  • 只能在async函数内部使用。

async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

如何使用:
(1)async 函数的几种使用形式

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
//函数声明 async function foo(){} //函数表达式 const foo=async function(){}; //对象的方法 let obj={async foo(){}}; obj.foo().then(()=>{console.log('balabala')}); //箭头函数 const foo=async()=>{};
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Class的方法 class Storage{ constructor(){ this.cachePromise=caches.open('avatars'); } async getAvatar(name){ const cache=await this.cachePromise; return cache.match(`/avatars/${name}.jpg`); } } const storage=new Storage(); storage.getAvatar('jake').then();

(2)await的用法则相对简单了许多,他后面需要是一个Promise对象,如果不是则会被转成Promise对象。只要其中一个Promise对象变为reject状态,那么整个async函数都会中断操作。如果状态是resolve,那么他的返回值则会变成then里面的参数,如下。

复制代码
1
2
3
4
5
6
7
async function f(){ return await 123; } f().then(v=>console.log(v)) //123

注意:

  • 怎样容错呢,犹豫await后面的promise运行结果可能是rejected,最好把await放入try{}catch{}中;
  • Await后的异步操作,如果彼此没有依赖关系最好同时触发;
  • Await只能在async函数之中,如果在普通函数中,会报错。

使用场景介绍:
那么什么情况下适合用,什么情况下不适合使用呢?
(1)场景一,我们同时发出三个不互相依赖的请求,如果用Async/Await就显得不明智了

复制代码
1
2
3
4
5
6
7
8
async function getABC(){ let A=await getValueA();//getValueA takes 2 seconds to finish let B=await getValueB();//getValueB takes 4 seconds to finish let C=await getValueC();//getValueC takes 3 seconds to finish return A*B*C; }

上面我们A需要2s,B需要4s,C需要3s,我们如上图所示发请求,就存在彼此依赖的关系,c等b执行完,b等a执行完,从开始到结束需要(2+3+4)9s。

此时我们需要用Promise.all()将异步调用并行执行,而不是一个接一个执行,如下所示:

复制代码
1
2
3
4
5
6
async function getABC(){ let results=await Promise.all([getValueA,getValueB,getValueC]); return results.reduce((total,value)=>total*value); }

这样将会节省我们不少的时间,从原来的的9s缩减到4s

(2)场景二,我曾经遇到过一个场景,一个提交表单的页面,里面有姓名、地址等巴拉巴拉的信息,其中有一项是手机验证码,我们不得不等待手机验证码接口通过,才能发出后续的请求操作,这时候接口之间就存在了彼此依赖的关系,Async跟Await就有了用武之地,让异步请求之间可以按顺序执行。

其中不用Async/Await的写法,我们不得不用.then()的方式,在第一个请求验证码的接口有返回值之后,才能执行后续的的Promise,并且还需要一个then输出结果,如下图:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//获取手机验证码的接口请求 const callPromise=fetch('//xxx.'); callPromise .then(response=>response.json()) .then(json=>{ //对验证码的返回值进行后续的操作,比如提交表单等 const call2Promise=fetch('//xxx.'); return call2Promise; }) .then(response=>response.json()) .then(json=>{ //输出执行完之后的结果 console.log(json.respCode); }) .catch(err=>{console.log(err);});

而用Async/Await的方式去写就是下面这样,我们将逻辑分装在一个async函数里。这样我们就可以直接对promise使用await了,也就规避了写then回调。最后我们调用这个async函数,然后按照普通的方式使用返回的promise。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//提交表单的请求 async function postForm(){ try{ //手机验证码是否通过,拿到返回的json const response1=await fetch('//xxx.'); const json1=await response1.json(); let json2; //对第一个请求的返回数据进行判断,满足条件的话请求第二个接口,并返回数据 if(json1.resCode===200){ const response2=await fetch('//xxx.'); json2=await response2.json(); } return json2; }catch(e){ console.log(e); } } //执行postForm postForm().then((json)=>console.log(json));

参考:浅谈Async/Await

9.为什么POST比GET安全?
如果不通过SSL加密,GET和POST方法都会把数据以明文方式发送到服务器上,安全性相差无几。

说POST比GET安全,有如下几点理由:

  • GET请求的数据会被浏览器作为URL的一部分而保存;
  • 服务器端的访问日志等一般都会记下URL;
  • HTTP头部的referer里会记下URL里请求数据

总之一般人都认为URL里不会包含敏感数据的,所以最好不要把敏感数据放到URL里,这种场合自然不适合使用GET。

10.垃圾回收机制
标记清除:
JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

该算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始(在JS中就是全局对象)扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

此算法可以分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。

  • 标记阶段:垃圾回收器会从根对象开始遍历。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象。
  • 清除阶段:垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作。

何时开始垃圾回收?
通常来说,在使用标记清除算法时,未引用对象并不会被立即回收。取而代之的做法是,垃圾对象将一直累计到内存耗尽为止。当内存耗尽时,程序将会被挂起,垃圾回收开始执行。

标记清除算法缺陷:

  • 那些无法从根对象查询到的对象都将被清除;
  • 垃圾收集后有可能会造成大量的内存碎片,垃圾收集后内存中存在三个内存碎片,假设一个方格代表1个单位的内存,如果有一个对象需要占用3个内存单位的话,那么就会导致Mutator一直处于暂停状态,而Collector一直在尝试进行垃圾收集,直到Out of Memory。

引用计数算法:
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

引用计数缺陷:
该算法有个限制:无法处理循环引用。如果两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

参考:前端面试查漏补缺–(二) 垃圾回收机制

11.前端加密
前端加密的几种做法:

  • JavaScript 加密后传输(具体可以参考后面的常见加密方法);
  • 浏览器插件内进行加密传输 (这个用得不是很多,这里暂不细究);
  • Https 传输。

加密算法:
加密(Encrypt)是将目标文本转换成具有不同长度的、可逆的密文。也就是说加密算法是可逆的,而且其加密后生成的密文长度和明文本身的长度有关。所以如果被保护数据在以后需要被还原成明文,则需要使用加密。

对称加密:
对称加密采用了对称密码编码技术,它的特点是文件加密和解密使用相同的密钥加密.也就是加密和解密都是用同一个密钥,这种方法在密码学中叫做对称加密算法。

常见的对称加密算法有DES、3DES、Blowfish、IDEA、RC4、RC5、RC6和AES。

注意: 因为前端的透明性,对于登录密码等敏感信息,就不要使用JavaScript来进行对称加密. 因为别人可以从前端得到密匙后,可以直接对信息进行解密!

非对称加密:
非对称加密算法需要两个密钥:公钥(publickey)和私钥(privatekey)。 公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。 因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公钥向其它方公开;得到该公钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。甲方只能用其专用密钥解密由其公钥加密后的任何信息。
常见的非对称加密算法有:RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)。

哈希加密算法:
哈希(Hash)是将目标文本转换成具有固定长度的字符串(或叫做消息摘要)。 当输入发生改变时,产生的哈希值也是完全不同的。从数学角度上讲,一个哈希算法是一个多对一的映射关系,对于目标文本 T,算法 H 可以将其唯一映射为 R,并且对于所有的 T,R 具有相同的长度,所以 H 不存在逆映射,也就是说哈希算法是不可逆的
基于哈希算法的特性,其适用于该场景:被保护数据仅仅用作比较验证且不需要还原成明文形式。比较常用的哈希算法是 MD5 和 SHA1 。

我们比较熟悉的使用哈希存储数据的例子是:当我们登录某个已注册网站时,在忘记密码的情况下需要重置密码,此时网站会给你发一个随机的密码或者一个邮箱激活链接,而不是将之前的密码发给你,这就是因为哈希算法是不可逆的。

对于简单的哈希算法的攻击方法主要有:寻找碰撞法和穷举法。所以,为了保证数据的安全,可以在哈希算法的基础上进一步的加密,常见的方法有:加盐、慢哈希、密钥哈希、XOR 等。

加盐(Adding Salt):
加盐加密是一种对系统登录口令的加密方式,它实现的方式是将每一个口令同一个叫做“盐”(salt)的 n 位随机数相关联。

使用salt加密,它的基本想法是这样的:

1.用户注册时,在密码上撒一些盐。生成一种味道,记住味道。
2.用户再次登陆时,在输入的密码上撒盐,闻一闻,判断是否和原来的味道相同,相同就让你吃饭。

由于验证密码时和最初散列密码时使用相同的盐值,所以salt的存储在数据库。并且这个值是由系统随机产生的,而非硬编码。这就保证了所要保护对象的机密性。

注册时:

(1)用户注册,系统随机产生salt值。
(2)将salt值和密码连接起来,生产Hash值。
(3)将Hash值和salt值分别存储在数据库中。
在这里插入图片描述
登陆时:

(1)系统根据用户名找到与之对应的密码Hash。
(2)将用户输入密码和salt值进行散列。
(3)判断生成的Hash值是否和数据库中Hash相同。
在这里插入图片描述
使用加盐加密时需要注意以下两点:
(1)短盐值(Short Slat)
如果盐值太短,攻击者可以预先制作针对所有可能的盐值的查询表。例如,如果盐值只有三个 ASCII 字符,那么只有 95x95x95=857,375 种可能性,加大了被攻击的可能性。还有,不要使用可预测的盐值,比如用户名,因为针对某系统用户名是唯一的且被经常用于其他服务。

(2)盐值复用(Salt Reuse)
在项目开发中,有时会遇到将盐值写死在程序里或者只有第一次是随机生成的,之后都会被重复使用,这种加盐方法是不起作用的。以登录密码为例,如果两个用户有相同的密码,那么他们就会有相同的哈希值,攻击者就可以使用反向查表法对每个哈希值进行字典攻击,使得该哈希值更容易被破解。

所以正确的加盐方法如下:
(1)盐值应该使用加密的安全伪随机数生成器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )产生,比如 C 语言的 rand() 函数,这样生成的随机数高度随机、完全不可预测;
(2)盐值混入目标文本中,一起使用标准的加密函数进行加密;
(3)盐值要足够长(经验表明:盐值至少要跟哈希函数的输出一样长)且永不重复;
(4)盐值最好由服务端提供,前端取值使用。

慢哈希函数(Slow Hash Function):
慢哈希函数是将哈希函数变得非常慢,使得攻击方法也变得很慢,慢到足以令攻击者放弃,而往往由此带来的延迟也不会引起用户的注意。降低攻击效率用到了密钥扩展( key stretching)的技术,而密钥扩展的实现使用了一种 CPU 密集型哈希函数( CPU-intensive hash function)。

密钥哈希:
密钥哈希是将密钥添加到哈希加密,这样只有知道密钥的人才可以进行验证。目前有两种实现方式:使用 ASE 算法对哈希值加密、使用密钥哈希算法 HMAC 将密钥包含到哈希字符串中。为了保证密钥的安全,需要将其存储在外部系统(比如一个物理上隔离的服务端)。
即使选择了密钥哈希,在其基础上进行加盐或者密钥扩展处理也是很有必要。目前密钥哈希用于服务端比较多,例如来应对常见的 SQL 注入攻击。

XOR:
XOR 指的是逻辑运算中的 “异或运算”。两个值相同时,返回 false,否则返回 true,用来判断两个值是否不同。

XOR 运算有一个特性:如果对一个值连续做两次 XOR,会返回这个值本身。这也是其可以用于信息加密的根本。

复制代码
1
2
3
message XOR key // cipherText cipherText XOR key // message

目标文本 message,key 是密钥,第一次执行 XOR 会得到加密文本;在加密文本上再用 key 做一次 XOR 就会还原目标文本 message。为了保证 XOR 的安全,需要满足以下两点:
(1)key 的长度大于等于 message ;
(2)key 必须是一次性的,且每次都要随机产生。

下面以登录密码加密为例介绍下 XOR 的使用:
第一步:使用 MD5 算法,计算密码的哈希;

复制代码
1
2
const message = md5(password);

第二步:生成一个随机 key 值;
第三步:进行 XOR 运算,求出加密后的 message。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getXOR(message, key) const arr = []; //假设 key 是32位的 for (let i = 0; i < 32; i++) { const m = parseInt(message.substr(i, 1), 16); const k = parseInt(key.substr(i, 1), 16); arr.push((m ^ k).toString(16)); } return arr.join(''); }

如上所示,使用 XOR 和一次性的密钥 key 对密码进行加密处理,只要 key 没有泄露,目标文本就不会被破解。

参考:前端面试查漏补缺–(八) 前端加密

12.前端软件架构模式MVC/MVP/MVVM
MVC,MVP和MVVM都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。不同于设计模式(Design Pattern),只是为了解决一类问题而总结出的抽象方法,一种架构模式往往使用了多种设计模式。
要了解MVC、MVP和MVVM,就要知道它们的相同点和不同点。不同部分是C(Controller)、P(Presenter)、VM(View-Model),而相同的部分则是MV(Model-View)。

MVC模式:
MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

  • 模型(Model) - Model层用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法。一旦数据发生变化,模型将通知有关的视图。
  • 视图(View) - View作为视图层,主要负责数据的展示,并且响应用户操作。
  • 控制器(Controller)- 控制器是模型和视图之间的纽带,接收View传来的用户事件并且传递给Model,同时利用从Model传来的最新模型控制更新View。
    在这里插入图片描述
    数据关系:
  • View 接受用户交互请求;
  • View 将请求转交给Controller;
  • Controller 操作Model进行数据更新;
  • 数据更新之后,Model通知View更新数据变化.PS: 还有一种是View作为Observer监听Model中的任意更新,一旦有更新事件发出,View会自动触发更新以展示最新的Model状态.这种方式提升了整体效率,简化了Controller的功能,不过也导致了View与Model之间的紧耦合。
  • View 更新变化数据。

方式:
所有方式都是单向通信。

结构实现:

  • View :使用组合(Composite)模式;
  • View和Controller:使用策略(Strategy)模式;
  • Model和 View:使用观察者(Observer)模式同步信息。

缺点:

  • View层过重: View强依赖于Model的,并且可以直接访问Model.所以不可避免的View还要包括一些业务逻辑.导致view过重,后期修改比较困难,且复用程度低.
  • View层与Controller层也是耦合紧密: View与Controller虽然看似是相互分离,但却是联系紧密.经常View和Controller一一对应的,捆绑起来作为一个组件使用.解耦程度不足.

MVP模式:
MVP(Model-View-Presenter)是MVC模式的改良.MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

  • Model - Model层依然是主要与业务相关的数据和对应处理数据的方法。
  • View - View依然负责显示,但MVP中的View并不能直接使用Model。
  • Presenter - Presenter作为View和Model之间的“中间人”,且MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。
    在这里插入图片描述
    数据关系:
  • View 接收用户交互请求;
  • View 将请求转交给 Presenter;
  • Presenter 操作Model进行数据更新;
  • Model 通知Presenter数据发生变化;
  • Presenter 更新View数据。

方式:
各部分之间都是双向通信

结构实现:

  • View :使用 组合(Composite)模式;
  • View和Presenter:使用 中介者(Mediator)模式;
  • Model和Presenter:使用 命令(Command)模式同步信息。

MVC和MVP关系:

  • MVP:是MVC模式的变种。
  • 项目开发中,UI是容易变化的,且是多样的,一样的数据会有N种显示方式;业务逻辑也是比较容易变化的。为了使得应用具有较大的弹性,我们期望将UI、逻辑(UI的逻辑和业务逻辑)和数据隔离开来,而MVP是一个很好的选择。
  • Presenter代替了Controller,它比Controller担当更多的任务,也更加复杂。Presenter处理事件,执行相应的逻辑,这些逻辑映射到Model操作Model。那些处理UI如何工作的代码基本上都位于Presenter。
  • MVC中的Model和View使用Observer模式进行沟通;MPV中的Presenter和View则使用Mediator模式进行通信;Presenter操作Model则使用Command模式来进行。基本设计和MVC相同:Model存储数据,View对Model的表现,Presenter协调两者之间的通信。在MVP 中 View 接收到事件,然后会将它们传递到 Presenter, 如何具体处理这些事件,将由Presenter来完成。

MVP的优点:

  • Model与View完全分离,修改互不影响
  • 更高效地使用,因为所有的逻辑交互都发生在一个地方—Presenter内部
  • 一个Preseter可用于多个View,而不需要改变Presenter的逻辑(因为View的变化总是比Model的变化频繁)。
  • 更便于测试。把逻辑放在Presenter中,就可以脱离用户接口来测试逻辑(单元测试)

MVP的缺点:

Presenter中除了业务逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,一旦视图需要变更,那么Presenter也需要变更,维护起来比较困难。

MVVM模式:
MVVM是Model-View-ViewModel的简写。由Microsoft提出,并经由Martin Fowler布道传播。在 MVVM 中,不需要Presenter手动地同步View和Model.View 是通过数据驱动的,Model一旦改变就会相应的刷新对应的 View,View 如果改变,也会改变对应的Model。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。

  • Model - Model层仅仅关注数据本身,不关心任何行为(格式化数据由View负责),这里可以把它理解为一个类似json的数据对象。
  • View - MVVM中的View通过使用模板语法来声明式的将数据渲染进DOM,当ViewModel对Model进行更新的时候,会通过数据绑定更新到View。
  • ViewModel - 类似与Presenter. ViewModel会对View 层的声明进行处理.当 ViewModel 中数据变化,View 层会进行更新;如果是双向绑定,一旦View对绑定的数据进行操作,则ViewModel 中的数据也会进行自动更新。
    在这里插入图片描述
    数据关系:
  • View 接收用户交互请求;
  • View 将请求转交给ViewModel;
  • ViewModel 操作Model数据更新;
  • Model 更新完数据,通知ViewModel数据发生变化;
  • ViewModel 更新View数据。

方式:
双向绑定。View/Model的变动,自动反映在 ViewModel,反之亦然。

实现数据绑定的方式:

  • 数据劫持 (Vue);
  • 发布-订阅模式 (Knockout、Backbone);
  • 脏值检查 (旧版Angular)。

使用:

  • 可以兼容你当下使用的 MVC/MVP 框架。
  • 增加你的应用的可测试性。
  • 配合一个绑定机制效果最好。

MVVM优点:
MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

(1)低耦合。View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
(2)可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
(3)独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,生成xml代码。
(4)4, 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

MVVM缺点:
类会增多,ViewModel会越加庞大,调用的复杂度增加。

参考:前端面试查漏补缺–(十一) 前端软件架构模式MVC/MVP/MVVM

13.CORS跨域
CORS(Cross-Origin ResourceSharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通
CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。 目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 因为需要通过 XDomainRequest 来实现。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信
服务器端对于CORS的支持,主要就是通过设置Access-Control-Allow-Origin来进行的。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问。

14.前端鉴权
常见鉴权方式:
(1)HTTP Basic Authentication (HTTP基本认证)
(2)session-cookie
(3)Token 验证(包括JWT,SSO)
(4)OAuth(开放授权)

HTTP Basic Authentication:
这种认证方式是浏览器遵守http协议实现的基本授权方式,HTTP协议进行通信的过程中,HTTP协议定义了基本认证认证允许HTTP服务器对客户端进行用户身份认证的方法。

目前基本没有再使用这种认证方式的,一些老项目的内网认证可能还会有.

认证过程:
(1)客户端向服务器请求数据,请求的内容可能是一个网页或者是一个ajax异步请求,此时,假设客户端尚未被验证,则客户端提供如下请求至服务器:

复制代码
1
2
3
Get /index.html HTTP/1.0 Host:www.google.com

(2)服务器向客户端发送验证请求代码401,(WWW-Authenticate: Basic realm=”google.com”这句话是关键,如果没有客户端不会弹出用户名和密码输入界面)服务器返回的数据大抵如下:

复制代码
1
2
3
4
5
6
HTTP/1.0 401 Unauthorised Server: SokEvo/1.0 WWW-Authenticate: Basic realm=”google.com” Content-Type: text/html Content-Length: xxx

(3)当符合http1.0或1.1规范的客户端(如IE,FIREFOX)收到401返回值时,将自动弹出一个登录窗口,要求用户输入用户名和密码。
(4)用户输入用户名和密码后,将用户名及密码以BASE64加密方式加密(base64不安全!),并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成如下内容:

复制代码
1
2
3
4
Get /index.html HTTP/1.0 Host:www.google.com Authorization: Basic d2FuZzp3YW5n

注:d2FuZzp3YW5n表示加密后的用户名及密码(用户名:密码 然后通过base64加密,加密过程是浏览器默认的行为,不需要我们人为加密,我们只需要输入用户名密码即可)
(5)服务器收到上述请求信息后,将Authorization字段后的用户信息取出、解密,将解密后的用户名及密码与用户数据库进行比较验证,如用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端

效果:
客户端未未认证的时候,会弹出用户名密码输入框,这个时候请求时属于pending状态, 这个时候其实服务当用户输入用户名密码的时候客户端会再次发送带Authentication头的请求。

session-cookie:
这个方式是利用服务器端的session(会话)和浏览器端的cookie来实现前后端的认证,由于http请求时是无状态的,服务器正常情况下是不知道当前请求之前有没有来过,这个时候我们如果要记录状态,就需要在服务器端创建一个会话(session),将同一个客户端的请求都维护在各自得会会话中,每当请求到达服务器端的时候,先去查一下该客户端有没有在服务器端创建session,如果有则已经认证成功了,否则就没有认证。

认证过程:
(1)服务器在接受客户端首次访问时在服务器端创建session,然后保存session(我们可以将session保存在内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一个唯一的标识字符串,然后在响应头中种下这个唯一标识字符串。
(2)签名。这一步只是对sid进行加密处理,服务端会根据这个secret密钥进行解密。(非必需步骤)
(3)浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求的请求头中会带上该域名下的cookie信息
(4)服务器在接受客户端请求时会去解析请求头cookie中的sid,然后根据这个sid去找服务器端保存的该客户端的session,然后判断该请求是否合法。

弊端:

  • 服务器内存消耗大: 用户每做一次应用认证,应用就会在服务端做一次记录,以方便用户下次请求时使用,通常来讲session保存在内存中,随着认证用户的增加,服务器的消耗就会很大.
  • 易受到CSRF攻击: 基于cookie的一种跨站伪造攻击, 基于cookie来进行识别用户的话,用户本身就携带了值,cookie被截获,用户就很容易被伪造.

Token验证:
token是用户身份的验证方式,我们通常叫它:令牌。当用户第一次登录后,服务器生成一个token并将此token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。还可以把不变的参数也放进token,避免多次查库。
我们可以把Token想象成一个安全的护照。你在一个安全的前台验证你的身份(通过你的用户名和密码),如果你成功验证了自己,你就可以取得这个。当你走进大楼的时候(试图从调用API获取资源),你会被要求验证你的护照,而不是在前台重新验证。

验证流程:
(1)客户端使用用户名跟密码请求登录
(2)服务端收到请求,去验证用户名与密码
(3)验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
(4)客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
(5)客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
(6)服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

总的来说就是客户端在首次登陆以后,服务端再次接收http请求的时候,就只认token了,请求只要每次把token带上就行了,服务器端会拦截所有的请求,然后校验token的合法性,合法就放行,不合法就返回401(鉴权失败)。

Token优点与缺点
优点:

  • Token 完全由应用管理,所以它可以避开同源策略. (Cookie是不允许垮域访问的,token不存在)
  • Token 可以避免 CSRF 攻击(也是因为不需要cookie了)
  • Token 可以是无状态的,可以在多个服务间共享
  • Token 支持手机端访问(Cookie不支持手机端访问)

服务器只需要对浏览器传来的token值进行解密,解密完成后进行用户数据的查询,如果查询成功,则通过认证.所以,即使有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端。

缺点:

  • 占带宽: 正常情况下token要比 session_id更大,需要消耗更多流量,挤占更多带宽.(不过几乎可以忽略)
  • 性能问题: 相比于session-cookie来说,token需要服务端花费更多的时间和性能来对token进行解密验证.其实Token相比于session-cookie来说就是一个"时间换空间"的方案.

Token与session的区别:
(1) 使用Token,服务端不需要保存状态. 在session中sessionid 是一个唯一标识的字符串,服务端是根据这个字符串,来查询在服务器端保持的session,这里面才保存着用户的登陆状态。但是token本身就是一种登陆成功凭证,他是在登陆成功后根据某种规则生成的一种信息凭证,他里面本身就保存着用户的登陆状态。服务器端只需要根据定义的规则校验这个token是否合法就行。
(2)Token不需要借助cookie, session-cookie是需要cookie配合的,那么在http代理客户端的选择上就只有浏览器了,因为只有浏览器才会去解析请求响应头里面的cookie,然后每次请求再默认带上该域名下的cookie。但是我们知道http代理客户端不只有浏览器,还有原生APP等等,这个时候cookie是不起作用的,或者浏览器端是可以禁止cookie的(虽然可以,但是这基本上是属于吃饱没事干的人干的事),但是token 就不一样,他是登陆请求在登陆成功后再请求响应体中返回的信息,客户端在收到响应的时候,可以把他存在本地的cookie,storage,或者内存中,然后再下一次请求的请求头重带上这个token就行了。简单点来说cookie-session机制他限制了客户端的类型,而token验证机制丰富了客户端类型。
(3)时效性。session-cookie的sessionid实在登陆的时候生成的而且在登出事时一直不变的,在一定程度上安全就会低,而token是可以在一段时间内动态改变的。
(4)可扩展性。token验证本身是比较灵活的,一是token的解决方案有许多,常用的是JWT,二来我们可以基于token验证机制,专门做一个鉴权服务,用它向多个服务的请求进行统一鉴权。

Token过期与Refresh Token
Token过期:
token是访问特定资源的凭证,出于安全考虑,肯定是要有过期时间的。要不然一次登录便可能一直使用,那token认证还有什么意义? token可定是有过期时间的,一般不会很长,不会超高一个小时.

Refresh Token :
如果token过期了,就要重新获取。继续重复第一次获取token的过程(比如登录,扫描授权等),每一小时就必须获取一次! 这样做是非常不好的用户体验。为了解决这个问题,于是就有了refresh token.通过refresh token去重新获取新的 token.
refresh token,也是加密字符串,并且和token是相关联的。与获取资源的token不同,refresh token的作用仅仅是获取新的token,因此其作用和安全性要求都较低,所以其过期时间也可以设置得长一些,可以以天为最小单位。当然如果refresh token过期了,还是需要重新登录验证的.

JWT (JSON Web Tokens):
JWT原理:
服务器认证以后,生成一个 JSON 对象,发回给用户.之后用户与服务器通信的时候.服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
jwt最大的特点就是: 服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 的数据结构:
它是一个很长的字符串,中间用点(.)分隔成三个部分。
分别是:Header(头部).Payload(负载).Signature(签名)

  • Header : 部分是一个 JSON 对象,描述 JWT 的元数据,例如:{ “alg”: “HS256”,“typ”: “JWT”}.alg属性表示签名的算法.默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。

头部的 JSON 对象使用 Base64URL 算法转成字符串。

  • Payload: 部分也是一个 JSON 对象,用来存放实际需要传递的数据。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

注意: JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

  • Signature: 部分是对前两部分的签名,防止数据篡改。 首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256).

JWT 的几个特点:
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。 对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

首次登录时,后端服务器判断用户账号密码正确之后,根据用户id、用户名、定义好的秘钥、过期时间生成 token ,返回给前端;
前端拿到后端返回的 token ,存储在 localStroage 和 Vuex 里;
前端每次路由跳转,判断 localStroage 有无 token ,没有则跳转到登录页,有则请求获取用户信息,改变登录状态;
每次请求接口,在 Axios 请求头里携带 token;
后端接口判断请求头有无 token,没有或者 token 过期,返回401;
前端得到 401 状态码,重定向到登录页面。

单点登录:
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
SSO一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作,当一个系统成功登录以后,passport将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport发起认证

单点登录流程:
在这里插入图片描述

  • 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  • sso认证中心发现用户未登录,将用户引导至登录页面
  • 用户输入用户名密码提交登录申请
  • sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
  • sso认证中心带着令牌跳转回最初的请求地址(系统1)
  • 系统1拿到令牌,去sso认证中心校验令牌是否有效
  • sso认证中心校验令牌,返回有效,注册系统1
  • 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  • 用户访问系统2的受保护资源
  • 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  • sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  • 系统2拿到令牌,去sso认证中心校验令牌是否有效
  • sso认证中心校验令牌,返回有效,注册系统2
  • 系统2使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系:

  • 局部会话存在,全局会话一定存在
  • 全局会话存在,局部会话不一定存在
  • 全局会话销毁,局部会话必须销毁

注销:
在这里插入图片描述
sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作。

  • 用户向系统1发起注销请求
  • 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
  • sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  • sso认证中心向所有注册系统发起注销请求
  • 各注册系统接收sso认证中心的注销请求,销毁局部会话
  • sso认证中心引导用户至登录页面

OAuth 2.0:
OAuth即开发授权,其实和SSO比较像.它允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供OAuth认证服务的厂商有QQ,微信,微博等。

参考:前端面试查漏补缺–(十) 前端鉴权

15.防抖和节流
相同:在不影响客户体验的前提下,将频繁的回调函数,进行次数缩减.避免大量计算导致的页面卡顿.
不同:防抖是将多次执行变为最后一次执行,节流是将多次执行变为在规定时间内只执行一次.

防抖:
定义:
指触发事件后在规定时间内回调函数只能执行一次,如果在规定时间内又触发了该事件,则会重新开始算规定时间。
四个字总结就是 延时执行

应用场景:
两个条件:
(1)如果客户连续的操作会导致频繁的事件回调(可能引起页面卡顿).
(2)客户只关心"最后一次"操作(也可以理解为停止连续操作后)所返回的结果.
例如:

  • 输入搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • 按钮点击:收藏,点赞,心标等

原理:
通过定时器将回调函数进行延时.如果在规定时间内继续回调,发现存在之前的定时器,则将该定时器清除,并重新设置定时器.这里有个细节,就是后面所有的回调函数都要能访问到之前设置的定时器,这时就需要用到闭包。

两种版本
防抖分为两种:
1)非立即执行版:事件触发->延时->执行回调函数;如果在延时中,继续触发事件,则会重新进行延时.在延时结束后执行回调函数.常见例子:就是input搜索框,客户输完过一会就会自动搜索
2)立即执行版:事件触发->执行回调函数->延时;如果在延时中,继续触发事件,则会重新进行延时.在延时结束后,并不会执行回调函数.常见例子:就是对于按钮防点击.例如点赞,心标,收藏等有立即反馈的按钮.

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//非立即执行版: //首先准备我们要使用的回调函数 function shotCat (content) { console.log('shotCat出品,必属精品!必须点赞!(滑稽)') } //然后准备包装函数: //1,保存定时器标识 //2,返回闭包函数: 1)对定时器的判断清除;2)一般还需要保存函数的参数(一般就是事件返回的对象)和上下文(定时器存在this隐式丢失,详情可以看我不知道的js上) //最后补充一句,这里不建议通过定义一个全局变量来替代闭包保存定时器标识. function debounce(fun, delay = 500) { //let timer = null 保存定时器 return function (args) { let that = this let _args = args //这里对定时器的设置有两种方法,第一种就是将定时器保存在函数(函数也是对象)的属性上, //这种写法,很简便,但不是很常用 clearTimeout(fun.timer) fun.timer = setTimeout(function () { fun.call(that, _args) }, delay) //另外一种写法就是我们比较常见的 //if (timer) clearTimeout(timer); 相比上面的方法,这里多一个判断 //timer = setTimeout(function () { // fun.call(that, _args) //}, delay) } } //接着用变量保存保存 debounce 返回的带有延时功能的函数 let debounceShotCat = debounce(shotCat, 500) //最后添加事件监听 回调debounceShotCat 并传入事件返回的对象 let input = document.getElementById('debounce') input.addEventListener('keyup', function (e) { debounceShotCat(e.target.value) }) //带有立即执行选项的防抖函数: //思路和上面的大致相同,如果是立即执行,则定时器中不再包含回调函数,而是在回调函数执行后,仅起到延时和重置定时器标识的作用 function debounce(fun, delay = 500,immediate = true) { let timer = null //保存定时器 return function (args) { let that = this let _args = args if (timer) clearTimeout(timer); //不管是否立即执行都需要首先清空定时器 if (immediate) { if ( !timer) fun.apply(that, _args) //如果定时器不存在,则说明延时已过,可以立即执行函数 //不管上一个延时是否完成,都需要重置定时器 timer = setTimeout(function(){ timer = null; //到时间后,定时器自动设为null,不仅方便判断定时器状态还能避免内存泄露 }, delay) } else { //如果是非立即执行版,则重新设定定时器,并将回调函数放入其中 timer = setTimeout(function(){ fun.call(that, _args) }, delay); } } }

节流:
当持续触发事件时,在规定时间段内只能调用一次回调函数。如果在规定时间内又触发了该事件,则什么也不做,也不会重置定时器.

与防抖比较:
防抖是将多次执行变为最后一次执行,节流是将多次执行变为在规定时间内只执行一次.一般不会重置定时器. 即不会if (timer) clearTimeout(timer);(时间戳+定时器版除外)

应用场景:
两个条件:
(1)客户连续频繁地触发事件
(2)客户不再只关心"最后一次"操作后的结果反馈.而是在操作过程中持续的反馈.
例如:

  • 鼠标不断点击触发,点击事件在规定时间内只触发一次(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

注意 :何为连续频繁地触发事件,就是事件触发的时间间隔至少是要比规定的时间要短.

节流有两种实现方式:
(1)时间戳方式:通过闭包保存上一次的时间戳,然后与事件触发的时间戳比较.如果大于规定时间,则执行回调.否则就什么都不处理.

特点:
一般第一次会立即执行,之后连续频繁地触发事件,也是超过了规定时间才会执行一次。最后一次触发事件,也不会执行(说明:如果你最后一次触发时间大于规定时间,这样就算不上连续频繁触发了).

(2)定时器方式:原理与防抖类似.通过闭包保存上一次定时器状态.然后事件触发时,如果定时器为null(即代表此时间隔已经大于规定时间),则设置新的定时器.到时间后执行回调函数,并将定时器置为null.

特点:
当第一次触发事件时,不会立即执行函数,到了规定时间后才会执行。 之后连续频繁地触发事件,也是到了规定时间才会执行一次(因为定时器)。当最后一次停止触发后,由于定时器的延时,还会执行一次回调函数(那也是上一次成功成功触发执行的回调,而不是你最后一次触发产生的)。一句话总结就是延时回调,你能看到的回调都是上次成功触发产生的,而不是你此刻触发产生的.

说明: 这两者最大的区别:是时间戳版的函数触发是在规定时间开始的时候,而定时器版的函数触发是在规定时间结束的时候。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//时间戳版: //这里fun指的就是回调函数,我就不写出来了 function throttle(fun, delay = 500) { let previous = 0; //记录上一次触发的时间戳.这里初始设为0,是为了确保第一次触发产生回调 return function(args) { let now = Date.now(); //记录此刻触发时的时间戳 let that = this; let _args = args; if (now - previous > delay) { //如果时间差大于规定时间,则触发 fun.apply(that, _args); previous = now; } } } //定时器版: function throttle(fun, delay = 500) { let timer; return function(args) { let that = this; let _args = args; if (!timer) { //如果定时器不存在,则设置新的定时器,到时后,才执行回调,并将定时器设为null timer = setTimeout(function(){ timer = null; fun.apply(that, _args) }, delay) } } } //时间戳+定时器版: 实现第一次触发可以立即响应,结束触发后也能有响应 (该版才是最符合实际工作需求) //该版主体思路还是时间戳版,定时器的作用仅仅是执行最后一次回调 function throttle(fun, delay = 500) { let timer = null; let previous = 0; return function(args) { let now = Date.now(); let remaining = delay - (now - previous); //距离规定时间,还剩多少时间 let that = this; let _args = args; clearTimeout(timer); //清除之前设置的定时器 if (remaining <= 0) { fun.apply(that, _args); previous = Date.now(); } else { timer = setTimeout(function(){ fun.apply(that, _args) }, remaining); //因为上面添加的clearTimeout.实际这个定时器只有最后一次才会执行 } } }

参考:前端面试查漏补缺–(一) 防抖和节流

16.内存泄漏
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能:变慢,延迟大等 ,重则导致进程崩溃。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

内存泄漏的识别方法:
(1)使用快捷键 F12 或者 Ctrl+Shift+J 打开 Chrome 浏览器的「开发者工具」。
(2)选择 Performance(老版为Timeline) 选项卡,在 Capture 选项中,只勾选 Memory。
(3)设置完成后,点击最左边的 Record 按钮,然后就可以访问网页了。
(4)打开一个网站,例如:www.taobao.com,当网页加载完成后,点击 Stop,等待分析结果。
(5)然后在 Chart View 上寻找内存急速下降的部分,查看对应的 Event Log,可以从中找到 GC 的日志。
在这里插入图片描述
内存泄露的常见原因及处理方式:
常见原因:
(1)意外的全局变量
下面代码中变量bar在foo函数内,但是bar并没有声明.JS就会默认将它变为全局变量,这样在页面关闭之前都不会被释放.

复制代码
1
2
3
4
5
function foo(){ bar=2 console.log('bar没有被声明!') }

b 没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用严格模式可以避免.

(2)dom清空时,还存在引用
很多时候,为了方便存取,经常会将 DOM 结点暂时存储到数据结构中.但是在不需要该DOM节点时,忘记解除对它的引用,则会造成内存泄露.

复制代码
1
2
3
4
5
6
7
var element = { shotCat: document.getElementById('shotCat') }; document.body.removeChild(document.getElementById('shotCat')); // 如果element没有被回收,这里移除了 shotCat 节点也是没用的,shotCat 节点依然留存在内存中.

与此类似情景还有: DOM 节点绑定了事件, 但是在移除的时候没有解除事件绑定,那么仅仅移除 DOM 节点也是没用的

(3)定时器中的内存泄漏

复制代码
1
2
3
4
5
6
7
8
var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { node.innerHTML = JSON.stringify(someResource)); } }, 1000);

如果没有清除定时器,那么 someResource 就不会被释放,如果刚好它又占用了较大内存,就会引发性能问题. 但是 setTimeout ,它计时结束后它的回调里面引用的对象占用的内存是可以被回收的. 当然有些场景 setTimeout 的计时可能很长, 这样的情况下也是需要纳入考虑的.

(4)不规范地使用闭包
例如下面的例子: 相互循环引用.这是经常容易犯的错误,并且有时也不容易发现.

复制代码
1
2
3
4
5
6
7
8
9
function foo() { var a = {}; function bar() { console.log(a); }; a.fn = bar; return bar; };

bar和a形成了相互循环引用.可能有人说bar里不使用console.log(a)不就没有引用了吗就不会造成内存泄露了.NONONO,bar作为一个闭包,即使它内部什么都没有,foo中的所有变量都还是隐式地被 bar所引用。 即使bar内什么都没有还是造成了循环引用,那真正的解决办法就是,不要将a.fn = bar.

避免策略:
(1)减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收(即赋值为null);
(2)注意程序逻辑,避免“死循环”之类的 ;
(3)避免创建过多的对象 原则:不用了的东西要记得及时归还。
(4)减少层级过多的引用

参考:前端面试查漏补缺–(十三) 内存泄漏

17.Event Loop
Event loop的初步理解:
(1)Javascript的事件分为同步任务和异步任务.
(2)遇到同步任务就放在执行栈中执行.
(3)遇到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件.

Event loop 相关概念:
在这里插入图片描述
JS调用栈:
Javascript 有一个 主线程(main thread)和 调用栈(call-stack),所有的代码都要通过函数,放到调用栈(也被称为执行栈)中的任务等待主线程执行。
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。

WebAPIs:
MDN的解释: Web 提供了各种各样的 API 来完成各种的任务。这些 API 可以用 JavaScript 来访问,令你可以做很多事儿,小到对任意 window 或者 element做小幅调整,大到使用诸如 WebGL 和 Web Audio 的 API 来生成复杂的图形和音效。

总结: 就是浏览器提供一些接口,让JavaScript可以调用,这样就可以把任务甩给浏览器了,这样就可以实现异步了!

任务队列(Task Queue):
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列"上第一位的事件就自动进入主线程。但是,如果存在"定时器”,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

同步任务和异步任务:
Javascript单线程任务被分为同步任务和异步任务.

  • 同步任务会在调用栈 中按照顺序等待主线程依次执行.
  • 异步任务会甩给在WebAPIs处理,处理完后有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

宏任务(MacroTask)和 微任务(MicroTask):
在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。

宏任务(MacroTask)
script(整体代码)、setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/O、UI Rendering。

微任务(MicroTask)
Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver(具体使用方式查看这里)

Event loop 执行过程:
首先宏观上是按照这样的顺序执行.也就是前面在"Event loop的初步理解"里讲到的过程。
在这里插入图片描述
注意:
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复
在上图的Event Table里存放着宏任务与微任务,所以在它里面 还发生了一些更细致的事情.

前面介绍宏任务的时候,提过script也属于其中.那么一段代码块就是一个宏任务。故所有一般执行代码块的时候,先执行的是宏任务script,也就是程序执行进入主线程了,主线程再会根据不同的代码再分微任务和宏任务等待主线程执行完成后,不停地循环执行。
主线程(宏任务) => 微任务 => 宏任务 => 主线程
在这里插入图片描述
事件循环的顺序是从script开始第一次循环,随后全局上下文进入函数调用栈,碰到macro-task就将其交给处理它的模块处理完之后将回调函数放进macro-task的队列之中,碰到micro-task也是将其回调函数放进micro-task的队列之中。直到函数调用栈清空只剩全局执行上下文,然后开始执行所有的micro-task。当所有可执行的micro-task执行完毕之后。 接着浏览器会执行下必要的渲染 UI,然后循环再次执行macro-task中的一个任务队列,执行完之后再执行所有的micro-task,就这样一直循环。
注意: 通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。

参考:前端面试查漏补缺–(十五) Event Loop

18.页面海量数据怎么优化加载
题目:10w 条记录的数组,一次性渲染到页面上,如何处理可以不冻结UI?
具体化:页面上有个空的无序列表节点 ul ,其 id 为 list-with-big-data ,现需要往列表插入 10w 个 li ,每个列表项的文本内容可自行定义,且要求当每个 li 被单击时,通过 alert 显示列表项内的文本内容。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>页面加载海量数据</title> </head> <body> <ul id="list-with-big-data">100000 数据</ul> <script> // 此处添加你的代码逻辑 </script> </body> </html>

分析:
可能在看到这个问题的第一眼,我们可能会想到这样的解决办法:获取 ul 元素,然后新建 li 元素,并设置好 li 的文本内容和监听器绑定,然后在循环里对 ul 进行 append 操作,即可能想到的是以下代码实现。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function() { const ulContainer = document.getElementById("list-with-big-data"); // 防御性编程 if (!ulContainer) { return; } for (let i = 0; i < 100000; i++) { const liItem = document.createElement("li"); liItem.innerText = i + 1; // EventListener 回调函数的 this 默认指向当前节点,若使用箭头函数,得谨慎 liItem.addEventListener("click", function() { alert(this.innerText); }); ulContainer.appendChild(liItem); } })();

实践上述代码,我们发现界面体验很不友好,卡顿感严重。出现卡顿感的主要原因是,在每次循环中,都会修改 DOM 结构,并且由于数据量大,导致循环执行时间过长,浏览器的渲染帧率过低。

事实上,包含 100000 个 li 的长列表,用户不会立即看到全部,只会看到少部分。因此,对于大部分的 li 的渲染工作,我们可以延时完成。

我们可以从减少 DOM 操作次数缩短循环时间两个方面减少主线程阻塞的时间。

我们知道可以通过 DocumentFragment 的使用,减少 DOM 操作次数,降低回流对性能的影响。
在缩短循环时间方面,我们可以通过 分治 的思想,将 100000 个 li 分批插入到页面中,并且我们通过 requestAniminationFrame 在页面重绘前插入新节点。

事件绑定:
如果我们想监听海量元素,推荐方法是使用 JavaScript 的事件机制,实现事件委托,这样可以显著减少 DOM 事件注册 的数量。

解决方案:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(function() { const ulContainer = document.getElementById("list-with-big-data"); // 防御性编程 if (!ulContainer) { return; } const total = 100000; // 插入数据的总数 const batchSize = 4; // 每次批量插入的节点个数,个数越多,界面越卡顿 const batchCount = total / batchSize; // 批处理的次数 let batchDone = 0; // 已完成的批处理个数 function appendItems() { // 使用 DocumentFragment 减少 DOM 操作次数,对已有元素不进行回流 const fragment = document.createDocumentFragment(); for (let i = 0; i < batchSize; i++) { const liItem = document.createElement("li"); liItem.innerText = batchDone * batchSize + i + 1; fragment.appendChild(liItem); } // 每次批处理只修改 1 次 DOM ulContainer.appendChild(fragment); batchDone++; doAppendBatch(); } function doAppendBatch() { if (batchDone < batchCount) { // 在重绘之前,分批插入新节点 window.requestAnimationFrame(appendItems); } } // kickoff doAppendBatch(); // 使用 事件委托 ,利用 JavaScript 的事件机制,实现对海量元素的监听,有效减少事件注册的数量 ulContainer.addEventListener("click", function(e) { const target = e.target; if (target.tagName === "LI") { alert(target.innerText); } }); })();

参考:面试题之页面海量数据怎么优化加载

19.异步解决方案----Promise与Await
异步编程模式在前端开发过程中,显得越来越重要。从最开始的XHR到封装后的Ajax都在试图解决异步编程过程中的问题。随着ES6新标准的到来,处理异步数据流又有了新的方案。我们都知道,在传统的ajax请求中,当异步请求之间的数据存在依赖关系的时候,就可能产生很难看的多层回调,俗称’回调地狱’(callback hell),这却让人望而生畏,Promise的出现让我们告别回调函数,写出更优雅的异步代码。在实践过程中,却发现Promise并不完美,Async/Await是近年来JavaScript添加的最革命性的的特性之一,Async/Await提供了一种使得异步代码看起来像同步代码的替代方法。

Promise的原理与基本语法:
Promise 是一种对异步操作的封装,可以通过独立的接口添加在异步操作执行成功、失败时执行的方法。主流的规范是 Promises/A+。

Promise中有几个状态:

  • pending: 初始状态, 非 fulfilled 或 rejected;
  • fulfilled: 成功的操作,为表述方便,fulfilled 使用 resolved 代替;
  • rejected: 失败的操作。
    在这里插入图片描述
    pending可以转化为fulfilled或rejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilled和rejected状态只能由pending转化而来,两者之间不能互相转换。

Promise的基本语法:

  • Promise实例必须实现then这个方法
  • then()必须可以接收两个函数作为参数
  • then()返回的必须是一个Promise实例
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//如果低版本浏览器不支持Promise,通过cdn这种方式 <script type="text/javascript"> function loadImg(src) { var promise = new Promise(function (resolve, reject) { var img = document.createElement('img') img.onload = function () { resolve(img) } img.onerror = function () { reject('图片加载失败') } img.src = src }) return promise } var src = 'https://www.imooc.com/static/img/index/logo_new.png' var result = loadImg(src) result.then(function (img) { console.log(1, img.width) return img }, function () { console.log('error 1') }).then(function (img) { console.log(2, img.height) }) </script>

Promise多个串联操作:
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。
有了Promise,我们只需要简单地写job1.then(job2).then(job3).catch(handleError);
其中job1、job2和job3都是Promise对象。
比如我们想实现第一个图片加载完成后,再加载第二个图片,如果其中有一个执行失败,就执行错误函数:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png' var result1 = loadImg(src1) //result1是Promise对象 var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg' var result2 = loadImg(src2) //result2是Promise对象 result1.then(function (img1) { console.log('第一个图片加载完成', img1.width) return result2 // 链式操作 }).then(function (img2) { console.log('第二个图片加载完成', img2.width) }).catch(function (ex) { console.log(ex) })

这里需注意的是:then 方法可以被同一个 promise 调用多次,then 方法必须返回一个 promise 对象。上例中result1.then如果没有明文返回Promise实例,就默认为本身Promise实例即result1,result1.then返回了result2实例,后面再执行.then实际上执行的是result2.then

Promise常用方法:
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); // 同时执行p1和p2,并在它们都完成后执行then: Promise.all([p1, p2]).then(function (results) { console.log(results); // 获得一个Array: ['P1', 'P2'] });

有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:

复制代码
1
2
3
4
5
6
7
8
9
10
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); Promise.race([p1, p2]).then(function (result) { console.log(result); // 'P1' });

由于p1执行较快,Promise的then()将获得结果’P1’。p2仍在继续执行,但执行结果将被丢弃。

总结:
Promise.all接受一个promise对象的数组,待全部完成之后,统一执行success;
Promise.race接受一个包含多个promise对象的数组,只要有一个完成,就执行success

接下来我们对上面的例子做下修改,加深对这两者的理解:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png' var result1 = loadImg(src1) var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg' var result2 = loadImg(src2) Promise.all([result1, result2]).then(function (datas) { console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png"> console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg"> }) Promise.race([result1, result2]).then(function (data) { console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg"> })

如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行.

Async/Await简介与用法:

  • Lasync/await是写异步代码的新方式,优于回调函数和Promise。
  • async/await是基于Promise实现的,它不能用于普通的回调函数。
  • async/await与Promise一样,是非阻塞的。
  • async/await使得异步代码看起来像同步代码,再也没有回调函数。但是改变不了JS单线程、异步的本质。

Async/Await的用法:

  • 使用await,函数必须用async标识
  • await后面跟的是一个Promise实例
  • 需要安装babel-polyfill,安装后记得引入 //npm i --save-dev babel-polyfill
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function loadImg(src) { const promise = new Promise(function (resolve, reject) { const img = document.createElement('img') img.onload = function () { resolve(img) } img.onerror = function () { reject('图片加载失败') } img.src = src }) return promise } const src1 = 'https://www.imooc.com/static/img/index/logo_new.png' const src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg' const load = async function(){ const result1 = await loadImg(src1) console.log(result1) const result2 = await loadImg(src2) console.log(result2) } load()

当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

Async/Await错误处理:
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。try…catch错误处理也比较符合我们平常编写同步代码时候处理的逻辑。

复制代码
1
2
3
4
5
6
7
8
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } }

为什么Async/Await更好?
(1)简洁
使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。
(2)中间值
你很可能遇到过这样的场景,调用promise1,使用promise1返回的结果去调用promise2,然后使用两者的结果去调用promise3。你的代码很可能是这样的:

复制代码
1
2
3
4
5
6
7
8
9
10
const makeRequest = () => { return promise1() .then(value1 => { return promise2(value1) .then(value2 => { return promise3(value1, value2) }) }) }

使用async/await的话,代码会变得异常简单和直观

复制代码
1
2
3
4
5
6
const makeRequest = async () => { const value1 = await promise1() const value2 = await promise2(value1) return promise3(value1, value2) }

(3)条件语句
下面示例中,需要获取数据,然后根据返回数据决定是直接返回,还是继续获取更多的数据。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const makeRequest = () => { return getJSON() .then(data => { if (data.needsAnotherRequest) { return makeAnotherRequest(data) .then(moreData => { console.log(moreData) return moreData }) } else { console.log(data) return data } }) }

代码嵌套(6层)可读性较差,它们传达的意思只是需要将最终结果传递到最外层的Promise。使用async/await编写可以大大地提高可读性:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
const makeRequest = async () => { const data = await getJSON() if (data.needsAnotherRequest) { const moreData = await makeAnotherRequest(data); console.log(moreData) return moreData } else { console.log(data) return data } }

参考:异步解决方案----Promise与Await

20.基于Html5 canvas实现裁剪图片和马赛克功能及又拍云上传图片功能
核心功能:

  • 图片裁剪(裁剪框拖动,裁剪框改变大小);
  • 图片马赛克(绘制马赛克,清除马赛克);
  • 图片预览、图片还原(返回原图、返回处理图);
  • 图片上传(获取签名、上传图片)。

核心逻辑:
(1)图片裁剪
  获取裁剪框(矩形)相对于画布的位置(左上)和裁剪框的height、width。获取(getImageData)canvas相应位置的图片对象(ImageData)。清空canvas画布。在canvas画布的相应位置绘制(putImageData)获取的图片对象(ImageData)。生成预览图。

(2)图片马赛克
  马赛克的绘制,就是在以鼠标划过路径(画笔宽度)为中心的区域,重新绘制成其他的颜色。一般结果是,会取周围的相近的颜色。
  取色方法:

1)比如现有一鼠标划过的点的坐标(x,y),定义一个矩形左上角坐标取(x,y),宽30px,高30px。我们把矩形宽高都除以5(分成5份,可以自定义为n份),所以现在是25个6px的小格子。每个小格子宽高都是6px。
2)然后,我们随机获取一个小格子,获取(getImageData)这个小格子的图片对象(ImageData);再随机获取此图片对象上某个像素点(宽1px,高1px)的颜色color(rgba:ImageData.data[0],ImageData.data[1],ImageData.data[2],ImageData.data[3]);最后我们把第一个6x6px的小格子的每个像素点的颜色都设置为color。
3)其他24个小格子的颜色,遍历2步骤即可。

(3)清除马赛克
  我们需要理解一个问题,不管是绘制马赛克,还是清除马赛克,其本质都是在绘制图片。我们在某个位置绘制了马赛克,清除的时候,就是把原图在当前位置的图片对象再画出来。就达到了清除的效果。所以,我们需要备份一个canvas,和原图一模一样,清除的时候,需要获取备份画布上对应位置的图像,绘制到马赛克的位置。

(4)图片预览
  图片预览就是获取裁剪框的区域,获取区域内的图片对象。再绘制到画布上。

(5)图片还原至原图
  清空画布,再次绘制原图

(6)还原至已操作图片
  预览是保存画布图片对象(ImageData),清空画布,绘制保存的图片对象至画布

(7)图片上传
  获取(toDataURL)canvas图片路径,将获取到的base64图片转化为File对象。进行上传。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
<template> <div class="canvas-clip" :loading="loading"> <div v-show="isDrop" class="canvas-mainBox" ref="canvas-mainBox" id="canvas-mainBox" @mousedown.stop="startMove($event)" > <div class="canvas-minBox left-up" @mousedown.stop="startResize($event,0)"></div> <div class="canvas-minBox up" @mousedown.stop="startResize($event,1)"></div> <div class="canvas-minBox right-up" @mousedown.stop="startResize($event,2)"></div> <div class="canvas-minBox right" @mousedown.stop="startResize($event,3)"></div> <div class="canvas-minBox right-down" @mousedown.stop="startResize($event,4)"></div> <div class="canvas-minBox down" @mousedown.stop="startResize($event,5)"></div> <div class="canvas-minBox left-down" @mousedown.stop="startResize($event,6)"></div> <div class="canvas-minBox left" @mousedown.stop="startResize($event,7)"></div> </div> <!-- 画布 --> <canvas class="canvas-area" ref="canvas" id="canvas" :width="canvasWidth" :height="canvasHeight" @mousedown.stop="startMove($event)" :class="{hoverPaint:isMa,hoverClear:isMaClear}" ></canvas> <!-- 备份画布 --> <canvas class="canvas-copy" ref="canvasCopy" :width="canvasWidth" :height="canvasHeight"></canvas> <div class="canvas-btns"> <button v-if="backBtn" @click="clipBack">返回</button> <button :class="{active:btnIndex==0}" @click="sourceImg">原图</button> <button :class="{active:btnIndex==1}" @click="paintRectReady" :disabled="isDisabled">马赛克</button> <button :class="{active:btnIndex==2}" @click="paintRectClearReady" :disabled="isDisabled">橡皮擦</button> <button :class="{active:btnIndex==3}" @click="clipReady" :disabled="isDisabled">裁剪</button> <button :class="{active:btnIndex==4}" @click="clipPosition">预览</button> <button @click="getSignature">上传</button> <button class="close" @click="canvasClose()">x</button> <!-- <div class="paint-size" v-if="isMaClear || isMa"> <span>画笔大小</span> <input :defaultValue="maSize" v-model="maSize" max="100" min="1" type="range"> <span class="size-num">{{maSize}}</span> </div> --> </div> </div> </template> <script> import axios from "axios"; import md5 from "js-md5"; import req from "../../axios/config"; export default { props: ["imgUrl"], data() { return { resizeFX: "", movePrev: "", canvasWidth: 800, // 画布宽 canvasHeight: 600, // 画布高 loading: false, isDrop: false, // 裁剪 isMa: false, // 马赛克 maSize: 30, // 马赛克大小 isMaClear: false, // 清除马赛克 backBtn: false, // 返回按钮 isDisabled: false,//禁用按钮 btnIndex: 0,//当前按钮 mouseX:'',// 鼠标位置 mouseY:'', clipEle: "", // 裁剪框元素 canvasDataSession: "", // 预览前的画布信息 canvas: "", // 画布 ctx: "", // 画布上下文 canvasCopy: "", // copy画布 ctxCopy: "", // copy画布上下文 uploadOption: { // 图片上传参数 path: "", policy: "", signature: "", username: "" } }; }, mounted() { this.clipEle = this.$refs["canvas-mainBox"]; this.canvas = this.$refs["canvas"]; this.ctx = this.canvas.getContext("2d"); this.canvasCopy = this.$refs["canvasCopy"]; this.ctxCopy = this.canvasCopy.getContext("2d"); this.draw(); }, methods: { // 创建图片 draw() { var img = new Image(); img.setAttribute('crossOrigin', 'anonymous'); img.onload = () => { this.ctx.drawImage(img, 0, 0, 800, 600); this.ctxCopy.drawImage(img, 0, 0, 800, 600); }; img.src = this.imgUrl + '?time=' + new Date().valueOf(); }, //预览 计算裁剪框的位置(左上坐标) clipPosition() { this.isDisabled = true; this.backBtn = true; this.isMa = false; this.isMaClear = false; this.btnIndex = 4; //画布位置 var canvasPx = this.canvas.offsetLeft, canvasPy = this.canvas.offsetTop; if (this.isDrop) { // 裁剪框位置 var clipPx = this.clipEle.offsetLeft, clipPy = this.clipEle.offsetTop, x = clipPx - canvasPx, y = clipPy - canvasPy, w = this.clipEle.offsetWidth, h = this.clipEle.offsetHeight, // 预览图居中 positionX = 400 - this.clipEle.offsetWidth / 2, positionY = 300 - this.clipEle.offsetHeight / 2; } else { // 没有裁剪框,保存完整图片 var x = 0, y = 0, w = this.canvas.offsetWidth, h = this.canvas.offsetHeight, // 预览图居中 positionX = 0, positionY = 0; } var imageData = this.ctx.getImageData(x, y, w, h); this.canvasDataSession = this.ctx.getImageData( 0, 0, this.canvasWidth, this.canvasHeight ); this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); this.ctx.putImageData(imageData, positionX, positionY); this.clipEle.style.display = "none"; this.canvasCopy.style.display = "none"; }, // 返回预览前状态 clipBack() { this.btnIndex = -1; this.backBtn = false; this.isDisabled = false; this.isDrop = false; this.ctx.putImageData(this.canvasDataSession, 0, 0); this.canvasCopy.style.display = "block"; }, // 原图 sourceImg() { this.isDisabled = false; this.btnIndex = 0; this.backBtn = false; this.isMa = false; this.isDrop = false; this.isMaClear = false; var img = new Image(); this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); img.setAttribute('crossOrigin', 'anonymous'); img.onload = () => { this.ctx.drawImage(img, 0, 0, this.canvasWidth, this.canvasHeight); }; img.src = this.imgUrl + '?time=' + new Date().valueOf(); this.canvasCopy.style.display = "block"; }, // 获取签名 getSignature() { // canvas图片base64 转 File 对象 var dataURL = this.canvas.toDataURL("image/jpg"), arr = dataURL.split(","), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } var obj = new Blob([u8arr], { type: mime }), time = new Date().toGMTString(), formData = new FormData(); formData.append("file", obj); // 获取文件后缀 var suffix = formData.get("file").type.split("/")[1]; req .get("/carsource-api/upyun/sign", { suffix: suffix }) .then(response => { if (response.data.code === 0) { this.uploadOption.path = response.data.data.path; formData.append("policy", response.data.data.policy); formData.append("authorization", response.data.data.signature); this.updateImg(formData); } }) .catch(function(error) {}); }, // 上传 updateImg(formData) { axios({ url: "http://v0.api.upyun.com/tmp-img", method: "POST", data: formData }).then(response => { if (response.data.code == 200) { this.$message.success("图片修改成功"); this.canvasClose("upload", response.data.url.slice(4)); } }); }, // 裁剪框缩放 移动 startResize(e, n) { this.resizeFX = n; $(document).mousemove(this.resizeDiv); document.addEventListener("mouseup", this.stopResize); }, stopResize(e) { $(document).off("mousemove", this.resizeDiv); document.removeEventListener("mouseup", this.stopResize); }, startMove(e) { this.movePrev = [e.pageX, e.pageY]; $(document).mousemove(this.moveDiv); document.addEventListener("mouseup", this.stopMove); }, stopMove(e) { $(document).off("mousemove", this.moveDiv); document.removeEventListener("mouseup", this.stopMove); }, moveDiv(e) { // 马赛克 if (this.isMa) { this.paintRect(e); } // 清除马赛克 if (this.isMaClear) { this.paintRectClear(e); } // 裁剪 if (this.isDrop) { var targetDiv = $("#canvas-mainBox"), offsetArr = targetDiv.offset(); var chaX = e.pageX - this.movePrev[0], chaY = e.pageY - this.movePrev[1], ox = parseFloat(targetDiv.css("left")), oy = parseFloat(targetDiv.css("top")); targetDiv.css({ left: ox + chaX + "px", top: oy + chaY + "px" }); this.movePrev = [e.pageX, e.pageY]; } }, resizeDiv(e) { e.preventDefault(); e.stopPropagation(); // 获取需要改变尺寸元素到页面的距离 var targetDiv = $("#canvas-mainBox"), offsetArr = targetDiv.offset(); var eleSWidth = targetDiv.width(), eleSHeight = targetDiv.height(), ox = parseFloat(targetDiv.css("left")), oy = parseFloat(targetDiv.css("top")); // 获取鼠标位置,和元素初始offset进行对比, var chaX = e.pageX - offsetArr.left, chaY = e.pageY - offsetArr.top; switch (this.resizeFX) { case 0: //如果移动距离接近宽度或高度,则不进行改变 if (chaX >= eleSWidth - 10 || chaY >= eleSHeight - 10) { return; } // 获得位置差(m-e),先设置宽度和高度,再设置位置 // 原始宽高+((m-e)*-1),原始位置+(m-e) targetDiv.css({ width: eleSWidth + chaX * -1 + "px", height: eleSHeight + chaY * -1 + "px", left: ox + chaX + "px", top: oy + chaY + "px" }); break; case 1: //如果移动距离接近宽度或高度,则不进行改变 if (chaY >= eleSHeight - 10) { return; } // 获得位置差(m-e),先设置宽度和高度,再设置位置 // 原始宽高+((m-e)*-1),原始位置+(m-e) targetDiv.css({ height: eleSHeight + chaY * -1 + "px", top: oy + chaY + "px" }); break; case 2: //如果移动距离接近宽度或高度,则不进行改变 if (chaX <= 10 || chaY >= eleSHeight - 10) { return; } // 获得位置差(m-e),先设置宽度和高度,设置位置 // 原始高+((m-e)*-1),原始宽+((m-e)),原始位置+(m-e) targetDiv.css({ width: chaX + "px", height: eleSHeight + chaY * -1 + "px", top: oy + chaY + "px" }); break; case 3: //如果移动距离接近宽度或高度,则不进行改变 if (chaX <= 10) { return; } // 获得位置差(m-e),先设置宽度和高度,再设置位置 // 原始宽高+((m-e)*-1),原始位置+(m-e) targetDiv.css({ width: chaX + "px" }); break; case 4: //如果移动距离接近宽度或高度,则不进行改变 if (chaX <= 10 || chaY <= 10) { return; } // 获得位置差(m-e),先设置宽度和高度,再设置位置 // 原始宽高+((m-e)*-1),原始位置+(m-e) targetDiv.css({ width: chaX + "px", height: chaY + "px" }); break; case 5: //如果移动距离接近宽度或高度,则不进行改变 if (chaY <= 10) { return; } // 获得位置差(m-e),先设置宽度和高度,再设置位置 // 原始宽高+((m-e)*-1),原始位置+(m-e) targetDiv.css({ height: chaY + "px" }); break; case 6: //如果移动距离接近宽度或高度,则不进行改变 if (chaX >= eleSWidth - 10 || chaY <= 10) { return; } // 获得位置差(m-e),先设置宽度和高度,再设置位置 // 原始宽高+((m-e)*-1),原始位置+(m-e) targetDiv.css({ width: eleSWidth + chaX * -1 + "px", height: chaY + "px", left: ox + chaX + "px" }); break; case 7: //如果移动距离接近宽度或高度,则不进行改变 if (chaX >= eleSWidth - 10) { return; } // 获得位置差(m-e),先设置宽度和高度,再设置位置 // 原始宽高+((m-e)*-1),原始位置+(m-e) targetDiv.css({ width: eleSWidth + chaX * -1 + "px", left: ox + chaX + "px" }); break; default: break; } }, // 裁剪 clipReady() { this.btnIndex = 3; this.isMa = false; this.isDrop = true; this.isMaClear = false; }, // 马赛克 paintRectReady() { this.btnIndex = 1; this.isMa = true; this.isDrop = false; this.isMaClear = false; }, // 橡皮擦 paintRectClearReady() { this.btnIndex = 2; this.isMa = false; this.isDrop = false; this.isMaClear = true; }, // 绘制马赛克 paintRect(e) { var offT = this.canvas.offsetTop, // 距离上边距离 offL = this.canvas.offsetLeft, // 距离左边距离 x = e.clientX, y = e.clientY; if(this.mouseX - x > this.maSize/2 || x - this.mouseX > this.maSize/2 || this.mouseY - y > this.maSize/2 || y - this.mouseY > this.maSize/2){ var oImg = this.ctx.getImageData(x - offL ,y - offT,this.maSize,this.maSize); var w = oImg.width; var h = oImg.height; //马赛克的程度,数字越大越模糊 var num = 6; //等分画布 var stepW = w/num; var stepH = h/num; //这里是循环画布的像素点 for(var i=0;i<stepH;i++){ for(var j=0;j<stepW;j++){ //获取一个小方格的随机颜色,这是小方格的随机位置获取的 var color = this.getXY(oImg,j*num+Math.floor(Math.random()*num),i*num+Math.floor(Math.random()*num)); //这里是循环小方格的像素点, for(var k=0;k<num;k++){ for(var l=0;l<num;l++){ //设置小方格的颜色 this.setXY(oImg,j*num+l,i*num+k,color); } } } } this.ctx.putImageData(oImg,x - offL ,y - offT); this.mouseX = e.clientX this.mouseY = e.clientY } }, getXY(obj,x,y){ var w = obj.width; var h = obj.height; var d = obj.data; var color = []; color[0] = d[4*(y*w+x)]; color[1] = d[4*(y*w+x)+1]; color[2] = d[4*(y*w+x)+2]; color[3] = d[4*(y*w+x)+3]; return color; }, setXY(obj,x,y,color){ var w = obj.width; var h = obj.height; var d = obj.data; d[4*(y*w+x)] = color[0]; d[4*(y*w+x)+1] = color[1]; d[4*(y*w+x)+2] = color[2]; d[4*(y*w+x)+3] = color[3]; }, // 清除马赛克 paintRectClear(e) { var offT = this.canvasCopy.offsetTop, // 距离上边距离 offL = this.canvasCopy.offsetLeft, // 距离左边距离 x = e.clientX, y = e.clientY, // 获取原图此位置图像数据 imageData = this.ctxCopy.getImageData( x - offL, y - offT, this.maSize, this.maSize ); this.ctx.putImageData(imageData, x - offL, y - offT); }, // 关闭画布 canvasClose(type, url) { this.$emit("isShowImgChange", type, url); } } }; </script> <style scoped> .canvas-clip { position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 9010; background: #000; } .canvas-mainBox { position: absolute; width: 400px; height: 300px; left: 50%; top: 50%; margin-left: -200px; margin-top: -150px; border: 1px solid #FFF; cursor: move; z-index: 9009; } .canvas-minBox { position: absolute; width: 8px; height: 8px; background: #FFF; } .left-up { top: -4px; left: -4px; cursor: nw-resize; } .up { top: -4px; left: 50%; margin-left: -4px; cursor: n-resize; } .right-up { top: -4px; right: -4px; cursor: ne-resize; } .right { top: 50%; margin-top: -4px; right: -4px; cursor: e-resize; } .right-down { bottom: -4px; right: -4px; cursor: se-resize; } .down { bottom: -4px; left: 50%; margin-left: -4px; cursor: s-resize; } .left-down { bottom: -4px; left: -4px; cursor: sw-resize; } .left { top: 50%; margin-top: -4px; left: -4px; cursor: w-resize; } .canvas-btns { position: fixed; right: 50px; top: 30px; z-index: 9003; } .canvas-btns button { display: inline-blovk; background: green; cursor: pointer; border: none; width: 60px; height: 30px; line-height: 30px; color: #fff; font-size: 15px; } .canvas-btns button.active { background: rgb(32, 230, 32); } .canvas-btns button.close { background: rgb(230, 72, 32); } .canvas-copy { position: absolute; top: 50%; left: 50%; margin-top: -300px; margin-left: -400px; z-index: 9007; } .canvas-mosatic { position: absolute; top: 50%; left: 50%; margin-top: -300px; margin-left: -400px; z-index: 9009; } .canvas-area { position: absolute; top: 50%; left: 50%; margin-top: -300px; margin-left: -400px; z-index: 9008; } .paint-size{ margin-top: 20px; font-size: 13px; color: #FFF; height: 30px; line-height: 30px; text-align: right; } .paint-size input{ vertical-align: middle; background: green; } .paint-size .size-num{ display: inline-block; width: 15px; } .hoverClear{ cursor: url('./paint.png'),auto; } .hoverPaint{ cursor: url('./paint.png'),auto; } </style>

参考:.基于Html5 canvas实现裁剪图片和马赛克功能及又拍云上传图片功能

最后

以上就是义气可乐最近收集整理的关于前端面试总结三的全部内容,更多相关前端面试总结三内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(95)

评论列表共有 0 条评论

立即
投稿
返回
顶部