Vue
keep-alive 是什么
缓存当前被包裹的组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们 场景:浏览一个商品列表,点击一个商品进入详情,返回后当前列表位置,不会刷新 对应使用的有两个生命周期
- activated: 被 keep-alive缓存的组件激活时调用
- deactivated: 被 keep-alive 缓存的组件停用时调用
缓存方式设置
1、prop:
- include: 字符串或正则表达式。只有匹配的组件会被缓存。
- exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存。
exclude优先级大于include
2、结合router,缓存部分页面
使用$route.meta的keepAlive属性:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
export default new Router({
routes: [
{
path: '/',
name: 'Hello',
component: Hello,
meta: {
keepAlive: false // 不需要缓存
}
},
{
path: '/page1',
name: 'Page1',
component: Page1,
meta: {
keepAlive: true // 需要被缓存
}
}
]
})
当然,也可以通过动态设置route.meta的keepAlive属性来实现其他需求,借鉴一下 vue-router 之 keep-alive这篇博客中的例子:
- 首页是A页面
- B页面跳转到A,A页面需要缓存
- C页面跳转到A,A页面不需要被缓存
思路是在每个路由的beforeRouteLeave(to, from, next)
钩子中设置to.meta.keepAlive
:
A的路由:
{
path: '/',
name: 'A',
component: A,
meta: {
keepAlive: true // 需要被缓存
}
}
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = true; // B 跳转到 A 时,让 A 缓存,即不刷新
next();
}
};
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = false; // C 跳转到 A 时让 A 不缓存,即刷新
next();
}
};
什么时候获取数据?
当引入keep-alive
的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。
我们知道 keep-alive
之后页面模板第一次初始化解析变成HTML片段后,再次进入就不在重新解析而是读取内存中的数据,即,只有当数据变化时,才使用VirtualDOM进行diff更新。故,页面进入的数据获取应该在activated中也放一份。数据下载完毕手动操作DOM的部分也应该在activated中执行才会生效。
所以,应该activated中留一份数据获取的代码,或者不要created部分,直接将created中的代码转移到activated中,避免再次进入页面时,created内部代码不执行的情况
路由懒加载
实现
const routes =[
{
path: '/about',
name: 'About',
component: () => {
import(/* webpackChunkName: "about" */ '../views/About.vue')
}
}
]
// 进入到对应的页面才开始加载页面 js
发布一个 npm 包
-
创建一个文件夹
-
npm init 版本自己定义
-
编辑 npm 内容
-
npm publish 发布前需要先登录 npm login 输入用户名和密码,包命名不能和已经发布过的重复
-
在 npm 中可以被搜索的到, 可以通过 npm install
vue 性能优化方法
- 路由懒加载 (按需引入页面,在打包的时候整体体积大幅缩减)
- keep-alive缓存页面 (细节处理 include 。。)
- 使用 v-show 复用DOM,较大组件,使用 v-show,比 v-if效果好(一个是完全的新增删除,一个是利用 display 控制显隐性)
- v-for遍历避免同时使用 v-if
- 长列表性能优化 a、如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化 b、如果是大数据长列表,可采用虚拟滚动,只渲染少数部分区域的内容
- 事件销毁 (vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件)
- 图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
- 第三方插件按需引入
像 element-ui这样的第三方组件库可以按需引入,避免体积太大
-
无状态的组件标记为函数式组件 (傻瓜式组件,避免创建组件实例)
-
子组件分割 (频繁动态更新的组件,或者比较耗时的组件可以抽离,子组件自己管理自己,不会影响到页面中其他部分)
-
变量本地化 (不要频繁引用 this.data)
-
SSR 引入后有利于 SEO,以及首屏加载速度,还有部分服务端的优化
VUE模板引擎的渲染
juejin.cn/post/684490… juejin.cn/post/684490…
vue 插槽
在2.6.0中,具名插槽 和 作用域插槽 引入了一个新的统一的语法 (即v-slot
指令)。它取代了 slot
和 slot-scope
内容插槽
//home.vue
<test>
Hello Word
</test>
//test.vue
<a href="#">
<slot></slot>
</a>
规则:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的
后背内容(默认内容)插槽
有时候我们需要给插槽设置一个具体的默认内容,当别的组件没有给你内容的时候,那么默认的内容就会被渲染
//test.vue
//在slot插槽里设置默认内容 Submit
<button>
<slot>Submit</slot>
</button>
具名插槽
有时候我们一个组件里需要多个插槽
那么怎么办呢? 对于这样的情况,<slot>
元素有一个特殊的特性:name
,这个特性可以用来定义额外的插槽
//home.vue
<test>
<p slot="header">Hello Word</p>
</test>
//test.vue
<a href="#">
<slot name="header"></slot>
</a>
作用域插槽 (原slot-scope ,现v-slot)
上面已经说了,插槽跟模板其他地方一样都可以访问相同的实例属性(也就是相同的"作用域"),而不能访问<test>
的作用域
那如果想访问<test>
作用域该怎么办呢?
我们把需要传递的内容绑到 <slot>
上,然后在父组件中用v-slot
设置一个值来定义我们提供插槽的名字:
//test.vue
<div>
<!-- 设置默认值:{{user.lastName}}获取 Jun -->
<!-- 如果home.vue中给这个插槽值的话,则不显示 Jun -->
<!-- 设置一个 usertext 然后把user绑到设置的 usertext 上 -->
<slot v-bind:usertext="user">{{user.lastName}}</slot>
</div>
//定义内容
data(){
return{
user:{
firstName:"Fan",
lastName:"Jun"
}
}
}
然后在home.vue
中接收传过来的值:
//home.vue
<div>
<test v-slot:default="slotProps">
{{slotProps.usertext.firstName}}
</test>
</div>
绑定在 <slot>
元素上的特性被称为插槽 prop
。在父组件中,我们可以用 v-slot
设置一个值来定义我们提供的插槽 prop 的名字,然后直接使用就好了
独占默认插槽的缩写语法
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot
直接用在组件上
<div>
<!-- 可以把 :default 去掉,仅限于默认插槽 -->
<test v-slot="slotProps">
{{slotProps.usertext.firstName}}
</test>
</div>
这样写法还可以更简单,因为不带参数的v-slot
就被假定为默认插槽
动态插槽名(2.6.0新增)
动态指令参数(需要自己了解)也可以用在v-slot
上,来定义动态的插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
具名插槽的缩写(2.6.0新增)
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:)
替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
原先是这样写:
<div>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here some contact info</p>
</template>
</div>
现在可以这样写:
<div>
<template #header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here some contact info</p>
</template>
</div>
该指令和其他指令一样,只在其有参数的时候使用
<test #="{ usertext }">
{{ usertext.firstName }}
</test>
简述MVVM
MVVM是Model-View-ViewModel
缩写,也就是把MVC
中的Controller
演变成ViewModel。Model
层代表数据模型,View
代表UI组件,ViewModel
是View
和Model
层的桥梁,数据会绑定到viewModel
层并自动将数据渲染到页面中,视图变化的时候会通知viewModel
层更新数据。
谈谈对vue生命周期的理解?
每个Vue
实例在创建时都会经过一系列的初始化过程,vue
的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件
create阶段
:vue实例被创建
beforeCreate
: 创建前,此时data和methods中的数据都还没有初始化 created
: 创建完毕,data中有值,未挂载
mount阶段
: vue实例被挂载到真实DOM节点beforeMount
:可以发起服务端请求,去数据mounted
: 此时可以操作Domupdate阶段
:当vue实例里面的data数据变化时,触发组件的重新渲染beforeUpdate
updated
destroy阶段
:vue实例被销毁beforeDestroy
:实例被销毁前,此时可以手动销毁一些方法destroyed
v-if和 v-for哪个优先级更高
结论:
1、显然 v-for优先于 v-if 被解析,自己写代码测试,并在源码中可以得到结论
2、如果同时出现,每次渲染都会先执行循环,再判断条件,无论如何循环都不可避免,浪费了性能,
3、要避免这种情况,则在外层嵌套 template,在这一层进行 v-if判断,然后在内部进行 v-for循环
4、如果一定要在同级使用 v-if 的话,可以利用计算属性将数据先过滤一遍
vue组件 data 为什么必须是个函数,而 vue的根实例则没有此限制
数据初始化时,会先判断 data类型,如果是函数,则执行,并将其结果作为 data选项的值
自定义组件走上面的逻辑,mergeFidld(key), 根实例走下面的逻辑 mergeOptions(key)
结论
Vue可能存在多个组件实例,如果使用对象形式定义 data, 则会导致它们共用一个 data 对象,那么状态变更将会影响所有组件实例,这是不合理的,采用函数形式定义,在 initData时,会将其作为工厂函数,每次都执行一下这个函数,返回一个全新的data对象,有效规避多实例之间状态污染问题,而在Vue根实例创建过程中则不存在该限制,整个应用程序里根实例只有一个,不需要担心这种情况,(我曾经看过数据初始化那块的源码,我发现它会检测 data的数据格式,从而去执行它的具体执行方式,根实例在合并选项的时候会有实例 vm 拿到,可以有效躲过 data选项校验 组件则没有)
watch和计算属性的比较
特点和区别
computed主要用于同步数据的处理,而 watch选项主要用于事件的派发,可异步,这两者都能达到同样的效果,但是基于它们各自的特点,使用场景会有区别
- computed 拥有缓存属性,只有当依赖数据发生变化时,关联的数据才会发生变化,适用于计算或者格式化数据场景,(同步处理数据)
- watch监听数据,有关联,但是没有依赖,只要某个数据发生变化,就可以处理一些数据或者派发事件,并同步/异步执行
应用场景
- computed计算属性: 购物车价格计算,贷款利息计算
- watch侦听器:
1、主要适用于事件和交互有关的场景,数据变化为条件,适用于一个数据同时出发多个事件,不同条件下,有不同的处理方式,满足一定条件才出发,而不是实时触发
抽象概念
弹窗提示等事件交互的,适用于 watch ,数据计算和字符串处理的适用于 computed
watch options
export default {
data: () => ({
dog: ""
}),
watch: {
dog: {
handler(newVal, oldVal) {
console.log(`Dog changed: ${newVal}`);
},
immediate: true,
deep: true
}
}
};
在某些情况下, 你可能需要在创建组件后立即运行监听程序,你可以在使用watch
时, 使用immediate: true
选项, 这样它就会在组件创建时立即执行
watch
中还有一个属性: deep
, 默认值为: false
, 即是否需要开启深度监听
deep
即深入观察, 监听器会层层遍历, 给对象的所有属性(及子属性)添加监听器. 这样做无疑会有很大的性能开销, 修改obj
中任何一个属性都会触发监听器中的处理函数.
动态添加 watch
export default {
data: () => ({
obj: {
hello: 'james'
}
}),
mounted(){
this.$watch('obj.hello', this.handler, {
immediate: true,
deep: false
})
},
methods: {
handler(newVal, oldVal) {
console.log(`obj changed: ${newVal}`);
}
}
}
注销 watch
若使用动态添加watch
, 就需要手动注销了. 从源代码中, 可以看出: this.$watch
调用后会有一个返回值, 通过调用此返回值, 即可注销watch.
let unWatch = null
export default {
data: () => ({
obj: {
hello: 'james'
}
}),
mounted(){
unWatch = this.$watch('obj.hello', this.handler, {
immediate: true,
deep: false
})
},
methods: {
handler(newVal, oldVal) {
console.log(`obj changed: ${newVal}`);
}
},
beforeMount(){
unWatch()
unWatch = null
}
};
v-for中key的作用
key
的作用主要是为了更高效的对比虚拟DOM中每个节点是否是相同节点;Vue在patch
过程中判断两个节点是否是相同节点,key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,Vue
只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch
过程比较低效,影响性能;- 从源码中可以知道,Vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是
undefined
,则可能永 远认为这是两个相同的节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。 - 另外,如果不设置 key 还可能在列表更新时引发一些隐藏 bug 比如刷新失败
你怎么理解 vue 中的 diff 算法
diff算法的一个大致过程
总结:
- diff 算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM做对比(即 diff),将变化的地方更新在真实的DOM上;另外,也需要 diff高效的执行对比过程,从而降低时间复杂度,
- vue 2.x中,为了降低 watcher 粒度,每个组件只有一个 watcher 与之对应,只有引入 diff,才能精确找到发生变化的地方
- vue 中 diff 执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果 oldVnode 和新的渲染结果 newVnode,此过程称为 patch
- diff过程整体遵循深度优先,同层比较的策略;两个节点之间比较,会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次比对尝试,如果没有找到相同节点,才按照通用方式遍历查找,查找结束再按情况处理剩下的节点,借助 key,通常可以非常精确找到相同节点,因此整个 patch 过程非常高效
谈谈对 vue组件化的理解
总结:
- 组件是独立和可复用的代码组件单元,组件系统是vue核心特性之一,它使开发者使用小型,独立和通常可复用的组件构建大型应用;
- 组件化开发能大幅提高应用开发效率,测试性,复用性等
- 组件使用按照分类有:页面组件、业务组件(登录,购物车)、通用组件(button, 表单,列表)
- vue 组件是基于配置的,我们通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,他们基于VueComponent, 扩展Vue
- vue中常见组件技术有: 属性props,自定义事件(完成子传父),插槽等,它们主要用于组件通信,扩展等
- 合理的划分组件,有利于提升应用性能
- 组件应该是高内聚,低耦合的
- 遵循单项数据流原则
谈一谈对 vue 设计原则的理解
- 渐进式 JavaScript 框架
- 易用、灵活和高效
渐进式 JavaScript 框架
与其他大型框架不同的是,Vue被设计为可以自底向上逐层应用,vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合,另一方面,当与现代化的工具链以及各种支持类库结合使用时,vue 也完全能为复杂单页应用提供驱动
易用性
vue 提供数据响应式、声明式模板语法和基于配置的组件系统等核心特性,这些使得我们只需要关注应用的核心业务即可,只要会写 html js css就能轻松编写 vue应用
灵活性
渐进式框架最大的优点就是灵活性,如果应用足够小,我们可能仅需要 vue的核心特性即可完成功能,随着应用功能不断扩大,我们才可能逐渐引入路由,状态管理,vue-cli等库和工具,不管是应用体积,还是学习难度都是一个逐渐增加的平和曲线
高效性
超快的虚拟DOM和 diff 算法使我们的应用拥有最佳的性能表现。
追求高效的过程还在继续,vue3中引入 proxy 对数据响应式改进以及编译器中对于静态内容编译的改进都会让 vue 更加高效
vue组件的通信方式: 父子组件通信,兄弟组件通信,跨层组件通信
1、props
2、on 发布订阅模式,先订阅 vm.emit 发布
3、vuex
4、children
5、listeners
6、provide/inject (官方不推荐使用,但是写组件库时很常用)
兄弟组件通信
Event Bus
实现跨组件通信 Vue.prototype.$bus = new Vue()
跨级组件通信
$attrs、$listeners
Provide、inject
如何选用: 简单数据传递,可以选用 props ,如果项目中需要保存状态的时候可以选用 vuex
双向绑定实现原理
理解
- 核心点:Object.defineProperty()
- 默认 vue 在初始化数据时,会给 data 中的属性使用 Object.defineProperty重新定义所有属性,Object.defineProperty可以在数据获取或者修改的时候增加一个拦截的作用,在获取属性时,调用Dep.patch() 收集依赖(也就是 watcher), 在修改数据时,调用Dep.notify() 通知视图层更新 并且Vue底层重写的数组方法,递归遍历数组中的每一项对象增加监听属性
数据劫持/数据代理
当一个Vue实例创建时,Vue会遍历data选项的属性,用 Object.defineProperty 将它们转为 getter/setter并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher重新计算,从而致使它关联的组件得以更新。
defineReactive 函数
function defineReactive(data, key, val) {
console.log(arguments)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('你试图访问 obj 的' + key + '属性')
return val
},
set(newValue) {
console.log('你试图修改 obj 的' + key + '属性')
if (val === newValue) {
return
}
val = newValue
}
})
}
defineReactive(obj, 'a', 29)
console.log(obj.a) // 29
obj.a = 9
obj.a++
console.log(obj.a) // 10
递归侦测对象全部属性
vue.nextTick 的原理
由于js引擎线程跟Gpu渲染线程是交替运行的,js引擎执行一个宏任务过程中,把遇到的微任务挂起,在执行完本次宏任务后,立即执行本次微任务队列, 然后执行渲染,再然后开启下一次的宏任务 nexttick 把代码加入到了本次的微任务队列末尾执行,既保证了js操作dom完成,又能在渲染之前开始执行,性能较快,但是如果本浏览器不支持微任务,就用 setTimeout(fn, 0) 来代替
flushBatcherQueue 更新dom的操作,会去重对一个数据的操作,等所有数据操作执行结束之后再进行ui渲染,节省性能
为何 vue采用异步渲染
理解:因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能考虑,vue 会在本轮数据更新后,再去异步更新视图
什么是递归组件
概念: 组件是可以在它们自己的模板中调用自身的 递归组件,一定要有一个结束的条件,否则就会使组件循环引用,最终出现错误,可以用 v-if 判断结束递归,注意递归组件的数据格式肯定是需要满足递归的条件(数据也是递归形式的)
vue 中的导航钩子有哪些
三种形式:全局导航钩子、路由配置中导航钩子、组件内部导航钩子
全局导航钩子
常用于验证用户权限
beforeEach(to,from,next) 路由改变前调用
- to: 即将进入的目标路由对象
- from: 当前正要离开的路由对象
- next: 路由控制参数 next(): 正常进入下一个页面 next(false): 取消导航,停留当前页面 next('/login'): 当前导航被中断,然后进行一个新的导航 next('error'): 如果一个 error实例,则导航会被终止且该错误会被传递给 router.onError()
afterEach(to, from)
路由配置中的导航钩子
beforeEnter(to , from, next)
组件内部钩子函数
beforeRouteEnter(to, from, next)
beforeRouteUpdate(to, from, next)
beforeRouteLeave(to, from, next)
v-model的实现以及它的实现原理吗?
vue
中双向绑定是一个指令v-model
,可以绑定一个动态值到视图,同时视图中变化能改变该值。v-model
是语法糖,默认情况下相于:value和@input
。- 使用
v-model
可以减少大量繁琐的事件处理代码,提高开发效率,代码可读性也更好 - 通常在表单项上使用
v-model
- 原生的表单项可以直接使用
v-model
,自定义组件上如果要使用它需要在组件内绑定value并处理输入事件 - 我做过测试,输出包含
v-model
模板的组件渲染函数,发现它会被转换为value属性的绑定以及一个事件监听,事件回调函数中会做相应变量更新操作,这说明神奇魔法实际上是vue的编译器完成的。
vnode的理解,compiler和patch的过程
vnode 虚拟DOM节点 创建:
export function Vnode (){
return {
tag:'div',
children: 'span',
attr:'',
text:'你好!'
}
}
复制代码
new Vue后整个的流程
initProxy
:作用域代理,拦截组件内访问其它组件的数据。initLifecycle
:建立父子组件关系,在当前组件实例上添加一些属性和生命周期标识。如[Math Processing Error]parent,parent,refs,$children,_isMounted
等。initEvents
:对父组件传入的事件添加监听,事件是谁创建谁监听,子组件创建事件子组件监听initRender
:声明[Math Processing Error]slots和slots和createElement()等。initInjections
:注入数据,初始化inject,一般用于组件更深层次之间的通信。initState
:重要)数据响应式:初始化状态。很多选项初始化的汇总:data,methods,props,computed和watch。initProvide
:提供数据注入。
思考:为什么先注入再提供呢??
答:1、首先来自祖辈的数据要和当前实例的data,等判重,相结合,所以注入数据的initInjections一定要在InitState
的上面。2. 从上面注入进来的东西在当前组件中转了一下又提供给后代了,所以注入数据也一定要在上面。
vm.[Math Processing Error]mount(vm.mount(vm.options.el)
:挂载实例。
keep-alive的实现
作用:实现组件缓存
钩子函数:
`activated `组件渲染后调用
`deactivated `组件销毁后调用
复制代码
原理:Vue.js
内部将DOM
节点抽象成了一个个的VNode
节点,keep-alive
组件的缓存也是基于VNode
节点的而不是直接存储DOM
结构。它将满足条件(pruneCache与pruneCache)
的组件在cache
对象中缓存起来,在需要重新渲染的时候再将vnode
节点从cache
对象中取出并渲染。
配置属性:
include
字符串或正则表达式。只有名称匹配的组件会被缓存
exclude
字符串或正则表达式。任何名称匹配的组件都不会被缓存
max
数字、最多可以缓存多少组件实例
vuex、vue-router实现原理
vuex
是一个专门为vue.js应用程序开发的状态管理库。 核心概念:
state
(单一状态树)getter/Mutation
显示提交更改state
Action类似Mutation
,提交Mutation
,可以包含任意异步操作。module
(当应用变得庞大复杂,拆分store
为具体的module
模块)
vue 性能优化方法
-
路由懒加载 (按需引入页面,在打包的时候整体体积大幅缩减)
-
keep-alive缓存页面 (细节处理 include 。。)
-
使用 v-show 复用DOM,较大组件,使用 v-show,比 v-if效果好(一个是完全的新增删除,一个是利用 display 控制显隐性)
-
v-for遍历避免同时使用 v-if
-
长列表性能优化 a、如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化 b、如果是大数据长列表,可采用虚拟滚动,只渲染少数部分区域的内容
-
事件销毁 (vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件)
-
图片懒加载 对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
-
第三方插件按需引入 像 element-ui这样的第三方组件库可以按需引入,避免体积太大
-
无状态的组件标记为函数式组件 (傻瓜式组件,避免创建组件实例)
-
子组件分割 (频繁动态更新的组件,或者比较耗时的组件可以抽离,子组件自己管理自己,不会影响到页面中其他部分)
-
变量本地化 (不要频繁引用 this.data)
-
SSR 引入后有利于 SEO,以及首屏加载速度,还有部分服务端的优化
汇总
编码阶段
尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
v-if和v-for不能连用
如果需要使用v-for给每项元素绑定事件时使用事件代理
SPA 页面采用keep-alive缓存组件
在更多的情况下,使用v-if替代v-show
key保证唯一
使用路由懒加载、异步组件
防抖、节流
第三方模块按需导入
长列表滚动到可视区域动态加载
图片懒加载
SEO优化
预渲染
服务端渲染SSR
打包优化
压缩代码
Tree Shaking/Scope Hoisting
使用cdn加载第三方模块
多线程打包happypack
splitChunks抽离公共文件
sourceMap优化
用户体验
骨架屏
PWA
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
你知道Vue3有哪些新特性吗?它们会带来什么影响?
- 性能提升
更小巧、更快速 支持自定义渲染器 支持摇树优化:一种在打包时去除无用代码的优化手段 支持Fragments和跨组件渲染
- API变动
模板语法99%保持不变 原生支持基于class的组件,并且无需借助任何编译及各种stage阶段的特性 在设计时也考虑TypeScript的类型推断特性 重写虚拟DOM
可以期待更多的编译时提示来减少运行时的开销 优化插槽生成
可以单独渲染父组件和子组件 静态树提升
降低渲染成本 基于Proxy的观察者机制
节省内存开销
- 不兼容IE11
检测机制
更加全面、精准、高效,更具可调试式的响应跟踪
实现双向绑定 Proxy 与 Object.defineProperty 相比优劣如何?
- Object.definedProperty的作用是劫持一个对象的属性,劫持属性的getter和setter方法,在对象的属性发生变化时进行特定的操作。而 Proxy劫持的是整个对象。
- Proxy会返回一个代理对象,我们只需要操作新对象即可,而Object.defineProperty只能遍历对象属性直接修改。
- Object.definedProperty不支持数组,更准确的说是不支持数组的各种API,因为如果仅仅考虑arry[i] = value 这种情况,是可以劫持 的,但是这种劫持意义不大。而Proxy可以支持数组的各种API。
- 尽管Object.defineProperty有诸多缺陷,但是其兼容性要好于Proxy。
2、你对虚拟dom和diff算法的理解,实现render函数
虚拟DOM
本质上是JavaScript
对象,是对真实DOM
的抽象表现。 状态变更时,记录新树和旧树的差异 最后把差异更新到真正的dom
中 render函数:
- 根据
tagName
生成父标签,读取props
,设置属性,如果有content
,设置innerHtml或innerText
, - 如果存在子元素,遍历子元素递归调用
render
方法,将生成的子元素依次添加到父元素中,并返回根元素。
11、对高阶组件的理解
高阶组件是参数为组件,返回值为新组件的函数。HOC
是纯函数,没有副作用。HOC
在React
的第三方库中很常见,例如Redux
的connect
组件。
高阶组件的作用:
- 代码复用,逻辑抽象,抽离底层准备(
bootstrap
)代码 - 渲染劫持
State
抽象和更改Props
更改
vuex
1、vuex 是什么
vuex是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间的数据共享
怎么使用
使用 vuex 统一管理状态的好处
- 能够在 vuex 中集中管理共享的数据,易于开发和后期维护
- 能够高效地实现组件之间的数据共享,提高开发效率
- 存储在 vuex中的数据都是响应式的,能够实时保持数据与页面的同步
2、核心概念是什么
-
state 单一状态树 Vuex 通过
store
选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用Vue.use(Vuex)
) -
getter Getter 会暴露为
store.getters
对象,你可以以属性的形式访问这些值this.$store.getters.getById()
好处:
对数据做处理,并且可以在多个地方使用 -
mutation 必须同步执行
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
})
组件中触发mutations
store.commit('increment', {
amount: 10
})
- action action类似于 mutation,不同在于:
1、action 提交的是 mutation,而不是直接变更状态
2、action 可以包含任意异步操作
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
actions 支持同样的载荷方式和对象方式进行分发
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation
actions: {
checkout ({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出结账请求,然后乐观地清空购物车
commit(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失败操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
- model 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
3、怎么做数据存储
4、什么情况下应该使用 vuex
5、vuex理解 => 源码层次
cdn作用:
使用户访问的物理节点更少,加速访问;分担原始服务器的压力
浏览器从输入url到渲染页面,发生了什么?
利用TCP/IP协议进行网络通信时,会通过分层顺序与对方进行通信,分层由高到低:应用层,传输层,网络层,数据链路层,
发送端从应用层往下走,接收端从数据链路层网上走。
“三次握手”
的过程是,发送端先发送一个带有SYN(synchronize)标志的数据包给接收端,在一定的延迟时间内等待接收的回复。接收端收到数据包后,传回一个带有SYN/ACK标志的数据包以示传达确认信息。接收方收到后再发送一个带有ACK标志的数据包给接收端以示握手成功。在这个过程中,如果发送端在规定延迟时间内没有收到回复则默认接收方没有收到请求,而再次发送,直到收到回复为止。
网络安全、HTTP协议
为了简化网络的复杂度,网络通信的不同方面被分解为多层次结构,每一层只与紧挨着的上层或者下层进行交互,将网络分层,这样就可以修改,甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。
补充
ES6里的symble
它的功能类似于一种标识唯一性的ID,每个Symbol
实例都是唯一的。 Symbol
类型的key是不能通过Object.keys()
或者for...in
来枚举的, 它未被包含在对象自身的属性名集合(property names
)之中。 所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。 // 使用Object的API
Object.getOwnPropertySymbols(obj)
// [Symbol(name)]
// 使用新增的反射API Reflect.ownKeys(obj)
// [Symbol(name), 'age', 'title']
ES6里的set和map
Map
对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。构造函数Map可以接受一个数组作为参数。Set
对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的值。
普通函数和箭头函数的区别
- 箭头函数是匿名函数,不能作为构造函数,不能使用new
- 箭头函数不绑定
arguments
,取而代之用rest
参数...解决 - 箭头函数不绑定
this
,会捕获其所在的上下文的this值,作为自己的this值 - 箭头函数通过
call() 或 apply()
方法调用一个函数时,只传入了一个参数,对 this 并没有影响。 - 箭头函数没有原型属性
- 箭头函数不能当做
Generator
函数,不能使用yield
关键字
总结:
- 箭头函数的
this
永远指向其上下文的this
,任何方法都改变不了其指向,如call() , bind() , apply()
- 普通函数的this指向调用它的那个对象
JS函数柯里化
- 参数复用
- 提前确认
- 延迟运行
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
复制代码
实现继承口述
原型链继承 写个父类、子类 子类的原型为父类的实例 子类.prootype = new 父类 修正子类原型为子类本身 子类.prototype.constructor=子类 new 子类即可调用父类方法 构造函数继承 写个父类、子类 在子类中父类.call(this) 即可实现
mapState, mapGetters, mapActions, mapMutations
当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性 mapMutations 其实跟mapState 的作用是类似的,将组件中的 methods 映射为 store.commit 调用
计算一个函数的难易程度公式
如上图可以粗略的分为两类,多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2n) 和 O(n!) 对应的增长率如下图所示