Vue 高频面试题汇总

.什么是vue生命周期

Vue 实例从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期,共八个阶段。
作用: 生命周期中有多个事件钩子,在控制整个 Vue实例 的过程时更容易形成好的逻辑。
beforeCreate: 完成实例初始化,this 指向被创建的实例,data,computed,watch,mothods方法 和 数据都不可以访问,数据观测之前(data observer)被调用。
created: 实例创建完成,data,computed,watch,methods 可被访问,未挂载 Dom,可对 data 进行操作,操作 Dom 需放到 nextTick 中。
beforeMount: 有了el,找到对应的 template 编译成 render 函数
mounted: 完成挂载 Dom 和 渲染,可对 Dom 进行获取节点等操作,可发起后端请求拿到数据。
beforeUpdate: 数据更新时调用,发生在虚拟 Dom 重新渲染 和 打补丁之前之调用。
updated: 组件 Dom 已完成更新,可执行依赖的 Dom 操作,不要操作数据会陷入死循环。
beforeDestroy: 实例销毁之前调用,可进行优化操作,如销毁定时器,解除绑定事件。
destroyed: 组件已经被销毁,事件监听器和子实例都会被移除销毁。

首次页面加载会触发四个钩子函数: beforeCreate, created, beforeMount, mounted
DMO 渲染在 mounted 中就已经完成了。

可以使用 $on('hook:')$once('hook:') 来简化生命周期的注册

.谈谈 MVVM 模式

Model: 代表数据模型,也可以在 Model 中定义 数据修改 和 操作 的业务逻辑。
View: 代表 UI 组件,它负责将 数据模型 转化成 UI 展现出来。
ViewModel: 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步 View 和 Model 的对象,连接 Model 和 View。

在 MVVM 架构下,View 和 Model 之间并没有直接的联系,而是通过 ViewMode 进行交互,Model 和 ViewModel 之间的交互是双向自动的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。而开发者只需关注业务逻辑,不需要手动操作 DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

MVVM 和 MVC区别?

mvcmvvm其实区别并不大。都是一种设计思想。主要就是 mvcController 演变成 mvvm 中的 viewModelmvvm 主要解决了mvc 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到 View

说下 Vue 实现数据双向绑定的原理

Vue 实现数据双向绑定主要是:采用 数据劫持结合发布者-订阅者模式 的方式,通过 Object.defineProperty() 来劫持各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty() 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue追踪依赖,在属性被访问和修改时通知变化。

.请说一下 Vue 响应式数据的原理是什么?

在 Vue 初始化数据时, 使用 Object.defineProperty 重新定义 data 中所有属性,增加了数据 获取(getter) / 设置(setter) 的拦截功能。在 获取 / 设置 时可增加一些逻辑,这个逻辑交叫作 依赖收集。当页面取到对应属性时会进行依赖收集, 如果属性发生变化, 则会通知收集的依赖进行更新,而负责收集的就是 watcher
如负责渲染的 watcher 会在页面渲染的时候对数据进行取值,并把当前 watcher 先存起来对应到数据上,当更新数据的时候告诉对应的 watcher 去更新, 从而实现了数据响应式。

data 一般分为两大类: 对象类型 和 数组:

对象:

在 Vue 初始化的时候,会调用 initData 方法初始化 data,它会拿到当前用户传入的数据。判断如果已经被观测过则不在观测,如果没有观测过则利用 new Observer 创建一个实例用来观测数据。如果数据是对象类型非数组的话会调用 this.walk(value) 方法把数据进行遍历,在内部使用 definReactive 方法重新定义( definReactive 是比较核心的方法: 定义响应式 ),而重新定义采用的就是 Object.defineProperty 。如当前对象的值还是个对象,会自动调用递归观测。当用户取值的时候会调用 get 方法并收集当前的 wacther 。在 set 方法里,数据变化时会调用 notify 方法触发数据对应的依赖进行更新。

数组:

