Vue面试题汇总

228 阅读35分钟

一、Vue基础

1、MVVM

  • MVC:将应用抽象为数据层(Model)、视图层(View)、逻辑层(Controller),降低了项目的耦合度。
    • View接收到用户请求时,将请求传递给Controller
    • Controller解析用户的请求之后,调用Model层,完成Model的修改
    • 之后Model层通知对应的View层更新(观察者模式)

这是最初的MVC设计,没有限制数据流,View和Model之间可以通信,两者耦合,后面又出现了变种MVC解决View对Model的依赖

  • MVP:限制了Model和View之间的通信,两者之间的交互必须要通过Presenter,现了Model和View的解耦,但与此同时Presenter负担过重,MVP之间的交互通过接口来进行。

  • MVVM:Model-View-ViewModel

    • Model:数据模型,是一切的核心
    • View:UI视图,数据最终体现在页面上
    • ViewModel:构建双向数据流的桥梁(数据变化,视图渲染;视图变化,数据改变)

Model和View之间没有直接关系,通过ViewModel进行关联,并且ViewModel实现了视图的数据之间的双向绑定,数据更新会自动更新视图,开发者不需要关注视图,只需要关注数据。

MVVM的缺点:

  • 对于大型的项目,视图状态较多,VM的构建和维护成本较高

2、Vue对于MVVM的实现/Vue的双向绑定原理

观察者设计模式与发布订阅消息范式

数据劫持结合发布者-订阅者方式

响应式原理

手写Vue

  • 创建Vue实例时,首先将data注入到Vue实例中(能够通过this直接访问数据)
  • 其次调用observer对象,将data转换为响应式对象
    • 递归遍历数据对象,通过Object.defineProperty()劫持数据,重写数据的set和get方法
    • getter中,将触发属性get的依赖添加到属性对应的依赖数组中
    • setter中,遍历属性维护的依赖数组,调用依赖的update方法进行更新
  • 调用compiler对象,对每个元素节点进行扫描分析,识别代码中的指令和插件表达式
    • 当遇到代码中使用data的数据的地方,创建对应的watcher实例
    • 在watcher的构造函数中,我们会将watcher实例挂载到一个静态对象上,并访问对应的属性值,可以在getter函数中将watcher实例添加到属性的订阅者数组中
    • 这样页面首次渲染完成之后,数据和视图就关联起来了

依赖收集是observe和compiler共同协作实现的,简单来说就是:

  • 数据劫持为每⼀个key创建⼀个Dep实例
  • 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  • 由于触发name1getter方法,便将watcher1添加到name1对应的Dep中
  • name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新

发布者订阅者模式

  • 发布者:属性被set时,向订阅中心发布消息
  • 订阅中心:Dep,存储着消息事件和订阅者的缓存队列
  • 订阅者:watcher,每个watcher实例都维护一个update方法

Vue

  • 接收初始化参数
  • data属性注入到vue实例中,转换成getter和setter
  • 负责调用observe监听data中所有属性的变化
  • 负责调用compiler解析指令/插值表达式
  class Vue {
      constructor (options) {
          // 1. 通过属性保存选项的数据
          this.$options = options || {}
          this.$data = options.data || {}
          this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
          // 2. 把data中的成员转换成 getter 和 setter,注入到 Vue 实例中
          this._proxyData(this.$data)
          // 3. 调用observer对象,监听数据的变化
          new Observer(this.$data)
          // 4. 调用compiler对象,解析指令和差值表达式
          new Compiler(this)
      }
  
      // 约定 _ 开头,为私有属性
      // 代理数据,即让 Vue 代理data中的属性
      _proxyData (data) {
          // 遍历data中的所有属性
          Object.keys(data).forEach(key => {
              // 把data的属性注入到vue实例中
              Object.defineProperty(this, key, {
                  enumerable: true,
                  configurable: true,
                  get () {
                      return data[key]
                  },
                  set (newValue) {
                      if (data[key] === newValue) {
                          return
                      }
                      data[key] = newValue 
                  }
              })
          })
      }
  }

Observer

  • 把data选项中的属性转换为响应式数据
  class Observer {
      constructor (data) {
          this.walk(data)
      }
  
      walk (data) {
          // 1. 判断 data 是否是对象
          if (!data || typeof data !== 'object') {
             return
          }
          // 2. 遍历data对象的所有属性
          Object.keys(data).forEach(key => {
              this.defineReactive(data, key, data[key])
          })
      }
  
      // 调用 Object.defineProperty() 将属性转换为 getter / setter
      defineReactive (obj, key, val) {
          const that = this
          // 负责收集依赖,并发送通知
          const dep = new Dep()
          // 如果val是对象,把val内部的属性转换成响应式数据
          this.walk(val)
          Object.defineProperty(obj, key, {
              enumerable: true,
              configurable: true,
              get () {
                  // 收集依赖
                  Dep.target && dep.addSub(Dep.target)
                  // 此处不可以写成 obj[key],否则会发生死递归
                  // 这里使用闭包,扩展了val变量的作用域
                  return val
              },
              set (newValue) { // function,改变this
                  if (newValue === val) {
                      return
                  }
                  val = newValue
                  // 如果newValue是对象,把newValue内部的属性转换成响应式数据
                  that.walk(newValue)
                  // 发送通知
                  dep.notify()
              }
          })
      }
  }

