likes
comments
collection
share

Vue 高频原理篇+详细解答之 二

作者站长头像
站长
· 阅读数 18

大家好,我是林一一,我们又见面了,这是一篇关于 Vue 高频原理的面试题,上一篇的面试题已经发过了,建议读完上一篇的 Vue 原理再来读这一篇。

n久没有发文章了,这一篇文章在草稿箱里面躺了很久了😥。上下两篇都是针对 Vue2.x 版本的原理。以后会添加上 Vue3的原理解析?!😂。

上一篇 Vue 高频原理文章

Vue高频原理的解答上

PS:记得点赞啊,亲们 😥😋

面试题篇

13. vue.$set(obj, key, value) 方法的实现原理

小tip:用于给 data 中响应式的对象/数组添加相应式的数据,才会触发试图更新。

  • $set() 之所以给数组和对象添加新属性可以触发相应式的数据,是因为最后调用 defineReactive() 给新增的属性添加 get/set
  • 调用 $set() 给对象新增属性时可以触发对象依赖收集的 Watcher 去更新。
  • 调用 $set() 修改数组索引时,内部会通过调用 重写的 splice() 方法更新数组。
  • 调用set() 不能给根实例 vm_data 添加属性,因为性能消耗大。
  • 扩展
// \src\core\observer\index.js

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }

  // 给数组添加响应式数据,调用数组的 splice 方法
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

// 给对象添加响应式数据,直接添加
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }

// 这里是给根实例 vm/_data 添加属性,会报错。
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
// 给不是响应式的对象添加属性,直接加上即可,但是不会更新视图
  if (!ob) {
    target[key] = val
    return val
  }

// 最后 defineReactive 给添加的属性增加 `get/set` 
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

14. vue 为什么需要虚拟 DOM?

主要由两个点:减少 DOM 操作,可以跨平台

  • visual DOM 是真实 dom 的描述对象,直接操作 DOM 可能会引发 DOM 的回流和重绘,性能消耗很大。
  • js层的操作效率高,将真实DOM转化成对象来操作,通过 diff 算法来比较对象差异更新 DOM,可以提高性能。
  • 而且虚拟 DOM 不依赖平台的差异,能实现跨平台运行。

在 vue 已经生成 AST 以后,如果页面的 DOM 某个节点被删除,不会影响到已经生成的 AST。

15. ⭐ diff 算法原理

Vue 中的 diff 算法的比较是 平级比较,没有跨级比较。使用深度递归和双指针遍历

  • 先比较前后虚拟 DOM,是否是相同的根节点。
  • 相同的节点比较属性,复用旧的节点。
  • 然后比较子的节点,新虚拟 dom 的子节点没有,老节点直接复用。新虚拟 dom 的子节点和老虚拟 DOM 子节点都有,比较标签名、属性等。
  • 优化比较:双指针头头比较,尾尾比较,头尾比较,尾头比较,对比之后进行复用

源码的 patchVnode()patchChildren() 都是虚拟节点的比较

16. vue 通过数据劫持可以得知响应式数据的变化,为什么还需要虚拟dom 进行 diff 算法比较?

在 vue1.0时期,确实只使用了数据劫持做到响应式的数据变化,2.0时才引入diff算法。

  • vue1.0时通过给每一个属性都添加一个渲染 Watcher (2.0是取值是添加一个 dep 来收集user watcher),但是每一个属性都有一个 Watcher 对性能消耗很大。
  • 所以最后采用的是组件级的 Watcher 来配合 diff 算法更新视图。组件级数据更新但是不知道那里更新所以需要配合 diff 算法来比较。

17. Vue 中 key 属性的作用和原理

v-for 中的 key。 key 的存在使 diff 操作可以更准确、更快速。

  • Vue 在调用 patch 生成真实 dom 过程中通过对比 Vnode 中的 key 可以判断两个虚拟节点是否相同。key 相同可以复用老的节点,可以提高性能。
  • 没有 key 会导致更新出现问题,尽量不使用下标 index 作为 key。

