Vuejs 2

对 MVVM 的理解

官方示意图

MVVM

  1. MVVM 由 Model、View、ViewModel 三部分组成;
  2. Model 表示数据层,数据和业务都在 Model 中定义和处理;
  3. View 表示视图层,负责数据的展示;
  4. 而 ViewModel 则是一个同步 Model 和 View 的对象,使用 new Vue 创建的实例就是 ViewModel;
  5. ViewModel 通过监听 Model 中的数据变化来控制 View 的更新,同时将 View 中由用户交互产生的数据变化同步给 Model,这个过程是由 ViewModel 连接起来的;
  6. MVVM 实现了 Model 和 View 的自动同步;

Vue 的优点

  1. 轻量化与可扩展性,核心功能就是视图与数据的双向绑定,其他强大的功能由插件扩展;
  2. 双向数据绑定,操作数据即可更新视图,视图交互也会改变数据;
  3. 组件化,单文件组件的可读性高,低耦合强关联,提高代码复用性;
  4. 虚拟 DOM,原生的 DOM 操作每次都会触发重新渲染,非常耗费性能,而且用户体验也不好,而 Vue 通过异步将多次 DOM 操作引起的渲染合并为一次,并且计算出最小变化 ,降低了 DOM 操作的成本,提升了用户体验;

描述 Vue 实例的生命周期

生命周期是指实例从构建到销毁的整个过程,Vue 实例的生命周期分为初始化阶段、挂载阶段、运行(更新)阶段以及销毁(卸载)阶段;

  1. 首先是初始化阶段,从 new Vue 实例开始,会初始化生命周期钩子函数以及默认事件,在这些工作完成后,触发 beforeCreate;
  2. 接着初始化 data、methods 等方法,检索依赖并注入,这个时候,实例已经被完全创建,但真实 DOM 还未生成,这些工作完成后,会触发 Created;
  3. 然后进入挂载阶段,首先是模版编译,检查实例的 el 属性,如果没有 el,则等待手动调用实例的 $mount 方法,然后检查 template 属性,如果没有 template 属性,则使用 el 挂载的 DOM 节点作为 template,接着根据 template 和数据生成 render 函数并编译成真实的 DOM,触发 beforeMount;
  4. 接着给实例添加 $el 属性,并用编译生成的 DOM 替换 el 挂载的 DOM,然后触发 Mounted,实例初始化和挂载完成,进入运行更新阶段;
  5. 在数据更新前,会触发 beforeUpdate,此时 data 中的数据已经发生变化,但 View 还未同步,而且由于虚拟 DOM 还没有重新渲染和打补丁,所以多次数据变化引起的视图更新不会触发额外的渲染;
  6. 在虚拟 DOM 更新完成后,会触发 Updated,更新完成;
  7. 在 keep-alive 缓存的组件被激活时,会触发 activated,失活时会触发 deactivated;
  8. 在销毁阶段,实例销毁前,可以调用 beforeDestroy,此时实例仍然可用,可以进行解除绑定、销毁子组件、销毁自定义事件等操作;
  9. 实例销毁完成后,触发 Destroyed,此时实例已经完全销毁不可用,销毁完成;

父子组件的生命周期

  1. 加载渲染过程中,先创建并挂载父组件,然后挂载并创建子组件,在这个过程中,依次触发以下生命周期钩子函数:父组件 beforeCreate,父组件 Created,父组件 beforeMount,子组件 beforeCreate,子组件 Created,子组件 beforeMount,子组件 Mounted,父组件 Mounted,挂载完成;
  2. 更新过程,先更新子组件,然后更新父组件,依次触发以下生命周期钩子函数:父组件 beforeUpdate,子组件 beforeUpdate,子组件 Updated,父组件 Updated,更新完成;
  3. 如果父组件的更新不影响子组件,则只触发父组件的 beforeUpdate 和 父组件的 Updated;
  4. 销毁过程,由内到外,依次触发以下生命周期钩子函数:父组件 beforeDestroy,子组件 beforeDestroy,子组件 Destroyed,父组件 Destroyed,销毁完成;