Compiler

  • 负责编译模板,解析指令和插值表达式
  • 负责页面的首次渲染
  • 当数据变化后,重新渲染视图
  class Compiler {
      constructor(vm) {
          this.el = vm.$el // 记录模板
          this.vm = vm     // 记录 Vue 实例
          this.compile(this.el)
      }
  
      // 编译模板,处理文本节点(差值表达式)和元素节点(指令)
      compile(el) {
          let childNodes = el.childNodes // 伪数组
          // 将伪数组转换成数组
          Array.from(childNodes).forEach(node => {
              if (this.isTextNode(node)) {
                  // 处理文本节点
                  this.compileText(node)
              } else if (this.isElementNode(node)) {
                  // 处理元素节点
                  this.compileElement(node)
              }
  
              // 判断node节点,是否有子节点,如果有子节点,要递归调用 compile
              if (node.childNodes && node.childNodes.length) {
                  this.compile(node)
              }
          })
      }
  
      // 编译元素节点,处理指令
      compileElement(node) {
          // 遍历所有的属性节点
          Array.from(node.attributes).forEach(attr => {
              // 判断是否是指令
              let attrName = attr.name // 获取属性名
              if (this.isDirective(attrName)) {
                  // v-text ---> text
                  attrName = attrName.substr(2)
                  const key = attr.value // 获取属性值
                  this.update(node, key, attrName)
              }
          })
      }
  
      update (node, key, attrName) {        
          const updateFn = this[attrName + 'Updater']
          // 改变 updateFn方法中的 this指向
          updateFn && updateFn.call(this, node, this.vm[key], key)
      }
  
      // 处理 v-text 指令
      textUpdater (node, value, key) {
          node.textContent = value
  
          new Watcher(this.vm, key, (newValue) => {
              node.textContent = newValue
          })
      }
  
      // 处理 v-model 指令
      modelUpdater (node, value, key) {
          node.value = value
  
          new Watcher(this.vm, key, (newValue) => {
              node.value = newValue
          })
          
          // 双向绑定
          node.addEventListener('input', () => {
              this.vm[key] = node.value
          })
      } 
  
      // 编译文本节点,处理 差值表达式
      compileText(node) {
          // {{ msg }}
          // . 匹配任意的单个字符,不包括换行
          // + 匹配前面修饰的字符出现一次或多次
          // ? 表示非贪婪模式,即尽可能早的结束匹配
          // 在正则表达式中,提取某个位置的内容,即添加(),进行分组        
          const reg = /\{\{(.+?)\}\}/ // 括号包裹的内容即为要提取的内容
          const value = node.textContent
          if (reg.test(value)) {
              // 使用RegExp的构造函数,获取第一个分组的内容,即.$1
              const key = RegExp.$1.trim()
              node.textContent = value.replace(reg, this.vm[key])
  
              // 创建watcher对象,当数据改变时更新视图
              new Watcher(this.vm, key, (newValue) => {
                  node.textContent = newValue
              })
          }
      }
  
      // 判断元素属性是否是指令
      isDirective(attrName) {
          // 判断attrName是否以 v- 开头
          return attrName.startsWith('v-')
      }
  
      // 判断节点是否是文本节点
      isTextNode(node) {
          return node.nodeType === 3
      }
  
      // 判断节点是否是元素节点
      isElementNode(node) {
          return node.nodeType === 1
      }
  }

Dep

  • 收集依赖(添加观察者watcher)
  • 通知所有的观察者
  class Dep {
      constructor() {
          // 存储所有的观察者
          this.subs = []
      }
  
      // 添加观察者
      addSub(sub) {
          if (sub && sub.update) {
              this.subs.push(sub)
          }
      }
  
      // 发送通知
      notify() {
          // 遍历所有的观察者
          this.subs.forEach(sub => {
              // 调用每一个观察者的update方法,更新视图
              sub.update()
          })
      }
  }

watcher

  • 数据变化时,dep通知所有的watcher的update方法
  • 自身实例化的时候,在Dep对象中添加自己
  class Watcher {
      constructor (vm, key, cb) {
          this.vm = vm
          // data 中的属性名称
          this.key = key
          // 回调函数负责更新视图
          this.cb = cb
  
          // 把watcher对象记录到Dep类的静态属性 target
          Dep.target = this
          // 触发get方法,在get方法中会调用addSub
          this.oldValue = vm[key]
          Dep.target = null // 防止重复添加
      }
  
      // 当数据发生变化的时候更新视图
      update () {
          const newValue = this.vm[this.key]
          if (newValue === this.oldValue) {
              return
          }
          // 当数据变化时,需要将新的值传递给回调函数,更新视图
          this.cb(newValue)
      }
  }

3、Computed和Watch的区别

Computed

  • 支持缓存,只有依赖的响应式数据(data/props)发生变化,才会重新计算
  • 不支持异步
  • 如果一个属性是由其他属性计算而来,这个属性依赖其它属性,一般会使用computed
  • 如果computed属性的值是函数,那么默认使用get方法,函数的返回值就是属性的属性值
computed: {
   example: {
       get () {
           return 'example'
       },
       set (newValue) {
           console.log(newValue)
       }
   },
   
   // 另一种形式的写法
   a: function() {
       return 'value'
   }
}

Watch

  • 侦听数据(响应式数据data/props)的变化,数据变化时,会触发函数,函数内部可以执行异步操作
  • deep属性Vue性能消耗较大,对于要监听数据中某个属性的响应时,可以只给对应属性添加deep
watch: {
  'obj.a': {
    // 函数接收两个参数,第一个参数是最新的值,第二个参数是旧值
    handler(newName, oldName) {
      console.log('obj.a changed');
    },
    // 组件加载立即触发回调函数
    immediate: true,
    // 深度监听
    deep: true
  },
  
  // 另一种形式写法
  b(newval,oldVal) {
      funtion()
  }
}

