首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Vue 2 常见面试题速查

Vue 2 常见面试题速查

作者头像
Cellinlab
发布2023-05-17 16:00:03
发布2023-05-17 16:00:03
1.4K0
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

# Vue 的基本原理

当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty 将它们转为 getter / setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

# 双向数据绑定原理

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty 来劫持各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相对应的监听回调。主要分以下步骤:

  1. 需要 Observer 的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter,这样给这个对象的某个值赋值,就会触发 setter,那么就能监听到数据变化;
  2. Compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听函数的订阅者,一旦有数据变动,收到通知,更新视图
  3. Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情时:
    • 在自身实例化时往属性订阅器(Dep)中添加自身;
    • 自身必须有一个 update() 方法;
    • 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调
  4. MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 Model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到 数据变化 -> 视图更新视图交互变化 -> 数据 Model 变更的双向绑定效果

# Vue data 中某一个属性的值发生改变后,视图会立即同步执行渲染吗

不会立即同步执行渲染。

Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的时间循环 tick 中,Vue 刷新队列并执行实际工作。

# v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?

v-if 和 v-for 同级

代码语言:javascript
复制
<div id="demo">
  <h1>v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?</h1>
  <p v-for="child in children" v-if="isFolder">{{child.title}}</p>
</div>

代码语言:javascript
复制
// app.$options.render
// 循环并对每一个循环子项做判断
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?")]),_v(" "),_l((children),function(child){return (isFolder)?_c('p',[_v(_s(child.title))]):_e()})],2)}
})

v-if 和 v-for 嵌套

代码语言:javascript
复制
<div id="demo">
  <h1>v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?</h1>
  <template v-if="isFolder">
    <p v-for="child in children">{{child.title}}</p>
  </template>
</div>

代码语言:javascript
复制
// app.$options.render
// 如果判断不通过就不展开循环
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?")]),_v(" "),(isFolder)?_l((children),function(child){return _c('p',[_v(_s(child.title))])}):_e()],2)}
})

产生原因

代码语言:javascript
复制
// src\compiler\codegen\index.js
export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  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'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    // ... 
  }
}

结论

  • v-for 优先于 v-if 被解析
  • 如果同级出现,每次渲染会先循环再判断,浪费性能
  • 可以通过将if置于for外层(使用template)解决

补充

  • 如果每个循环子项的判断情况值独立,可通过计算属性过滤出需要渲染的所有子项直接将循环数组绑定为过滤结果

# Vue组件data为什么必须是个函数而Vue的根实例则没有限制?

代码语言:javascript
复制
<div id="demo">
  <h1>Vue组件data为什么必须是个函数而Vue的根实例则没有限制?</h1>
  <comp></comp>
  <comp></comp>
</div>
<script>
  Vue.component('comp', {
    template: '<div @click="counter++">{{counter}}</div>',
    data: { counter: 0 }
  })
  const app = new Vue({
    el: '#demo',
  });
</script>

data使用逻辑

代码语言:javascript
复制
// src\core\instance\state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // ...
}

结论

  • Vue组件一般会有多个实
    • 如果对象形式定义data,会导致所有实例共用一个data对象,数据会相互影响
    • 使用函数形式定义,在initData时会将其作为工厂函数返回新的data对象,有效解决多实例数据相关污染
  • 根实例中不存在该限制是因为根实例只有一个,不需要考虑相互影响
  • 组件会走校验,根实例不会走校验,无警告

# key的作用和原理

代码语言:javascript
复制
<div id="demo">
  <p v-for="item in items" :key="item">{{item}}</p>
</div>
<script>
  const app = new Vue({
    el: '#demo',
    data: {
      items: ['a', 'b', 'c', 'd', 'e'],
    },
    mounted () {
      setTimeout(() => {
        this.items.splice(2, 0, 'f')
      }, 1000)
    }
  });
</script>