因为index 作为 key 和没有设置 key 是一样的,假设有4个 li 标签(A,B,C,D)项删除第一项li标签,但是删除的是最后一项 li,这是 diff 算法的比较的复用策略导致的。上面的比较过程是B复用A,C复用B,D复用C, D被删除,不论数组怎么变化下标 index 都是从0,1,2,3...开始的重写渲染后还是不会变化。所有不要使用index作为下标。

组件部分

18. 对组件化开发的理解

  • 复用性高,能大幅度提高开发效率,还可以进行单元测试。
  • vue 中的每一个组件都对应一个 Watcher,组件发生更新只需要 Watcher 更新即可。
  • vue 中的数据流都是挡线的数据流

19. vue 中的函数式组件优势实现原理

函数式组件就是一个普通函数。

Vue.component('functional', {
  functional: true,
   props: {
    items: {
      type: Array,
      required: true
    }
  },
  render: h => h('div', {}, 'hello')
})
  • 函数式组件的优势:无状态(没有响应式的数据data属性),无生命周期,没有 this。性能高:内部没有任何生命周期处理函数
  • 具备组件的特点:可以传参 props,可以编写render函数
  • 使用场景:只负责渲染,不需要状态。

20.老生常谈之,Vue 中组件的通信方式有哪些和区别是什么?

Vue 中的组件的传递方式常用的主要有 7 种。

  • props$emit 父子组件的传递。props 父组件向子组件传递,$emit 子组件向父组件传递。这个方法的内部使用的式发布订阅模式,源码位置 \src\core\instance\event.js
  • $parent$children 组件实例的获取,$parent 获取当前组件的父组件的实例来执行相应操作,$children数组,获取当前组件的子组件实例,通过实例的 _uid 辨别实例。实例获取后可以执行添加数据。
// src\core\instance\lifecycle.js 
export function initLifecycle (vm: Component) {
  const options = vm.$options
  
  // 获取父组件 
  let parent = options.parent
  if (parent && !options.abstract) {  // 排除抽象组件
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm) // 父组件记录子组件的实例放入到数组中
  }

  vm.$parent = parent // 增加 $parent 属性表示父组件
  vm.$root = parent ? parent.$root : vm
// ...
}

注意通过 $children 添加的数据,也不是响应式数据,数组中的实例也不保证顺序。

  • $attrs 和 $listeners 子组件批量获取父组件传入的属性和事件,具备响应式,在组件上写入的数据会被写入到虚拟节点$vnode.data.attrs 中,例如<my-component name="林一一" age=18 @handle="handle"></my-component>$vnode.data.attrs={name:"林一一", age:18},通过 this.$attrs 获取属性, this.$listeners获取事件,需要注意的是没有使用 props 接收的变量才可以使用 $attrs 获取,可以声明inheritAttrs:false
// \src\core\instance\render.js
export function initRender (vm: Component) {
  vm._vnode = null
  vm._staticTrees = null
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode

  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // 创建的虚拟 DOM 
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  const parentData = parentVnode && parentVnode.data
 //... some code
    // defineReactive,在$attrs  $listeners 中的数据具备响应式
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
  • provide和inject,父组件中通过 provide 来提供变量,子组件中通过 inject 来注入提供的变量。只要父组件中有 provide 提供的数据,深层的子组件中依然可以 inject 不停的向上查找父组件提供的 provide 属性,找到后定义在当前组件的身上。provide和inject不是响应式的,除非申明的属性是响应式的

  • $refs 可以获取真实 DOM 节点,也可以获取组件实例。在 v-for 中获取的 DOM 是一个数组。

  • eventBus 平级组件的数据传递方式

  • vuex 状态管理

  • slot 插槽

父子组件的通信考虑 props/$emit, $parent/$children, $refs,隔代组件的通信考虑使用provide/inject, $attrs/$listener,平级组件可以考虑 EventBus/VuexVuex 使用于所有的组件通信。

21. 组件通信的 $attrs 出现的目的和 provide/inject 能解决 $attrs 的问题嘛?

  • $attrs 主要用于批量的获取父组件传递的数据,传入的数据具备响应式
  • provide/inject 可以跨级的传递数据,传入的数据除非是响应是的数据否则不具备响应式数据。

22. v-if 和 v-for 哪个优先级更高

v-if 和 v-for 同时最用在同一个节点或组件上 vue 会抛出异常,不建议同时使用。

  • v-if 和 v-for 编译阶段 v-for 的优先级比v-if高,如果需要使用到这样的场景可以考虑使用computed 代替 v-if 的判断。
  • 扩展
// vue\src\compiler\codegen\index.js
// ...
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)  // 先循环
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)   // 再判断条件
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  }
  // ...