computed与watch的执行顺序

  • 初始加载情况下不会执行watch,可以设置immediate设置初始化的时候执行watch逻辑
  • 此时watch的执行时机在created之前
  • 如果watch监听的是一个computed属性,那么被监听的computed属性要排在watch之前,其次是watch,然后是created(其他未被监听的computed依旧在beforeCreate和created之间执行

4、slot的作用以及原理

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

  • 默认插槽:又名匿名插槽,slot没有指定name,一个组件内只有一个默认插槽
  • 具名插槽:可以指定name,将内容分发到不同的插槽中,一个组件可以有多个具名插槽
  • 作用域插槽:默认插槽和具名插槽的一个变体,可以是具名插槽,也可以是匿名插槽。子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件

5、过滤器的作用以及实现

Vue中使用filters过滤数据,不会修改数据,只会改变用户看到的输出(computed和methods通过修改数据改变页面显示)

  • 需要格式化数据并展示在页面上:改变时间、价格的输出显示
  • 过滤器是一个函数,会把表达式中的值始终当做函数的第一个参数,过滤器用在插值表达式{{ }}v-bind表达式中
<li>商品价格:{{item.price | filterPrice}}</li>

 filters: {
    filterPrice (price) {
      return price ? ('¥' + price) : '--'
    }
  }

6、如何保存组件当前的状态

  • 将状态存储在localStorage/sessionStorage中
    • Storage中存储的值是字符串,需要将对象转换为JSON字符串存储
  • 路由传值
  • keep-alive

7、常见的事件修饰符

  • .stop:防止事件冒泡,等同于JavaScript中的event.stopPropagation()
  • .prevent:防止事件的默认行为,等同于JavaScript的event.preventDefault()
  • .capture:改变事件冒泡的方向,事件捕获由外到内
  • .self:只会触发自己范围内的事件,不包含子元素
  • .once:只触发一次

8、v-if、v-show、v-html

  • v-if:根据v-if的值判断是否生成vnode,v-if为false的时候,不会render节点
  • v-show:会生成vnode,render时也会渲染成真实节点,只是在render的时候会再节点属性中修改属性值,设置display
  • v-html:会首先移除所有子元素节点,将节点的innerHtml设置为v-html的值

v-if和v-show的区别

区别v-ifv-show
显隐的方式动态向DOM树中添加/删除节点设置DOM的display属性
编译过程切换值会有一个局部编译/卸载的过程(销毁、重建内部的监听事件和子组件)基于CSS的切换
编译条件惰性的,如果初始条件为假,则什么也不做,为真时进行编译无论是否为真都会编译
性能消耗切换消耗高初始渲染消耗高
场景适用于切换条件不易改变适用于频繁切换条件

9、v-model的实现

(1)用在表单上

  • input的 value值与数据绑定,数据改变引起视图改变
  • 触发input输入事件时,修改数据的值,视图改变因此数据改变
<input v-model="sth" />
//  等同于
<input 
    v-bind:value="message" 
    v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;

(2)用在自定义组件上

  • 本质是父子通信的语法糖,通过prop和$emit实现
<child v-model="message"></child>
<child :value="message"  @input="function(e){message = e}"></child>

10、data为什么是一个函数而不是对象

JavaScript中的对象是引用类型,当多个实例引用同一个对象时,如果一个实例对对象进行了修改,其他实例中的数据也会修改。

Vue组件复用时,同一个组件的不同实例要有自己的数据。

因此不能将data写成对象,要写成函数的形式,每次复用组件时,都会返回一个新的数据对象,每个组件维护自己的数据,不会干扰其他组件实例。

11、keep-alive

(1)keep-alive

keep-alivevue中的内置组件,能在组件切换过程中缓存不活动的组件实例,而不是销毁它,防止重复渲染DOMkeep-alive不会向DOM添加额外节点。

keep-alive 包裹动态组件时,只会渲染第一个子组件。

有以下三个属性:

  • include:字符串或正则表达式,只有名称匹配的组件会被匹配
  • exclude:字符串或正则表达式,被名称匹配的组件不会被缓存
  • max:最多可以缓存多少组件实例

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配。

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > createdmounted > activated > ... ... > beforeRouteLeave > deactivated
  • 再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

(2)原理

  • 获取keep-alive下第一个子组件的实例对象,通过实例对象获取组件名
  • 通过组件名去匹配include和exclude,判断当前组件是否需要缓存
    • 不需要缓存,返回vnode
    • 需要缓存,判断缓存数组中是否存在实例
      • 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU),
      • 不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
      • 最后将这个组件的 keepAlive 设置为 true
// 源码 => vue/src/core/components/keep-alive.js 
export default {
  name: 'keep-alive',
  abstract: true, //定义抽象组件 判断当前组件虚拟dom是否渲染成真实dom

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null) // 缓存VNode
    this.keys = [] // 缓存VNode的key
  },

  destroyed () {
    // 销毁时删除所有缓存的VNode
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    // 监听 include和exclude属性,及时的更新缓存
    // pruneCache 对cache做遍历,把不符合新规则的VNode从缓存中移除
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render() {
    /* 获取默认插槽中的第一个组件节点 */
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
      /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
      const name = getComponentName(componentOptions)

      const { include, exclude } = this
      /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
      if (
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      /* 获取组件的key值 */
      const key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
     /*  拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      }
        /* 如果没有命中缓存,则将其设置进缓存 */
        else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

(3)渲染流程

首次渲染时,keep-alive的render函数先执行,判断组件是否缓存,设置keepAlive的值。如果节点的keepAlive为true,首次渲染时isReactivated为undefined,执行组件的mount逻辑。因此对于首次渲染而言,除了在 <keep-alive> 中建立缓存,和普通组件渲染没什么区别。

非首次渲染时,会先执行keep-alive组件的render方法,如果命中缓存,从缓存中返回实例。

(4)LRU缓存策略

LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。

LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高"

12、$nextTick原理以及作用

官方定义:在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM。

也就是说:Vue更新DOM是异步操作,当数据发生变化之后,Vue将更新操作放在异步队列中,视图需要等同一事件循环中所有的数据变化完成之后,再进行更新。

因此,在修改数据之后,立即获取视图中更新的DOM节点,会发现获取到的是旧值。

(1)为什么要有nextTick

{{num}}
for(let i=0; i<100000; i++){
    num = i
}

如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略

// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
// nextTick第一个参数为原始的值,第二个参数为执行上下文
Vue.nextTick(function () {
  // DOM 更新了
  console.log(vm.$el.textContent) // 修改后的值
})

// 组件内使用vm.$nextTick()实例方法只需要通this.$nextTick()
// 并且回调函数中的this将自动绑定到当前的Vue实例上
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
    console.log(this.$el.textContent) // => '修改后的值'
})

// nextTick函数返回promise对象,可以使用async/await
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'

(2)nextTick实现原理

  • 将回调函数放入callbacks等待执行
  • 将执行函数放入任务队列
  • 事件循环到了任务队列,依次取出更新函数并执行
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;

  // cb 回调函数会经统一处理压入 callbacks 数组
  callbacks.push(() => {
    if (cb) {
      // 给 cb 回调函数执行加上了 try-catch 错误处理
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });

  // 执行异步延迟函数 timerFunc
  if (!pending) {
    pending = true;
    timerFunc();
  }

  // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve;
    });
  }
}

timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:Promise.thenMutationObserversetImmediatesetTimeout通过上面任意一种方法,进行降级操作

export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  //判断1:是否原生支持Promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  //判断2:是否原生支持MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  //判断3:是否原生支持setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  //判断4:上面都不行,直接用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

无论是微任务还是宏任务,都会放到flushCallbacks使用

这里将callbacks里面的函数复制一份,同时callbacks置空

依次执行callbacks里面的函数

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

13、Vue的数据对象添加新属性

添加的属性不是响应式数据,可以通过$set()方法手动将数据转变为响应式数据

image.png

addObjB () (
   this.$set(this.obj, 'b', 'obj.b')
   console.log(this.obj)
}

14、vue中重新封装的数组方法

Object.defineProperty可以监听到数组方法,但是由于性能开销太大,vue中弃用了这种方法,重写数组方法以便执行方法之后更新视图。

  • 执行数组方法,得到返回结果
  • 如果有新增数组元素,监听数组元素
  • 通知订阅者进行更新

在observer函数中,当数据类型为数组时,将数据的__ptoto__指向arrayMethods,如果浏览器不支持原型,将arrayMethods中的方法直接定义在当前对象上。

// src/core/observer/array.js

// 获取数组的原型Array.prototype,上面有我们常用的数组方法
const arrayProto = Array.prototype
// 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)

// 列出需要重写的数组方法名
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
// 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
methodsToPatch.forEach(function (method) {
  // 保存一份当前的方法名对应的数组原始方法
  const original = arrayProto[method]
  // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
  def(arrayMethods, method, function mutator (...args) {
    // 调用数组原始方法,并传入参数args,并将执行结果赋给result
    const result = original.apply(this, args)
    // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 将当前数组的变更通知给其订阅者
    ob.dep.notify()
    // 最后返回执行结果result
    return result
  })
})

15、单页应用与多页应用

  • SPA:SinglePage Web Application,只有一个主页面,一开始只需要加载一次静态资源。所有的内容都包含在主页面中,对每一个功能模块组件化。单页面应用跳转就是切换组件,仅仅刷新局部资源。
  • MPA:MultiPage Application,有多个独立页面的应用,每个页面都需要加载静态资源。多页应用跳转需要整个页面资源刷新。

单页应用优点:

  • 用户体验好,内容的改变不需要重新加载整个页面
  • 良好的前后端分离,前端负责交互逻辑,后端负责数据处理
  • 减轻服务器负载压力

单页应用缺点:

  • 首次渲染慢
  • 不利于搜索引擎抓取
区别单页面应用(SPA)多页面应用(MPA)
组成一个主页面 + 许多功能模块组件多个完整的页面
刷新方式局部刷新整页刷新
url模式哈希模式历史模式
SEO搜索引擎优化难以实现,可使用SSR方式改善容易实现
数据传递容易通过url、cookie、storage等传递
页面切换速度较快,用户体验好较慢
维护成本较容易较复杂

SEO优化

  • SSR服务端渲染
  • 静态化(?)
  • 使用Phantomjs针对爬虫处理
    • 通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫

16、vue template 到 render过程

编译主要过程:template --> AST --> render

(1)调用parse方法将template转化为AST(抽象语法树)

利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本时,会分别执行对应的回调函数,来达到构造AST树的目的

AST元素节点共有三种类型:type为1表示普通元素,2表达式,3纯文本

(2)对静态节点做优化

标记静态节点,后续渲染更新时可以跳过静态节点(DOM不会改变)

(3)generate方法将AST编译成render字符串,最后生成render函数

17、mixin、extends的覆盖逻辑

mixin和extends均用于合并、拓展组件。两者均使用mergeOptions方法实现合并

mixin:接受一个混入对象的数组,混入对象可以像正常实例一样包含各种实例选项,这些选项会被合并到最终的选项中

  • 在多个组件之间重用一组组件选项
  • mixin的hook优先级高于组件自己的hook

extends:为了便于扩展单文件组件,接收一个对象或构造函数,生成一个实例挂载到一个DOM元素上

  • 作用是扩展组件生成一个构造器,通常会与 $mount 一起使用
// 创建组件构造器let 
Component = Vue.extend({  template: '<div>test</div>'})
// 挂载到 #app 上
new Component().$mount('#app')

// 除了上面的方式,还可以用来扩展已有的组件
let SuperComponent = Vue.extend(Component)
new SuperComponent({   
    created() {        
      console.log(1)    
}})
new SuperComponent().$mount('#app')