使用函数劫持的方式重写了数组的方法,并进行了原型链重写。使 data 中的数组指向了自己定义的数组原型方法。这样的话,当调用数组 API 时,可以通知依赖更新。如果数组中包含着引用类型,则会对数组中的引用类型进行再次监控。

也就是当创建了 Observer 观测实例后,如果数据是数组的话,判断是否支持自己原型链,如果不支持则调用 protoAugment 方法使目标指向 arrayMethods 方法。arrayMethods 就是重写的数组方法,包括 pushpopshiftunshiftsplicesortreverse 共七个可以改变数组的方法,内部采用函数劫持的方式。在数组调用重写的方法之后,还是会调用原数组方法去更新数组。只不过重写的方法会通知视图更新。如果使用 pushunshiftsplice 等方法新增数据,会调用 observeArray 方法对插入的数据再次进行观测。

如果数组中有引用类型,则继续调用 observeArray 方法循环遍历每一项,继续深度观测。前提是每一项必须是对象类型, 否则 observe 方法会直接 return

.为何 Vue 采用异步渲染?

如不采用异步更新, 则每次更新数据都会对当前组件进行重新渲染, 因此为了性能考虑 Vue 在本轮数据更新结束后,再去异步更新视图。
当数据变化之后, 会调用 notify 方法去通知 watcher 进行数据更新。而 watcher 会调用 update 方法进行更新( 这里就是发布订阅模式 )。更新时并不是让 wathcer 立即执行,而是放在一个 queueWatcher 队列里进行过滤,相同的 watcher 只存一个。最后在调用 nextTick 方法通过 flushSchedulerQueue 异步清空 watcher 队列。

.nextTick 实现原理?

nextTick 方法主要是使用了 宏任务微任务 定义了一个异步方法。多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。所以 nextTick 方法就是异步方法。
默认在内部调用 nextTick 时会传入 flushSchedulerQueue 方法, 存在一个数组里并让它执行。用户有时也会调用 nextTick,调用时把用户传过来的 cb 也放在数组里,都是同一个数组 callbacks 。多次调用 nextTick 只会执行一次, 等到代码都执行完毕后,会调用 timerFunc 这个异步方法依次进行判断所支持的类型:

  1. 如支持 Promise 则把 timerFunc 包裹在了 Promise 中并把 flushCallbacks 放在了 then 中, 相当于异步执行了 flushCallBacksflushCallBacks 函数作用就是让传过来的方法依次执行。

  2. 如不是 IE 、支持 Mutationobserve 并且是原生的 Mutationobserve。首先声明一个变量并创建一个文本节点。接着创建 Mutationobserve 实例并把 flushCallBacks 传入, 调用 observe 方法去观测每一个节点。如果节点变化会异步执行 flushCallBacks方法。

  3. 如果支持 setImmediate , 则调用 setImmediate 传入flushCallBacks 异步执行。

  4. 以上都不支持就只能调用 setTimeout 传入 flushCallBacks

作用:$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM

.请说一下 Vue 中 Computed 和 watch ?

默认 computedwatch 内部都是用一个 watcher 实现的 。
computed有缓存功能, 不会先执行,只有当依赖的属性发生变化才会通知视图跟新。
watcher 没有缓存,默认会先执行,只要监听的属性发生变化就会更新视图。

computed

调用 initComputed 方法初始化计算属性时,会获取到用户定义的方法,并创建一个 watcher 把用户定义传进去, 这个 watcher 有个标识: lazy = true,默认不会执行用户定义的函数。还有个标识 dirty = true 默认去求值 。watcher 内部调用 defineComputed 方法将计算属性定义在实例上,其底层也是用的 Object.defineProperty。并且传入了 createComputedGetter 方法定义一个计算属性。在用户取值时,调用的是 createComputedGetter 返回函数 computedGetter。判断当前的 watcher.dirty 是否为 true。如果为 true 则调用 watcher.evaluate 方法求值。在求值时是调用的 this.get() 方法。其实 this.get() 就是用户传入的方法,执行时会把方法里的属性依次取值。而在取值前调用了 pushTarget 方法将 watcher 放在了全局上,当取值时会进行依赖收集,把当前的计算属性的 watcher 收集起来。等数据变化则通知 watcher 重新执行,也就是进入到了 update 方法中。update 并没有直接让 watcher 执行,而是将 dirty = true。这样的好处就是,如果 dirty = true,就进行求值,否则就返回上次计算后的值,从而实现了缓存的机制。