如何理解单向数据流

  1. 单向数据流就是数据在某个节点发生变化后,只会影响一个方向上的其他节点,即,数据由父组件向子组件进行传递和更新;

  2. 在组件通讯中,父组件使用 props 属性将数据传递给子组件;

  3. 但 props 中的数据只能由父组件直接修改,如果子组件想要修改,需要使用 $emit 触发事件去通知父组件修改;

  4. 如果子组件在已有数据上想要进行修改,可以使用以下两种方式:

    • 在子组件的 data 中将 props 中的数据作为初始值进行拷贝;
    • 如果只是想对 props 中的数据进行转换,可以使用计算属性 computed;

为什么是单向的,不能是双向的?

如果父组件的数据通过 props 传递给子组件,而子组件更新了 props,这会导致父组件和其他关联组件的数据更新,View 也会随数据变化而更新,其他组件却不知道数据变化的来源,毫无疑问,这会导致严重的数据紊乱和不可控;

$nextTick()

  1. $nextTick是 Vue 在 DOM 更新完成后触发的回调;
  2. $nextTick可以保证所有 DOM 是最新的,并且渲染已完成,依赖 DOM 的操作可以使用最新的 DOM;
  3. $nextTick属于微任务,返回的是 promise 对象;

为什么需要它?

  1. 因为 Vue 的 DOM 更新是异步的,在观察到数据变化后,Vue 会开启一个队列来缓冲同一事件循环中发生的所有更改,并在下一次事件循环中清空队列并执行 DOM 更新,这样可以去掉重复数据和多次更改造成的不必要计算和 DOM 渲染;
  2. 所以需要 $nextTick 来保证所有 DOM 更新已完成,才能执行依赖 DOM 的操作;

Vue 双向绑定(响应式)的原理

官方示意图

响应式

  1. 采用数据劫持和发布-订阅者模式;
  2. 在初始化阶段,通过 Object.defineProperty 劫持属性的 getter 和 setter,监听数据变化,同时,为每个属性创建 Dep 用来存储依赖(watcher);
  3. 在编译模版阶段,对使用的属性触发 getter 进行依赖收集,将 watcher 添加到属性对应的 Dep 中;
  4. 在数据变化时,触发属性的 setter,循环依赖列表,通知 Dep 中的所有 watcher 执行更新;

Vue 实例中的 data 是对象,组件中的 data 是函数?

  1. 组件的目的是用来复用的;
  2. JS 中对象的赋值是引用传递;
  3. 如果组件中的 data 是对象,那么所有组件会共享一份数据,修改会相互影响,同时也没有了复用的可能性(因为所有组件的数据都相同);
  4. 而如果是函数的话,每个组件都会维护一份属于自己的数据拷贝,修改不会相互影响;
  5. Vue 根实例是不会被复用的,不存在对象引用的问题,所以 data 是对象没有问题;

组件通讯

  1. 父子组件:props 和 $emit;
  2. 隔代组件:provide 和 inject;
  3. 任何组件:使用空的 Vue 实例作为事件中心const Event = new Vue(),监听和触发自定义事件,使用 Event.$emit 触发事件,使用 Event.$on 监听事件,使用 Event.$off 解绑事件;
  4. 复杂场景:vuex;

computed 和 watch 的区别和应用场景

computed

如果只需要动态计算某个属性的值,或着一个属性的值受多个属性的影响,可以使用 computed;

  1. 主要是为了简化模版内的复杂计算;
  2. 支持缓存,只有依赖的数据发生变化,才会重新计算;
  3. 不支持异步;
  4. 依赖多个数据时,只要有一个数据发生变化,就会触发重新计算;
  5. 计算的属性不需要在 data 中定义;

watch

如果一个属性的变化会影响多个属性,或者需要在数据变化后执行业务逻辑,可以使用 watch;

  1. 不支持缓存,数据改变,直接触发相应的操作;
  2. 支持异步;
  3. 监听函数需要两个参数,一个是新值,一个是旧值;
  4. 监听的属性必须在 data 中定义或者由 props 传递;
  5. watch 的依赖是单一的,每次只能监听一个数据的变化;

methods

methods 定义的方法需要手动调用,没有缓存,支持异步;

路由模式 hash 和 history 的区别

