vue相关问题

简述MVVM

视图模型双向绑定,是Model-View-ViewModel的缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图。

MVVM的优点

  • 1、低耦合,视图(View)可以独立于Model变化和修改,一个Model可以绑定到不同的View上,当View变化的时候Model可以不变化,当Model变化的时候View也可以不变;
  • 2、可重用性,你可以把一些视图逻辑放在一个Model里面,让很多View重用这段视图逻辑
  • 3、独立开发
  • 4、可测试

Vue2.x双向绑定数据绑定

采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调

vue的双向绑定是指,数据变化更新视图变化,视图变化更新数据变化。
view变化更新Data可以通过事件监听的方式来实现,所以vue的数据双向绑定的工作主要是如何根据data变化更新view
主要通过以下4个步骤来实现数据双向绑定:

  • 1、实现一个监听器Observer:对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()对这个属性都加上setter 和getter。当给对象的某个属性赋值的时候就会触发setter,监听到数据变化(在getter收集依赖,在setter通知依赖更新)
  • 2、实现一个解析器Complie: 解析Vue模版指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
  • 3、实现一个订阅者Watcher: Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
  • 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

谈谈对vue2生命周期的理解?

每个vue实例在创建的时候会经过一系列的初始化过程,vue的生命周期钩子,就是在达到某个阶段或条件的时候出发的函数,目的是为了完成一些动作或事件。

  • create阶段:vue实例被创建
    beforeCreate: 创建前,此时data和methods中的数据都还没有初始化
    created: 创建完毕,data中有值,未挂载
  • mount阶段: vue实例被挂载到真实DOM节点
    beforeMount:可以发起服务端请求,去数据
    mounted: 此时可以操作DOM
  • update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染
    beforeUpdate :更新前
    updated:更新后
  • destroy阶段:vue实例被销毁
    beforeDestroy:实例被销毁前,此时可以手动销毁一些方法
    destroyed:销毁后

组件生命周期
生命周期(父子组件) 父组件beforeCreate –> 父组件created –> 父组件beforeMount –> 子组件beforeCreate –> 子组件created –> 子组件beforeMount –> 子组件 mounted –> 父组件mounted –>父组件beforeUpdate –>子组件beforeDestroy–> 子组件destroyed –> 父组件updated

computed与watch

能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed, 重点在于 computed 的缓存功能 computed 计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量 改变时,计算属性也会跟着改变; watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法。

  • watch属性监听: 是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用
  • computed计算属性: 属性的结果会被缓存,当computed中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用 computed中的函数必须用return返回最终的结果 computed更高效,优先使用。data 不改变,computed 不更新。
    使用场景 computed:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能 watch:当一条数据影响多条数据的时候使用,例:搜索数据

组件中的data为什么是一个函数?

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。 2.如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

为什么v-for和v-if不建议用在一起

当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费,建议使用computed,先对数据进行过滤。
3.x 版本中 v-if 总是优先于 v-for 生效

React/Vue 项目中 key 的作用

  • key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度,更高效的更新虚拟DOM;
  • 为了在数据变化时强制更新组件,以避免“就地复用”带来的副作用
    当 Vue.js 用 v-for 更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误

vue组件的通信方式

  • props/$emit 父子组件之间通信
  • provide/inject 父->子孙组件之间
  • ref/refs,获取子组件实例,并调用其方法和访问数据
  • eventBus ,又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。 缺点:难以维护。
1
2
3
4
// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

导出EventBus,通过$emit发送事件,通过$on接收事件,通过$off移除事件监听者

  • Vuex: Vue.js 应用程序开发的状态管理模式(state、getters、mutations、actions、mudules)
  • $attrs与 $listeners ,父->子,如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false, $listeners绑定父组件的所有事件
    总结
    常见使用场景可以分为三类:
  • 父子通信: 父向子传递数据是通过 props,子向父是通过 events( $emit);通过父链 / 子链也可以通信( $parent / $children);ref 也可以访问组件实例; provide/inject、$attrs/$listeners
  • 兄弟通讯: eventBus; VueX
  • 跨级通讯:eventBus、VueX、provide/inject、$attrs/$listeners

nextTick的实现

  • nextTick是Vue提供的一个全局API,是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调中获取更新后的DOM
  • Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启1个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中-次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用;
  • 我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数,然后用timerFunc异步方式调用它们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。
    nextTick的实现原理是什么?
    在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。 nextTick主要使用了宏任务和微任务。 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

匿名插槽、具名插槽、作用域插槽

子组件:
(匿名插槽)
(具名插槽)
(作用域插槽)

父之间:
旧版本:

新版本:

keep-alive的实现

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染
Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

mixin(混入)

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到mixin
多个组件有相同的逻辑,抽离出来
mixin并不是完美的解决方案,会有一些问题
vue3提出的Composition API旨在解决这些问题
劣势:
1.变量来源不明确,不利于阅读
2.多mixin可能会造成命名冲突
3.mixin和组件可能出现多对多的关系,使得项目复杂度变高

vue3.0 中为什么要使用 Proxy,它相比以前的实现方式有什么改进

