Vue
01 vue的基本原理
当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将每一个属性身上绑定一个 getter和setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
02 使用 Object.defineProperty()
来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
03 双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤
- M(model--data)
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- V(view)
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- VM(vue)
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice() 通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退
- MVVM
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令, 最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
04 Computed 和 Watch 的区别
Computed
- 它支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当Computed中有异步操作时,无法监听数据的变化
- computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的
- 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
- 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法
Watch
- 它不支持缓存,数据变化时,它就会触发相应的操作
- 支持异步监听
- 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
- 当一个属性发生变化时,就需要执行相应的操作
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数
- immediate:组件加载立即触发回调函数
- deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化
总结
- computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
- watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
运用场景
- 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
- 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ) ,限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
05 对Vue组件化的理解
- 组件是独立和可复用的代码组织单元。组件系统是Vue核心特性之一,它使开发者使用小型、独立和通常可复用的组件构建大型应用
- 组件化开发能大幅提高应用开发效率、测试性、复用性等
- 遵循单向数据流的原则。
06 对keep-alive的理解
keep-alive
是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子 activated
和 deactivated
07 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适合频繁切换
08 v-model的实现原理
vue中v-model可以实现数据的双向绑定,但是为什么这个指令就可以实现数据的双向绑定呢?其实v-model是vue的一个语法糖。即利用v-model绑定数据后,既绑定了数据,又添加了一个input事件监听。
实现原理
- v-bind绑定响应数据
<input v-model="text">
等价于<input :value="message" @input="message = $event.target.value">
- 触发input事件并传递数据
09 data为什么是一个函数而不是对象
Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致它们共用一个data对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义, 在initData时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。而在Vue根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况
10 Vue中封装的数组方法有哪些,其如何实现页面更新
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化
Vue中封装的数组方法
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
如何实现页面更新
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的ob ,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过targetproto == arrayMethods来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。
11 vue如何监听对象或者数组某个属性的变化
先判断属性是否原本就存在,若存在,则利用自身的setter和getter实现数据的实时更新
当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty() 限制,监听不到变化。
解决方式
- 数组
this.$set(你要改变的数组,你要改变的位置,你要改成什么value)
- vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。
- 对象
this.$set(你要改变的对象,你要改变的key,你要改成什么value)
vm.$set
的实现原理- 如果目标是数组,直接使用数组的 splice 方法触发响应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
12 Vue模版编译原理
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所以需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。
解析阶段: 使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST
优化阶段: 遍历AST,找到其中的一些静态节点并进行标记,方便在页面重新渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能
生成阶段: 将最终的AST转化为render函数字符串,生成render函数,浏览器执行render函数,在页面中渲染出对应的HTML元素
13 Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。 Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
14 常见的Vue性能优化方法
- 路由懒加载
- keep-alive缓存页面
- 使用v-show复用DOM 两个模块来回切换
- v-for 遍历避免同时使用 v-if
- 长列表滚动到可视区域动态加载
- 图片懒加载: 对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。
- 第三方插件按需引入: 像element-ui这样的第三方组件库可以按需引入避免体积太大
- SSR(服务端渲染)
- 变量本地化
- 子组件分隔
- 无状态的组件标记为函数式组件
15 说一下Vue的生命周期
Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
beforeCreate
(创建前)- 数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据
created
(创建后)- 实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,可以访问data数据以及methods方法,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性(vm实例身上)。
beforeMount
(挂载前)- 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上,虚拟DOM生成,此时页面渲染的是未经vue编译的DOM结构
mounted
(挂载后)- 在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
beforeUpdate
(更新前)- 响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。
updated
(更新后)- 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy
(销毁前)- 实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。
destroyed
(销毁后)- 实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
- 另外还有
keep-alive
独有的生命周期,分别为activated
和deactivated
。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,缓存渲染后会执行 activated 钩子函数。
16 created和mounted的区别
created
在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图,此时可以访问到data数据及methods中的方法等。
mounted
在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作(尽量避免),至此初始化过程结束。
17 常用的组件间通信方式有哪些?
组件间通信
18 Vuex 的原理
Vuex概述
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。
核心流程及主要功能
- Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;
- 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
- 然后 Mutations 就去改变(Mutate)State 中的数据;
- 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。
各模块在核心流程中的主要功能
Vue Components
∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。dispatch
∶操作行为触发方法,是唯一能执行action的方法。actions
∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。commit
∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。mutations
∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。state
∶ 页面状态管理容器对象。集中存储VueComponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,并进行状态更新。getters
∶ state对象读取方法。
总结
Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,还是需要通过Mutation 来修改State的数据。最后,根据 State 的变化,渲染到视图上。
19 Vuex有哪几种属性?
state
=> 基本数据(数据源存放地)getters
=> 从基本数据派生出来的数据mutations
=> 提交更改数据的方法,同步actions
=> 像一个装饰器,包裹mutations
,使之可以异步。modules
=> 模块化Vuex
20 Vuex 和 localStorage 的区别
存储位置区别
- vuex存储在内存中
- localstorage 则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要 JSON的stringify和parse方法进行处理。 读取内存比读取硬盘速度要快
应用场景区别
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex用于组件之间的传值。
- localstorage是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用 。
响应式区别
- Vuex能做到数据的响应式
- localstorage不能做到数据的响应式
对于不变的数据确实可以用localstorage可以代替vuex,但是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件改变了该数据源,希望另一个组件响应该变化时,localstorage无法做到
时效区别
- 刷新页面时vuex存储的值会丢失
- 刷新页面时localstorage存储的值不会丢失
21 $route
和 $router
的区别
$route
是“路由信息对象”,包括 name,path,params,query,meta,fullPath,hash、matched等路由信息参数.用来获取路由信息(路径,query,params)
$router
是“路由实例”对象包括了路由的跳转方法,钩子函数等,可以使用$router.push()
、$router.replace()
、$router.go()
等.一般进行编程式导航进行路由跳转(push/replace)
22 params和query的区别
用法
- query可以用name和path来引入;接收参数
this.$route.query.name
;在路由信息配置时路径path不需要占位 - params要用name来引入;接收参数
this.$route.params.name
;在路由信息配置时路径path需要占位
url地址显示
- query更加类似于ajax中get传参,在浏览器地址栏中显示参数
- params则类似于post,在浏览器地址栏中不显示参数
刷新
- query刷新不会丢失query里面的数据
- params刷新会丢失 params里面的数据(可考虑采取本地存储解决此问题)
23 Vue-router 导航守卫有哪些
全局守卫(前置/后置):beforeEach
、beforeResolve
、afterEach
路由独享的守卫:beforeEnter
组件内的守卫:beforeRouteEnter
、beforeRouteUpdate
、beforeRouteLeave
24 对虚拟DOM的理解?
虚拟DOM就是用来描述真实DOM的javaScript对象,可以将多次修改的DOM一次性渲染到页面上,减少页面的重排重绘,提高渲染性能。 在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
25 虚拟DOM的解析过程
- 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来并将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
- 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
- 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
26 DIFF算法的原理
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况( 如果新的没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
- 更新差异,复用节点
27 Vue中key的作用
第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
总结
- vue为了更高效的渲染元素,会尽可能的复用元素,而非从头渲染,key可以为节点打标记,而非简单的复用节点。当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】, 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则
- 旧虚拟DOM中找到了与新虚拟DOM相同的key
- 若虚拟DOM中内容没变, 直接使用之前的真实DOM
- 若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM
- 旧虚拟DOM中未找到与新虚拟DOM相同的key
- 创建新的真实DOM,随后渲染到到页面
28 为什么不建议用index作为key?
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低
- 如果逆序添加、逆序删除等破坏顺序的操作且结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题
29 vue中的key有什么作用?(key的内部原理)
虚拟DOM中key的作用:
- key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】
- 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下: 对比规则:
- 旧虚拟DOM中找到了与新虚拟DOM相同的key:
- 若虚拟DOM中内容没变, 直接使用之前的真实DOM!
- 若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
- 旧虚拟DOM中未找到与新虚拟DOM相同的key
- 创建新的真实DOM,随后渲染到到页面。
用index作为key可能会引发的问题:
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
- 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
开发中如何选择key?:
- 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
- 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。