共计 23675 个字符,预计需要花费 60 分钟才能阅读完成。
Vue 中如何进行依赖收集?
- 每个属性都有本人的
dep
属性,寄存他所依赖的watcher
,当属性变动之后会告诉本人对应的watcher
去更新 - 默认会在初始化时调用
render
函数,此时会触发属性依赖收集dep.depend
- 当属性产生批改时会触发
watcher
更新dep.notify()
依赖收集简版
let obj = {name: 'poetry', age: 20};
class Dep {constructor() {this.subs = [] // subs [watcher]
}
depend() {this.subs.push(Dep.target)
}
notify() {this.subs.forEach(watcher => watcher.update())
}
}
Dep.target = null;
observer(obj); // 响应式属性劫持
// 依赖收集 所有属性都会减少一个 dep 属性,// 当渲染的时候取值了,这个 dep 属性 就会将渲染的 watcher 收集起来
// 数据更新 会让 watcher 从新执行
// 观察者模式
// 渲染组件时 会创立 watcher
class Watcher {constructor(render) {this.get();
}
get() {
Dep.target = this;
render(); // 执行 render
Dep.target = null;
}
update() {this.get();
}
}
const render = () => {console.log(obj.name); // obj.name => get 办法
}
// 组件是 watcher、计算属性是 watcher
new Watcher(render);
function observer(value) { // proxy reflect
if (typeof value === 'object' && typeof value !== null)
for (let key in value) {defineReactive(value, key, value[key]);
}
}
function defineReactive(obj, key, value) {
// 创立一个 dep
let dep = new Dep();
// 递归察看子属性
observer(value);
Object.defineProperty(obj, key, {get() { // 收集对应的 key 在哪个办法(组件)中被应用
if (Dep.target) { // watcher
dep.depend(); // 这里会建设 dep 和 watcher 的关系}
return value;
},
set(newValue) {if (newValue !== value) {observer(newValue);
value = newValue; // 让 key 对应的办法(组件从新渲染)从新执行
dep.notify()}
}
})
}
// 模仿数据获取,触发 getter
obj.name = 'poetries'
// 一个属性一个 dep,一个属性能够对应多个 watcher(一个属性能够在任何组件中应用、在多个组件中应用)// 一个 dep 对应多个 watcher
// 一个 watcher 对应多个 dep(一个视图对应多个属性)// dep 和 watcher 是多对多的关系
前端 vue 面试题具体解答
异步组件是什么?应用场景有哪些?
剖析
因为异步路由的存在,咱们应用异步组件的次数比拟少,因而还是有必要两者的不同。
体验
大型利用中,咱们须要宰割利用为更小的块,并且在须要组件时再加载它们
import {defineAsyncComponent} from 'vue'
// defineAsyncComponent 定义异步组件,返回一个包装组件。包装组件依据加载器的状态决定渲染什么内容
const AsyncComp = defineAsyncComponent(() => {
// 加载函数返回 Promise
return new Promise((resolve, reject) => {
// ... 能够从服务器加载组件
resolve(/* loaded component */)
})
})
// 借助打包工具实现 ES 模块动静导入
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
答复范例
- 在大型利用中,咱们须要宰割利用为更小的块,并且在须要组件时再加载它们。
- 咱们不仅能够在路由切换时懒加载组件,还能够在页面组件中持续应用异步组件,从而实现更细的宰割粒度。
- 应用异步组件最简略的形式是间接给
defineAsyncComponent
指定一个loader
函数,联合 ES 模块动静导入函数import
能够疾速实现。咱们甚至能够指定loadingComponent
和errorComponent
选项从而给用户一个很好的加载反馈。另外Vue3
中还能够联合Suspense
组件应用异步组件。 - 异步组件容易和路由懒加载混同,实际上不是一个货色。异步组件不能被用于定义懒加载路由上,解决它的是
vue
框架,解决路由组件加载的是vue-router
。然而能够在懒加载的路由组件中应用异步组件
v-if 和 v -show 的区别
- 伎俩:v-if 是动静的向 DOM 树内增加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 款式属性管制显隐;
- 编译过程:v-if 切换有一个部分编译 / 卸载的过程,切换过程中适合地销毁和重建外部的事件监听和子组件;v-show 只是简略的基于 css 切换;
- 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始部分编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,而后被缓存,而且 DOM 元素保留;
- 性能耗费:v-if 有更高的切换耗费;v-show 有更高的初始渲染耗费;
- 应用场景:v-if 适宜经营条件不大可能扭转;v-show 适宜频繁切换。
什么是递归组件?举个例子阐明下?
剖析
递归组件咱们用的比拟少,然而在 Tree
、Menu
这类组件中会被用到。
体验
组件通过组件名称援用它本人,这种状况就是递归组件
<template>
<li>
<div> {{model.name}}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 留神这里:组件递归渲染了它本人 -->
<TreeItem
class="item"
v-for="model in model.children"
:model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: 'TreeItem',
// ...
}
</script>
答复范例
- 如果某个组件通过组件名称援用它本人,这种状况就是递归组件。
- 理论开发中相似
Tree
、Menu
这类组件,它们的节点往往蕴含子节点,子节点构造和父节点往往是雷同的。这类组件的数据往往也是树形构造,这种都是应用递归组件的典型场景。 - 应用递归组件时,因为咱们并未也不能在组件外部导入它本人,所以设置组件
name
属性,用来查找组件定义,如果应用SFC
,则能够通过SFC
文件名推断。组件外部通常也要有递归完结条件,比方model.children
这样的判断。 - 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent
,这样理论获取的组件就是以后组件自身
原理
递归组件编译后果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在传递maybeSelfReference
export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}
resolveAsset
中最终返回的是组件本身:
if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component
}
父组件能够监听到子组件的生命周期吗
比方有父组件 Parent
和子组件 Child
,如果父组件监听到子组件挂载 mounted
就做一些逻辑解决,能够通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {this.$emit("mounted");
}
以上须要手动通过 $emit
触发父组件的事件,更简略的形式能够在父组件援用子组件时通过 @hook
来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输入程序为:// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
当然 @hook
办法不仅仅是能够监听 mounted
,其它的生命周期事件,例如:created
,updated
等都能够监听
vue-router 中如何爱护路由
剖析
路由爱护在利用开发过程中十分重要,简直每个利用都要做各种路由权限治理,因而相当考查使用者基本功。
体验
全局守卫:
const router = createRouter({...})
router.beforeEach((to, from) => {
// ...
// 返回 false 以勾销导航
return false
})
路由独享守卫:
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
组件内的守卫:
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用},
beforeRouteUpdate(to, from) {// 在以后路由扭转,然而该组件被复用时调用},
beforeRouteLeave(to, from) {// 在导航来到渲染该组件的对应路由时调用},
}
答复
vue-router
中爱护路由的办法叫做路由守卫,次要用来通过跳转或勾销的形式守卫导航。- 路由守卫有三个级别:
全局
、路由独享
、组件级
。影响范畴由大到小,例如全局的router.beforeEach()
,能够注册一个全局前置守卫,每次路由导航都会通过这个守卫,因而在其外部能够退出管制逻辑决定用户是否能够导航到指标路由;在路由注册的时候能够退出单路由独享的守卫,例如beforeEnter
,守卫只在进入路由时触发,因而只会影响这个路由,管制更准确;咱们还能够为路由组件增加守卫配置,例如beforeRouteEnter
,会在渲染该组件的对应路由被验证前调用,管制的范畴更准确了。 - 用户的任何导航行为都会走
navigate
办法,外部有个guards
队列按程序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会勾销原有的导航。
原理
runGuardQueue(guards)
链式的执行用户在各级别注册的守卫钩子函数,通过则持续下一个级别的守卫,不通过进入 catch
流程勾销本来导航
// 源码
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = []
for (const guard of beforeGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check the route beforeEnter
guards = []
for (const record of to.matched) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter && !from.matched.includes(record)) {if (isArray(record.beforeEnter)) {for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
} else {guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check global guards beforeResolve
guards = []
for (const guard of beforeResolveGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// catch any navigation canceled
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
源码地位(opens new window)
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 还蕴含了其余的响应逻辑。
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 我的项目性能优化 - 具体
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 中给 data 中的对象属性增加一个新的属性时会产生什么?如何解决?
<template>
<div>
<ul>
<li v-for="value in obj" :key="value"> {{value}} </li> </ul> <button @click="addObjB"> 增加 obj.b</button> </div>
</template>
<script>
export default {data () {return { obj: { a: 'obj.a'} } }, methods: {addObjB () {this.obj.b = 'obj.b' console.log(this.obj) } } }
</script>
点击 button 会发现,obj.b 曾经胜利增加,然而视图并未刷新。这是因为在 Vue 实例创立时,obj.b 并未申明,因而就没有被 Vue 转换为响应式的属性,天然就不会触发视图的更新,这时就须要应用 Vue 的全局 api $set():
addObjB () (this.$set(this.obj, 'b', 'obj.b')
console.log(this.obj)
}
$set()办法相当于手动的去把 obj.b 解决成一个响应式的属性,此时视图也会跟着扭转了。
如何在组件中重复使用 Vuex 的 mutation
应用 mapMutations 辅助函数, 在组件中这么应用
import {mapMutations} from 'vuex'
methods:{
...mapMutations({setNumber:'SET_NUMBER',})
}
而后调用 this.setNumber(10)
相当调用this.$store.commit('SET_NUMBER',10)
过滤器的作用,如何实现一个过滤器
依据过滤器的名称,过滤器是用来过滤数据的,在 Vue 中应用 filters
来过滤数据,filters
不会批改数据,而是过滤数据,扭转用户看到的输入(计算属性 computed
,办法 methods
都是通过批改数据来解决数据格式的输入显示)。
应用场景:
- 须要格式化数据的状况,比方须要解决工夫、价格等数据格式的输入 / 显示。
- 比方后端返回一个 年月日的日期字符串 ,前端须要展现为 多少天前 的数据格式,此时就能够用
fliters
过滤器来解决数据。
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在 插值表达式 {{}}
和 v-bind
表达式 中,而后放在操作符“|
”前面进行批示。
例如,在显示金额,给商品价格增加单位:
<li> 商品价格:{{item.price | filterPrice}}</li>
filters: {filterPrice (price) {return price ? ('¥' + price) : '--'
}
}
Vuex 中 actions 和 mutations 有什么区别
题目剖析
mutations
和actions
是vuex
带来的两个独特的概念。老手程序员容易混同,所以面试官喜爱问。- 咱们只需记住批改状态只能是
mutations
,actions
只能通过提交mutation
批改状态即可
答复范例
- 更改
Vuex
的store
中的状态的惟一办法是提交mutation
,mutation
十分相似于事件:每个mutation
都有一个字符串的类型 (type
)和一个 回调函数 (handler
)。Action
相似于mutation
,不同在于:Action
能够蕴含任意异步操作,但它不能批改状态,须要提交mutation
能力变更状态 - 开发时,蕴含异步操作或者简单业务组合时应用
action
;须要间接批改状态则提交mutation
。但因为dispatch
和commit
是两个API
,容易引起混同,实际中也会采纳对立应用dispatch action
的形式。调用dispatch
和commit
两个API
时简直齐全一样,然而定义两者时却不甚雷同,mutation
的回调函数接管参数是state
对象。action
则是与Store
实例具备雷同办法和属性的上下文context
对象,因而个别会解构它为{commit, dispatch, state}
,从而不便编码。另外dispatch
会返回Promise
实例便于解决外部异步后果 - 实现上
commit(type)
办法相当于调用options.mutations[type](state)
;dispatch(type)
办法相当于调用options.actions[type](store)
,这样就很容易了解两者应用上的不同了
实现
咱们能够像上面这样简略实现 commit
和dispatch
,从而分别两者不同
class Store {constructor(options) {this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
// 传入上下文和参数 1 都是 state 对象
this.options.mutations[type].call(this.state, this.state, payload)
}
dispatch(type, payload) {
// 传入上下文和参数 1 都是 store 自身
this.options.actions[type].call(this, this, payload)
}
}
虚构 DOM 的优缺点?
长处:
- 保障性能上限: 框架的虚构 DOM 须要适配任何下层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;然而比起粗犷的 DOM 操作性能要好很多,因而框架的虚构 DOM 至多能够保障在你不须要手动优化的状况下,仍然能够提供还不错的性能,即保障性能的上限;
- 无需手动操作 DOM: 咱们不再须要手动去操作 DOM,只须要写好 View-Model 的代码逻辑,框架会依据虚构 DOM 和 数据双向绑定,帮咱们以可预期的形式更新视图,极大进步咱们的开发效率;
- 跨平台: 虚构 DOM 实质上是 JavaScript 对象, 而 DOM 与平台强相干,相比之下虚构 DOM 能够进行更不便地跨平台操作,例如服务器渲染、weex 开发等等。
毛病:
- 无奈进行极致优化: 尽管虚构 DOM + 正当的优化,足以应答绝大部分利用的性能需求,但在一些性能要求极高的利用中虚构 DOM 无奈进行针对性的极致优化。
为什么 Vue 采纳异步渲染呢?
Vue
是组件级更新,如果不采纳异步更新,那么每次更新数据都会对以后组件进行从新渲染,所以为了性能, Vue
会在本轮数据更新后,在异步更新视图。核心思想 nextTick
。
dep.notify()
告诉 watcher 进行更新, subs[i].update
顺次调用 watcher 的 update
, queueWatcher
将 watcher 去重放入队列,nextTick( flushSchedulerQueue
)在下一 tick 中刷新 watcher 队列(异步)。
请阐明 Vue 中 key 的作用和原理,谈谈你对它的了解
key
是为Vue
中的VNode
标记的惟一id
,在patch
过程中通过key
能够判断两个虚构节点是否是雷同节点,通过这个key
,咱们的diff
操作能够更精确、更疾速diff
算法的过程中, 先会进行新旧节点的首尾穿插比照, 当无奈匹配的时候会用新节点的key
与旧节点进行比对, 而后检出差别- 尽量不要采纳索引作为
key
- 如果不加
key
, 那么vue
会抉择复用节点(Vue 的就地更新策略), 导致之前节点的状态被保留下来, 会产生一系列的bug
- 更精确:因为带
key
就不是就地复用了,在sameNode
函数a.key === b.key
比照中能够防止就地复用的状况。所以会更加精确。 - 更疾速 :
key
的唯一性能够被Map
数据结构充分利用,相比于遍历查找的工夫复杂度O(n)
,Map
的工夫复杂度仅仅为O(1)
,比遍历形式更快。
源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
答复范例
剖析
这是一道特地常见的问题,次要考查大家对虚构 DOM
和patch
细节的把握水平,可能反映面试者了解档次
思路剖析:
- 给出论断,
key
的作用是用于优化patch
性能 key
的必要性- 理论应用形式
- 总结:可从源码层面形容一下
vue
如何判断两个节点是否雷同
答复范例:
key
的作用次要是为了更高效的更新虚构DOM
vue
在patch
过程中 判断两个节点是否是雷同节点是key
是一个必要条件 ,渲染一组列表时,key
往往是惟一标识,所以如果不定义key
的话,vue
只能认为比拟的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch
过程比拟低效,影响性能- 理论应用中在渲染一组列表时
key
必须设置,而且必须是惟一标识,应该防止应用数组索引作为key
,这可能导致一些荫蔽的bug
;vue
中在应用雷同标签元素过渡切换时,也会应用key
属性,其目标也是为了让vue
能够辨别它们,否则vue
只会替换其外部属性而不会触发过渡成果 - 从源码中能够晓得,
vue
判断两个节点是否雷同时次要判断两者的key
和标签类型 (如 div)
等,因而如果不设置key
,它的值就是undefined
,则可能永远认为这是两个雷同节点,只能去做更新操作,这造成了大量的dom
更新操作,显著是不可取的
如果不应用
key
,Vue
会应用一种最大限度缩小动静元素并且尽可能的尝试就地批改 / 复用雷同类型元素的算法。key
是为Vue
中vnode
的惟一标记,通过这个key
,咱们的diff
操作能够更精确、更疾速
diff 程能够概括为:
oldCh
和newCh
各有两个头尾的变量 StartIdx
和EndIdx
,它们的2
个变量互相比拟,一共有4
种比拟形式。如果4
种比拟都没匹配,如果设置了key
,就会用key
进行比拟,在比拟的过程中,变量会往两头靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至多有一个曾经遍历完了,就会完结比拟, 这四种比拟形式就是首
、尾
、旧尾新头
、旧头新尾
相干代码如下
// 判断两个 vnode 的标签和 key 是否雷同 如果雷同 就能够认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;}
// 依据 key 来创立老的儿子的 index 映射表 相似 {'a':0,'b':1} 代表 key 为 'a' 的节点在第一个地位 key 为 'b' 的节点在第二个地位
function makeIndexByKey(children) {let map = {};
children.forEach((item, index) => {map[item.key] = index;
});
return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);
什么是 mixin?
- Mixin 使咱们可能为 Vue 组件编写可插拔和可重用的性能。
- 如果心愿在多个组件之间重用一组组件选项,例如生命周期 hook、办法等,则能够将其编写为 mixin,并在组件中简略的援用它。
- 而后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
二、如何解决
解决跨域的办法有很多,上面列举了三种:
- JSONP
- CORS
- Proxy
而在 vue
我的项目中,咱们次要针对 CORS
或Proxy
这两种计划进行开展
CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享)是一个零碎,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域申请的响应
CORS
实现起来十分不便,只须要减少一些 HTTP
头,让服务器能申明容许的拜访起源
只有后端实现了 CORS
,就实现了跨域
!
以 koa
框架举例
增加中间件,间接设置 Access-Control-Allow-Origin
响应头
app.use(async (ctx, next)=> {ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
if (ctx.method == 'OPTIONS') {ctx.body = 200;} else {await next();
}
})
ps: Access-Control-Allow-Origin
设置为 * 其实意义不大,能够说是形同虚设,理论利用中,上线前咱们会将Access-Control-Allow-Origin
值设为咱们指标host
Proxy
代理(Proxy)也称网络代理,是一种非凡的网络服务,容许一个(个别为客户端)通过这个服务与另一个网络终端(个别为服务器)进行非间接的连贯。一些网关、路由器等网络设备具备网络代理性能。个别认为代理服务有利于保障网络终端的隐衷或平安,避免攻打
计划一
如果是通过 vue-cli
脚手架工具搭建我的项目,咱们能够通过 webpack
为咱们起一个本地服务器作为申请的代理对象
通过该服务器转发申请至指标服务器,失去后果再转发给前端,然而最终公布上线时如果 web 利用和接口服务器不在一起仍会跨域
在 vue.config.js
文件,新增以下代码
amodule.exports = {
devServer: {
host: '127.0.0.1',
port: 8084,
open: true,// vue 我的项目启动时主动关上浏览器
proxy: {
'/api': { // '/api' 是代理标识,用于通知 node,url 后面是 /api 的就是应用代理的
target: "http://xxx.xxx.xx.xx:8080", // 指标地址,个别是指后盾服务器地址
changeOrigin: true, // 是否跨域
pathRewrite: {// pathRewrite 的作用是把理论 Request Url 中的 '/api' 用 ""代替'^/api':""}
}
}
}
}
通过 axios
发送申请中,配置申请的根门路
axios.defaults.baseURL = '/api'
计划二
此外,还可通过服务端实现代理申请转发
以 express
框架为例
var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false}));
module.exports = app
计划三
通过配置 nginx
实现代理
server {
listen 80;
location / {
root /var/www/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Vue 中的 key 到底有什么用?
key
是为 Vue 中的 vnode 标记的惟一 id, 通过这个 key, 咱们的 diff 操作能够更精确、更疾速
diff 算法的过程中, 先会进行新旧节点的首尾穿插比照, 当无奈匹配的时候会用新节点的 key
与旧节点进行比对, 而后超出差别.
diff 程能够概括为:oldCh 和 newCh 各有两个头尾的变量 StartIdx 和 EndIdx,它们的 2 个变量互相比拟,一共有 4 种比拟形式。如果 4 种比拟都没匹配,如果设置了 key,就会用 key 进行比拟,在比拟的过程中,变量会往两头靠,一旦 StartIdx>EndIdx 表明 oldCh 和 newCh 至多有一个曾经遍历完了,就会完结比拟, 这四种比拟形式就是首、尾、旧尾新头、旧头新尾.
- 精确: 如果不加
key
, 那么 vue 会抉择复用节点(Vue 的就地更新策略), 导致之前节点的状态被保留下来, 会产生一系列的 bug. - 疾速: key 的唯一性能够被 Map 数据结构充分利用, 相比于遍历查找的工夫复杂度 O(n),Map 的工夫复杂度仅仅为 O(1).
Vuex 和 localStorage 的区别
(1)最重要的区别
- vuex 存储在内存中
- localstorage 则以文件的形式存储在本地,只能存储字符串类型的数据,存储对象须要 JSON 的 stringify 和 parse 办法进行解决。读取内存比读取硬盘速度要快
(2)利用场景
- Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。它采纳集中式存储管理利用的所有组件的状态,并以相应的规定保障状态以一种可预测的形式发生变化。vuex 用于组件之间的传值。
- localstorage 是本地存储,是将数据存储到浏览器的办法,个别是在跨页面传递数据时应用。
- Vuex 能做到数据的响应式,localstorage 不能
(3)永久性
刷新页面时 vuex 存储的值会失落,localstorage 不会。
留神: 对于不变的数据的确能够用 localstorage 能够代替 vuex,然而当两个组件共用一个数据源(对象或数组)时,如果其中一个组件扭转了该数据源,心愿另一个组件响应该变动时,localstorage 无奈做到,起因就是区别 1。