合并规则

image.png

18、vue的自定义指令

(1)如何实现一个自定义指令

全局注册:Vue.directive

// 注册一个全局自定义指令 `v-focus`
// 第一个参数是指令的名字,不需要加上v-前缀,第二个参数可以是对象数据,也可以是指令函数
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()  // 页面加载完成之后自动让输入框获取到焦点的小功能
  }
})

局部注册:options选项的directives属性

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
    }
  }
}

(2)自定义指令的钩子函数

钩子说明
bind只调用一次,指令第一次绑定元素时调用。在这里可以进行一次性的初始化设置
inserted被绑定元素插入父节点时调用
update组件的Vnode更新时调用
componentUpdated所有组件(包含子组件)更新完成后调用
unbind只调用一次,元素解绑时调用

钩子函数的参数

参数说明
el指令所绑定的元素,可以用来操作DOM
binding一个对象,包含指令名name、指令绑定的值value、oldValue指令绑定的前一个值、expression字符串形式的指令表达式、arg传给指令的参数、modifiers包含修饰符的对象、vnode虚拟节点、oldVnode上一个虚拟节点

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行

(3)使用场景

使用场景

  • 表单防止重复提交
  • 图片懒加载
  • 一键copy功能
// 1.设置v-throttle自定义指令
Vue.directive('throttle', {
  bind: (el, binding) => {
    let throttleTime = binding.value; // 节流时间
    if (!throttleTime) { // 用户若不设置节流时间,则默认2s
      throttleTime = 2000;
    }
    let cbFun;
    el.addEventListener('click', event => {
      if (!cbFun) { // 第一次执行
        cbFun = setTimeout(() => {
          cbFun = null;
        }, throttleTime);
      } else {
        event && event.stopImmediatePropagation();
      }
    }, true);
  },
});
// 2.为button标签设置v-throttle自定义指令
<button @click="sayHello" v-throttle>提交</button>

19、子组件可以直接修改父组件的数据吗

不可以,Vue提倡单项数据流,父级的props属性的更新会流向子组件,反之不成立。防止多个子组件意外改变父组件的状态,导致页面数据流混乱,维护困难。

只能通过$emit派发一个自定义事件,父组件监听到之后,由父组件修改数据

20、谈谈你对Vue的理解、Vue的优点、对比React

(1)template和jsx

  • 对于runtime(程序在运行时的状态和行为)来说,只要保证组件存在render函数即可,浏览器运行函数生成页面
    • webpack使用vue-loader编译文件。内部依赖的vue-template-compiler模块将template编译成render函数
    • react将jsx代码解析成render函数

template和jsx的都是render的一种表现形式

  • JSX:html结合js,比template更加灵活,在复杂的组件项目中,更具优势
  • template:html、css、js逻辑分开,更简单直观,更好维护

(2)组件通信

  • Vue中的组件通信:props/emit、eventBus、provide/inject、ref、attrs/attrs/listeners、parent/parent/children、Vuex/Pinia
  • React中的组件通信:props、context、redux

react的组件通信方式过于复杂,vue的组件通信相对于来说简单

(3)状态管理

  • Vue:Vuex、Pinia
  • React:Redux

(4)生命周期

  • Vue:开始创建、初始化数据、编译模板、挂载DOM、渲染更新、卸载
  • React:挂载、更新、卸载

(5)diff算法

(6)组件化方式 vue组件和react组件

vue和react区别

21、assets和static的区别

相同点:

  • 存放静态资源

不同点:

  • assets中存放的资源会被打包,打包后的资源会被放在static文件中,与index.html一同上传服务器
  • static中的文件不会被打包,直接进入打包目录

static中的文件不会被打包,因此减少了打包时间,但是由于资源没有经历打包的压缩等操作,文件体积和放在assets中的相比略大

建议:将页面的css、js文件放在assets中,打包压缩,减少体积。将已经被处理好的第三方文件放在static中,不需要处理,直接上传

22、delete和Vue.delete删除数组的区别

  • delete只是被删除的元素变成了empty/undefined,其他元素的键值不变,数组的索引位置不变,length不变
  • Vue.delete直接删除了数组元素,改变的数组的键值,length改变

23、Vue的生命周期

Vue实例有一个完整的生命周期: 开始创建、初始化数据、编译模板、挂载DOM、渲染更新、卸载

  • beforeCreate(创建前):数据观测和初始化事件还没有开始
  • created(创建后):实例选项data、computed、watch、methods等都配置完成
  • beforeMount(挂载前):首次调用render函数
  • mounted(挂载后):将虚拟DOM挂载到真实DOM上
  • beforeUpdated(更新前):响应式数据修改时调用,此时数据改变,但是视图没有更新
  • updated(更新后):DOM已更新
  • beforeDestory(销毁前):实例销毁之前调用。此时this仍然可用
  • destoryed(销毁后):实例销毁后调用

24、父子组件钩子的执行顺序

  • 加载渲染过程:父beforeCreate --> 父created --> 父beforeMount --> 子beforeCreate --> 子created --> 子beforeMount --> 子mounted --> 父mounted
  • 更新过程:父beforeUpdate --> 子beforeUpdate --> 子updated --> 父updated
  • 销毁过程:父beforeDestory --> 子beforeDestory --> 子destoryed --> 父destoryed

25、父子组件通信

(1)props、$emit

  • 父组件通过props向子组件传值
    • 如果props中数据的命名使用了驼峰形式,在模板中需要使用短横线
  • 子组件通过触发$emit事件,将值传给父组件,父组件监听事件并进行数据处理

(2)eventBus事件总线

eventBus适用于父子组件、非父子组件之间的通信

  • 创建事件中心管理组件之间的通信
  • 假设有两个兄弟组件firstCom和secondCom