watch

调用 initWatch 方法初始化 watch 的时候,内部传入用户定义的方法调用了 createWatcher 方法。在 createWatcher 方法中比较核心的就是 $watch 方法,内部调用了 new Watcher 并传入了 expOrFn 和 回调函数。expOrFn 如果是个字符串的话, 会包装成一个函数并返回这个字符串。这时 lazy = false 了, 则直接调用了 this.get() 方法取属性的值。同 computed 在取值前也执行 pushTarget 方法将 watcher 放在了全局上, 当用户取值时就收集了 watcher。 因此当属性值发生改变时, watcher 就会更新。
如果监听的属性值是个对象,则取对象里的值就不会更新了,因为默认只能对属性进行依赖收集,不能对属性值是对象的进行依赖收集。想要不管属性值是否是对象都能求值进行收集依赖,可设置 deep = true 。如设置了deep = true ,则会调用 traverse 方法进行递归遍历。

.Vue 组件中 data 为什么必须是一个函数?

因为 js 本身的特性带来的,同一个组件被复用多次,会创建多个实例。这些实例是同一个构造函数。如果 data 是一个对象的话,那么所有组件都共享了同一个对象。为了保证组件中数据的独立性要求每个组件必须通过 data 函数返回一个对象作为组件的状态。
Vue 通过 extend 创建子类之后,会调用 mergeOptions 方法合并父类和子类的选项,选中就包括 data。在循环完父类和子类之后调用 mergeField 函数的中的 strat 方法去合并 data,如果 data 不是函数而是个对象,则会报错提示 data 应该是个函数。

.Vue 中事件绑定原理

Vue 中事件绑定分为两种:

  1. 原生事件绑定: 采用的是 addEventListener 实现
  2. 组件事件绑定: 采用的是 $on 方法实现

click 事件为例,普通 Dom 元素绑定事件是 @click ,编译出来是 onclick事件,组件绑定事件是 @click 组件自定义事件 和 @click.native原生事件两种,编译出来分别是 onclick 事件, nativeOnclick 事件。组件的 nativeOn 等价于普通元素的 on ,而组件的 on 单独处理。
渲染页面时,普通 Dom 会调用 updateDOMListeners 方法,内部先把 data.on 方法拿出来,然后调用 updateListeners 方法来添加一个监听事件,同时会传入一个 add$1 方法。内部调用 addEventListener 方法直接把事件绑定到元素上。

而组件会调用 updateComponentListeners 方法。内部也是调用 updateListeners 方法但传入的是 add 方法。这里的 add 方法与普通元素的 Domadd$1 方法略有不同,采用的是自己定义的发布订阅模式 $on 方法,解析的是 on 方法,组件内部通过 $emit 方法触发的。还有 click.native 方法是直接把事件绑在了最外层元素上,用的也是 updateListeners 方法传入 add$1方法。

.v-model 的实现原理是什么?

通俗讲 v-model 可以看成是 value + input 的语法糖。
组件的 v-model 也确实是这样 。在组件初始化的时候, 如果检测到有 model 属性,就会调用 transformModel 方法转化 model。如果没有 prop 属性和 event 属性, 则默认会给组件 propvalue 属性, 给 eventinput 事件 。把 prop 的属性赋给了 data.attrs 并把值也给了它,即 data.attrs.value = '我们所赋的值'。会给 on 绑定 input 事件,对应的就是 callback
如果在组件内自定义 modelpropevent, 这样的话组件初始化的时候, 接受 属性事件 时不再是 valueinput 了, 而是我们自定义的 属性事件
如果是普通的标签, 则在运行时会自动判断标签的类型, 生成不同的属性 domProp 和 事件 on。还增加了指令 directive, 针对输入框的输入法加上了一些逻辑并做了校验和处理。