代码语言:javascript
复制
// src\core\vdom\patch.js 
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

  • 作用 diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行对比,然后找出差异
  • 结论
    • key 的作用主要是为了更高效地更新虚拟 DOM,其原理是 vue 在 patch 过程中通过 key 可以精准判断两个节点是否相同,从而避免频繁更新不同元素,使得整个 patch 过程更加高效,减少了 DOM 操作量,提高性能
    • 若不设置 key 还可能在列表更新时引发一些隐蔽的 bug
    • vue 中在使用相同标签名元素的过渡切换时,也会使用 key 属性,目的是为了让vue可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果

# 怎么理解vue中的diff算法

必要性 每个组件对应一个watcher,组件中可能存在很多个data中的key的使用,为了在执行过程中精确知道谁在发生变化,需要使用diff比较

代码语言:javascript
复制
// src\core\instance\lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // ...
  return vm
}

执行方式 patchVnode()是diff发生的地方,整体策略是:深度优先,同层比较

代码语言:javascript
复制
// src\core\vdom\patch.js
function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // ...
  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

高效性

代码语言:javascript
复制
// src\core\vdom\patch.js
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

总结

  • diff算法是虚拟DOM技术的必然产物,通过新旧虚拟DOM比较(即diff),将变化的地方更新在真实DOM上;另外,也需要diff高效的执行对比过程,从而降低时间复杂度为O(n)
  • vue 2.x中为了降低Watcher的粒度,每个组件只有一个Watcher与之对应,只有引入diff才能精确找到发生变化的地方
  • vue 中diff执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果oldVnode和新的渲染结果newVnode,此过程称为patch
  • diff 过程遵循深度优先,同层比较策略:
    • 两个节点之间比较会根据他们是否拥有子节点或文本节点做不同操作;
    • 比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次对比尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;
    • 借助key通常可以非常精确找到相同节点,因此整个patch过程非常高效

# 对组件化的理解

源码分析

  1. 组件定义 全局定义
代码语言:javascript
复制
Vue.component('comp', {
  template: '<div>this is a component</div>'
})
// 具体实现见
// src\core\global-api\assets.js
// src\core\global-api\extend.js

单文件组件:vue-loader会编译template为render函数,最终导出的依然是组件配置对象

代码语言:javascript
复制
<template>
  <div>this is a component</div>
</template>

  1. 组件化优点 src\core\instance\lifecycle.js -> mountComponent() 组件、Watcher、渲染函数和更新函数直接的关系
  2. 组件化实现 构造函数 src\core\global-api\extend.js 实例化及挂载 src\core\vdom\patch.js createElm()

总结

  • 组件是独立和可复用的代码组织单元。组件系统是Vue核心特性之一,使开发者使用小型、独立和通用可复用的组件构建大型应用
  • 组件化开发能大幅提高应用开发效率、测试性、复用性等
  • 组件使用按分类有:页面组件、业务组件、通用组件
  • vue的组件是基于配置的,通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,他们基于VueComponent,扩展于Vue
  • vue中常见的组件化技术有:属性prop,自定义事件,插槽等,他们主要用于组件通信、扩展等
  • 合理的划分组件,有助于提升应用性能
  • 组件应该是高内聚、低耦合的
  • 遵循单向数据流的原则

# vue设计理念

  • 渐进式JS框架
    • 自底向上逐层应用
    • 核心库只关心视图层,易于上手,便于与第三方库或既有项目整合
    • 当与现代化的工具链以及各类支持类库结合使用时,Vue也完全能够为复杂的单页应用提供驱动
  • 易用
    • vue提供数据响应式、声明式模板语法和基于配置的组件系统等核心特性
    • 只需要关注应用的核心业务即可
  • 灵活
    • 如果应用足够小,可能只需要vue核心特性即可完成功能
    • 随着应用规模的不断扩大,才可能引入路由、状态管理、vue-cli等库和工具
    • 不管应用体积和学习难度都是一个逐渐增加的平和曲线
  • 高效
    • 超快的虚拟DOM和diff算法带来最佳性能表现
    • 追求高效的过程还在继续,vue3中引入Proxy对数据响应式改进及编译器中对静态内容编译的改进会使vue更高效

