Vuejs 2
对 MVVM 的理解
官方示意图
- MVVM 由 Model、View、ViewModel 三部分组成;
- Model 表示数据层,数据和业务都在 Model 中定义和处理;
- View 表示视图层,负责数据的展示;
- 而 ViewModel 则是一个同步 Model 和 View 的对象,使用 new Vue 创建的实例就是 ViewModel;
- ViewModel 通过监听 Model 中的数据变化来控制 View 的更新,同时将 View 中由用户交互产生的数据变化同步给 Model,这个过程是由 ViewModel 连接起来的;
- MVVM 实现了 Model 和 View 的自动同步;
Vue 的优点
- 轻量化与可扩展性,核心功能就是视图与数据的双向绑定,其他强大的功能由插件扩展;
- 双向数据绑定,操作数据即可更新视图,视图交互也会改变数据;
- 组件化,单文件组件的可读性高,低耦合强关联,提高代码复用性;
- 虚拟 DOM,原生的 DOM 操作每次都会触发重新渲染,非常耗费性能,而且用户体验也不好,而 Vue 通过异步将多次 DOM 操作引起的渲染合并为一次,并且计算出最小变化 ,降低了 DOM 操作的成本,提升了用户体验;
描述 Vue 实例的生命周期
生命周期是指实例从构建到销毁的整个过程,Vue 实例的生命周期分为初始化阶段、挂载阶段、运行(更新)阶段以及销毁(卸载)阶段;
- 首先是初始化阶段,从 new Vue 实例开始,会初始化生命周期钩子函数以及默认事件,在这些工作完成后,触发 beforeCreate;
- 接着初始化 data、methods 等方法,检索依赖并注入,这个时候,实例已经被完全创建,但真实 DOM 还未生成,这些工作完成后,会触发 Created;
- 然后进入挂载阶段,首先是模版编译,检查实例的 el 属性,如果没有 el,则等待手动调用实例的 $mount 方法,然后检查 template 属性,如果没有 template 属性,则使用 el 挂载的 DOM 节点作为 template,接着根据 template 和数据生成 render 函数并编译成真实的 DOM,触发 beforeMount;
- 接着给实例添加 $el 属性,并用编译生成的 DOM 替换 el 挂载的 DOM,然后触发 Mounted,实例初始化和挂载完成,进入运行更新阶段;
- 在数据更新前,会触发 beforeUpdate,此时 data 中的数据已经发生变化,但 View 还未同步,而且由于虚拟 DOM 还没有重新渲染和打补丁,所以多次数据变化引起的视图更新不会触发额外的渲染;
- 在虚拟 DOM 更新完成后,会触发 Updated,更新完成;
- 在 keep-alive 缓存的组件被激活时,会触发 activated,失活时会触发 deactivated;
- 在销毁阶段,实例销毁前,可以调用 beforeDestroy,此时实例仍然可用,可以进行解除绑定、销毁子组件、销毁自定义事件等操作;
- 实例销毁完成后,触发 Destroyed,此时实例已经完全销毁不可用,销毁完成;
父子组件的生命周期
- 加载渲染过程中,先创建并挂载父组件,然后挂载并创建子组件,在这个过程中,依次触发以下生命周期钩子函数:父组件 beforeCreate,父组件 Created,父组件 beforeMount,子组件 beforeCreate,子组件 Created,子组件 beforeMount,子组件 Mounted,父组件 Mounted,挂载完成;
- 更新过程,先更新子组件,然后更新父组件,依次触发以下生命周期钩子函数:父组件 beforeUpdate,子组件 beforeUpdate,子组件 Updated,父组件 Updated,更新完成;
- 如果父组件的更新不影响子组件,则只触发父组件的 beforeUpdate 和 父组件的 Updated;
- 销毁过程,由内到外,依次触发以下生命周期钩子函数:父组件 beforeDestroy,子组件 beforeDestroy,子组件 Destroyed,父组件 Destroyed,销毁完成;
如何理解单向数据流
单向数据流就是数据在某个节点发生变化后,只会影响一个方向上的其他节点,即,数据由父组件向子组件进行传递和更新;
在组件通讯中,父组件使用 props 属性将数据传递给子组件;
但 props 中的数据只能由父组件直接修改,如果子组件想要修改,需要使用 $emit 触发事件去通知父组件修改;
如果子组件在已有数据上想要进行修改,可以使用以下两种方式:
- 在子组件的 data 中将 props 中的数据作为初始值进行拷贝;
- 如果只是想对 props 中的数据进行转换,可以使用计算属性 computed;
为什么是单向的,不能是双向的?
如果父组件的数据通过 props 传递给子组件,而子组件更新了 props,这会导致父组件和其他关联组件的数据更新,View 也会随数据变化而更新,其他组件却不知道数据变化的来源,毫无疑问,这会导致严重的数据紊乱和不可控;
$nextTick()
$nextTick
是 Vue 在 DOM 更新完成后触发的回调;$nextTick
可以保证所有 DOM 是最新的,并且渲染已完成,依赖 DOM 的操作可以使用最新的 DOM;$nextTick
属于微任务,返回的是 promise 对象;
为什么需要它?
- 因为 Vue 的 DOM 更新是异步的,在观察到数据变化后,Vue 会开启一个队列来缓冲同一事件循环中发生的所有更改,并在下一次事件循环中清空队列并执行 DOM 更新,这样可以去掉重复数据和多次更改造成的不必要计算和 DOM 渲染;
- 所以需要 $nextTick 来保证所有 DOM 更新已完成,才能执行依赖 DOM 的操作;
Vue 双向绑定(响应式)的原理
官方示意图
- 采用数据劫持和发布-订阅者模式;
- 在初始化阶段,通过 Object.defineProperty 劫持属性的 getter 和 setter,监听数据变化,同时,为每个属性创建 Dep 用来存储依赖(watcher);
- 在编译模版阶段,对使用的属性触发 getter 进行依赖收集,将 watcher 添加到属性对应的 Dep 中;
- 在数据变化时,触发属性的 setter,循环依赖列表,通知 Dep 中的所有 watcher 执行更新;
Vue 实例中的 data 是对象,组件中的 data 是函数?
- 组件的目的是用来复用的;
- JS 中对象的赋值是引用传递;
- 如果组件中的 data 是对象,那么所有组件会共享一份数据,修改会相互影响,同时也没有了复用的可能性(因为所有组件的数据都相同);
- 而如果是函数的话,每个组件都会维护一份属于自己的数据拷贝,修改不会相互影响;
- Vue 根实例是不会被复用的,不存在对象引用的问题,所以 data 是对象没有问题;
组件通讯
- 父子组件:props 和 $emit;
- 隔代组件:provide 和 inject;
- 任何组件:使用空的 Vue 实例作为事件中心
const Event = new Vue()
,监听和触发自定义事件,使用 Event.$emit 触发事件,使用 Event.$on 监听事件,使用 Event.$off 解绑事件; - 复杂场景:vuex;
computed 和 watch 的区别和应用场景
computed
如果只需要动态计算某个属性的值,或着一个属性的值受多个属性的影响,可以使用 computed;
- 主要是为了简化模版内的复杂计算;
- 支持缓存,只有依赖的数据发生变化,才会重新计算;
- 不支持异步;
- 依赖多个数据时,只要有一个数据发生变化,就会触发重新计算;
- 计算的属性不需要在 data 中定义;
watch
如果一个属性的变化会影响多个属性,或者需要在数据变化后执行业务逻辑,可以使用 watch;
- 不支持缓存,数据改变,直接触发相应的操作;
- 支持异步;
- 监听函数需要两个参数,一个是新值,一个是旧值;
- 监听的属性必须在 data 中定义或者由 props 传递;
- watch 的依赖是单一的,每次只能监听一个数据的变化;
methods
methods 定义的方法需要手动调用,没有缓存,支持异步;
路由模式 hash 和 history 的区别
hash
- hash 路由基于 location.hash 实现;
- URL 中 # 号后面的内容就是当前页面的路由地址;
- 通过 hashchange 事件来监听 URL 的变化;
- URL 变化不会向服务器发送数据,是纯前端的实现;
history
- history 路由通过 HTML5 的两个 API:pushState 和 replaceState 实现;
- 通过 API 操作浏览器的历史记录来达到页面跳转的目的;
- 通过 popstate 事件来监听 URL 的变化;
- 需要服务器的配合;
vuex 是什么?应用场景
vuex 是 Vue 中的状态管理器,可以方便的实现组件之间的数据共享;
vuex 中的状态存储是响应式的,如果 store 中的数据发生变化,所有依赖这个数据的组件也会相应的更新;
vuex 有 store、getter、mutation、action、module 属性;
- store 是存储数据的核心,类似于实例中的 data;
- getter 类似于实例的 computed 属性,可以对 store 中的数据进行动态计算;
- mutation 用于更新 store 中的数据;
- action 类似于 mutation,不同的是,action 提交的是 mutation,而不是直接改变 store 中的数据,同时,action 中可以进行异步操作;
- module 属性则是将以上属性进行细分,在复杂项目中方便管理;
为什么 vuex 的 mutation 中不能进行异步操作?
- 每个 mutation 执行都会对应一个状态变更,这样 devtools 可以使用快照将其存储下来,方便追踪状态的变化;
- 如果 mutation 支持异步操作,就没有办法知道状态是何时变更的,无法进行状态追踪,给调试带来困难;
vuex 和 localStorage 的区别
- vuex 的数据在刷新页面后会丢失,而 localStorage 的数据不会;
- vuex 存储的数据在内存中,而 localStorage 则存储在本地,永久存储,除非手动删除;
- vuex 可以存储复杂类型的数据,localStorage 只能存储字符串类型,复杂对象需要使用 JSON 的 stringify 和 parse 来处理;
- vuex 的数据是响应式的;
v-if 和 v-for 的优先级
- v-for 的优先级更高,如果一个元素上同时出现 v-if 和 v-for,每次循环后都会进行条件判断,耗费性能;
- 可以在外层嵌套 template 并使用 v-if,然后在内部使用 v-for;
- 如果 v-if 和 v-for 必须出现在同一标签上,对数据进行过滤,可以使用 computed 属性代替 v-if;
v-show 和 v-if 的区别
- v-show 初始渲染成本高,通过 CSS 的 display 属性来控制元素的显示和隐藏;
- v-if 通过挂载和销毁来切换组件的显示状态,切换成本高;
- 如果频繁切换使用 v-show,其他情况使用 v-if;
vue 中 key 的原理及其必要性
- key 是给每个 vnode 的唯一标识,在 diff 算法中通过 key 来判断同一层级的 vnode 是否相同,从而确定复用或重建,减少 DOM 操作,更高效的更新 DOM;
- 不能使用 index 作为 key 值,因为 index 是连续的,如果变化的是开头或中间位置的元素,可能会导致后面所有的元素 key 值发生变化,引起不必要的渲染;
- v-for 中,同一元素的子元素必须具有独特的 key,否则会渲染出错;
- 使用 key 可以让共享组件达到刷新的目的,触发过渡,而不是就地复用;
vue 路由传参 params 和 query 的区别
- params 使用 name 属性,可以使用动态路由;
- query 使用 path 来定义路由地址;
- query 传递的参数会拼接在 URL 中;
- params 传递的参数在页面刷新后会丢失;
vue 路由的钩子函数
全局路由钩子
- beforeEach:参数有三,to,from,next,必须调用 next 方法,否则会阻塞路由;
- afterEach:参数有两个,to,from;
路由独享钩子
- beforeEnter:和全局路由 beforeEach 的参数相同,只在当前路由有效;
组件路由钩子
- beforeRouteEnter:进入组件前触发,无法获取组件的 this,因为组件尚未创建;
- beforeRouteUpdate:路由改变,但当前组件被复用时触发,可以获取组件的 this;
- beforeRouteLeave:离开组件前触发,可以访问组件的 this;
v-slot
简写为#
;
内容分发指令,父组件用动态内容替换子组件中的 slot 标签来实现组件的自定义,如果子组件中没有 slot 标签,则传递的内容被丢弃;
- 默认插槽,子组件中用 name 属性声明插槽的名称,没有 name 属性的 slot 是默认插槽,具有隐藏的
name='default'
属性,用于接收和父组件中 slot 名称不匹配的内容; - 具名插槽,子组件中有 name 属性的 slot 元素,在父组件中用
v-slot:name
来匹配对应的插槽; - 作用域插槽:又叫做接收 props 的插槽,在父组件中定义
v-slot='xxx'
或v-slot:name='xxx'
属性,使用xxx.子组件自定义属性名
即可接收子组件传递的自定义属性,不同的是前者是默认作用域插槽,后者是具名作用域插槽; - v-slot 简写时不能省略插槽名,包括默认插槽,如
#default
;
自定义 v-model
- 自定义组件必须有 model 和 props 属性;
- model 中使用 prop 定义当前组件要双向绑定的属性,使用 event 定义当前组件要触发的事件名
- props 中定义 model.prop 绑定的数据类型和默认值;
效果如下:
动态组件
- 引入组件并在 components 和 data 中定义;
- 使用 component 标签;
- 在 component 标签上使用 is 属性动态引入组件在 data 中声明的名称;
mixin
抽离组件的公共逻辑,提高代码复用;
格式和实例的数据格式相同,有 data、methods、mounted 等属性;
缺点:
- 变量来源不明确,不利于阅读;
- 多个 mixin 可能造成命名冲突;
- 可能出现多对多的情况,复杂度较高;
如何理解 vue 中的 diff 算法?
- diff 算法是虚拟 DOM 的必然产物,通过新旧 DOM 对比,可以将最小变化更新到真实的 DOM 上;
- diff 需要高效的执行,降低时间复杂度;
- 为了降低 watcher 的粒度,Vue 中每个组件只有一个 watcher 与之对应,只有引入 diff 算法才能精确的找到变化发生的位置;
- diff 是在组件执行更新函数时执行,比对上一次渲染的 vnode 和新生成的 vnode,这个过程称为 patch;
- diff 过程遵循深度优先、同层比较的策略,判断两个节点的 key、挂载点以及子节点是否相同来进行不同的操作;
- 比较两组子节点是算法的重点,通过假设头尾节点可能相同进行
头和头
、尾和尾
、头和尾
、尾和头
4 次比较,如果没有找到相同的节点,则使用通用方式遍历查找; - 通过 key 可以精准的找到相同的节点,因此整个 patch 过程非常高效,同时也表明了 key 的重要性;
vue 常用的修饰符
表单修饰符
.lazy
将 input 事件转为 change 事件,在 input 失去焦点时同步 value;.number
自动将用户输入转换为数字类型;.trim
自动过滤用户输入的首尾空格;
事件修饰符
.stop
阻止事件冒泡,stopPropagation;.default
阻止默认行为,preventDefault;.self
响应元素自身触发的事件,不处理捕获和冒泡,即event.currentTarget === event.target
;.capture
事件的触发从当前元素的顶层开始向下触发,捕获;.once
事件只触发一次;
v-bind 修饰符.sync
对 prop 实现双向绑定
如何获取 data 中某个数据的初始状态
this.$options.data()
vue 中数据相关属性的优先级
props > methods > data > computed > watch
Vue 性能优化
- 减少 data 中数据的嵌套层级,因为初始化会递归所有属性,添加 getter 和 setter;
- 在 v-for 中,如果每个元素都需要绑定事件,使用事件代理;
- key 保证唯一性;
- 更多情况下,使用 v-if;
- 使用路由懒加载,异步组件;
- 第三方模块按需导入;
- 使用 keep-alive 缓存组件
- 组件销毁前,及时解绑自定义事件;