. Vue中的 v-show 和 v-if 是做什么用的, 两者有什么区别?

v-if:会在 with 方法里进行判断,如果条件为 true 则创建相应的虚拟节点,否则就创建一个空的虚拟节点也就是不会渲染 DOM
v-show: 会在 with 方法里创建了一个指令就 v-show,在运行的时候处理指令,添加了 style: display = none / originalDisplay

v-if 才是“真正的”条件渲染, 因为它会确保在切换过程中条件块内的事件监听器和子组件适当的被销毁和重建。
v-if也是惰性的, 如果在初次渲染时条件为假, 则什么也不做,一直到条件第一次变为真时, 才会渲染条件块。

相比之下, v-show 就简单的多,不管初始条件是什么,元素总会被渲染, 并且只是简单的基于 css 进行切换。

一般来说,v-if 有更高的切换开销,v-show 有更高的初始渲染开销。
因此,如需要频繁的切换则使用 v-show 较好,如在运行时条件不大可能改变则使用 v-if 较好。

. v-if 和 v-for 为什么不能连用?

v-for 的优先级会比 v-if 要高, 在 调用 with 方法编译时会先进行循环, 然后再去做 v-if的条件判断, 因此性能不高。
因此一般会把 v-if 提出来放在 v-for 外层, 或者想要连用把渲染数据放在计算属性里进行过滤。

.Vue 中的 v-html 会导致哪些问题

v-html 其原理就是用 innerHtml 实现的的, 如果不能保证内容是完全可以被依赖的, 则可能会导致 xxs 攻击。
在运行的时候, 调用 updateDOMProps 方法或解析配置的属性, 如果判断属性是 innerHTML 的话, 会清除所有的子元素。

.Vue 中父子组件的调用顺序

组件的调用都是先父后子,渲染完成的过程顺序都是先子后父
组件的销毁操作是先父后子,销毁完成的顺序是先子后父
在页面渲染的时候,先执行父组件的 beforeCreate -> created -> befroreMount,当父组件实例化完成的时候会调用 rander 方法,判断组件是不是有子组件,如果有子组件则继续渲染子组件以此类推。当子组件实例化完成时候,会把子组件的插入方法先存起来放到 instertedVNodeQueue 队列里, 最后会调用 invokeIntertHook 方法把当前的队列依次执行。
更新也是一样,先父beforeUpdate -> 子beforeUpdate 再到 子 updated -> 父 updated

  1. 加载渲染过程
    父beforeCreate-> 父created-> 父beforeMount-> 子beforeCreate-> 子created-> 子beforeMount- > 子mounted-> 父mounted

  2. 子组件更新过程
    父beforeUpdate-> 子beforeUpdate-> 子updated-> 父updated

  3. 父组件更新过程
    父beforeUpdate -> 父updated

  4. 销毁过程
    父beforeDestroy-> 子beforeDestroy-> 子destroyed-> 父destroyed

# Vue中父组件能监听到子组件的生命周期吗

父组件通过@hook: 能够监听到子组件的生命周期,举个栗子:

// 这里是父组件
<template>
    <child @hook:mounted="getChildMounted"  />
</template>
<script>
method: {
    getChildMounted () {
        // 这里可以获取到子组件mounted的信息
    }
}
</script>