# vue为什么要求组件模板只能有一个根元素

  • new Vue({ el: 'App' }) 确保挂载正常
  • 单文件组件中,template下的元素div,就是树状数据结构中的根 template标签特性
    • 隐藏性:不会显示在页面的任何地方,即便里面有多少内容,它都是隐藏的状态,设置了display: none
    • 任意性:可以写在任何地方,甚至是head、body、script中
    • 无效性:标签里的任何HTML内容都是无效的,不会起任何作用;只能innerHTML来获取里面的内容 一个vue单文件组件就是一个Vue实例,如果template下有多个div那么如何指定vue实例的根入口呢,为了让组件可以正常生成一个vue实例,这个div会自然地处理成程序的入口,通过这个根节点,来递归遍历整个vue树下的所有节点,并处理为VDOM,最后再渲染成真正的HTML,插入正确的位置
  • diff算法要求(见 src\core\vdom\patch.js 中 patchVnode())

# MVC、MVP 和 MVVM

  • web1.0 时代
    • 开发 web 应用多数采用 ASP.NET / Java / PHP,项目通常由多个 aspx / jsp / php 文件构成,每个文件中同时包含了 HTML、CSS、JavaScript、C# / Java / PHP 代码
  • 优点:简单快捷
  • 缺点:JSP代码难维护
  • 为了让开发更加便捷,代码更易维护,前后端职责更清晰。便衍生出MVC开发模式和框架,前端展示以模板的形式出现。典型框架有Spring,Structs,Hibernate。这种分层架构,职责清晰,代码易维护。但这里的MVC仅限于后端,前后端形成了一定的分离,前端只完成了后端开发中的view层。存在问题:前端页面开发效率不高,前后端职责不清。
  • web 2.0时代 Ajax的出现让前后端职责更加清晰,因为前端可以通过Ajax与后端进行数据交互。存在问题:缺乏可行的开发模式承载更复杂的业务需求,页面内容杂糅在一起,一旦规模增大,就会导致难以维护。
  • MVC 前端的MVC与后端类似,理论可行,但是实际开发中不灵活,如一个小操作也需要按流程拆分,开发不便捷。 Model:负责保存应用数据,与后端数据进行同步 Controller:负责业务逻辑,根据用户行为对Model数据进行修改 View:负责视图展示,将Model中的数据可视化表达
  • MVP MVP与MVC很接近,P指Presenter,可理解为中间人,负责View和Model之间的数据流动,防止View和Model之间直接交流。前端不常见,在安卓等原生开发中可能会考虑。虽然分离了View和Model,但是应用逐渐变大之后,导致presenter体积增大,难以维护。
  • MVVM Model-View-ViewModel,ViewModel可以理解为在presenter基础上的进阶版。ViewModel通过实现一套数据响应式机制自动响应Model中数据变化;同时ViewModel会实现一套更新策略自动将数据变化转换为视图更新;通过事件监听响应View中用户交互修改Model中数据。MVVM在保持View和Model松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性。
  • 总结
    • 都是框架模式,设计的目的是为了解决Model和View的耦合问题
    • MVC模式出现较早,主要应用在后端,如Spring MVC、ASP.NET MVC 等,在前端领域的早期也有应用,如Backbone.js。优点是分层清晰,缺点是数据流混乱,灵活性带来的维护问题
    • MVP模式是MVC的进化形式,Presenter作为中间层负责MV通信,解决了两者耦合的问题,但P层过于臃肿会导致维护问题
    • MVVM模式在前端领域有广泛应用,它不仅解决 MV 耦合问题,还同时解决了维护两者映射关系的大量繁杂代码和DOM操作代码,在提高开发效率,可读性同时还保持了优越的性能表现