23.Vue 中 v-model 的实现原理

v-model 是 vue 提供的一种语法糖,

  • v-model 使用在表单中,也可以使用在组件上,实现双向数据绑定;v-model 对输入法做了处理,防止中文在输入时就进行双向数据绑定,要注意如果采用<input :value="msg" @input="chang" /> 代替v-model 是不会对输入法有限制,这就是两者的差别。
  • v-model 原理:使用 v-model 指令在表单 input、textarea、select、等表单元素 上可以创建双向数据绑定,v-model 作用在不同的表单元素上,会触发不同的表单相应事件,比如
  • texttextarea 元素使用 value 属性和 input 事件·
  • checkboxradio 使用 checked 属性和 change 事件
  • select 字段将 value 作为 propchange 作用事件。

一个页面内有大量的表单,导致页面卡死,如果使用v-model可以采用修饰符.lazy,类似防抖

// ...

// 这里是 v-model 作用在组件上的调用方法
  if (el.component) {
    genComponentModel(el, value, modifiers);  // modifiers代表模板中添加的修饰符
    // component v-model doesn't need extra runtime
    return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers);
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers);
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers);
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers);
  } else if (!config.isReservedTag(tag)) {
    // ...
  }

要注意:v-model 也可以使用在组件上而不是只可以使用在表单元素上。genComponentModel() 方法,会将使用在组件上的 v-modelAST树 上添加model={value,expression, callback} 属性。

24. Vue.use() 的作用和实现

  • Vue.use() 是用于插件的,可以在插件中扩展全局的组件和指令等。例如插件 VueRouter
  • Vue.use() 会调用插件的 install 方法,将会 Vue 中的构造函数默认传入。如果插件没有install 方法会直接调用插件
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 判断是否被缓存过,不能多次缓存
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // 取出第一项
    const args = toArray(arguments, 1)
    args.unshift(this)

      // 有install,调用 install 方法,
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {   // 没有install,直接调用插件(函数),
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }

25.Vue 组件中属性 name 的作用

只有作为组件选项时起作用。name 具备3个作用

    1. 组件递归时,必须具备 name 属性,因为需要使用到 name 属性查找组件。
    1. 当使用 keep-alive 时,可以使用这个 name 进行过滤。例如 <keep-alive exclude="Home"></keep-alive>
    1. 具备 name 属性的命名组件可以方便我们在 vue-devtools 调式。

26. Vue 中自定义指定的实现原理。

自定义指令钩子

自定义指令也具备和组件一样的声明周期钩子。钩子都是可选的

    1. bind: 只调用一次,在指令绑定到节点上时被调用。
    1. inserted:被绑定指令的元素插入到父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
    1. update: VNode 更新时也可能在 VNode 更新之前调用。
    1. componentUpdated: 指令所在的VNode及其子 VNode 更新后调用。
    1. unbind: 只调用一次,在指令和元素解绑时调用。

自定义组件的实现原理

    1. 在创建AST语法树时,遇到指定会在当前元素上添加 directives 属性
    1. 通过 genDirectives 生成指令代码
    1. patch 生成真实DOM前,将指令的钩子提取出来放入到cbs,在patch过程中执行对应的钩子
    1. 调用对应指令的方法

27.Vue-Router 的导航守卫和执行流程。

