Vue2 中需要深入理解的一些点

简介: Vue2 中需要深入理解的一些点

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

  • v-if 和 v-for 同级

    <div id="demo">
      <h1>v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?</h1>
      <p v-for="child in children" v-if="isFolder">{{child.title}}</p>
    </div>
    // 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 嵌套

    <div id="demo">
      <h1>v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该如何优化?</h1>
      <template v-if="isFolder">
        <p v-for="child in children">{{child.title}}</p>
      </template>
    </div>
    // 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)}
    })
  • 产生原因

    // 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的根实例则没有限制?

<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使用逻辑

    // 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的作用和原理

<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>
// 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比较

    // 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发生的地方,整体策略是:深度优先,同层比较

    // 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)
      }
    }
  • 高效性

    // 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. 组件定义
      全局定义

      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函数,最终导出的依然是组件配置对象

      <template>
        <div>this is a component</div>
      </template>
    2. 组件化优点
      src\core\instance\lifecycle.js -> mountComponent()
      组件、Watcher、渲染函数和更新函数直接的关系
    3. 组件化实现

    构造函数 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 代理对象进行类似的操作。

// 要劫持的对象
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性能优化方法

  • 路由懒加载

    const router = new VueRouter({
      routes: [
        { path: '/foo', component: () => import('./Foo.vue') }
      ]
    })
  • keep-alive缓存页面

    <template>
      <div id="app">
        <keep-alive>
          <router-view/>
        </keep-alive>
      </div>
    </template>
  • 使用v-show复用DOM

    <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

    <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>
  • 长列表性能优化

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

      export default {
        data: () => ({
          users: []
        }),
        async created() {
          const users = await axios.get('/api/users')
          this.users = Object.freeze(users)
        }
      }
    • 如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容

      <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组件销毁时会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件

    create() {
      this.timer = setInterval(this.refresh, 2000)
    },
    beforeDestroy() {
      clearInterval(this.timer)
    }
  • 图片懒加载
    对于图片过多的页面,为了加速页面加载速度,很多时候需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载

    <img v-lazy="/static/img/1.png">

    参考vue-lazyload

  • 第三方插件按需引入

    import Vue from 'vue'
    import { Button, Select } from 'element-ui'
    
    Vue.use(Button)
    Vue.use(Select)
  • 无状态的组件标记为函数式组件

    <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>
  • 子组件分割

    <template>
      <div>
        <ChildComp>
      </div>
    </template>
    <script>
    export default {
      components: {
        ChildComp: {
          methods: {
            heavy() {/* 耗时任务 */}
          },
          render (h) {
            return h('div', this.heavy())
          }
        }
      }
    }
    </script>
  • 变量本地化

    <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,而且许多包被解耦,更加模块化。

vue扩展现有组件

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

    <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>

    全局混入

    // src\main.js
    Vue.mixin({
      updated: function() {
        console.log('全局混入')
      }
    })

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

  • 加slot扩展

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

      <template id="hello">
        <div>
          <h1>slot</h1>
          <slot>如果没有原内容就显示该内容</slot><!-- 默认插槽 -->
        </div>
      </template>
      <script>
      var vm = new Vue({
        el: '#app',
        components: {
          'my-hello': {
            template: '#hello'
          }
        }
      })
    - 具名插槽
    • aaa
    • bbb
    • ccc
    1. 111
    2. 222
    3. 333

    <div>
      <slot name="s2"></slot>
      <h3>具名插槽</h3>
      <slot name="s1"></slot>
    </div>


    el: '#app',
    components: {
      'my-hello': {
        template: '#hello'
      }
    }

    })

watch 和 computed 的区别及怎么选用

区别

  • 定义、语义区别
    watch

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

    computed

    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的降级策略
相关文章
|
4月前
|
JavaScript
|
JavaScript API UED
vue3(二)
vue3(二)
86 0
|
4月前
|
JavaScript
【vue】 vue2 实现飘窗效果
【vue】 vue2 实现飘窗效果
85 1
|
4月前
|
JavaScript
selectpicker 与vue整合
selectpicker 与vue整合
|
4月前
|
JavaScript 前端开发 开发者
new Vue() 发生了什么
new Vue() 发生了什么
|
10月前
|
JavaScript 前端开发 API
vue3介绍
vue3介绍
301 0
|
存储 数据处理
Vue3中shallowRef和shallowReactive的使用?
Vue3中shallowRef和shallowReactive的使用?
107 0
|
存储 缓存 监控
|
JavaScript