# MVVM 的优缺点

  • 优点
    • 分离视图和模型,降低代码耦合,提高视图或逻辑的重用性
    • 提高可测试性:ViewModel 的存在可以帮助开发者更好地编写测试代码
    • 自动更新 DOM:利用双向绑定,数据更新后视图自动更新,让开发者从繁琐的手动 DOM 中解放
  • 缺点
    • Bug 很难被调试:因为使用双向绑定的模式,当你看到界面异常了,有可能是 View 的问题,也有可能是 Model 的问题。数据绑定使得一个位置的 Bug 被快速传递到别的地方,难以定位。另外,数据绑定的声明时指令式地写在 View 的模板中的,这些内容是没办法去打断点 debug 的
    • 一个大模块中 Model 也会很大,虽然使用方便也很容易保证了数据的一致性,但长期持有,不释放内存就造成浪费
    • 对于大型的图形应用程序,视图状态较多,ViewModel 的构建和维护的成本会较高

# 如何实现双向绑定

利用 Object.defineProperty() 劫持对象的访问器,在属性值发生发生变化时可以获取变化,然后根据变化进行后续响应。在 Vue3.0 中通过 Proxy 代理对象进行类似的操作。

代码语言:javascript
复制
// 要劫持的对象
const data = {
  name: '',
};

function say(name) {
  if (name === '古天乐') {
    console.log('给大家推荐一款好玩的游戏');
  } else if (name === '渣渣辉') {
    console.log('戏我养过很多,可游戏我只玩贪玩蓝月');
  } else {
    console.log('系兄弟就来砍我');
  }
}

// 遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function (key) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log('get');
    },
    set: function(newVal) {
      console.log(`大家好,唔系${newVal}`);
      say(newVal);
    },
  });
});
data.name = '渣渣辉';
// 大家好,唔系渣渣辉
// 戏我养过很多,可游戏我只玩贪玩蓝月

# Proxy 与 Object.defineProperty 的优劣对比

Proxy 优势

  • Proxy 可以直接监听对象而非属性
  • Proxy 可以直接监听数组的变化
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等,是 Object.defineProperty 不具备的
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化(新标准的性能红利) Object.defineProperty 的优势
  • 兼容性好,支持 IE9

# 如何理解 Vue 的响应式系统

  • 任何一个 Vue Component 都有一个与之对应的 Watcher 实例
  • Vue 的 data 上的属性会被添加 getter 和 setter 属性
  • 当 Vue Component render 函数被执行的时候,data 上会被 touch,即被读,getter 方法会被调用,此时 Vue 会去记录此 Vue component 所依赖的所有 data (依赖收集)
  • data 被改动时(主要是用户操作),即被写, setter 方法会被调用,此时 Vue 会通知所有依赖于此 data 的组件去调用他们的 render 函数进行更新

# Vue 的变化侦测原理

现代前端框架有两种方式侦测变化,一种是 pull ,一种是 push

  • pull
    • 代表为 React ,通常会用 setState API 显示更新,然后 React 会进行一层层的 Virtual DOM Diff 操作找出差异,然后 Patch 到 DOM 上
    • React 从一开始就不知道到底是哪发生了变化,只是知道 ‘有变化了’,然后进行比较暴力的 Diff 操作查找 ‘哪发生变化了’
    • 另一个代表就是 Angular 的脏检查操作。
  • push
    • Vue 的响应式系统是 push 的代表
    • 当 Vue 程序初始化的时候就会对数据 data 进行依赖的收集,一旦数据发生变化,响应式系统就会立刻得知,因此 Vue 是一开始就知道 ‘在哪发生变化了’,但是这又会产生一个问题,通常一个绑定一个数据就需要一个 Watcher,一旦我们绑定的细粒度过高就会产生大量的 Watcher,会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此 Vue 的设计是选择中等细粒度的方案,在组件级别进行 push 侦测的方式,也就是那套响应式系统。通常会第一时间侦测到发生变化的组件,然后在组件内部进行 Virtual DOM Diff 获取更具体的差异,而 Virtual DOM Diff 则是 pull 操作,Vue 是 push + pull 结合的方式进行变化侦测的

# Vue 为什么没有类似 React 的 shouldComponentUpdate 生命周期

根本原因是 Vue 与 React 的变化侦测方式有所不同

React 是 pull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual DOM Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,此时就需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能。

