双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
如何从真实DOM到虚拟DOM
涉及到Vue中的模板编译原理,主要过程:
- 将模板转换成
ast
树,ast
用对象来描述真实的JS语法(将真实DOM转换成虚拟DOM) - 优化树
- 将
ast
树生成代码
怎样理解 Vue 的单向数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会 防止从子组件意外改变父级组件的状态 ,从而导致你的应用的数据流向难以理解
注意 :在子组件直接用 v-model
绑定父组件传过来的 prop
这样是不规范的写法 开发环境会报警告
如果实在要改变父组件的 prop
值,可以在 data
里面定义一个变量 并用 prop
的值初始化它 之后用$emit
通知父组件去修改
有两种常见的试图改变一个 prop 的情形 :
- 这个
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop
数据来使用。 在这种情况下,最好定义一个本地的data
属性并将这个prop
用作其初始值
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- 这个
prop
以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个prop
的值来定义一个计算属性
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
你有使用过vuex的module吗?
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action
- 用过
module
,项目规模变大之后,单独一个store
对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护 - 可以按之前规则单独编写子模块代码,然后在主文件中通过
modules
选项组织起来:reateStore({modules:{...}})
- 不过使用时要注意访问子模块状态时需要加上注册时模块名:
store.state.a.xxx
,但同时getters
、mutations
和actions
又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子块加上namespace
选项,此时再访问它们就要加上命名空间前缀。 - 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。
pinia
显然在这方面有了很大改进,是时候切换过去了
Vue中常见性能优化
编码优化 :
- 使用
v-show
复用DOM
:避免重复创建组件
- 合理使用路由懒加载、异步组件,有效拆分
App
尺寸,访问时才异步加载
const router = createRouter({
routes: [
// 借助webpack的import()实现异步组件
{ path: '/foo', component: () => import('./Foo.vue') }
]
})
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态
v-once
和v-memo
:不再变化的数据使用v-once
This will never change: {{msg}}
comment
{{msg}}
- {{i}}
按条件跳过更新时使用v-momo
:下面这个列表只会更新选中状态变化项
ID: {{ item.id }} - selected: {{ item.id === selected }}
...more child nodes
- 长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
- 防止内部泄漏,组件销毁后把全局变量和事件销毁:
Vue
组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
export default {
created() {
this.timer = setInterval(this.refresh, 2000)
},
beforeUnmount() {
clearInterval(this.timer)
}
}
- 图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
- 滚动到可视区域动态加载
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-router 路由钩子在生命周期的体现
一、Vue-Router导航守卫
有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。
为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的
- 全局路由钩子
vue-router全局有三个路由钩子;
- router.beforeEach 全局前置守卫 进入路由之前
- router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
- router.afterEach 全局后置钩子 进入路由之后
具体使用∶
- beforeEach(判断是否登录了,没登录就跳转到登录页)
router.beforeEach((to, from, next) => {
let ifInfo = Vue.prototype.$common.getSession('userData'); // 判断是否登录的存储信息
if (!ifInfo) {
// sessionStorage里没有储存user信息
if (to.path == '/') {
//如果是登录页面路径,就直接next()
next();
} else {
//不然就跳转到登录
Message.warning("请重新登录!");
window.location.href = Vue.prototype.$loginUrl;
}
} else {
return next();
}
})
- afterEach (跳转之后滚动条回到顶部)
router.afterEach((to, from) => {
// 跳转之后滚动条回到顶部
window.scrollTo(0,0);
});
- 单个路由独享钩子
beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next
export default [
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {
console.log('即将进入登录页面')
next()
}
}
]
- 组件内钩子
beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
这三个钩子都有三个参数∶to、from、next
- beforeRouteEnter∶ 进入组件前触发
- beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foa组件,这个钩子在这种情况下就会被调用
- beforeRouteLeave∶ 离开组件被调用
注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:
beforeRouteEnter(to, from, next) {
next(target => {
if (from.path == '/classProcess') {
target.isFromProcess = true
}
})
}
二、Vue路由钩子在生命周期函数的体现
- 完整的路由导航解析流程(不包括其他生命周期)
- 触发进入其他路由。
- 调用要离开路由的组件守卫beforeRouteLeave
- 调用局前置守卫∶ beforeEach
- 在重用的组件里调用 beforeRouteUpdate
- 调用路由独享守卫 beforeEnter。
- 解析异步路由组件。
- 在将要进入的路由组件中调用 beforeRouteEnter
- 调用全局解析守卫 beforeResolve
- 导航被确认。
- 调用全局后置钩子的 afterEach 钩子。
- 触发DOM更新(mounted)。
- 执行beforeRouteEnter 守卫中传给 next 的回调函数
- 触发钩子的完整顺序
路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶
- beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
- beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等。
- beforeEnter:路由独享守卫
- beforeRouteEnter:路由组件的组件进入路由前钩子。
- beforeResolve:路由全局解析守卫
- afterEach:路由全局后置钩子
- beforeCreate:组件生命周期,不能访问tAis。
- created;组件生命周期,可以访问tAis,不能访问dom。
- beforeMount:组件生命周期
- deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
- mounted:访问/操作dom。
- activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
- 执行beforeRouteEnter回调函数next。
- 导航行为被触发到导航完成的整个过程
- 导航行为被触发,此时导航未被确认。
- 在失活的组件里调用离开守卫 beforeRouteLeave。
- 调用全局的 beforeEach守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnteY。
- 解析异步路由组件(如果有)。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
- 触发 DOM 更新。
- 用创建好的实例调用 beforeRouteEnter守卫中传给 next 的回调函数。
- 导航完成
参考 前端进阶面试题详细解答
理解Vue运行机制全局概览
全局概览
首先我们来看一下笔者画的内部流程图。
大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。相信讲完之后大家对Vue.js
内部运行机制会有一个大概的认识。
初始化及挂载
在
new Vue()
之后。 Vue 会调用_init
函数进行初始化,也就是这里的init
过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过Object.defineProperty
设置setter
与getter
函数,用来实现「 响应式 」以及「 依赖收集 」,后面会详细讲到,这里只要有一个印象即可。初始化之后调用
$mount
会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「 编译 」步骤。
编译
compile编译可以分成 parse
、optimize
与 generate
三个阶段,最终需要得到 render function。
1. parse
parse
会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。
2. optimize
optimize
的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当update
更新界面时,会有一个patch
的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了patch
的性能。
3. generate
generate
是将 AST 转化成render function
字符串的过程,得到结果是render
的字符串以及 staticRenderFns 字符串。
- 在经历过
parse
、optimize
与generate
这三个阶段以后,组件中就会存在渲染VNode
所需的render function
了。
响应式
接下来也就是 Vue.js 响应式核心部分。
这里的getter
跟setter
已经在之前介绍过了,在init
的时候通过Object.defineProperty
进行了绑定,它使得当被设置的对象被读取的时候会执行getter
函数,而在当被赋值的时候会执行setter
函数。
- 当
render function
被渲染的时候,因为会读取所需对象的值,所以会触发getter
函数进行「 依赖收集 」,「 依赖收集 」的目的是将观察者Watcher
对象存放到当前闭包中的订阅者Dep
的subs
中。形成如下所示的这样一个关系。
在修改对象的值的时候,会触发对应的setter
,setter
通知之前「 依赖收集 」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用update
来更新视图,当然这中间还有一个patch
的过程以及使用队列来异步更新的策略,这个我们后面再讲。
Virtual DOM
我们知道,render function
会被转化成VNode
节点。Virtual DOM
其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
比如说下面这样一个例子:
{
tag: 'div', /*说明这是一个div标签*/
children: [ /*存放该标签的子节点*/
{
tag: 'a', /*说明这是一个a标签*/
text: 'click me' /*标签的内容*/
}
]
}
渲染后可以得到
这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。
更新视图
- 前面我们说到,在修改一个对象值的时候,会通过
setter -> Watcher -> update
的流程来修改对应的视图,那么最终是如何更新视图的呢? - 当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的
VNode
节点,然后用innerHTML
直接全部渲染到真实DOM
中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「 浪费 」。 - 那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「
patch
」了。我们会将新的VNode
与旧的VNode
一起传入patch
进行比较,经过 diff 算法得出它们的「 差异 」。最后我们只需要将这些「 差异 」的对应 DOM 进行修改即可。
再看全局
回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?
Vue与Angular以及React的区别?
Vue与AngularJS的区别
Angular
采用TypeScript
开发, 而Vue
可以使用javascript
也可以使用TypeScript
AngularJS
依赖对数据做脏检查,所以Watcher
越多越慢;Vue.js
使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。AngularJS
社区完善,Vue
的学习成本较小
Vue与React的区别
相同点:
Virtual DOM
。其中最大的一个相似之处就是都使用了Virtual DOM
。(当然Vue
是在Vue2.x
才引用的)也就是能让我们通过操作数据的方式来改变真实的DOM
状态。因为其实Virtual DOM
的本质就是一个JS
对象,它保存了对真实DOM
的所有描述,是真实DOM
的一个映射,所以当我们在进行频繁更新元素的时候,改变这个JS
对象的开销远比直接改变真实DOM
要小得多。- 组件化的开发思想。第二点来说就是它们都提倡这种组件化的开发思想,也就是建议将应用分拆成一个个功能明确的模块,再将这些模块整合在一起以满足我们的业务需求。
Props
。Vue
和React
中都有props
的概念,允许父组件向子组件传递数据。- 构建工具、Chrome插件、配套框架。还有就是它们的构建工具以及Chrome插件、配套框架都很完善。比如构建工具,
React
中可以使用CRA
,Vue
中可以使用对应的脚手架vue-cli
。对于配套框架Vue
中有vuex、vue-router
,React
中有react-router、redux
。
不同点
- 模版的编写。最大的不同就是模版的编写,
Vue
鼓励你去写近似常规HTML
的模板,React
推荐你使用JSX
去书写。 - 状态管理与对象属性。在
React
中,应用的状态是比较关键的概念,也就是state
对象,它允许你使用setState
去更新状态。但是在Vue
中,state
对象并不是必须的,数据是由data
属性在Vue
对象中进行管理。 - 虚拟
DOM
的处理方式不同。Vue
中的虚拟DOM
控制了颗粒度,组件层面走watcher
通知,而组件内部走vdom
做diff
,这样,既不会有太多watcher
,也不会让vdom
的规模过大。而React
走了类似于CPU
调度的逻辑,把vdom
这棵树,微观上变成了链表,然后利用浏览器的空闲时间来做diff
Vue 中 computed 和 watch 有什么区别?
计算属性 computed:
(1)**支持缓存**,只有依赖数据发生变化时,才会重新进行计算函数;
(2)计算属性内**不支持异步操作**;
(3)计算属性的函数中**都有一个 get**(默认具有,获取计算属性)**和 set**(手动添加,设置计算属性)方法;
(4)计算属性是自动监听依赖值的变化,从而动态返回内容。
侦听属性 watch:
(1)**不支持缓存**,只要数据发生变化,就会执行侦听函数;
(2)侦听属性内**支持异步操作**;
(3)侦听属性的值**可以是一个对象,接收 handler 回调,deep,immediate 三个属性**;
(3)监听是一个过程,在监听的值变化时,可以触发一个回调,并**做一些其他事情**。
构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么
vue.js
:vue-cli
工程的核心,主要特点是 双向数据绑定 和 组件系统。vue-router
:vue
官方推荐使用的路由框架。vuex
:专为Vue.js
应用项目开发的状态管理器,主要用于维护vue
组件间共用的一些 变量 和 方法。axios
( 或者fetch
、ajax
):用于发起GET
、或POST
等http
请求,基于Promise
设计。vuex
等:一个专为vue
设计的移动端UI组件库。- 创建一个
emit.js
文件,用于vue
事件机制的管理。 webpack
:模块加载和vue-cli
工程打包器。
delete和Vue.delete删除数组的区别?
delete
只是被删除的元素变成了empty/undefined
其他的元素的键值还是不变。Vue.delete
直接删除了数组 改变了数组的键值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b,0)
console.log(b) //[2,3,4]
使用vue渲染大量数据时应该怎么优化?说下你的思路!
分析
企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。
回答
- 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树
- 处理时要根据情况做不同处理:
- 可以采取分页的方式获取,避免渲染大量数据
- vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据
- 如果不需要更新,可以使用v-once方式只渲染一次
- 通过v-memo (opens new window)可以缓存结果,结合
v-for
使用,避免数据变化时不必要的VNode
创建 - 可以采用懒加载方式,在用户需要的时候再加载数据,比如
tree
组件子树的懒加载 - 还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以
v-once
处理,需要更新可以v-memo
进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案
了解nextTick吗?
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
vue 中使用了哪些设计模式
- 工厂模式 传入参数即可创建实例:虚拟
DOM
根据参数的不同返回基础标签的Vnode
和组件Vnode
- 单例模式 整个程序有且仅有一个实例:
vuex
和vue-router
的插件注册方法install
判断如果系统存在实例就直接返回掉 - 发布-订阅模式 (vue 事件机制)
- 观察者模式 (响应式数据原理)
- 装饰模式: (@装饰器的用法)
- 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
请说出vue cli项目中src目录每个文件夹和文件的用法
assets
文件夹是放静态资源;components
是放组件;router
是定义路由相关的配置;view
视图;app.vue
是一个应用主组件;main.js
是入口文件
用VNode来描述一个DOM结构
虚拟节点就是用一个对象来描述一个真实的DOM元素。首先将 template
(真实DOM)先转成 ast
, ast
树通过 codegen
生成 render
函数, render
函数里的 _c
方法将它转为虚拟dom
vue-loader是什么?它有什么作用?
回答范例
vue-loader
是用于处理单文件组件(SFC
,Single-File Component
)的webpack loader
- 因为有了
vue-loader
,我们就可以在项目中编写SFC
格式的Vue
组件,我们可以把代码分割为、
和
,代码会异常清晰。结合其他
loader
我们还可以用Pug
编写,用
SASS
编写,用
TS
编写。我们的
还可以单独作用当前组件
webpack
打包时,会以loader
的方式调用vue-loader
vue-loader
被执行时,它会对SFC
中的每个语言块用单独的loader
链处理。最后将这些单独的块装配成最终的组件模块
原理
vue-loader
会调用@vue/compiler-sfc
模块解析SFC
源码为一个描述符(Descriptor
),然后为每个语言块生成import
代码,返回的代码类似下面
// source.vue被vue-loader处理之后返回的代码
// import the block
import render from 'source.vue?vue&type=template'
// import the