路由的hash和history模式的区别
Vue-Router有两种模式:hash模式和history模式。默认的路由模式是hash模式。
1. hash模式
简介: hash模式是开发中默认的模式,它的URL带着一个#
特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。
原理: hash模式的主要原理就是onhashchange()事件:
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。
2. history模式
简介: history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。 特点: 相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。 API: history api可以分为两大部分,切换历史状态和修改历史状态:
- 修改历史状态:包括了 HTML5 History Interface 中新增的
pushState()
和replaceState()
方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。 - 切换历史状态: 包括
forward()
、back()
、go()
三个方法,对应浏览器的前进,后退,跳转操作。
虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。
如果想要切换到history模式,就要进行以下配置(后端也要进行配置):
const router = new VueRouter({
mode: 'history',
routes: [...]
})
3. 两种模式对比
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
- pushState() 可额外设置 title 属性供后续使用。
- hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。
hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
vue和react的区别
=> 相同点:
1. 数据驱动页面,提供响应式的试图组件
2. 都有virtual DOM,组件化的开发,通过props参数进行父子之间组件传递数据,都实现了webComponents规范
3. 数据流动单向,都支持服务器的渲染SSR
4. 都有支持native的方法,react有React native, vue有wexx
=> 不同点:
1.数据绑定:Vue实现了双向的数据绑定,react数据流动是单向的
2.数据渲染:大规模的数据渲染,react更快
3.使用场景:React配合Redux架构适合大规模多人协作复杂项目,Vue适合小快的项目
4.开发风格:react推荐做法jsx + inline style把html和css都写在js了
vue是采用webpack + vue-loader单文件组件格式,html, js, css同一个文件
虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
- 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
- diff 算法 — 比较两棵虚拟 DOM 树的差异;
- pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
Vue.js的template编译
简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点),详细步骤如下:
首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。
然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)
Vue是如何收集依赖的?
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
function defieneReactive (obj, key, val){
const dep = new Dep();
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
if(Dep.target){
dep.depend();
...
}
return val
}
...
})
}
以上只保留了关键代码,主要就是 const dep = new Dep()
实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend()
进行依赖收集。 (1)Dep Dep是整个依赖收集的核心,其关键代码如下:
class Dep {
static target;
subs;
constructor () {
...
this.subs = [];
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.sub, sub)
}
depend () {
if(Dep.target){
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subds.slice();
for(let i = 0;i < subs.length; i++){
subs[i].update()
}
}
}
Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
(2)Watcher
class Watcher {
getter;
...
constructor (vm, expression){
...
this.getter = expression;
this.get();
}
get () {
pushTarget(this);
value = this.getter.call(vm, vm)
...
return value
}
addDep (dep){
...
dep.addSub(this)
}
...
}
function pushTarget (_target) {
Dep.target = _target
}
Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相关过程如下∶
初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。
初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,
updateComponent = () => {
vm._update(vm._render())
}
new Watcher(vm, updateComponent)
get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。
MVVM、MVC、MVP的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。
(1)MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
(2)MVVM
MVVM 分为 Model、View、ViewModel:
- Model代表数据模型,数据和业务逻辑都在Model层中定义;
- View代表UI视图,负责数据的展示;
- ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
(3)MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
参考:前端vue面试题详细解答
为什么vue组件中data必须是一个函数?
对象为引用类型,当复用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题。
Vue中封装的数组方法有哪些,其如何实现页面更新
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。 那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:
// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function(method) {
// 缓存原生数组方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 执行并缓存原生数组功能
const result = original.apply(this, args);
// 响应式处理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift会新增索引,所以要手动observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
// notify change
ob.dep.notify();// 通知依赖更新
// 返回原生数组方法的执行结果
return result;
});
});
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods
来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。
vue-cli 工程常用的 npm 命令有哪些
- 下载
node_modules
资源包的命令:
npm install
- 启动
vue-cli
开发环境的 npm命令:
npm run dev
vue-cli
生成 生产环境部署资源 的npm
命令:
npm run build
- 用于查看
vue-cli
生产环境部署资源文件大小的npm
命令:
npm run build --report
在浏览器上自动弹出一个 展示
vue-cli
工程打包后app.js
、manifest.js
、vendor.js
文件里面所包含代码的页面。可以具此优化vue-cli
生产环境部署的静态资源,提升 页面 的加载速度
简述 mixin、extends 的覆盖逻辑
(1)mixin 和 extends mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
- mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
- extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。
(2)mergeOptions 的执行过程
- 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
- 对未合并的选项,进行判断
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
- 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
- 返回合并结果 options。
Vue项目本地开发完成后部署到服务器后报404是什么原因呢
如何部署
前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web
容器指定的静态目录下即可
我们知道vue
项目在构建后,是生成一系列的静态文件
常规布署我们只需要将这个目录上传至目标服务器即可
让web
容器跑起来,以nginx
为例
server {
listen 80;
server_name www.xxx.com;
location / {
index /data/dist/index.html;
}
}
配置完成记得重启nginx
// 检查配置是否正确
nginx -t
// 平滑重启
nginx -s reload
操作完后就可以在浏览器输入域名进行访问了
当然上面只是提到最简单也是最直接的一种布署方式
什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开
404问题
这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?
我们先还原一下场景:
vue
项目在本地时运行正常,但部署到服务器中,刷新页面,出现了404错误
先定位一下,HTTP 404 错误意味着链接指向的资源不存在
问题在于为什么不存在?且为什么只有history
模式下会出现这个问题?
为什么history模式下有问题
Vue
是属于单页应用(single-page application)
而SPA
是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html
现在,我们回头来看一下我们的nginx
配置
server {
listen 80;
server_name www.xxx.com;
location / {
index /data/dist/index.html;
}
}
可以根据 nginx
配置得出,当我们在地址栏输入 www.xxx.com
时,这时会打开我们 dist
目录下的 index.html
文件,然后我们在跳转路由进入到 www.xxx.com/login
关键在这里,当我们在 website.com/login
页执行刷新操作,nginx location
是没有相关配置的,所以就会出现 404
的情况
为什么hash模式下没有问题
router hash
模式我们都知道是用符号#表示的,如 website.com/#/login
, hash
的值为 #/login
它的特点在于:hash
虽然出现在 URL
中,但不会被包括在 HTTP
请求中,对服务端完全没有影响,因此改变 hash
不会重新加载页面
hash
模式下,仅 hash
符号之前的内容会被包含在请求中,如 website.com/#/login
只有 website.com
会被包含在请求中 ,因此对于服务端来说,即使没有配置location
,也不会返回404
错误
解决方案
看到这里我相信大部分同学都能想到怎么解决问题了,
产生问题的本质是因为我们的路由是通过JS来执行视图切换的,
当我们进入到子路由时刷新页面,web
容器没有相对应的页面此时会出现404
所以我们只需要配置将任意页面都重定向到 index.html
,把路由交由前端处理
对nginx
配置文件.conf
修改,添加try_files $uri $uri/ /index.html;
server {
listen 80;
server_name www.xxx.com;
location / {
index /data/dist/index.html;
try_files $uri $uri/ /index.html;
}
}
修改完配置文件后记得配置的更新
nginx -s reload
这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html
文件
为了避免这种情况,你应该在 Vue
应用里面覆盖所有的路由情况,然后在给出一个 404 页面
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '*', component: NotFoundComponent }
]
})
父子组件生命周期调用顺序(简单)
渲染顺序:先父后子,完成顺序:先子后父
更新顺序:父更新导致子更新,子更新完成后父
销毁顺序:先父后子,完成顺序:先子后父
SPA首屏加载速度慢的怎么解决
一、什么是首屏加载
首屏时间(First Contentful Paint
),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中最重要的环节
关于计算首屏时间
利用performance.timing
提供的数据:
通过DOMContentLoad
或者performance
来计算出首屏时间
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime
// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
name: "first-contentful-paint",
entryType: "paint",
startTime: 507.80000002123415,
duration: 0,
};
二、加载慢的原因
在页面渲染的过程,导致加载速度慢的因素可能如下:
- 网络延时问题
- 资源文件体积是否过大
- 资源是否重复发送请求去加载了
- 加载脚本的时候,渲染内容堵塞了
三、解决方案
常见的几种SPA首屏优化方式
- 减小入口文件积
- 静态资源本地缓存
- UI框架按需加载
- 图片资源的压缩
- 组件重复打包
- 开启GZip压缩
- 使用SSR
1. 减小入口文件体积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
在vue-router
配置路由的时候,采用动态加载路由的形式
routes:[
path: 'Blogs',
name: 'ShowBlogs',
component: () => import('./components/ShowBlogs.vue')
]
以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
2. 静态资源本地缓存
后端返回资源问题:
- 采用
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头 - 采用
Service Worker
离线缓存
前端合理利用localStorage
3. UI框架按需加载
在日常使用UI
框架,例如element-UI
、或者antd
,我们经常性直接引用整个UI
库
import ElementUI from 'element-ui'
Vue.use(ElementUI)
但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)
4. 组件重复打包
假设A.js
文件是一个常用的库,现在有多个路由使用了A.js
文件,这就造成了重复下载
解决方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks: 3
minChunks
为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
5. 图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的icon
,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http
请求压力。
6. 开启GZip压缩
拆完包之后,我们再用gzip
做一下压缩 安装compression-webpack-plugin
cnmp i compression-webpack-plugin -D
在vue.congig.js
中引入并修改webpack
配置
const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}
在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip
,就发送给它gzip
格式的文件 我的服务器是用express
框架搭建的 只要安装一下compression
就能使用
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用
7. 使用SSR
SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
从头搭建一个服务端渲染是很复杂的,vue
应用建议使用Nuxt.js
实现服务端渲染
四、小结
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化
和 页面渲染优化
下图是更为全面的首屏优化的方案
大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化
Vue项目性能优化-详细
Vue
框架通过数据双向绑定和虚拟DOM
技术,帮我们处理了前端开发中最脏最累的DOM
操作部分, 我们不再需要去考虑如何操作DOM
以及如何最高效地操作DOM
;但Vue
项目中仍然存在项目首屏优化、Webpack
编译配置优化等问题,所以我们仍然需要去关注Vue
项目性能方面的优化,使项目具有更高效的性能、更好的用户体验
代码层面的优化
1. v-if 和 v-show 区分使用场景
v-if
是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块v-show
就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS
display
的none/block
属性进行切换。- 所以,
v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show
则适用于需要非常频繁切换条件的场景
2. computed 和 watch 区分使用场景
computed
: 是计算属性,依赖其它属性值,并且computed
的值有缓存,只有它依赖的属性值发生改变,下一次获取computed
的值时才会重新计算 computed 的值;watch
: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用
computed
,因为可以利用computed
的缓存特性,避免每次获取值时,都要重新计算; - 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用
watch
,使用watch
选项允许我们执行异步操作 ( 访问一个API
),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的
3. v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
v-for
遍历必须为item
添加key
- 在列表数据进行遍历渲染时,需要为每一项
item
设置唯一key
值,方便Vue.js
内部机制精准找到该条列表数据。当state
更新时,新的状态值和旧的状态值对比,较快地定位到diff
- 在列表数据进行遍历渲染时,需要为每一项
v-for
遍历避免同时使用v-if
vue2.x
中v-for
比v-if
优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成computed
属性
推荐:
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
不推荐:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
</li>
</ul>
4. 长列表性能优化
Vue
会通过Object.defineProperty
对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止Vue
劫持我们的数据呢?可以通过Object.freeze
方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
5. 事件的销毁
Vue
组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js
内使用 addEventListener
等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
created() {
addEventListener('click', this.click, false)
},
beforeDestroy() {
removeEventListener('click', this.click, false)
}
6. 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue
的 vue-lazyload
插件
npm install vue-lazyload --save-dev
在入口文件 man.js
中引入并使用
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 或者添加自定义选项
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})
在 vue
文件中将 img
标签的 src
属性直接改为 v-lazy
,从而将图片显示方式更改为懒加载显示
<img v-lazy="/static/img/1.png">
以上为 vue-lazyload
插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址(opens new window)
7. 路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak
打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来
路由懒加载:
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
8. 第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component
,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui
组件库为例
npm install babel-plugin-component -D
将 .babelrc
修改为:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
在 main.js
中引入部分组件:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)
9. 优化无限列表性能
如果你的应用存在非常长或者无限滚动的列表,那么需要采用虚拟列表
的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom
节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window)来优化这种无限列表的场景的
10. 服务端渲染 SSR or 预渲染
服务端渲染是指 Vue
在客户端将标签渲染成的整个 html
片段的工作在服务端完成,服务端形成的 html
片段直接返回给客户端这个过程就叫做服务端渲染。
- 如果你的项目的
SEO
和首屏渲染
是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和SEO
- 如果你的
Vue
项目只需改善少数营销页面(例如/
,/about
,/contact
等)的SEO
,那么你可能需要预渲染,在构建时简单地生成针对特定路由的静态HTML
文件。 优点是设置预渲染更简单 ,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin (opens new window) 就可以轻松地添加预渲染
Webpack 层面的优化
1. Webpack 对图片进行压缩
对小于 limit
的图片转化为 base64
格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader
来压缩图片
npm install image-webpack-loader --save-dev
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
2. 减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码
class HelloWebpack extends Component{...}
这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
babel-runtime/helpers/createClass // 用于实现 class 语法
babel-runtime/helpers/inherits // 用于实现 extends 语法
在默认情况下, Babel
会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass')
的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime
插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小
npm install babel-plugin-transform-runtime --save-dev
修改 .babelrc
配置文件为:
"plugins": [
"transform-runtime"
]
3. 提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
- 相同的资源被重复加载,浪费用户的流量和服务器的成本。
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack
内置了专门用于提取多个Chunk
中的公共部分的插件 CommonsChunkPlugin
,我们在项目中 CommonsChunkPlugin
的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module, count) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
);
}
}),
// 抽取出代码模块的映射关系
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
4. 模板预编译
- 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
- 预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
- 如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数
5. 提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存
6. 优化 SourceMap
我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap
出现了,它就是为了解决不好调式代码问题的
SourceMap
的可选值如下(+
号越多,代表速度越快,-
号越多,代表速度越慢,o
代表中等速度)
- 开发环境推荐:
cheap-module-eval-source-map
- 生产环境推荐:
cheap-module-source-map
原因如下:
cheap
: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加cheap
的基本类型来忽略打包前后的列信息;module
:不管是开发环境还是正式环境,我们都希望能定位到bug
的源代码具体的位置,比如说某个Vue
文件报错了,我们希望能定位到具体的Vue
文件,因此我们也需要module
配置;soure-map
:source-map
会为每一个打包后的模块生成独立的soucemap
文件 ,因此我们需要增加source-map
属性;eval-source-map
:eval
打包代码的速度非常快,因为它不生成map
文件,但是可以对eval
组合使用eval-source-map
使用会将map
文件以DataURL
的形式存在打包后的js
文件中。在正式环境中不要使用eval-source-map
, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。
7. 构建结果输出分析
Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
执行 $ npm run build --report
后生成分析报告如下
基础的 Web 技术优化
1. 开启 gzip 压缩
gzip
是GNUzip
的缩写,最早用于UNIX
系统的文件压缩。HTTP
协议上的gzip
编码是一种用来改进web
应用程序性能的技术,web
服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip
压缩效率非常高,通常可以达到70%
的压缩率,也就是说,如果你的网页有30K
,压缩之后就变成了9K
左右
以下我们以服务端使用我们熟悉的 express
为例,开启 gzip
非常简单,相关步骤如下:
npm install compression --save
var compression = require('compression');
var app = express();
app.use(compression())
重启服务,观察网络面板里面的 response header
,如果看到如下红圈里的字段则表明 gzip
开启成功
Nginx开启gzip压缩
#是否启动gzip压缩,on代表启动,off代表开启
gzip on;
#需要压缩的常见静态资源
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
#由于nginx的压缩发生在浏览器端而微软的ie6很坑爹,会导致压缩后图片看不见所以该选
项是禁止ie6发生压缩
gzip_disable "MSIE [1-6]\.";
#如果文件大于1k就启动压缩
gzip_min_length 1k;
#以16k为单位,按照原始数据的大小以4倍的方式申请内存空间,一般此项不要修改
gzip_buffers 4 16k;
#压缩的等级,数字选择范围是1-9,数字越小压缩的速度越快,消耗cpu就越大
gzip_comp_level 2;
要想配置生效,记得重启nginx
服务
nginx -t
nginx -s reload
2. 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)
3. CDN 的使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率
4. 使用 Chrome Performance 查找性能瓶颈
Chrome
的 Performance
面板可以录制一段时间内的 js
执行细节及时间。使用 Chrome
开发者工具分析页面性能的步骤如下。
- 打开
Chrome
开发者工具,切换到Performance
面板 - 点击
Record
开始录制 - 刷新页面或展开某个节点
- 点击
Stop
停止录制
Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
SPA、SSR的区别是什么
我们现在编写的Vue
、React
和Angular
应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO
不友好的问题,因此有了SSR
,这也是为什么面试中会问到两者的区别
SPA
(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称CSR
。SSR
(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称MPA
SPA
应用只会首次请求html
文件,后续只需要请求JSON
数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO
不友好。为了解决以上缺点,就有了SSR
方案,由于HTML
内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR方案也会有性能,开发受限等问题- 在选择上,如果我们的应用存在首屏加载优化需求,
SEO
需求时,就可以考虑SSR
- 但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑预渲染(
prerender
)方案。另外nuxt.js/next.js
中给我们提供了SSG(Static Site Generate)
静态网站生成方案也是很好的静态站点解决方案,结合一些CI
手段,可以起到很好的优化效果,且能节约服务器资源
内容生成上的区别:
SSR
SPA
部署上的区别
Vue中常见性能优化
编码优化 :
- 使用
v-show
复用DOM
:避免重复创建组件
<template>
<div class="cell">
<!-- 这种情况用v-show复用DOM,比v-if效果好 -->
<div v-show="value" class="on">
<Heavy :n="10000"/>
</div>
<section v-show="!value" class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>
- 合理使用路由懒加载、异步组件,有效拆分
App
尺寸,访问时才异步加载
const router = createRouter({
routes: [
// 借助webpack的import()实现异步组件
{ path: '/foo', component: () => import('./Foo.vue') }
]
})
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
v-once
和v-memo
:不再变化的数据使用v-once
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
按条件跳过更新时使用v-momo
:下面这个列表只会更新选中状态变化项
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<p>...more child nodes</p>
</div>
- 长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{ item }">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>
- 防止内部泄漏,组件销毁后把全局变量和事件销毁:
Vue
组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
export default {
created() {
this.timer = setInterval(this.refresh, 2000)
},
beforeUnmount() {
clearInterval(this.timer)
}
}
- 图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png">
- 滚动到可视区域动态加载
https://tangbc.github.io/vue-virtual-scroll-list(opens new window)
- 第三方插件按需引入:(
babel-plugin-component
)
像element-plus
这样的第三方组件库可以按需引入避免体积太大
import { createApp } from 'vue';
import { Button, Select } from 'element-plus';
const app = createApp()
app.use(Button)
app.use(Select)
- 服务端渲染:SSR
如果SPA
应用有首屏渲染慢的问题,可以考虑SSR
以及下面的其他方法
- 不要将所有的数据都放在
data
中,data
中的数据都会增加getter
和setter
,会收集对应的watcher
v-for
遍历为item
添加key
v-for
遍历避免同时使用v-if
- 区分
computed
和watch
的使用 - 拆分组件(提高复用性、增加代码的可维护性,减少不必要的渲染 )
- 防抖、节流
用户体验
app-skeleton
骨架屏pwa
serviceworker
SEO优化
- 预渲染插件
prerender-spa-plugin
- 服务端渲染
ssr
打包优化
Webpack
对图片进行压缩- 使用
cdn
的方式加载第三方模块 - 多线程打包
happypack
splitChunks
抽离公共文件- 优化
SourceMap
- 构建结果输出分析,利用
webpack-bundle-analyzer
可视化分析工具
基础的 Web 技术的优化
- 服务端
gzip
压缩 - 浏览器缓存
CDN
的使用- 使用
Chrome Performance
查找性能瓶颈
Vue项目中你是如何解决跨域的呢
一、跨域是什么
跨域本质是浏览器基于同源策略的一种安全手段
同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能
所谓同源(即指在同一个域)具有以下三个相同点
- 协议相同(protocol)
- 主机相同(host)
- 端口相同(port)
反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域
一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。
如果让你从零开始写一个vuex,说说你的思路
思路分析
这个题目很有难度,首先思考vuex
解决的问题:存储用户全局状态并提供管理状态API。
vuex
需求分析- 如何实现这些需求
回答范例
- 官方说
vuex
是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
- 要实现一个
Store
存储全局状态 - 要提供修改状态所需API:
commit(type, payload), dispatch(type, payload)
- 实现
Store
时,可以定义Store
类,构造函数接收选项options
,设置属性state
对外暴露状态,提供commit
和dispatch
修改属性state
。这里需要设置state
为响应式对象,同时将Store
定义为一个Vue
插件 commit(type, payload)
方法中可以获取用户传入mutations
并执行它,这样可以按用户提供的方法修改状态。dispatch(type, payload)
类似,但需要注意它可能是异步的,需要返回一个Promise
给用户以处理异步结果
实践
Store
的实现:
class Store {
constructor(options) {
this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
this.options.mutations[type].call(this, this.state, payload)
}
}
vuex简易版
/**
* 1 实现插件,挂载$store
* 2 实现store
*/
let Vue;
class Store {
constructor(options) {
// state响应式处理
// 外部访问: this.$store.state.***
// 第一种写法
// this.state = new Vue({
// data: options.state
// })
// 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更
this._vm = new Vue({
data: {
$$state: options.state
}
})
this._mutations = options.mutations
this._actions = options.actions
this.getters = {}
options.getters && this.handleGetters(options.getters)
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state () {
return this._vm._data.$$state
}
set state (val) {
return new Error('Please use replaceState to reset state')
}
handleGetters (getters) {
Object.keys(getters).map(key => {
Object.defineProperty(this.getters, key, {
get: () => getters[key](this.state)
})
})
}
commit (type, payload) {
let entry = this._mutations[type]
if (!entry) {
return new Error(`${type} is not defined`)
}
entry(this.state, payload)
}
dispatch (type, payload) {
let entry = this._actions[type]
if (!entry) {
return new Error(`${type} is not defined`)
}
entry(this, payload)
}
}
const install = (_Vue) => {
Vue = _Vue
Vue.mixin({
beforeCreate () {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
},
})
}
export default { Store, install }
验证方式
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 0
},
mutations: {
// state从哪里来的
add (state) {
state.counter++
}
},
getters: {
doubleCounter (state) {
return state.counter * 2
}
},
actions: {
add ({ commit }) {
setTimeout(() => {
commit('add')
}, 1000)
}
},
modules: {
}
})
标签:面试题,vue,渲染,Vue,组件,页面,合集,加载
From: https://www.cnblogs.com/bbxiaxia1998/p/16831779.html