hash

  1. hash 路由基于 location.hash 实现;
  2. URL 中 # 号后面的内容就是当前页面的路由地址;
  3. 通过 hashchange 事件来监听 URL 的变化;
  4. URL 变化不会向服务器发送数据,是纯前端的实现;

history

  1. history 路由通过 HTML5 的两个 API:pushState 和 replaceState 实现;
  2. 通过 API 操作浏览器的历史记录来达到页面跳转的目的;
  3. 通过 popstate 事件来监听 URL 的变化;
  4. 需要服务器的配合;

vuex 是什么?应用场景

  1. vuex 是 Vue 中的状态管理器,可以方便的实现组件之间的数据共享;

  2. vuex 中的状态存储是响应式的,如果 store 中的数据发生变化,所有依赖这个数据的组件也会相应的更新;

  3. vuex 有 store、getter、mutation、action、module 属性;

    • store 是存储数据的核心,类似于实例中的 data;
    • getter 类似于实例的 computed 属性,可以对 store 中的数据进行动态计算;
    • mutation 用于更新 store 中的数据;
    • action 类似于 mutation,不同的是,action 提交的是 mutation,而不是直接改变 store 中的数据,同时,action 中可以进行异步操作;
    • module 属性则是将以上属性进行细分,在复杂项目中方便管理;

为什么 vuex 的 mutation 中不能进行异步操作?

  1. 每个 mutation 执行都会对应一个状态变更,这样 devtools 可以使用快照将其存储下来,方便追踪状态的变化;
  2. 如果 mutation 支持异步操作,就没有办法知道状态是何时变更的,无法进行状态追踪,给调试带来困难;

vuex 和 localStorage 的区别

  1. vuex 的数据在刷新页面后会丢失,而 localStorage 的数据不会;
  2. vuex 存储的数据在内存中,而 localStorage 则存储在本地,永久存储,除非手动删除;
  3. vuex 可以存储复杂类型的数据,localStorage 只能存储字符串类型,复杂对象需要使用 JSON 的 stringify 和 parse 来处理;
  4. vuex 的数据是响应式的;

v-if 和 v-for 的优先级

  1. v-for 的优先级更高,如果一个元素上同时出现 v-if 和 v-for,每次循环后都会进行条件判断,耗费性能;
  2. 可以在外层嵌套 template 并使用 v-if,然后在内部使用 v-for;
  3. 如果 v-if 和 v-for 必须出现在同一标签上,对数据进行过滤,可以使用 computed 属性代替 v-if;

v-show 和 v-if 的区别

  1. v-show 初始渲染成本高,通过 CSS 的 display 属性来控制元素的显示和隐藏;
  2. v-if 通过挂载和销毁来切换组件的显示状态,切换成本高;
  3. 如果频繁切换使用 v-show,其他情况使用 v-if;

vue 中 key 的原理及其必要性

  1. key 是给每个 vnode 的唯一标识,在 diff 算法中通过 key 来判断同一层级的 vnode 是否相同,从而确定复用或重建,减少 DOM 操作,更高效的更新 DOM;
  2. 不能使用 index 作为 key 值,因为 index 是连续的,如果变化的是开头或中间位置的元素,可能会导致后面所有的元素 key 值发生变化,引起不必要的渲染;
  3. v-for 中,同一元素的子元素必须具有独特的 key,否则会渲染出错;
  4. 使用 key 可以让共享组件达到刷新的目的,触发过渡,而不是就地复用;

vue 路由传参 params 和 query 的区别

  1. params 使用 name 属性,可以使用动态路由;
  2. query 使用 path 来定义路由地址;
  3. query 传递的参数会拼接在 URL 中;
  4. params 传递的参数在页面刷新后会丢失;

vue 路由的钩子函数

  1. 全局路由钩子

    • beforeEach:参数有三,to,from,next,必须调用 next 方法,否则会阻塞路由;
    • afterEach:参数有两个,to,from;
  2. 路由独享钩子

    • beforeEnter:和全局路由 beforeEach 的参数相同,只在当前路由有效;
  3. 组件路由钩子

    • beforeRouteEnter:进入组件前触发,无法获取组件的 this,因为组件尚未创建;
    • beforeRouteUpdate:路由改变,但当前组件被复用时触发,可以获取组件的 this;
    • beforeRouteLeave:离开组件前触发,可以访问组件的 this;