// 事件中心:event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

// 父组件
<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}
</script>

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>

// 使用$emit发送事件
<script>
import {EventBus} from './event-bus.js' // 引入事件中心

export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

// 使用$on接受事件
<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

相当于将num存储在事件总线中,其他组件可以直接访问。事件总线相当于桥梁,不同组件可以通过它进行通信。

如果项目较大,使用这种方式通信,维护困难。

(3)依赖注入(provide、inject)

适用于父子/祖孙之间进行数据传递,当嵌套的层级过多时,可以使用这种方法。

依赖注入提供的属性是非响应式的

provide和inject是Vue提供的两个钩子,provide钩子用来发送数据或方法,inject用来接收数据或方法。

// 父组件中(和data同级)
provide() { 
    return {     
        num: this.num  
    };
}

//在子组件中
inject: ['num']

// 这种写法可以访问父组件的所有属性
provide() {
   return {
      app: this
    };
}
data() {
   return {
      num: 1
    };
}

inject: ['app']
console.log(this.app.num)

(4)ref、$refs

父子组件通信

ref:将这个属性用在子组件上,它的引用就指向了子组件的实例,可以通过实例访问子组件的属性和方法。

<template>
  <child ref="child"></component-a>
</template>

<script>
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }
</script>

(5)$parent、$children

$parent:可以访问父组件的实例(上一级父组件的属性和方法),可以通过$root访问根组件的实例

$children:访问所有子组件的实例,但是并不保证顺序,访问的数据也不是响应式的

根组件#app``的$parentnew Vue的实例,再往上是undefined

最底层组件的$children是空数组

// 子组件中
<template>
  <div>
    <span>{{message}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Vue'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

// 父组件中
// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change">点击改变子组件值</button>
  </div>
</template>

<script>
import child from './child.vue'
export default {
  components: { child },
  data() {
    return {
      msg: 'Welcome'
    }
  },
  methods: {
    change() {
      // 获取到子组件
      this.$children[0].message = 'JavaScript'
    }
  }
}
</script>

(6)$attrs、$listeners

实现组件之间的隔代通信

  • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style )
  • $listeners:包含了作用在这个组件上的所有监听器

简单地说:通过子组件作为中间层,孙子组件可以拿到父组件的数据,触发父组件的监听器

<template>
  <div class="father">
    <div>爸爸</div>
    <son
      :text="text"          //父组件向子组件传入textmsg和content
      :msg="msg"
      :content="content"
      @handle1="handle1"  //父组件中定义事件监听器监听handle1和handle2事件
      @handle2="handle2"
    ></son>
  </div>
</template>
<template>
  <div class="son">
    <div @click="handle">子组件</div>
    <grandson v-bind="$attrs" v-on="$listeners" @handle1="handle1"></grandson>
    //子组件中通过 v-bind="$attrs" v-on="$listeners" 将父组件的数据和监听器传递给孙子组件
    //子组件中定义事件监听器,监听handle1事件
  </div>
</template>

<script>
import grandson from "./EventCustomChildChild.vue";

export default {
  //为true,子组件的根元素为<div class="son" msg="12" content="123"> 没有被props接收的attribute会显示
  //为false,子组件的根元素为<div class="son"></div>
  inheritAttrs: false,
  components: {
    grandson,
  },
  props: ["text"],
  methods: {
    handle() {
      // {msg: '12', content: '123'},获取父组件传过来,没有被props接收
      console.log(this.$attrs);
      // {handle1: ƒ, handle2: ƒ},获取父作用域中的(不含 `.native` 修饰器的) `v-on` 事件监听器
      console.log(this.$listeners);   
    },
    handle1() {
      console.log("儿子组件中的handle1");
    },
  },
};
</script>

<template>
  <div class="grandson">
    <div @click="handle">孙子组件</div>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
  props: ["msg"],
  methods: {
    handle() {
      // {content: '123'},子组件通过v-bind="$attrs"将非props传递到孙子组件
      console.log(this.$attrs);
      // 如果没有使用props,结果为{msg: '12', content: '123'},但如果调用孙子组件时,发生下述情况,msg值会被覆盖,结果变为{msg: '11111', content: '123'}
      // <grandson v-bind="$attrs" v-on="$listeners" @handle1="handle1" :msg="11111"></grandson>
      
      // {handle1: ƒ, handle2: ƒ},获取上级组件的所有(不含 `.native` 修饰器的) `v-on` 事件监听器
      console.log(this.$listeners);
      
      // 子组件和父组件都定义了handle1事件监听器,但他们不会覆盖,展开handle1,会发现里面包含两个函数
      this.$emit("handle1");//儿子组件中的handle1、父组件的handle1函数,先触发子组件,再触发父组件
      this.$emit("handle2");//父组件的handle2函数
    },
  },
};
</script>

二、路由

1、vue-router的懒加载实现

(1)import动态加载

const router = new VueRouter({
  routes: [
    { path: '/list', component: () => import('@/components/list.vue') }
  ]
})

(2)require

const router = new Router({
  routes: [
   {
     path: '/list',
     component: resolve => require(['@/components/list'], resolve)
   }
  ]
})

(3)webpack的require.ensure

// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的写法  这种是官方推荐的写的 按模块划分懒加载 
const router = new Router({
  routes: [
  {
    path: '/list',
    component: List,
    name: 'list'
  }
 ]
}))

2、hash和history的区别

vue默认是哈希路由模式

(1)hash模式

  • hash出现在url中的#后面,但不会随着请求发送到服务端,对服务端没有任何影响,改变hash值不会重新加载页面。
  • hash模式的主要原理是onhashchange事件,页面hash发生变化时,不需要向服务器请求,即可按照规则加载相应的代码。
  • hash变化对应的url会被浏览器记录下来,这样浏览器就能实现页面的前进后退。