.Vue 中组件怎么通讯?

  1. 父子通讯: 父 → 子 props, 子 → 父 $on / $emit
    通过 eventsMixin 方法中的 $on 方法维护一个事件的数组,然后将函数名传入 $emit 方法,循环遍历出函数并执行。

  2. 获得父子组件实例的方式:$parent / $children
    在初始化的时候调用 initLifecycle 方法初始化 $parent$children 放在实例上

  3. 在父组件中提供数据供子组件/孙子组件注入进来: Provide / Inject
    通过 initProvideinitInjections 方法分别把 providereject 放在 $options 上。在调用 reject 的时候,调用 resolveInject 方法遍历,查看父级是否有此属性,有则就直接 return 并把它定义在自己的实例上。

  4. Ref 获得实例的方式调用组件的属性或方法
    ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。
    用在 DOM 上就是 DOM 实例,用在组件上就是组件实例。

  5. Event bus 实现跨组件通讯
    实质上还是基于 $on$emit,因为每个实例都有 $on$emit 并且事件的绑定和触发必须在同一个实例,所以一般会专门定义一个实例去用于通信,如 Vue.prototype.$bnts = new Vue

  6. Vuex 状态管理实现通讯

  7. $attrs$Listeners 实现数据 和 事件的传递,还有 v-bind="$prop"

.为什么使用异步组件?

可使用异步的方式加载组件,减少打包体积,主要依赖 import() 语法,可实现文件的分割加载

components:{
  testCpt: (resove) => import("../components/testCpt")  或
  testCpt: r => require(['@/views/assetsInfo/assetsProofList'],r)
}

加载组件的时候,如果组件是个函数会调用 resolveAsyncComponent 方法, 并传入组件定义的函数 asyncFactory , 并让其马上执行。因为是异步的所以执行后并不会马上返回结果。而返回的是一个 promise,因此没有返回值, 返回的是一个占位符。
加载完成后,会执行 factory 函数并传入了成功/失败的回调。在回调 resolve 成功的回调时会调用 forceRander 方法, 内部调用 $forceUpdate 强制刷新。之后 resolveAsyncComponent 判断已经执行成功,就是去创建组件、初始化组件和渲染组件。

# Vue中的事件修饰符主要有哪些?分别是什么作用

.stop:阻止事件冒泡 .native:绑定原生事件
.once:事件只执行一次
.self:将事件绑定在自身身上,相当于阻止事件冒泡
.prevent:阻止默认事件 .caption:用于事件捕获

# v-for 里面数据层次太多,数据不刷新怎么办

运用 this.$forceUpdate() 迫使 Vue 实例重新渲染。
注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

.说说对 keep-alive 的了解

keep-alive 是一个抽象组件,可实现组件缓存。当组件切换时不会对当前组件进行卸载。
算法: LRU → 最近最久未使用法
常用的生命周期: activateddeactivated

声明 keep-alive 时在函数里设置了几个属性: propscreateddestroyedmountedrander 等;

  1. props: 调用 keep-alive 组件可设置的属性,共有三个属性如下:
    include: 想缓存的组件
    exclude: 不想缓存的组件
    max: 最多缓存多少个
  2. created: 创建一个缓存列表
  3. destroyed: 销毁时清空所有缓存列表
  4. mounted: 会监听 include 和 exclude, 动态添加 或 移除缓存
  5. rander: 渲染时拿到第一个组件,拿到第一个组件,判断是不是在缓存里

.$route$router 的区别是什么?

$routerVueRouter 实例,是个全局路由对象,包含路由跳转方法、钩子函数等。
$route 是 路由信息对象 || 跳转的路由对象,每一个路由都会有一个route对象,是一个局部对象,包含path,params,hash,query,fullPath,matched,name 等路由信息参数。

. Vue 路由的钩子函数

首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。
一些需要登录才能调整页面的重定向功能。
beforeEach主要有3个参数to,from,next:
to:route即将进入的目标路由对象,
from:route当前导航正要离开的路由
next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。