1、在vue2.x中通过Object.defineProperty为每个对象添加getter、setter方法来监听数据的变化,vue3.x通过Proxy代理目标对象,且一开始只代理最外层,嵌套对象惰性监听(lazy by default),性能更好。

2、支持数组索引修改,对象属性的增加、删除

vue 数组中 key 的作用是什么

diff算法需求对比虚拟dom的修改,然后异步的渲染到页面中,当出现大量的相同标签的时候,vnode会首先判断key和标签名是否一致,如果一致再判断子节点一致,使用key可以帮助diff算法提升判断的速度,在页面重新渲染时更快、消耗更少。

vue 中 v-if 和 v-show 的区别是什么

v-show 总是会进行编译和渲染的工作 - 它只是简单的在元素上添加了 display: none; 的样式。v-show 具有较高的初始化性能成本上的消耗,但是使得转换状态变得很容易。 相比之下,v-if 才是真正「有条件」的:它的加载是惰性的,因此,若它的初始条件是 false,它就不会做任何事情。这对于初始加载时间来说是有益的,当条件为 true 时,v-if 才会编译并渲染其内容。切换 v-if 下的块儿内容实际上时销毁了其内部的所有元素,比如说处于 v-if 下的组件实际上在切换状态时会被销毁并重新生成,因此,切换一个较大 v-if 块儿时会比 v-show 消耗的性能多。

vue中computed的原理是什么

computed的实现是基于响应式原理中Watcher对象的实现,在初始化data和computed的时候会涉及到以下几个对象:1、Observer对象(现在数据监听)、2、Dep对象(收集依赖,触发watcher数据更新)3、Watcher对象(执行更新函数,更新依赖变化)。

computed有两种定义方式,一种是方法、一种是get和set属性,其内部监听的对象必须是已经定义响应式的属性。

vue在创建computed属性的时候,会循环所有的计算属性,每个计算属性都会创建一个watch,通过defineProperty定义监听,在get中收集依赖,在set中重新执行计算方法,computed是懒执行,在第一次初始化后,不会执行计算,下一次变更时在set中执行计算。

vue-loader 的实现原理是什么

vue-loader会把sfc(单文件组件规范)中的内容拆分为template、script、style三个虚拟模块,然后分别匹配webpack配置中的对应rules,比如script模块会匹配所有跟处理javascript和typescript相关的loader。

template中的内容会通过vue compiler转换为render函数合并到script虚拟模块中。

scoped style会经过vue-loader/style-post-loader的处理,成为只匹配特定元素的私有样式。

vue实现一个messageAPI

可以先写好一个 render 函数,作用是把某一 HTML 片段挂载到 #root 下 / 从 #root 删除该片段。然后写一个 Vue 插件,就是一个暴露了包含 install 方法的模块,install 方法中将 设置 Vue.prototype.$message = message 对象。最后使用 Vue.use 全局注册这个插件即可。

如果使用 SSR,可以在 created/componentWillMount 中访问 localStorage 吗

不可以,created/componentWillMount 时,还未挂载,代码仍然在服务器中执行,此时没有浏览器环境,因此此时访问 localStorage 将会报错

在 vue 中数组是否可以以在数组中的次序为 key

不可,key 应为唯一标识,在数组变更时插入或删除后,index 无法确保始终指向对应的序列

vue中的router实现原理

前端路由实现的本质是监听url的变化,实现方式有2种,Hash模式和History模式,无需刷新页面就能重新加载相应的页面。通过location.hash 跳转路由,通过hashchange监听路由变化。History url 通过history.pushState 和 history.replaceState改变url,通过popstate 事件监听路由变化。

两种模式的区别:

  • hash 只能改变#后的值,而history模式可以随意设置同源url
  • hash 只能添加字符串类的数据,而history可以通过API添加多种类型的数据
  • hash 的历史记录只显示之前的www.a.com而不会显示 hash 值,而 history 的每条记录都会进入到历史记录;
  • hash 无需后端配置且兼容性好,而 history 需要配置index.html用于匹配不到资源的情况。

vue实例挂载的过程中发生了什么?

1、new Vue 的时候调用了_init方法

  • 定义$set、$get、$delete、$watch 等方法
  • 定义$on $off $emit 等事件
  • 定义_update $forceUpdate $destory 生命周期

2、调用$mount 进行页面的挂载,挂载的时候主要通过mountComponent方法

3、定义updateComponent更新函数

4、执行render函数生成虚拟dom

5、_update将虚拟dom生成真实dom结构,渲染到页面中

在created和mounted这两个生命周期中请求数据有什么区别呢?

created后vue实力化完成,可以获取到属性和方法等,无法获取dom,而在mounted的时候dom已经挂载了,因此可以获取到dom,如果在mounted请求数据,页面可能会闪动一下。放在created中则不会。