(2)history模式

  • url中不包含#,看起来比hash模式好看,但是一旦发生变化,会请求服务端,如果后台不支持当前路由,会报错404。
  • API:可以分为两大部分,切换历史状态和修改历史状态
    • 切换历史状态:forward、back、go(浏览器的前进、后退、跳转操作)
    • 修改历史状态:修改浏览器历史记录栈,修改后并不会立即加载url,不会重新刷新页面,比如pushState、replaceState

(3)模式对比

  • url值与历史栈
    • history可以设置任意路径(与当前url同源的值),如果是一模一样的url,也会刷新并将记录添加到栈中。
    • hash只能修改#后面的值,如果值不变,记录不会被添加到历史栈中。
  • 路由参数
    • history:任意类型数据,可以将数据存在一个特定的对象中(history.state)
    • hash:基于url,只能传递字符串,且有体积限制
  • 404错误
    • history任一路由在服务端找不到会报错404
    • hash只会判断#之前的链接能否在服务器上找到
  • 兼容性
    • history的兼容性略差,低版本浏览器不支持history API

3、获取页面的hash变化

(1)window.location.hash

  • 读取hash值
  • 修改hash,可以添加历史记录(前提是修改后的hash不能和当前页面的hash一致,否则不会添加到历史栈),但不会重载网页

(2)监听$route的变化

// 监听,当路由发生变化的时候执行
watch: {
  $route: {
    handler: function(val, oldVal){
      console.log(val);
    },
    // 深度观察监听
    deep: true
  }
}

4、$route$router

  • $route:路由信息对象,包括path、params、hash、query、fullpah、matched、name等路由参数信息
  • $router:路由实例对象,包括了路由的跳转方法,钩子函数

5、动态路由

this.$router.push({
    path:`/home/${id}`,
})

// 路由要配置/:id
{
    path:"/home/:id",
    name:"Home",
    component:Home
}
// 在组件中获取参数
this.$route.params.id
this.$router.push({
    name:'Home',
    params:{
        id:id
    }
})

// params用name传递参数,不使用/:id
{
    path:'/home',
    name:Home,
    component:Home
}

this.$route.params.id
this.$router.push({
    path:'/home',
    query:{
        id:id,
        name:jack
    }
})

{
    path:'/home',
    name:Home,
    component:Home
}

this.$route.query.id

name + params:

  • 刷新会丢失数据
  • 参数不会显示在浏览器url中

path + query:

  • 刷新不会丢失数据
  • 参数会显示在浏览器的url中

6、路由钩子

  • 全局前置:beforeEach、beforeResolve、afterEach
  • 路由独享守卫:beforeEnter
  • 组件守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

三、Vuex

1、什么是Vuex

Vuex是一个专为vue.js开发的状态管理模式, 集中式存储管理应用的所有组件状态。

  • vuex的状态存储是响应式的,vue组件从store中读取状态,若store的状态发生变化,相应的组件也会更新
  • vuex的原理,vuex生成了一个store实例,并把这个实例挂在了所有的组件上,所有的组件引用的是同一个store实例。

2、vuex有哪几种属性

State、Mutation、Action、Getter、Module

  • State:存储应用中的状态
  • Mutation:修改State中的状态
  • Action:用于提交Mutation
  • Getter:从State中派生出一些状态
  • Module:将Store分割成模块

State

Vuex使用单一状态树,即用一个对象就包含了全部的应用层级状态。每个应用仅包含一个store实例,我们在使用时能够简单直接的获取数据。

这个状态树是响应式的,当状态发生变化时,相关的组件将自动更新。

Mutation

用来更改state中的状态,mutation是唯一用来更改Vuex中状态的方法。

Action

action类似于mutation,不同在于

  • action支持异步操作,但最终还是通过调用mutation来修改state,不会直接变更状态
  • mutation必须是同步的,为了便于状态的追踪

Getter

从state中派生出一些状态,类似于Vue中的computed

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象中。当应用变得非常复杂时,state会变得非常臃肿。

为了解决以上问题,Vuex允许我们将store分割成模块,每个模块拥有自己的state、mutation、action、Getter、甚至是嵌套子模块

3、页面刷新后,Vuex数据丢失

原因:store中的数据保存在运行内存中的,当页面刷新时,页面会重新加载vue实例,store里面的数据就被被重新赋值初始化。

解决:vuex-along、web storage(页面刷新之前将数据放在storage中,刷新之后从storage中获取数据)

vuex-along的实质也是将vuex中的数据放到storage里面,只是存取的过程由组件帮我们完成。

4、vuex中的辅助函数

通过辅助函数mapStatemapGettersmapActionsmapMutations,把vuex.store中的属性映射到vue实例身上,这样在vue实例中就能访问vuex.store中的属性了,对于操作vuex.store就很方便了。

// 把state
computed:{ 
  ...Vuex.mapState({ 
    key:state=>state.属性
  })
} 

5、vuex和redux区别

相同点:

  • 使用state共享数据
  • 流程一致:定义全局state,修改state,视图state变化
  • 原理相似:通过全局注入store

不同点:

  • 实现原理
    • redux使用的是不可变数据,每次都是使用新的state替换旧的state;vuex可以直接修改数据
    • redux在检测数据变化时,通过diff方式比较差异,vuex和vue原理一样,通过getter/setter
  • 表现层
    • vuex定义了state、getter、mutation、action,redux定义了state、reducer、action
    • vuex触发使用commit同步,dispatch异步,redux中同步操作和异步操作都使用dispatch

6. Vuex的严格模式是什么,有什么作用,如何开启?

在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

在Vuex.Store 构造器选项中开启,如下

const store = new Vuex.Store({
    strict:true,
})

四、Vue3