Vue 是 pull + push 的方式侦测变化的,在一开始就知道哪个组件发生了变化,因此在 push 阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期。

# Vue 生命周期

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载 DOM -> 渲染、更新 -> 渲染、卸载等一系列过程。

生命周期

描述

beforeCreate

组件实例被创建之初,组件的属性生效之前(data 和 methods 中的数据还没有初始化)

created

组件实例已经完全创建,属性也绑定,但真实 DOM 还没有生成,$el 还不可用(data 和 methods 都已经初始化好了,可以进行操作)

beforeMount

在挂载开始之前被调用:相关的 render 函数首次被调用(模板已经编译好,但尚未挂载到页面中去)

mounted

el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子

beforeUpdate

组件数据更新之前调用,发生在 VDOM 打补丁之前

update

组件数据更新之后

activated

keep-alive 专属,组件被激活时调用

deactivated

keep-alive 专属,组件被销毁时调用

beforeDestroy

组件销毁前调用

destroyed

组件销毁后调用

  • 异步请求适合在哪个生命周期调用? 官方例子是在 mounted 生命周期中,但是也可以在 created 中调用

# vue中组件通信

  • 方法 props 数据自上而下传递 emit / on(v-on) 从下到上传递信息 vuex 全局数据管理库,通过 vuex 管理全局的数据流 parent / children attrs / listeners 跨级组件通信 provide / inject EventBus
  • 场景
    • 父子
    • 兄弟
    • 跨层组件

# Vue性能优化方法

路由懒加载

代码语言:javascript
复制
const router = new VueRouter({
  routes: [
    { path: '/foo', component: () => import('./Foo.vue') }
  ]
})

keep-alive缓存页面

代码语言:javascript
复制
<template>
  <div id="app">
    <keep-alive>
      <router-view/>
    </keep-alive>
  </div>
</template>

使用v-show复用DOM

代码语言:javascript
复制
<template>
  <div class="cell">
    <!-- 这种情况使用v-show复用DOM比v-if效果好 -->
    <div v-show="value" class="on">
      <Heavy :n="10000" /><!-- 超级大组件 -->
    </div>
    <section v-show="!value" class="off">
      <Heavy :n="10000" />
    </section>
  </div>
</template>

v-for遍历避免同时使用v-if

代码语言:javascript
复制
<template>
  <ul>
    <li
      v-for="user in activeUsers"
      :key="user.id">
      {{user.name}}
    </li>
  </ul>
</template>
<script>
export default {
  computed: {
    activeUsers: function() {
      return this.users.filter(function(user) {
        return user.isActive
      })
    }
  }
}
</script>

长列表性能优化

如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化

代码语言:javascript
复制
export default {
  data: () => ({
    users: []
  }),
  async created() {
    const users = await axios.get('/api/users')
    this.users = Object.freeze(users)
  }
}

如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容

代码语言:javascript
复制
<recycle-scroll
  class="items"
  :items="items"
  :item-size="24">
  <template v-slot="{item}">
    <FetchItemView
      :item="item"
      @vote="voteItem(item)"/>
  </template>
</recycle-scroll>

参考vue-virtual-scroller、vue-virtual-scroll-list

事件的销毁 Vue组件销毁时会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件

代码语言:javascript
复制
create() {
  this.timer = setInterval(this.refresh, 2000)
},
beforeDestroy() {
  clearInterval(this.timer)
}

图片懒加载 对于图片过多的页面,为了加速页面加载速度,很多时候需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载

代码语言:javascript
复制
<img v-lazy="/static/img/1.png">

参考vue-lazyload

第三方插件按需引入

代码语言:javascript
复制
import Vue from 'vue'
import { Button, Select } from 'element-ui'

Vue.use(Button)
Vue.use(Select)

无状态的组件标记为函数式组件

代码语言:javascript
复制
<template functional>
  <div class="cell">
    <div v-if="props.value" class="on"></div>
    <section v-else class="off"></section>
  </div>
</template>
<script>
export default {
  props: ['value']
}
</script>

子组件分割

代码语言:javascript
复制
<template>
  <div>
    <ChildComp>
  </div>