. vue-router有哪几种路由守卫?

  1. 全局守卫 ( vue-router 全局有三个守卫 )
    router.beforeEach 全局前置守卫 进入路由之前
    router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
    router.afterEach 全局后置钩子 进入路由之后
  // main.js 入口文件
  import router from './router'; // 引入路由
  router.beforeEach((to, from, next) => { 
    next();
  });
  router.beforeResolve((to, from, next) => {
    next();
  });
  router.afterEach((to, from) => {
    console.log('afterEach 全局后置钩子');
  });
  1. 路由独享守卫
  const router = new VueRouter({
    routes: [
      {
        path: '/foo',
        component: Foo,
        beforeEnter: (to, from, next) => { 
          // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
        }
      }
    ]
  })
  1. 路由组件内的守卫
    beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创建
    beforeRouteUpdate (2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 this
    beforeRouteLeave 离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例 this

.hash 模式 和 history模式

hash: 在 url 中带有 #,其原理是 onhashchange 事件。
可以在 window 对象上监听这个事件:

window.onhashchange = function(event){
     ...
}

history: 没有原 # , 其原理是 popstate 事件,需要后台配置支持。
html5 中新增两个操作历史栈的API: pushState()replaceState() 方法。

history.pushState(data[,title][,url]); // 向历史记录中追加一条记录
history.replaceState(data[,title][,url]); // 替换当前页在历史记录中的信息。

这两个方法也可以改变url,页面也不会重新刷新,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。

.Vuex 是什么? 怎么使用它? 哪种功能场景使用?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 只能使用在 vue 上,因为其高度依赖于 vue 的双向绑定 和 插件系统。
调用了 Vue.mixin,在所有组件的 beforeCreate 生命周期注入了设置 this.$store 这样一个对象。
场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车
state: Vuex 使用单一状态树,存放的数据状态,不可以直接修改里面的数据。
mutations: 定义方法动态修改 Vuex 的 store 中的状态或数据。
getters: 类似 vue 的计算属性,主要用来过滤一些数据。
actions: 可以理解为通过将 mutations 里面处理数据的方法变成可异步的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action
modules: 项目特别复杂的时候,可以让每一个模块拥有自己的 state、mutation、action、getters,使得结构非常清晰,方便管理。

actions 和 mutations的区别

action 主要处理的是异步的操作,mutation 必须同步执行,而 action 既可以处理同步,也可以处理异步的操作。action 提交的是 mutation,而不是直接变更状态。
如果请求来的数据不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入 vuexstate 里。
如果被其他地方复用,请将请求放入 action 里方便复用,并包装成 promise 返回。

.assets 和 static的区别

相同点: assetsstatic 两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下。
不相同点:
assets 中存放的静态资源文件在项目打包时,会将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在static文件中跟着index.html 一同上传至服务器。
static 中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议:将项目中 template 需要的样式文件js文件等都可以放置在 assets 中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css等文件可以放置在 static 中,因为这些引入的第三方文件已经经过处理,我们不再需要处理,直接上传。

Vue 中 key 的作用是什么?

需要使用 key 给每一个节点做唯一标识,可让 diff 算法可以正确识别此节点,以更高效的更新虚拟 DOM。

新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的
需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识

.用vnode描述一个DOM结构

虚拟节点就是用一个对象描述真实的dom元素
会将 template 先转换成 ast 树, ast 通过代码生成 codegen 转成 rander函数, rander函数内部调用 $createElement 方法简称 _c, 传入 tag (创建的元素), data(元素的属性), children(子元素) . 会判断 children 是不是一个字符串, 否则会做深度递归, 最后返回的结果就是一个对象,可描述出DOM 结构.

.简述 Vue 中 diff 算法原理

  1. 先同级比较, 在比较子节点.
  2. 判断出一方有子节点另一方没有子节点的情况.
    如果新的一方有子节点,老的没有,则把子节点直接插入到老节点里即可.
    如果老的一方有子节点,新的没有,则把老的子节点直接删除.
  3. 判断出都有子节点的情况, 递归遍历子采用双指针(头/尾指针)的方式比对节点.

Vue.use 与 Vue.component 的区别

都用于注册全局组件/插件的

Vue.component() 每次只能注册一个组件,功能很单一。
Vue.component('draggable', draggable)

Vue.use() 内部调用的仍是 Vue.component() 去注册全局组件/插件,但它可以做更多事情,比如多次调用 Vue.component() 一次性注册多个组件,还可以调用Vue.directive()、Vue.mixins()、Vue.prototype.xxx=xxx 等等,其第二个可选参数又可以传递一些数据

Vue.use({
    install:function (Vue, options) {
        // 接收传递的参数: { name: 'My-Vue', age: 28 }
        console.log(options.name, options.age)
        Vue.directive('my-directive',{
            inserted(el, binding, vnode) { }
        })
        Vue.mixin({
            mounted() { }
        })
        Vue.component('draggable', draggable)
        Vue.component('Tree', Tree)
    }
}, 
{ name: 'My-Vue', age: 28 })

在main.js 文件里 动态注册全局组件时, 或用到 require.context

require.context(): 一个 Webpack 的API,获取一个特定的上下文(创建自己的context),主要用来实现自动化导入模块。
它会遍历文件夹中的指定文件,然后自动化导入,而不需要每次都显式使用 import / require 语句导入模块!
在前端工程中,如果需要一个文件夹引入很多模块,则可以使用 require.context()

require.context(directory, useSubdirectories = false, regExp = /^\.\//)

directory {String} 读取目录的路径
useSubdirectories {Boolean} 是否递归遍历子目录
regExp {RegExp} 匹配文件的正则

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异?

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

pull: 其代表为 React,通常会用 setStateAPI 显式更新,然后 React 会进行一层层的 Virtual Dom Diff 操作找出差异,然后 PatchDOM上,React 从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的 Diff 操作查找「哪发生变化了」,另外一个代表就是 Angular 的脏检查操作。

push: Vue 的响应式系统则是 push 的代表,当 Vue 程序初始化的时候就会对数据 data 进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此 Vue 是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉 Vue 的响应式系统就知道,通常一个绑定一个数据就需要一个 Watcher ,一但我们的绑定细粒度过高就会产生大量的 Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此 Vue 的设计是选择中等细粒度的方案,在组件级别进行 push 侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行 Virtual Dom Diff 获取更加具体的差异,而Virtual Dom Diff 则是 pull 操作,Vuepush+pull 结合的方式进行变化侦测的。

Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期?

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

Reactpull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual Dom Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate 进行手动操作来减少diff,从而提高程序整体的性能。
Vuepull+push 的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push 的阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前 Vue并没有考虑引入shouldComponentUpdate 这种手动优化的生命周期。

.Vue 中常见的性能优化

  1. 编码优化
    (1). 不要将所有的数据放在data里,data中的数据都会增加 getter和setter,收收集对应的 watcher
    (2). 在 v-for 时给每项元素绑定事件必须使用时间代理
    (3). SPA页面采用 keep-alive 缓存组件
    (4). 拆分组件(提高复用性,增加代码的可维护性,减少不必要的渲染)
    (5). v-if 当值为 false 时内部指令不执行具有阻断功能,很多情况下使用v-if 代替 v-show
    (6). 使用 key 保证唯一性
    (7). 使用 Object.freeze 冻结数据,冻结后不再有 gettersetter
    (8). 合理使用路由懒加载和异步组件
    (9). 数据持久化问题如: 防抖、节流
  2. Vue 加载性能优化
    (1). 第三方模块按需导入(babel-plugin-component)
    (2). 滚动可视区域动态加载(vue-virtual-scroll-list / 'vue-virtual-scroller') -- 长列表优化
    (3). 图片懒加载(vue-lazyload)
  3. 用户体验
    (1). app-skeleton 骨架屏
    (2). app-sheapp
  4. SEO 优化
    (1). 预加载插件 prerender-spa-plugin
    (2). 服务端渲染 ssr
  5. 打包优化
    (1). 使用 CDN 的方式加载第三方模块
    (2). 多线程打包
    (3). splitChunk 抽离公共文件
  6. 缓存 压缩
    (1). 客户端缓存和服务端缓存
    (2). 服务端gzip压缩

.什么是作用域插槽?

  1. 插槽: 创建组件虚拟节点时,会将组件儿子的虚拟节点先保存起来。初始化组件时,通过插槽属性将儿子进行分类。(作用域为父组件)
    渲染组件时会拿对应的 slot 属性的节点进行替换操作。
  2. 作用域插槽: 在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(作用域为子组件)

普通插槽编译时调用 createElement 方法创建组件,并把子节点生成虚拟 dom 做好标识存起来。渲染时调用 randerSlot 方法循环匹配出对应的虚拟节点在父组件替换当前位置。
而作用域插槽在编译时会把子组件编译成函数,函数不调用就不会渲染。也就是说在初始化组件的时候并不会渲染子节点。渲染页面时调用 randerSlot 方法执行子节点的函数并把对应的属性传过来。当节点渲染完成之后在组件内部替换当前位置。

.Vue与Angular以及React的区别?

1.与AngularJS的区别
相同点:

  1. 都支持指令:内置指令和自定义指令。
  2. 都支持过滤器:内置过滤器和自定义过滤器。
  3. 都支持双向数据绑定。
  4. 都不支持低端浏览器。

不同点:

  1. AngularJS 采用 TypeScript 开发, 而 Vue 可以使用 javascript 也可以使用 TypeScript。
  2. 在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢。
    Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
    对于庞大的应用来 说,这个优化差异还是比较明显的。
  3. AngularJS社区完善, Vue的学习成本较小

2.与React的区别
相同点:

  1. React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用。
  2. 中心思想相同:一切都是组件,组件实例之间可以嵌套。
  3. 都提供合理的钩子函数,可以让开发者定制化地去处理需求。
  4. 都不内置AJAX,Route等功能核心包,而是以插件的方式加载。
  5. 在组件开发中都支持mixins的特性。

不同点:

  1. vue 组件分为全局注册和局部注册,在 react 中都是通过 import 相应组件,然后模版中引用;
  2. props 是可以动态变化的,子组件也实时更新,在 react 中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过 props 来更改视图
  3. vue 多了指令系统,让模版可以实现更丰富的功能,而 React 只能使用JSX语法
  4. react 是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合 redux-form,组件的横向拆分一般是通过高阶组件。而 vue 是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用 mixin。
  5. Vue增加的语法糖computed和watch,而在React中需要自己写一套逻辑来实现。

高精度全局权限处理

权限控制由前端处理时,通常使用 v-if / v-show 控制元素对不同权限的响应效果。这种情况下,就会导致很多不必要的重复代码,不容易维护,因此可以造一个小车轮,挂在全局上对权限进行处理。

  // 注册全局自定义指令,对底层原生DOM操作
  Vue.directive('permission', {
        // inserted → 元素插入的时候
        inserted(el, binding){
            // 获取到 v-permission 的值
            const { value } = binding
            if(value) {
                // 根据配置的权限,去当前用户的角色权限中校验
                const hasPermission = checkPermission(value)
                if(!hasPermission){
                    // 没有权限,则移除DOM元素
                    el.parentNode && el.parentNode.removeChild(el)
                }
            } else{
                throw new Error(`need key! Like v-permission="['admin','editor']"`)
            }
        }
    })
    // --> 在组件中使用 v-permission
    <button v-permission="['admin']">权限1</button>
    <button v-permission="['admin', 'editor']">权限2</button>

.对于 vue3.0 特性你有什么了解的吗?

(1). 监测机制的改变

3.0 基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。替代了Vue 2采用 defineProperty去定义get 和 set, 意味着彻底放弃了兼容IE, 这也取消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
=>只能监测属性,不能监测对象:
=>检测属性的添加和删除;
=>检测数组索引和长度的变更;
=>支持 Map、Set、WeakMap 和 WeakSet。
新的 observer 还提供了以下特性:
用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2). 模板

模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
同时,对于 render 函数的方面,vue3.0 也进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3). 对象式的组件声明方式

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

此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4). 其它方面的更改

支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
基于 treeshaking 优化,提供了更多的内置功能。

.Vue等单页面应用(spa)及其优缺点

优点: Vue的目标是通过尽可能简单的 API实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好;即第一次就将所有的东西都加载完成,因此,不会导致页面卡顿。
缺点: 不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容