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

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性

  • vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 使用Vuex解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State 中能有效解决多层级组件嵌套的跨组件通信问题

vuexState 在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State 中呢? 目前主要有两种数据会使用 vuex 进行管理:

  • 组件之间全局共享的数据
  • 通过后端异步请求的数据

包括以下几个模块

  • stateVuex 使用单一状态树,即每个应用将仅仅包含一个store 实例。里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性
  • mutations:更改Vuexstore中的状态的唯一方法是提交mutation
  • gettersgetter 可以对 state 进行计算操作,它就是 store 的计算属性虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
  • actionaction 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态action 可以包含任意异步操作
  • modules:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuexstore对象分割成模块(modules)

modules:项目特别复杂的时候,可以让每一个模块拥有自己的statemutationactiongetters,使得结构非常清晰,方便管理

回答范例

思路

  • 给定义
  • 必要性阐述
  • 何时使用
  • 拓展:一些个人思考、实践经验等

回答范例

  1. Vuex 是一个专为 Vue.js 应用开发的 状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念
  3. Vuex 解决状态管理的同时引入了不少概念:例如statemutationaction等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。
  4. 我在使用vuex过程中感受到一些等

可能的追问

  1. vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?
  • 刷新浏览器,vuex中的state会重新变为初始状态。解决方案-插件 vuex-persistedstate
  1. actionmutation的区别是什么?为什么要区分它们?
  • action中处理异步,mutation不可以
  • mutation做原子操作
  • action可以整合多个mutation的集合
  • mutation 是同步更新数据(内部会进行是否为异步方式更新数据的检测) $watch 严格模式下会报错
  • action 异步操作,可以获取数据后调佣 mutation 提交最终数据
  • 流程顺序:“相应视图—>修改State”拆分成两部分,视图触发ActionAction再触发Mutation`。
  • 基于流程顺序,二者扮演不同的角色:Mutation:专注于修改State,理论上是修改State的唯一途径。Action:业务代码、异步请求
  • 角色不同,二者有不同的限制:Mutation:必须同步执行。Action:可以异步,但不能直接操作State

Watch中的deep:true是如何实现的

当用户指定了 watch 中的deep属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新

源码相关

get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }

Vue3速度快的原因

Vue3.0 性能提升体现在哪些方面

  • 代码层面性能优化主要体现在全新响应式API,基于Proxy实现,初始化时间和内存占用均大幅改进;
  • 编译层面做了更多编译优化处理,比如静态标记pachFlagdiff算法增加了一个静态标记,只对比有标记的dom元素)、事件增加缓存静态提升(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff过程;
  • 打包时更好的支持tree-shaking,因此整体体积更小,加载更快
  • ssr渲染以字符串方式渲染

一、编译阶段

试想一下,一个组件结构如下图

<template>    <div id="content">        <p class="text">静态文本</p>        <p class="text">静态文本</p>        <p class="text">{ message }</p>        <p class="text">静态文本</p>        ...        <p class="text">静态文本</p>    </div></template>

可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费

因此,Vue3在编译阶段,做了进一步优化。主要有如下:

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

1. diff 算法优化

  • Vue 2x 中的虚拟 dom 是进行全量的对比。
  • Vue 3x 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容化

Vue2.x的diff算法

vue2.xdiff算法叫做全量比较,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom对比,即使有些内容是永恒固定不变的

Vue3.0的diff算法

vue3.0diff算法有个叫静态标记(PatchFlag)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了

已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高

export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)                        //上面这个1就是静态标记 ]))}

关于静态类型枚举如下

TEXT = 1 // 动态文本节点CLASS=1<<1,1 // 2//动态classSTYLE=1<<2// 4 //动态stylePROPS=1<<3,// 8 //动态属性,但不包含类名和样式FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。HYDRATE_ EVENTS = 1 << 5// 32 //带有监听事件的节点STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragmentKEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有keyUNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragmentNEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slotHOISTED = -1 // 静态节点// 指示在diff算法中退出优化模式BALL = -2

2. hoistStatic 静态提升

  • Vue 2x : 无论元素是否参与更新,每次都会重新创建。
  • Vue 3x : 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
<p>HelloWorld</p><p>HelloWorld</p><p>{ message }</p>

开启静态提升前

export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ]))}

开启静态提升后编译结果

const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _hoisted_1,  _hoisted_2,  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ]))}

可以看到开启了静态提升后,直接将那两个内容为helloworldp标签声明在外面了,直接就拿来用了。同时 _hoisted_1_hoisted_2 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

3. cacheHandlers 事件监听缓存

  • 默认情况下 绑定事件会被视为动态绑定 ,所以每次都会去追踪它的变化
  • 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
<div> <button @click = 'onClick'>点我</button></div>

开启事件侦听器缓存之前:

export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])                       // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式 ]))})

这里有一个8,表示着这个节点有了静态标记,有静态标记就会进行diff算法对比差异,所以会浪费时间

开启事件侦听器缓存之后:

export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("button", {   onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))  }, "点我") ]))}

上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用

4. SSR优化

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

<div>    <div>        <span>你好</span>    </div>    ...  // 很多个静态属性    <div>        <span>{{ message }}</span>    </div></div>

编译后

import { mergeProps as _mergeProps } from "vue"import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {  const _cssVars = { style: { color: _ctx.color }}  _push(`${    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))  }>你好...你好${    _ssrInterpolate(_ctx.message)  }`)}

二、源码体积

相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking

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

import { computed, defineComponent, ref } from 'vue';export default defineComponent({  setup(props, context) {    const age = ref(18)    let state = reactive({      name: 'test'    })    const readOnlyAge = computed(() => age.value++) // 19    return {        age,        state,        readOnlyAge    }  }});

三、响应式系统

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

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

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

什么是递归组件?举个例子说明下?

分析

递归组件我们用的比较少,但是在TreeMenu这类组件中会被用到。

体验

组件通过组件名称引用它自己,这种情况就是递归组件

<template>  <li>    <div> {{ model.name }}</div>    <ul v-show="isOpen" v-if="isFolder">            <TreeItem        class="item"        v-for="model in model.children"        :model="model">      </TreeItem>    </ul>  </li><script>export default {  name: 'TreeItem',  // ...}</script>

回答范例

  1. 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
  2. 实际开发中类似TreeMenu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
  3. 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。
  4. 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent,这样实际获取的组件就是当前组件本身

原理

递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)

const _component_Comp = _resolveComponent("Comp", true)

就是在传递maybeSelfReference

export function resolveComponent(  name: string,  maybeSelfReference" />

What is Composition API?(opens new window)

  • Composition API出现就是为了解决Options API导致相同功能代码分散的现象

体验

Composition API能更好的组织代码,下面用composition api可以提取为useCount(),用于组合、复用

compositon api提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

回答范例

  1. Composition API是一组API,包括:Reactivity API生命周期钩子依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件
  2. Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options APImixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对ts支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixinsprovide/inject
  3. Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益

可能的追问

  1. Composition API能否和Options API一起使用?

可以在同一个组件中使用两个script标签,一个使用vue3,一个使用vue2写法,一起使用没有问题

<script setup>  // vue3写法</script><script>  export default {    data() {},    methods: {}  }</script>

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。
  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。
components:{   AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }

原理

export function ( Ctor: Class<Component> | Function | Object | void, data: " />

组件通信常用方式有以下几种

  • props / $emit 适用 父子组件通信
    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref$parent / $children(vue3废弃) 适用 父子组件通信
    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法
  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信
    • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信
    • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
  • provide / inject 适用于 隔代组件通信
    • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系
  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用
  • Vuex 适用于 父子、隔代、兄弟组件通信
    • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

根据组件之间关系讨论组件通信最为清晰有效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯

1. 父子组件通信

使用props,父组件可以使用props向子组件传递数据。

父组件vue模板father.vue:

<template>  <child :msg="message"></child></template><script>import child from './child.vue';export default {  components: {    child  },  data () {    return {      message: 'father message';    }  }}</script>

子组件vue模板child.vue:

<template>    <div>{{msg}}</div></template><script>export default {  props: {    msg: {      type: String,      required: true    }  }}</script>

回调函数(callBack)

父传子:将父组件里定义的method作为props传入子组件

// 父组件Parent.vue:<Child :changeMsgFn="changeMessage">methods: {    changeMessage(){        this.message = 'test'    }}
// 子组件Child.vue:<button @click="changeMsgFn">props:['changeMsgFn']

子组件向父组件通信

父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件

父组件vue模板father.vue:

<template>    <child @msgFunc="func"></child></template><script>import child from './child.vue';export default {    components: {        child    },    methods: {        func (msg) {            console.log(msg);        }    }}</script>

子组件vue模板child.vue:

<template>    <button @click="handleClick">点我</button></template><script>export default {    props: {        msg: {            type: String,            required: true        }    },    methods () {        handleClick () {          //........          this.$emit('msgFunc');        }    }}</script>

2. provide / inject 跨级访问祖先组件的数据

父组件通过使用provide(){return{}}提供需要传递的数据

export default {  data() {    return {      title: '我是父组件',      name: 'poetry'    }  },  methods: {    say() {      alert(1)    }  },  // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法  provide() {    return {      message: '我是祖先组件提供的数据',      name: this.name, // 传递属性      say: this.say    }  }}

子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数

<template>  <p>曾孙组件</p>  <p>{{message}}</p></template><script>export default {  // inject 注入/接收祖先组件传递的所需要的数据即可   //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}  inject: [ "message","say"],  mounted() {    this.say();  },};</script>

3. $parent + $children 获取父组件实例和子组件实例的集合

  • this.$parent 可以直接访问该组件的父实例或组件
  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的
<template><div>  <child1></child1>     <child2></child2>   <button @click="clickChild">$children方式获取子组件值</button></div></template><script>import child1 from './child1'import child2 from './child2'export default {  data(){    return {      total: 108    }  },  components: {    child1,    child2    },  methods: {    funa(e){      console.log("index",e)    },    clickChild(){      console.log(this.$children[0].msg);      console.log(this.$children[1].msg);    }  }}</script>
<template>  <div>    <button @click="parentClick">点击访问父组件</button>  </div></template><script>export default {  data(){    return {      msg:"child1"    }  },  methods: {    // 访问父组件数据    parentClick(){      this.$parent.funa("xx")      console.log(this.$parent.total);    }  }}</script>
<template>  <div>    child2  </div></template><script>export default {  data(){    return {     msg: 'child2'    }  }}</script>

4. $attrs + $listeners多级组件通信

$attrs 包含了从父组件传过来的所有props属性

// 父组件Parent.vue:<Child :name="name" :age="age"/>// 子组件Child.vue:<GrandChild v-bind="$attrs" />// 孙子组件GrandChild<p>姓名:{{$attrs.name}}</p><p>年龄:{{$attrs.age}}</p>

$listeners包含了父组件监听的所有事件

// 父组件Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>// 子组件Child.vue:<button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

// 父组件Parent.vue:<Child ref="childComp"/><button @click="changeName"></button>changeName(){    console.log(this.$refs.childComp.age);    this.$refs.childComp.changeAge()}// 子组件Child.vue:data(){    return{        age:20    }},methods(){    changeAge(){        this.age=15  }}

6. 非父子, 兄弟组件之间通信

vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js可以是这样:

// Bus.js// 创建一个中央时间总线类  class Bus {    constructor() {      this.callbacks = {};   // 存放事件的名字    }    $on(name, fn) {      this.callbacks[name] = this.callbacks[name] || [];      this.callbacks[name].push(fn);    }    $emit(name, args) {      if (this.callbacks[name]) {        this.callbacks[name].forEach((cb) => cb(args));      }    }  }  // main.js  Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  // 另一种方式  Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能  
<template>    <button @click="toBus">子组件传给兄弟组件</button></template><script>export default{    methods: {    toBus () {      this.$bus.$emit('foo', '来自兄弟组件')    }  }}</script>

另一个组件也在钩子函数中监听on事件

export default {  data() {    return {      message: ''    }  },  mounted() {    this.$bus.$on('foo', (msg) => {      this.message = msg    })  }}

7. $root 访问根组件中的属性或方法

  • 作用:访问根组件中的属性或方法
  • 注意:是根组件,不是父组件。$root只对根组件有用
var vm = new Vue({  el: "#app",  data() {    return {      rootInfo:"我是根元素的属性"    }  },  methods: {    alerts() {      alert(111)    }  },  components: {    com1: {      data() {        return {          info: "组件1"        }      },      template: "

{{ info }}

"
, components: { com2: { template: "

我是组件1的子组件

"
, created() { this.$root.alerts()// 根组件方法 console.log(this.$root.rootInfo)// 我是根元素的属性 } } } } }});

8. vuex

  • 适用场景: 复杂关系的组件数据传递
  • Vuex作用相当于一个用来存储共享变量的容器

  • state用来存放共享变量的地方
  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
  • mutations用来存放修改state的方法。
  • actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作

小结

  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

怎么监听vuex数据的变化

分析

  • vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
  • 既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()

回答范例

  1. 我知道几种方法:
  • 可以通过watch选项或者watch方法监听状态
  • 可以使用vuex提供的API:store.subscribe()
  1. watch选项方式,可以以字符串形式监听$store.state.xxsubscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。
  2. watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中

实践

watch方式

const app = createApp({    watch: {      '$store.state.counter'() {        console.log('counter change!');      }    }})

subscribe方式:

store.subscribe((mutation, state) => {    if (mutation.type === 'add') {      console.log('counter change in subscribe()!');    }})

vue3.2 自定义全局指令、局部指令

// 在src目录下新建一个directive文件,在此文件夹下新建一个index.js文件夹,接着输入如下内容const directives =  (app) => {  //这里是给元素取得名字,虽然是focus,但是实际引用的时候必须以v开头  app.directive('focus',{    //这里的el就是获取的元素    mounted(el) {      el.focus()      }  })}//默认导出 directivesexport default directives
// 在全局注册directiveimport { createApp } from 'vue'import App from './App.vue'import router from './router'import store from './store'import directives from './directives'const app = createApp(App)directives(app)app.use(store).use(router).mount('#app')
<template>  <div class="container">    <div class="content">      <input type="text"  v-focus>      内容    </div>  </div></template><script setup>import { reactive, ref } from 'vue'// const vMove:Directive = () =>{// }</script>

vue3.2 setup语法糖模式下,自定义指令变得及其简单

<input type="text" v-model="value" v-focus><script setup>//直接写,但是必须是v开头const vFocus = {  mounted(el) {    // 获取input,并调用其focus()方法    el.focus()  }}</script>
<template>  <div class="container">    <div class="content" v-move="{ background: value }">      内容      <input type="text" v-model="value" v-focus @keyup="see">    </div>  </div></template><script setup>import { reactive, ref } from 'vue'const value = ref('')const vFocus = {  mounted(el) {    // 获取input,并调用其focus()方法    el.focus()  }}let timer = nullconst vMove = (el, binding) => {  if (timer !== null) {    clearTimeout(timer)  }  timer = setTimeout(() => {    el.style.background = binding.value.background    console.log(el);  }, 1000);}</script><style lang="scss" scoped>.container {  width: 100%;  height: 100%;  display: flex;  justify-content: center;  align-items: center;  .content {    border-top: 5px solid black;    width: 200px;    height: 200px;    cursor: pointer;    border-left: 1px solid #ccc;    border-right: 1px solid #ccc;    border-bottom: 1px solid #ccc;  }}</style>

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;
  • 属性改变后,通知计算属性重新计算

实现时,主要如下

  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。
  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

Vue中diff算法原理

DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用diff算法

vuediff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)的方式进行比较。

简单来说,Diff算法有以下过程

  • 同级比较,再比较子节点(根据keytag标签名判断)
  • 先判断一方有子节点和一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较都有子节点的情况(核心diff)
  • 递归比较子节点
  • 正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以VueDiff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
  • Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比ReactDiff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅
  • 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升

vue3中采用最长递增子序列来实现diff优化

回答范例

思路

  • diff算法是干什么的
  • 它的必要性
  • 它何时执行
  • 具体执行方式
  • 拔高:说一下vue3中的优化

回答范例

  1. Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换
  2. 最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOMpatching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新
  3. vuediff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作
  4. patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3patch为例
  • 首先判断两个节点是否为相同同类节点,不同则删除重新创建
  • 如果双方都是文本则更新文本内容
  • 如果双方都是元素节点则递归更新子元素,同时更新元素属性
  • 更新子节点时又分了几种情况
    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则直接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
  • vue3中引入的更新策略:静态节点标记等

vdom中diff算法的简易实现

以下代码只是帮助大家理解diff算法的原理和流程

  1. vdom转化为真实dom
const createElement = (vnode) => {  let tag = vnode.tag;  let attrs = vnode.attrs || {};  let children = vnode.children || [];  if(!tag) {    return null;  }  //创建元素  let elem = document.createElement(tag);  //属性  let attrName;  for (attrName in attrs) {    if(attrs.hasOwnProperty(attrName)) {      elem.setAttribute(attrName, attrs[attrName]);    }  }  //子元素  children.forEach(childVnode => {    //给elem添加子元素    elem.appendChild(createElement(childVnode));  })  //返回真实的dom元素  return elem;}
  1. 用简易diff算法做更新操作
function updateChildren(vnode, newVnode) {  let children = vnode.children || [];  let newChildren = newVnode.children || [];  children.forEach((childVnode, index) => {    let newChildVNode = newChildren[index];    if(childVnode.tag === newChildVNode.tag) {      //深层次对比, 递归过程      updateChildren(childVnode, newChildVNode);    } else {      //替换      replaceNode(childVnode, newChildVNode);    }  })}

动态给vue的data添加一个新的属性时会发生什么?怎样解决?

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

Vue.set()

Vue.set( target, propertyName/index, value )

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过Vue.set向响应式对象中添加一个property,并确保这个新 property同样是响应式的,且触发视图更新

关于Vue.set源码(省略了很多与本节不相关的代码)

源码位置:src\core\observer\index.js

function set (target: Array<any> | Object, key: any, val: any): any {  ...  defineReactive(ob.value, key, val)  ob.dep.notify()  return val}

这里无非再次调用defineReactive方法,实现新增属性的响应式

关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦截

大致代码如下:

function defineReactive(obj, key, val) {    Object.defineProperty(obj, key, {        get() {            console.log(`get ${key}:${val}`);            return val        },        set(newVal) {            if (newVal !== val) {                console.log(`set ${key}:${newVal}`);                val = newVal            }        }    })}

Object.assign()

直接使用Object.assign()添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

$forceUpdate

如果你发现你自己需要在 Vue中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate迫使Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结

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

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

v-if和v-show区别

  • v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
  • 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
  • v-showfalse变为true的时候不会触发组件的生命周期
  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗

v-show与v-if的使用场景

  • v-ifv-show 都能控制dom元素在页面的显示
  • v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)
  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行时条件很少改变,则使用 v-if 较好

v-show与v-if原理分析

  1. v-show原理

不管初始条件是什么,元素总是会被渲染

我们看一下在vue中是如何实现的

代码很好理解,有transition就执行transition,没有就直接设置display属性

// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.tsexport const vShow: ObjectDirective<VShowElement> = {  beforeMount(el, { value }, { transition }) {    el._vod = el.style.display === 'none' ? '' : el.style.display    if (transition && value) {      transition.beforeEnter(el)    } else {      setDisplay(el, value)    }  },  mounted(el, { value }, { transition }) {    if (transition && value) {      transition.enter(el)    }  },  updated(el, { value, oldValue }, { transition }) {    // ...  },  beforeUnmount(el, { value }) {    setDisplay(el, value)  }}
  1. v-if原理

v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分

返回一个node节点,render函数通过表达式的值来决定是否生成DOM

// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.tsexport const transformIf = createStructuralDirectiveTransform(  /^(if|else|else-if)$/,  (node, dir, context) => {    return processIf(node, dir, context, (ifNode, branch, isRoot) => {      // ...      return () => {        if (isRoot) {          ifNode.codegenNode = createCodegenNodeForBranch(            branch,            key,            context          ) as IfConditionalExpression        } else {          // attach this branch's codegen node to the v-if root.          const parentCondition = getParentCondition(ifNode.codegenNode!)          parentCondition.alternate = createCodegenNodeForBranch(            branch,            key + ifNode.branches.length - 1,            context          )        }      }    })  })

了解history有哪些方法吗?说下它们的区别

history 这个对象在html5的时候新加入两个api history.pushState()history.repalceState() 这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

从参数上来说:

window.history.pushState(state,title,url)//state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取//title:标题,基本没用,一般传null//url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。//如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/window.history.replaceState(state,title,url)//与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录

另外还有:

  • window.history.back() 后退
  • window.history.forward()前进
  • window.history.go(1) 前进或者后退几步

从触发事件的监听上来说:

  • pushState()replaceState()不能被popstate事件所监听
  • 而后面三者可以,且用户点击浏览器前进后退键时也可以

从0到1自己构架一个vue项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织

综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可

思路

  • 构建项目,创建项目基本结构
  • 引入必要的插件:
  • 代码规范:prettiereslint
  • 提交规范:husky,lint-staged`
  • 其他常用:svg-loadervueusenprogress
  • 常见目录结构

回答范例

  1. 0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
  2. 目前vue3项目我会用vite或者create-vue创建项目
  3. 接下来引入必要插件:路由插件vue-router、状态管理vuex/piniaui库我比较喜欢element-plus和antd-vuehttp工具我会选axios
  4. 其他比较常用的库有vueusenprogress,图标可以使用vite-svg-loader
  5. 下面是代码规范:结合prettiereslint即可
  6. 最后是提交规范,可以使用huskylint-stagedcommitlint
  7. 目录结构我有如下习惯: .vscode:用来放项目中的 vscode 配置
  • plugins:用来放 vite 插件的 plugin 配置
  • public:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下
  • src:用来放项目代码文件
  • api:用来放http的一些接口配置
  • assets:用来放一些 CSS 之类的静态资源
  • components:用来放项目通用组件
  • layout:用来放项目的布局
  • router:用来放项目的路由配置
  • store:用来放状态管理Pinia的配置
  • utils:用来放项目中的工具方法类
  • views:用来放项目的页面文件