1、Vue3的设计目标是什么

(1)设计目标

在vue3之前我们或许会面临以下问题:

  • 随着功能增长,复杂组件代码越来越难以维护
  • 缺少一种比较干净的在多个组件之间提取和复用逻辑的机制
  • 类型推断不太友好
  • bundle时间太久了

vue3做了哪些

  • 更小
    • 移除一些不常用的API
    • 优化Tree-Shaking
  • 更快
    • diff算法优化
    • 静态提升
    • 事件监听缓存
    • SSR优化
  • TypeScript支持
  • API设计一致性
  • 提高自身的可维护性
  • 开放更多底层功能

(2)优化方案

可以分成三个方面:源码、性能、语法API

1)源码

源码可以从两个层面展开:源码管理、TypeScript

源码管理

vue3整个源码是通过monorepo方式维护的,根据功能将不同模块拆分到packages目录下面的不同子目录中,使模块的拆分更细化,职责更明确,模块之间的依赖关系也更加明确,开发人员更容易阅读、理解和更改模块源码,提高代码的可维护性。

另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue 使用的,这样用户如果只想使用 Vue3 的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue

TypeScript

Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导

2)性能

性能:体积优化、编译优化、数据劫持优化

3)语法API

优化逻辑组织、优化逻辑复用

逻辑组织

Vue3:相同功能代码编写到一块 vue2:各个功能代码混杂在一起

逻辑复用

在vue2中,通过mixin实现功能混合,但有两个明显的问题:命名冲突和数据来源不清晰。

vue3可以将这些复用代码抽离成一个函数

2、Vue3和Vue2有什么区别

  • 响应式系统:Vue3引入了Composition API,这是一个新的响应式系统。

    • Composition提供了更灵活和更强大的组件状态和逻辑管理方式,使代码组织和重用更加方便
    • Composition使用函数而不是对象,可以Tree Shaking的优化效果
  • 更小的包体积

    • Tree Shaking和更高效的运行时代码生成
  • 性能优化

    • 更快、更高效的渲染机制
  • 作用域插槽替代为<slot>

    • 2.x 的机制导致作用域插槽变了,父组件会重新渲染
    • 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能
  • 引入Teleport组件:可以在DOM树的不同位置渲染内容,用于创建模态框、工具提示和其他覆盖层效果

  • 片段(Fragments):允许将多个元素进行分析,而无需添加额外的包装元素

  • 更好的TypeScript支持

    • vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦
    • 3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易
  • 简化API

<template>
  <button @click="increment">
    Count: {{ count }}
  </button>
</template>
 
<script>
// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数
import { ref, computed, onMounted } from 'vue'
 
export default {
  setup() {
// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数
    const count = ref(0)
 
// Vue2中需要在methods option中声明的函数,现在直接声明
    function increment() {
      count.value++
    }
 // 对应于Vue2中的mounted声明周期
    onMounted(() => console.log('component mounted!'))
 
    return {
      count,
      increment
    }
  }
}
</script>

3、Vue3的性能提升主要是通过哪几个方面体现

(1)编译阶段

优化点:diff算法优化、静态提升、事件监听缓存、SSR优化

diff算法优化

vue3diff算法中相比vue2增加了静态标记。

关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较,静态节点直接不进行比较。

静态提升

Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用,不需要再重复进行节点的创建。

事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化

SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染。

(2)源码体积

相比于Vue2,Vue3项目的整体体积变小了,Vue3源码中移除了一些不常用的API,此外就是Tree-Shaking

任何一个函数,如refreavtivedcomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小。

(3)响应式系统

vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式。

vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历

  • 可以监听动态属性的添加
  • 可以监听到数组的索引和数组length属性
  • 可以监听删除属性

4、Vue3为什么要用Proxy

(1)Object.defineProperty

定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

  • 检测不到对象属性的添加和删除
  • 一些数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

(2)Proxy

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

Proxy可以直接监听数组的变化(pushshiftsplice

Proxy不兼容IE,也没有 polyfilldefineProperty 能支持到IE9

5、Composition API 和 Options API

(1)Options API

选项API:methods、computed、watch、data

当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解

(2)Composition API

Composition API:组件根据功能组织到一起,一个功能的所有API会放在一起(高内聚、低耦合)

在进行逻辑复用时,可以通过composition API将逻辑抽成一个函数,在需要的地方直接引用,得到逻辑属性/方法,而不是使用mixin。

  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • Composition API对 tree-shaking 友好,代码也更容易压缩
  • Composition API中见不到this的使用,减少了this指向不明的情况
  • 如果是小型组件,可以继续使用Options API,也是十分友好的

五、虚拟DOM

1. 对虚拟DOM的理解?

从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。

将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。

在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。

2. 虚拟DOM的解析过程

  • 首先对将要插入到文档中的 DOM 树结构进行分析
    • 使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性
  • 将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异
  • 最后将记录的有差异的地方应用到真正的 DOM 树中去

3. 为什么要用虚拟DOM

(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能

  • 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
  • 虚拟DOM∶ 生成vNode + DOMDiff + 必要的dom更新

Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。

尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。

(2)跨平台

Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。

4. 虚拟DOM真的比真实DOM性能好吗

  • 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
  • 正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。

5. DIFF算法的原理

在新老虚拟DOM对比时:

  • 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
  • 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
  • 匹配时,找到相同的子节点,递归比较子节点

在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低至O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

6. Vue中key的作用

vue 中 key 值的作用可以分为两种情况来考虑:

  • v-if 中使用 key
    • 由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
    • 因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。
    • 如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。
    • 因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
  • v-for 中使用 key
    • 用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。
    • 如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。
    • 因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。

key 是 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速

  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

7. 为什么不建议用index作为key?

使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。

参考文章:juejin.cn/post/696477…