Vue-Router 的导航守卫(钩子)中分为:全局守卫,路由守卫和组件守卫。路由执行流程

  • 触发进入其他路由。
  • 调用要离开路由的组件守卫 beforeRouteLeave
  • 调用全局前置守卫:beforeEach
  • 在重用的组件里调用 beforeRouteUpdate
  • 调用路由独享守卫 beforeEnter
  • 解析异步路由组件。
  • 在将要进入的路由组件中调用 beforeRouteEnter 组件守卫
  • 调用全局解析守卫 beforeResolve
  • 导航被确认。
  • 调用全局后置钩子的 afterEach 钩子。
  • 触发DOM更新 (mounted)
  • 执行 beforeRouteEnter 守卫中传给 next 的回调函数

上面的顺序调用是 vueRouter 在内部将上面的钩子函数放入到一个队列中最后,调用runQueue()依次将队列中的钩子执行。

28. Vue-Router 中的路由模式和区别

Vue-Router 中有三种路由模式分别是 history/hash/abstractvueRouter 默认是 hash 模式。

  • hash 模式,使用 URL 中的 # 作为路由,支持所有的浏览器。hash+popState/hashChange 兼容性好,不刷新页面,因为服务端无法获取hash值,对 seo 优化不好,比如https://baidu.com/a 会直接显示未找到,而 https://baidu.com/#a 还是显示的是百度的首页,因为服务器获取不到# 后面的数据。
  • history 模式,浏览器提供 historyApi( go/back/forward ) 和新增的两个 popState()/replaceState (),这两个api可以在不刷新的情况下读取浏览器的历史记录,history模式美观,但是需要服务端的至此否则会出现404状态。
  • abstract 模式,在不支持浏览器的环境下使用。

上线时采用history 模式,cli 内部的配置可以防止出现404的情况。

29. Vux 的理解

Vue 高频原理篇+详细解答之 二

Vuex 是为了大型项目开发的,实现不同组件之间的状态管理(数据共享)。适用于多个组件之间的数据交互

  • Vuex 应用核心就是 store(仓库)store 就是一个容器,包含了应用中大部分的 state(状态)
  • Vuex 是单向的数据流,组件不能直接修改容器中 state 的状态(数据)
  • 改变 store 中的状态的唯一途径就是显示 commit(提交) mutation,组件状态 state 发生变化。
  • 在非严格模式 strict:false 允许直接来修改状态 state,但是一帮都是在严格模式中通过mutations 来修改数据。
  • 缺点:Vuex 的数据不能持久化。 vuex 在页面刷新后数据不会再存在

主要有以下几个模块:

  • State: 定义了应用状态的数据结构,可以在这里设置默认的初始状态
  • Getter: 允许组件从 store 中获取数据, mapGetters 辅助函数仅仅是将 store 中的 getter映射到计算属性。
  • Mutation: 唯一更改 store 中状态的方法,且必须是同步函数。
  • Action: 用于提交 mutation, 而不是直接变更状态,可以包含任意异步操作。
  • Module: 允许将单一的 store 拆分为多个 store且同时保存在单一的状态树中 扩展:action 和 mutation 之间的区别
  1. mutation对象 是 Vuex中唯一一个可以修改 state 状态的方式。mutation对象 中方法必须是同步的,因为提交的 mutation 触发时回调函数还没有被调用,那么数据的状态就无法准确知道是不是最新的。
  2. action对象 用于提交的是一个 mutation 事件,不可以修改数据的状态。action 中的方法支持异步。

30.vue 中的性能优化策略有哪些

  • 对象或数组的层级不要过深。
  • 善于使用 computed 将值缓存,不频繁取值
  • 设置v-for中的key值,便于diff算法的优化
  • 分辨好 v-show/v-for 的使用场景
  • 结合业务逻辑封装组件,控制组件的粒度
  • 使用keep-alive缓存组件
  • 。。。

参考

Vue 模板编译原理

Vue.nextTick 的原理和用途

Vue 组件渲染原理

Vue渲染过程浅析

Vue render函数

vue中8种组件通信方式

你了解v-model的语法糖吗

手写Vue2.0源码(六)-diff算法原理

Vue 中的自定义指令