</template>
<script>
export default {
  components: {
    ChildComp: {
      methods: {
        heavy() {/* 耗时任务 */}
      },
      render (h) {
        return h('div', this.heavy())
      }
    }
  }
}
</script>

变量本地化

代码语言:javascript
复制
<template>
  <div :style="{opacity: start / 300 }">
    {{result}}
  </div>
</template>
<script>
import { heavy } from '@/utils'
export default {
  props: ['start'],
  computed: {
    base() {
      return 42
    },
    result() {
      const base = this.base // 不要频繁引用this.base
      let result = this.start
      for (let i = 0; i < 1000; i++) {
        result += heavy(base)
      }
      return result
    }
  }
}
</script>

SSR

# Vue 3.0有哪些新特性

  • 更快
    • 虚拟DOM重写
    • 优化slots的生成
    • 静态树提升
    • 静态属性提升
    • 基于Proxy的响应式系统
  • 更小:通过treeshaking优化核心库体积
  • 更容易维护:TypeScript + 模块化
  • 更加友好:
    • 跨平台:编译器核心和运行时核心与平台无关,是的Vue更容易与任何平台(web、Android、iOS)一起使用
  • 更容易使用
    • 改进的TypeScript支持,编辑器能提供强有力的类型检查和错误及警告
    • 更好的调试支持
    • 独立的响应化模块
    • Composition API
  • 虚拟DOM重写
    • 期待更多的编译时提示来减少运行时开销,使用更有效的代码来创建虚拟节点
    • 组件快速路径+单个调用+子节点类型检测
      • 跳过不必要的条件分支
      • JS引擎更容易优化
  • 优化slots的生成 Vue3可以单独重新渲染父级和子级
    • 确保实例正确的跟踪依赖关系
    • 避免不必要的父子组件重新渲染
  • 静态树提升 使用静态树提升,即Vue3的编译器将能够检测到什么是静态的,然后将其提升,从而降低了渲染成本
    • 跳过修改整棵树,从而降低渲染成本
    • 即使多次出现也能正常工作
  • 静态属性提升 使用静态属性提升,Vue3打补丁时将跳过这些属性不会改变的节点
  • 基于Proxy的数据响应式
    • 组件实例初始化的速度提高100%
    • 使用Proxy节省以前一般的内存开销,加载速度,但是存在低浏览器版本的不兼容
    • 为了继续支持IE11,Vue3将发布一个支持旧观察者机制和新Proxy版本的构建
  • 高维护性 Vue3将带来更可维护的源代码。不仅会使用TS,而且许多包被解耦,更加模块化。

# slot 原理及作用

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定。

slot 分三类:

  • 默认插槽:匿名插槽,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只能有一个匿名插槽
  • 具名插槽:带具体名字的插槽,即带有 name 属性的 slot,一个组件可以出现多个具名插槽
  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件传递过来的数据决定如何渲染该插槽

实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在 vm.slots 中,默认插槽为 vm.slot.default,具名插槽为 vm.slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用 slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可以称该插槽为作用域插槽。

# vue 扩展现有组件

使用 Vue.mixin 全局混入 mixins是一种分发Vue组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。mixins选项接受一个混合对象的数组

代码语言:javascript
复制
<template>
  <div id="app">
    <p>num: {{num}}</p>
    <button @click="add">add</button>
  </div>
</template>
<script>
var addLog = {
  updated: function() {
    console.log('数据发生变化' + this.num)
  }
}
export default {
  name: 'app',
  data() {
    return {
      num: 1
    }
  },
  methods: {
    add() {
      this.num++
    }
  },
  updated() {
    console.log('原生updated')
  },
  mixins: [addLog] // 混入
}
</script>

全局混入

代码语言:javascript
复制
// src\main.js
Vue.mixin({
  updated: function() {
    console.log('全局混入')
  }
})

调用顺序:混入对象的钩子将在组件自身钩子之前调用,如果遇到全局混入,全局混入的执行要早于混入和组件里的方法

加 slot 扩展