v-slot

简写为#

内容分发指令,父组件用动态内容替换子组件中的 slot 标签来实现组件的自定义,如果子组件中没有 slot 标签,则传递的内容被丢弃;

  1. 默认插槽,子组件中用 name 属性声明插槽的名称,没有 name 属性的 slot 是默认插槽,具有隐藏的name='default'属性,用于接收和父组件中 slot 名称不匹配的内容;
  2. 具名插槽,子组件中有 name 属性的 slot 元素,在父组件中用v-slot:name来匹配对应的插槽;
  3. 作用域插槽:又叫做接收 props 的插槽,在父组件中定义v-slot='xxx'v-slot:name='xxx'属性,使用xxx.子组件自定义属性名即可接收子组件传递的自定义属性,不同的是前者是默认作用域插槽,后者是具名作用域插槽;
  4. v-slot 简写时不能省略插槽名,包括默认插槽,如#default

自定义 v-model

  1. 自定义组件必须有 model 和 props 属性;
  2. model 中使用 prop 定义当前组件要双向绑定的属性,使用 event 定义当前组件要触发的事件名
  3. props 中定义 model.prop 绑定的数据类型和默认值;

自定义 v-model

效果如下:

v-model

动态组件

  1. 引入组件并在 components 和 data 中定义;
  2. 使用 component 标签;
  3. 在 component 标签上使用 is 属性动态引入组件在 data 中声明的名称;

mixin

抽离组件的公共逻辑,提高代码复用;

格式和实例的数据格式相同,有 data、methods、mounted 等属性;

缺点:

  1. 变量来源不明确,不利于阅读;
  2. 多个 mixin 可能造成命名冲突;
  3. 可能出现多对多的情况,复杂度较高;

如何理解 vue 中的 diff 算法?

  1. diff 算法是虚拟 DOM 的必然产物,通过新旧 DOM 对比,可以将最小变化更新到真实的 DOM 上;
  2. diff 需要高效的执行,降低时间复杂度;
  3. 为了降低 watcher 的粒度,Vue 中每个组件只有一个 watcher 与之对应,只有引入 diff 算法才能精确的找到变化发生的位置;
  4. diff 是在组件执行更新函数时执行,比对上一次渲染的 vnode 和新生成的 vnode,这个过程称为 patch;
  5. diff 过程遵循深度优先、同层比较的策略,判断两个节点的 key、挂载点以及子节点是否相同来进行不同的操作;
  6. 比较两组子节点是算法的重点,通过假设头尾节点可能相同进行头和头尾和尾头和尾尾和头 4 次比较,如果没有找到相同的节点,则使用通用方式遍历查找;
  7. 通过 key 可以精准的找到相同的节点,因此整个 patch 过程非常高效,同时也表明了 key 的重要性;

vue 常用的修饰符

表单修饰符

  1. .lazy将 input 事件转为 change 事件,在 input 失去焦点时同步 value;
  2. .number自动将用户输入转换为数字类型;
  3. .trim自动过滤用户输入的首尾空格;

事件修饰符

  1. .stop阻止事件冒泡,stopPropagation;
  2. .default阻止默认行为,preventDefault;
  3. .self响应元素自身触发的事件,不处理捕获和冒泡,即 event.currentTarget === event.target
  4. .capture事件的触发从当前元素的顶层开始向下触发,捕获;
  5. .once事件只触发一次;

v-bind 修饰符
.sync对 prop 实现双向绑定

如何获取 data 中某个数据的初始状态

this.$options.data()

vue 中数据相关属性的优先级

props > methods > data > computed > watch

Vue 性能优化

  1. 减少 data 中数据的嵌套层级,因为初始化会递归所有属性,添加 getter 和 setter;
  2. 在 v-for 中,如果每个元素都需要绑定事件,使用事件代理;
  3. key 保证唯一性;
  4. 更多情况下,使用 v-if;
  5. 使用路由懒加载,异步组件;
  6. 第三方模块按需导入;
  7. 使用 keep-alive 缓存组件
  8. 组件销毁前,及时解绑自定义事件;