SPA首屏加载速度慢的怎么解决?

  • 减少入口文件体积(路由懒加载)

  • 静态资源本地缓存

    后端返回资源:

    1、采用HTTP缓存,强缓存:Cache-Control, 协商缓存:Last-Modified, Etag

    2、Service Worker离线缓存

    客户端合理运用localStorage

  • UI框架按需加载

  • 图片资源的压缩(压缩图片,使用icon,精灵图)

  • 组件重复打包(在webpackconfig文件中,修改CommonsChunkPlugin的配置)

  • 开启GZip压缩

    1、安装compression-webpack-plugin,在vue.congig.js中引入并修改webpack配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const CompressionPlugin = require('compression-webpack-plugin')

    configureWebpack: (config) => {
    if (process.env.NODE_ENV === 'production') {
    // 为生产环境修改配置...
    config.mode = 'production'
    return {
    plugins: [new CompressionPlugin({
    test: /\.js$|\.html$|\.css/, //匹配文件名
    threshold: 10240, //对超过10k的数据进行压缩
    deleteOriginalAssets: false //是否删除原文件
    })]
    }
    }

    2、服务端设置

  • 使用SSR

Nuxt.js

小结:减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化

为什么组件data属性是一个函数而不是一个对象?

  • 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
  • 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

Vue中给对象添加新的属性,界面不刷新?

因为新加的属性不是响应式的数据

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

Vue组件与插件的区别

组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue

插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身

简单来说,插件就是指对Vue的功能的增强或补充

vue的mixin的理解,有什么应用场景

  • 替换型策略有propsmethodsinjectcomputed,就是将新的同名参数替代旧的参数
  • 合并型策略是data, 通过set方法进行合并和重新赋值
  • 队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
  • 叠加型有componentdirectivesfilters,通过原型链进行层层的叠加

不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立

这时,可以通过Vuemixin功能将相同或者相似的代码提出来

Vue.observable你有了解过吗?

Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象

Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象

Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择

自定义指令的应用场景

自定义指令也像组件那样存在钩子函数:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
  • unbind:只调用一次,指令与元素解绑时调用

所有的钩子函数的参数都有以下

  • el
  • binding
  • vnode
  • oldVnode

应用场景

  • 防抖
  • 图片懒加载
  • 一键copy功能
  • 权限校验

vue 中diff算法的作用

用于虚拟dom渲染成真实dom的新旧VNode节点比较

特点:

  • 比较只会在同层级进行,不会跨层级比较
  • 在diff比较的过程中,循环从两边向中间比较

vue项目中有封装过axios吗?主要封装哪方面?

axios是基于Promise的Http网络请求库

封装:对超时时间、设置请求头、根据项目环境使用请求地址、错误处理等。

通过请求拦截器和响应拦截器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const instance = axios.create({
baseURL: `${host}/lightning/api/backend`
})

const errorHandle = (status, message, url) => {
switch (status) {
case '0001':
url !== '/activity/icid' && store.dispatch('logout')
break
case '5301':
break
default:
Message.error(message)
}
}
instance.interceptors.request.use(
config => {
config.headers.token = localStorage.getItem('lightning_auth_ssoToken') // sso token
return config
},
error => {
return Promise.reject(error)
}
)

instance.interceptors.response.use(
(response) => {
const code = response.data.code
if (code === '0000') {
Message.success(response.data.message)
} else {
if (code === '0001') {
Message.error(response.data.message)
}
errorHandle(code, response.data.message, response.config.url)
}
return response
},
error => {
const { response } = error
response
? errorHandle(response.data.code, response.data.message, response.config.url)
: Message.warning('网络连接失败,请稍后重试!')
return Promise.reject(error)
}
)

export const createAPI = (url, method, params, config = {}) => {
if (method === 'get') {
config.params = params
} else {
config.data = params
}
return instance({
url,
method,
...config
})
}

vue的SSR(Nuxt)

1、Vue SSR 是在SPA上进行改良的服务端渲染

2、通过Vue SSR渲染的页面,需要在客户端激活才能实现交互

3、Vue SRR将包含两部分:服务端渲染首屏,包含交互的SPA

SSR主要解决两个问题:

  • 1、SEO
  • 2、首屏呈现渲染

vue3做了哪些优化?

  • 源码

    1、源码管理(monorepo的方式维护)

    2、TS

  • 性能

    1、体积优化(tree-shanking)

    2、编译优化(优化diff算法)

    3、数据劫持优化(Proxy)

  • 语法API(Composition API)

    1、优化逻辑组织

    2、优化逻辑复用

vue3性能提升主要通过哪几个方面体现

1、编译阶段

  • diff算法的优化

    增加静态标记,在dff的过程中不会比较

  • 静态提升

    对不参与更新的元素,会做静态提升,只会被创建一次,在渲染的时直接复用,避免重复创建,优化运行时的内存占用

  • 事件监听缓存

  • SSR优化

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

2、源码体积

移除了不常用的一些API,使用Tree-shanking,把没有用到的模块摇掉,打包的整体体积变小。

3、响应式系统

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

为什么要用Proxy API代替defineProperty API

Object.defineProperty需要遍历属性进行监听,如果嵌套对象,需要深层监听,造成性能问题,不能监听对象属性的添加和删除,不能监听数组的变化(vue2通过重写数组方法实现监听)。

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的,可以监听数据的变化,但它不兼容ie


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!