01.如何理解MVVM原理?
MVVM
是Model-View-ViewModel
缩写,也就是把MVC
中的Controller
演变成ViewModel
。Model
层代表数据模型,View
代表UI
组件,ViewModel
是View
和Model
层的桥梁,数据会绑定到viewModel
层并自动将数据渲染到页面中,视图变化的时候会通知viewModel
层更新数据。
- 传统的
MVC
指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染 MVVM
:传统的前端会将数据手动渲染到页面上,MVVM
模式不需要用户收到操作dom
元素,将数据绑定到viewModel
层上,会自动将数据渲染到页面中,视图变化会通知viewModel
层更新数据。ViewModel
就是我们MVVM
模式中的桥梁。
02.响应式数据的原理是什么?
核心是利用Object.defineProperty
方法
- 组件在初始化的时候会调用
Object.defineProperty
方法(不兼容IE8
)将data
中的所有属性定义成访问器属性,也就是为他们定义getter/setter
方法。 - 当
render function
被渲染的时候,因为会读取所需对象的值,所以会触发getter
函数进行「依赖收集」,代码层面就是调用Dep
的addSubs
方法将观察者的Watcher
对象添加进subs
数组中,每个组件都有一个自己的Watcher
,而data
中的每个属性会维护一个这样的依赖数组。 - 当数据发生变化或者视图导致的数据发生了变化时,会触发数据劫持的
setter
函数,setter
方法会调用Dep
的notify
方法通知目前Dep
对象的subs
中的所有Watcher
对象触发更新操作,Wather
就会再次通过update
方法来更新视图。
03.Vue中是如何检测数组和对象变化?
首先对于data
中的属性是对象时候,vue
会遍历对象的所有属性进行依赖收集,如果对象属性还是对象,那么会继续递归遍历这个对象的所有属性进行依赖收集。
对于数组,会遍历数组的每一项进行依赖收集,如果数组的单项是数组或者对象,会继续进行递归依赖收集。所以我们修改数组的任何一项或者任何一项的任意属性,都会触发对应的setter
方法,触发更新操作。
同时,对于数组,vue
重写了数组原型链的方法,在调用原型链方法的时候,会自动触发更新操作。其中,如果调用的是会增加数组项的push
、unshift
和splice
这三个方法,那么在触发更新前,会对新增的项进行依赖收集。
03.直接给一个数组项赋值,Vue 能检测到变化吗?
vue
的监听机制都是通过defineproperty
来实现的,它是不能检测对象新添加的属性,对象可以初始化的时候就设置好属性,不加新属性,数组也是对象,然而如果要求数组初始化的时候就设置好所有属性(索引),不能新增索性(改变长度),显然是不可能的,那么要数组也就没用了。
由于JavaScript
的限制,Vue
不能检测到以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
为了解决第一个问题,Vue
提供了以下操作方法:通过索引来修改数组,使其能成为响应式,解决直接使用赋值不能响应的问题
Vue.set(vm.data,2,'huanpu','name')
对数组
Vue.$set(vm.data,'K','V')
对对象
为了解决第二个问题,Vue
提供了以下操作方法:
vm.items.splice(newLength) // newLength 就是指的你更新的长度
04.为何Vue采用异步渲染?
当然也可以采用同步渲染,只是同步渲染的话,对数据的每次修改,都会立刻引发dom
的修改,而对dom
的操作是比较费时的,从性能的角度考虑,vue
选择了异步更新。
实际上,Vue
在默认情况下,每次触发某个数据的setter
方法后,对应的Watcher
对象其实会被push
进一个队列queue
中,在下一个tick
的时候将这个队列queue
全部拿出来run
(Watcher
对象的一个方法,用来触发patch
操作) 一遍。
05.nextTick实现原理?
目前浏览器平台并没有实现nextTick
方法,所以Vue
源码中会依次检测Promise
、MutationObserver
、setTimeout
、setImmediate
等方式浏览器是否支持,如果支持就用它来创建一个微任务或宏任务,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。
nextTick
内部维护一个了全局的回调函数队列,每次调用nextTick
时,其回调函数会被push
到这个全局的函数队列中,我们刚才说的创建的微任务或者宏任务被执行的时候,把这个队列里的函数取出依次执行。
06.Vue组件的生命周期?
beforeCreate
是new Vue()
之后触发的第一个钩子,在当前阶段data
、methods
、computed
以及watch
上的数据和方法都不能被访问。created
在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,但这时真实dom
还没有被创建。beforeMount
发生在挂载之前,在这之前template
模板已导入渲染函数编译。而当前阶段虚拟Dom
已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated
。mounted
在挂载完成后发生,在当前阶段,真实的Dom
挂载完毕,数据完成双向绑定,可以访问到Dom
节点,使用$refs
属性对Dom
进行操作。beforeUpdate
发生在更新之前,也就是响应式数据发生更新,虚拟dom
重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重新渲染。updated
发生在更新完成之后,当前阶段组件Dom
已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。beforeDestroy
发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。destroyed
发生在实例销毁之后,这个时候只剩下了dom
空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
除此之外,如果组件被keep-alive
修饰,则会有另外另个生命周期
activited
: 组件被激活时调用deactivated
: 组件被隐藏时调用
07.Ajax请求放在哪个生命周期中?
官方实例的异步请求是在mounted
生命周期中调用的,而实际上也可以在created
生命周期中调用。
08.何时需要beforeDestory?
当需要在组件销毁前做一些善后收尾工作的时候,比如清除计时器,可以在这个回调里写。
09.Vue父子组件生命周期调用顺序?
- 组件的
调用
顺序都是先父后子,渲染完成
的顺序是先子后父; - 组件的
销毁
操作是先父后子,销毁完成
的顺序是先子后父。
加载渲染过程
父beforeCreate
->父created
->父beforeMount
->子beforeCreate
->子created
->子beforeMount
- >子mounted
->父mounted
子组件更新过程
父beforeUpdate
->子beforeUpdate
->子updated
->父updated
父组件更新过程
父beforeUpdate
-> 父updated
销毁过程
父beforeDestroy
->子beforeDestroy
->子destroyed
->父destroyed
10.Vue中Computed的特点?
computed
会拥有自己的watcher
,它内部有个属性dirty
开关来决定computed
的值是需要重新计算还是直接复用之前的值。,假设计算属性sum
依赖响应式属性count
- 在
sum
第一次进行求值的时候会读取响应式属性count
,收集到这个响应式数据作为依赖。并且计算出一个值来保存在自身的value
上,把dirty
设为false
,接下来在模板里再访问sum
就直接返回这个求好的值value
,并不进行重新的求值。 - 而
count
发生变化了以后会通知sum
所对应的watcher
把自身的dirty
属性设置成true
,这也就相当于把重新求值的开关打开来了。这个很好理解,只有count
变化了,sum
才需要重新去求值。 - 那么下次模板中再访问到
this.sum
的时候,才会真正的去重新调用sum
函数求值,并且再次把dirty
设置为false
。
11.Watch中的deep:true是如何实现的?
不光是数组类型,对象类型也会对深层属性进行依赖收集,比如deep watch
了obj
,那么对obj.a.b.c = 5
这样深层次的修改也一样会触发watch
的回调函数。本质上是因为Vue
内部对需要deep watch
的属性会进行递归的访问,而在此过程中也会不断发生依赖收集。(只要此属性也是响应式属性)
这种方式会有性能损耗
12.Vue中事件绑定的原理?
原生事件绑定是通过addEventListener
绑定给真实元素的,组件事件绑定是通过Vue
自定义的$on
实现的。
13.Vue中v-html会导致哪些问题?
v-html
指令最终调用的是innerHTML
方法将指令的value
插入到对应的元素里,这很容易导致XSS
跨站脚本攻击,一般情况下我们只对可信内容使用HTML
插值,绝不要对用户提供的内容插值。
如果一定要用可以用<pre>
标签代替<div>
之类,主要是利用<pre>
的一个属性:被包围在<pre>
元素中的文本通常会保留空格和换行符,并且文本也会呈现为等宽字体。
站在项目全局的角度,如果不放心可以使用xss
的npm
包,在webpack
里配置对所有innerHTML
方法进行覆盖,对innerHTML
方法的值外面包上一层xss
方法。
14.Vue中v-if和v-show的区别?
v-if
是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS
的display
属性进行切换。
所以,v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show
则适用于需要非常频繁切换条件的场景。
15.为什么v-for和v-if不能连用?
v-for
比v-if
优先级高,所以嵌套使用的的话,每次v-for
都会执行v-if
,造成不必要的计算,影响性能,尤其是当之需要渲染很小一部分的时候。必要时候可以使用computed
,或者在v-for
的数组先filter
筛选代替v-if
。
16.v-model中的实现原理及如何自定义v-model?
v-model
本质上不过是语法糖,可以用v-model
指令在表单<input>
、<textarea>
及<select>
元素上创建双向数据绑定,会根据控件类型自动选取正确的方法来更新元素,同时负责监听用户的输入事件以更新数据。
17.组件中的data为什么是一个函数?
同一个组件被复用多次,会创建多个实例。这些实例用的是同一个构造函数,如果data
是一个对象的话。那么所有组件都共享了同一个对象。为了保证组件的数据独立性要求每个组件必须通过data
函数返回一个对象作为组件的状态。
18.Vue组件如何通信?
- 父子组件通信:
props + $emit
,或者$parent、$children
获取父子实例,此外获取实例也可以用ref
。 - 兄弟,跨级通信可以用
Event Bus
或者vuex
provide/inject
:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础
19.什么是作用域插槽?
插槽用于决定将所携带的内容,插入到子组件指定的某个位置,但内容必须在父组件中子组件的标签内定义,在子组件中用<slot></slot>
标签接收。slot
可以说是组件内部的占位符。
20.用vnode来描述一下dom结构?
- 属性:
tag
:当前节点标签名,data
:当前节点的数据,children
:当前节点的子节点,text
:当前节点的文本,elm
:当前节点对应的真是dom
节点,context
:当前节点上下文,isStatic
:是否为静态节点,isComment
:是否为注释节点。 - 方法:
createEmptyVNode
,createTextVNode
,cloneVNode
。
21.diff算法的时间复杂度?
两个树的完全的diff
算法是一个时间复杂度为O(n3)
,Vue进行了优化O(n3)
复杂度的问题转换成O(n)
复杂度的问题(只比较同级不考虑跨级问题) 在前端当中, 你很少会跨越层级地移动Dom元素。 所以Virtual Dom
只会对同一个层级的元素进行对比。
22.简述vue中diff算法原理?
- 先同级比较,在比较子节点
- 先判断一方有儿子一方没儿子的情况
- 比较都有儿子的情况
- 递归比较子节点
vue的Diff算法其核心是基于两个简单的假设:
- 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
- 同一层级的一组节点,他们可以通过唯一的id进行区分。
23.v-for中为什么要用key?
要使用key
来给每个节点做一个唯一标识,Diff
算法就可以正确的识别此节点。作用主要是为了高效的更新虚拟DOM
。
24.描述组件渲染和更新过程?
渲染组件时,会通过Vue.extend
方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行patchVnode
流程.核心就是diff
算法
25.vue中模板编译原理?
简单说,vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:
解析(parse)
:解析模版,用正则等方式将template
模板中进行字符串解析,得到指令
、class
、style
等数据,形成 一棵AST树。优化(optimize)
:深度遍历AST
树,按照相关条件对树节点进行静态节点标记,其实就是给每个节点设置isStatic
值,被设置成true
的节点在后面进行diff
分析的时候可以直接跳过。生成(generate)
:将优化后的AST树转换为可执行的代码。
26.vue中常见性能优化?
1. 代码层面的优化
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景 参考
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 长列表性能优化:数组冻结
Object.freeze
- 事件的销毁
- 图片资源懒加载
- 路由懒加载
- 第三方插件的按需引入
- 优化无限列表性能
- 服务端渲染 SSR or 预渲染
2. Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
3. 基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
27.vue中相同逻辑如何抽离?
Vue.mixin
用法 给组件每个生命周期,函数等都混入一些公共逻辑。
28.为什么要使用异步组件?
如果组件功能多打包出的结果会变大,我可以采用异步的方式来加载组件。主要依赖import()
这个语法,可以实现文件的分割加载。
29.谈谈你对keep-alive的了解?
keep-alive
可以实现组件的缓存,当组件切换时不会对当前组件进行卸载,常用的2个属性include/exclude
,2个生命周期activated
,deactivated
。
30.实现hash路由和history路由?
hash模式
:url中会有#
存在如www.baidu.com/#/a
,看起来丑陋且不好做SEO
,但是由于路由器向服务器发请求的时候会忽略#后面的值,所以在浏览器刷新的时候都是很正常的不会再次发请求。history模式
:利用了HTML5 History Interface
中新增的pushState()
和replaceState()
方法。这两个方法应用于浏览器的历史记录栈,在当前已有的back
、forward
、go
的基础之上,它们提供了对历史记录进行修改的功能。但当没有后端进行相应配置时,url
就是我们看的正常的路由样子,就是因为看起来很像真的,所以刷新浏览器的时候(浏览器向服务器请求)会发现服务器根本没有这个路径资源,所以返回404
。
31.vue-router中导航守卫有哪些?
32.action和mutation的区别?
mutation
是同步更新数据(内部会进行是否为异步方式更新数据的检测)action
异步操作,可以获取数据后调佣mutation
提交最终数据
33.简述vuex工作原理?
Vuex
是专门为Vuejs
应用程序设计的状态管理工具,它采用集中式存储管理应用的所有组件的状态。
vuex主要由四部分构成:
state
mutations
actions
getters
vuex的响应式原理包括两部分:
- vuex的store是如何挂载注入到组件中呢?
利用vue
的插件机制,项目初始化调用Vue.use(vuex)
时,会调用vuex
的install
方法,装载vuex
。具体来说就是vuex
利用vue
的mixin
混入机制,在beforeCreate
钩子前混入vuexInit
方法,vuexInit
方法实现了store
注入vue
组件实例,并注册了vuex store
的引用属性$store
。
- 响应式原理
Vuex
的state
状态是响应式,是借助vue
的data
是响应式,将state
存入vue
实例组件的data
中;Vuex
的getters
则是借助vue
的计算属性computed
实现数据实时监听。
34.vue3.0你知道有哪些改进?
- Vue3采用了TS来编写
- 支持 Composition API
- Vue3中响应式数据原理改成proxy
- vdom的对比算法更新,只更新vdom的绑定了动态数据的部分