默认插槽和匿名插槽 slot用来获取组件中的原内容

代码语言:javascript
复制
  <template id="hello">
    <div>
      <h1>slot</h1>
      <slot>如果没有原内容就显示该内容</slot><!-- 默认插槽 -->
    </div>
  </template>
  <script>
  var vm = new Vue({
    el: '#app',
    components: {
      'my-hello': {
        template: '#hello'
      }
    }
  })
  </script>

具名插槽

代码语言:javascript
复制
<div id="app">
  <my-hello>
    <ul slot="s1">
      <li>aaa</li>
      <li>bbb</li>
      <li>ccc</li>
    </ul>
    <ol slot="s2">
      <li>111</li>
      <li>222</li>
      <li>333</li>
    </ol>
  </my-hello>
</div>
<template id="hello">
  <div>
    <slot name="s2"></slot>
    <h3>具名插槽</h3>
    <slot name="s1"></slot>
  </div>
</template>
<script>
var vm = new Vue({
  el: '#app',
  components: {
    'my-hello': {
      template: '#hello'
    }
  }
})
</script>

# watch 和 computed 的区别及怎么选用

区别

定义、语义区别 watch

代码语言:javascript
复制
var vm = new Vue({
  el: '#app',
  data: {
    foo: 1
  },
  watch: {
    foo: function(newVal, oldVal) {
      console.log(newVal + '-' +oldVal)
    }
  }
})
vm.foo = 2 // 2 - 1

computed

代码语言:javascript
复制
var vm = new Vue({
  el: '#app',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
  }.
  computed: {
    fullName: function() {
      return this.firstName + ' ' + this.lastName
    }
  }
})
vm.fullName // Foo Bar

computed

  • 计算值,更多用于计算值的场景
  • 具有缓存性,computed 的值在 getter 执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取 computed 的值时才会重新调用对应的 getter 来计算
  • 适用于较消耗性能的计算场景

watch

  • 更多的是观察作用,类似于某些数据的监听回调,用于观察 props $emit 或本组件的值,当数据变化时来执行回调进行后续操作
  • 无缓存性,页面重新渲染时值不变化也会执行

功能区别 watch更通用,computed派生功能都能实现,计算属性底层来自于watch,但是做了更多,例如缓存

用法区别 computed更简单高效,优先使用 有些必须watch,比如值变化后要和后端交互

使用场景 watch:需要在数据变化时执行异步或开销较大的操作时使用,简单讲,当一条数据影响多条数据的时候,如搜索数据 computed:对于任何复杂逻辑或一个数据属性在它所依赖的属性发生变化时,也要发生变化,简单讲,当一个属性受多个属性影响的时候,如购物车商品结算时

# nextTick原理

它可以在 DOM 更新完毕之后执行一个回调,以此来确保我们操作的是更新后的 DOM 。

实现原理:

  • Vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  • mircotask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  • 因为兼容性问题,vue 对 mircotask 做了向 macrotask 的降级策略
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/2/22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # Vue 的基本原理
  • # 双向数据绑定原理
  • # Vue data 中某一个属性的值发生改变后,视图会立即同步执行渲染吗
  • # v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?
  • # Vue组件data为什么必须是个函数而Vue的根实例则没有限制?
  • # key的作用和原理
  • # 怎么理解vue中的diff算法
  • # 对组件化的理解
  • # vue设计理念
  • # vue为什么要求组件模板只能有一个根元素
  • # MVC、MVP 和 MVVM
  • # MVVM 的优缺点
  • # 如何实现双向绑定
  • # Proxy 与 Object.defineProperty 的优劣对比
  • # 如何理解 Vue 的响应式系统
  • # Vue 的变化侦测原理
    • # Vue 为什么没有类似 React 的 shouldComponentUpdate 生命周期
  • # Vue 生命周期
  • # vue中组件通信
  • # Vue性能优化方法
  • # Vue 3.0有哪些新特性
  • # slot 原理及作用
  • # vue 扩展现有组件
  • # watch 和 computed 的区别及怎么选用
  • # nextTick原理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档