2024高频前端合集之Vue(上篇)

172 阅读14分钟

近期整理了一下高频的前端面试题,分享给大家一起来学习。如有问题,欢迎指正!

欢迎大家关注该专栏:点赞👍 + 收藏🤞

vue笔面试题汇总(上篇)

1. 说一下 ref 的作用是什么?

参考答案:

ref 的作用是被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。其特点是:

  • 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素
  • 如果用在子组件上,引用就指向组件实例

所以常见的使用场景有:

  1. 基本用法,本页面获取 DOM 元素
  2. 获取子组件中的 data
  3. 调用子组件中的方法

Vue 中的 ref 属性详解
我们先来读一下vue的官方文档

在这里插入图片描述
我们来分析官方文档
首先ref的引用是相当于一个DOM节点(如果是子组件则指向的是其实例),而且是一个string类型的值。
通俗的将就类似于原生js用document.getElementById("#id")
但是只是类似,他们的不同点是Vue是操控虚拟DOM ,也就是说在渲染初期并没有这个ref的属性,这个属性是在创建Vue实例以后才被加到虚拟DOM中的。所以在官方文档的最后提醒开发者不能将ref的结果在模版中进行数据绑定。
说了这么多那么如何具体使用呢?
首先尝试普通DOM元素

<div id="app">
            <input type="text" value="HelloWorld" alt="captcha"  ref="text">
            <button @click="changeText">change word</button>
        </div>

这是一段特别简单的html中加input和button代码
目的是运用ref属性点击按钮更改input中的文字。

我们在标签中加入ref属性

var app = new Vue({ 
    el: '#app',
    data: {
    },
    //添加一个方法
     methods:{
  //改变文字
  	changeText () {
  		this.$refs.text.value = 'Hello Vue!'
  	}
  }
});

网页初始时input现实的文字是HelloWorld

在这里插入图片描述
当点击change word按钮时,input中的文字发生变化

在这里插入图片描述 
那么刚才代码中的this.refs是什么呢?通俗的将就是搜集所有的ref的一个对象。通过this.refs是什么呢? 通俗的将就是搜集所有的ref的一个对象。通过this.refs 可以访问到此vue实例中的所有设置了ref属性的DOM元素,并对其进行操作。

其实组件中的子组件的ref原理也类似,只是指向的不是原组件而是组件的实例。
用法和普通DOM元素一样。

一、用途

应用场景:需要在视图更新之后,基于新的视图进行操作。

this.nextTick()方法主要是用在数据改变,dom改变应用场景中。vue中数据和dom渲染由于是异步的,所以,要让dom结构随数据改变这样的操作都应该放进this.nextTick()方法主要是用在数据改变,dom改变应用场景中。vue中数据和dom渲染由于是异步的,所以,要让dom结构随数据改变这样的操作都应该放进this.nextTick()的回调函数中。created()中使用的方法时,dom还没有渲染,如果此时在该钩子函数中进行dom赋值数据(或者其它dom操作)时无异于徒劳,所以,此时this.$nextTick()就会被大量使用,而与created()对应的是mounted()的钩子函数则是在dom完全渲染后才开始渲染数据,所以在mounted()中操作dom基本不会存在渲染问题。

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

三、原理: this.$nextTick()将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

假设我们更改了某个dom元素内部的文本,而这时候我们想直接打印出这个被改变后的文本是需要dom更新之后才会实现的,也就好比我们将打印输出的代码放在setTimeout(fn, 0)中;

异步说明

事件循环说明

实例 (1)实例一

可以根据打印的顺序看到,在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作并无作用,而在created()里使用this.$nextTick()可以等待dom生成以后再来获取dom对象

(1)实例二

根据上面的例子可以看出,在方法里直接打印的话, 由于dom元素还没有更新, 因此打印出来的还是未改变之前的值,而通过this.$nextTick()获取到的值为dom更新之后的值

2.scoped用来干嘛?

scoped 是 Vue 中的一个特殊修饰符,用于限制样式的作用范围。

当我们在组件中使用 scoped 修饰符时,它会将样式限制在该组件的作用域内。这意味着该组件中的样式只会影响该组件内的元素,而不会影响到其他组件或全局样式。

例如,如果我们在一个组件的 <style> 标签中使用 scoped 修饰符,那么这些样式只会影响该组件内的元素:

<template>
  <div class="component">
    I am a component
  </div>
</template>

<style scoped>
.component {
  color: blue;
}
</style>

在这个例子中,.component 的样式只会影响该组件内的 <div> 元素,而不会影响到其他组件或全局样式。

使用 scoped 修饰符可以帮助我们更好地组织和维护样式,避免样式之间的冲突。

Vue scoped,原理,涉及到 vue-loader 的处理策略:

一、首先呢,是 VueLoaderPlugin 策略:

VueLoaderPlugin 先获取了 webpack 原来的 rules( 即compiler.option.module.rule 的比如 test:/.vue$/ 规则),然后创建了pitcher 规则,pitcher 中的 pitcher-loader 可以通过 resourceQuery 识别引入文件的 query 带的关键字,进行 loader 解析;(pitcher-loader 提供了前置运行和熔断运行的机制)

然后 VueLoaderPlugin 将进行 clonedRule( 即对 vueRule 以外的 rule 进行处理),具体是重写 resourceresourceQuery,使得 loader 最终能匹配上文件;

举例:对于 vue+ts 的写法,会在 vue 的 script 标签中加上 lang='ts’,重写后 fakeresourceQuery 文件路径为 xx.vue.ts,然后结合ts-loader 的 resource 过滤方法/.tsx?$/ 匹配上文件

然后才来到:vueRule 的 vue-loader 执行阶段;

这里简单理解:VueLoaderPlugin 就是来处理 rule 的,让 loader 能够和文件匹配。处理顺序:pitcherclonedRulevueRule

二、 有了上面的匹配文件,接着来到了 vue-loader 处理环节,首先 @vue/component-compiler-utils .parse 方法可以将 .vue 文件按照 template/script/style 分成代码块,此时会根据文件路径和文件内容生成 hash 值,并赋给 id ,跟在文件参数后面;

javascript
复制代码
// 形如 `id=7ba5bd90` :
// template
import {render,staticRenderFns} from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&";
// script
import script from "./App.vue?vue&type=script&lang=js&";
// style 
import style0 from "./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css&";

三、对于 style 代码块,vue-loader 会在 css-loader 前增加stylePostLoaderstylePostLoader 正是 Vue scoped 的原理核心之一,它会给每个选择器增加属性[data-v-hash] ,这里的 hash 值就是上面的 id 值;

四、同时,对于 template 的 render 块,vue-loader 的 normalizeComponent 方法,判断如果 vue 文件中有 scoped 的 style,则其返回的 options._ScopedId 为上面的 scopedId;在 vnode 渲染生成 DOM 的时候会在 dom元素上增增加 scopedId,也就是增加 data-v-hash。

这样,经过上面的过程,Vue scoped 实现了 CSS 的模块私有化。

4.Vue 组件传值的方法

  1. 父子通讯。父传子,在子组件的标签上通过自定义属性传递父组件的数据,子组件的内部通过 props 接收父向子传递的数据。子传父,在子组件的标签上自定义事件,自定义事件的值是父组件的方法,在子组件内部通过 this.$emit()方法触发事件,第一个参数为自定义事件,第二个参数可以传递子组件的内部数据,此时父组件中的方法就可以执行了。 还有v-model.sync 等语法糖
  2. 兄弟组件通信: 可以采取 eventbus 实现数据传递。
  3. 跨组件层级组件通信
  • 依赖注入。在祖先组件中通过 provide 提供数据,在后代组件中通过 inject 接收数据。
  • vuex。

其他的还有: ref,ref, root, parent,parent, children 等引用,或者直接把信息绑定到 window 对象上。

#组件间通信的概念

开始之前,我们把组件间通信这个词进行拆分

  • 组件
  • 通信

都知道组件是vue最强大的功能之一,vue中每一个.vue我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。

广义上,任何信息的交通都是通信组件间通信即指组件(.vue)通过某种方式来传递信息以达到某个目的举个栗子我们在使用UI框架中的table组件,可能会往table组件中传入某些数据,这个本质就形成了组件之间的通信

#组件间通信解决了什么

在古代,人们通过驿站、飞鸽传书、烽火报警、符号、语言、眼神、触碰等方式进行信息传递,到了今天,随着科技水平的飞速发展,通信基本完全利用有线或无线电完成,相继出现了有线电话、固定电话、无线电话、手机、互联网甚至视频电话等各种通信方式从上面这段话,我们可以看到通信的本质是信息同步,共享回到vue中,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统

#组件间通信的分类

组件间通信的分类可以分成以下四类:

  1. 父子组件之间的通信
  2. 兄弟组件之间的通信
  3. 祖孙与后代组件之间的通信
  4. 非关系组件间之间的通信

关系图:

#组件间通信的方案

整理vue中 8 种常规的通信方案

  1. 父子

    1. props +$emit
    2. v-model
    3. .sync
    4. 使用 ref
  2. 兄弟

    1. EventBus
  3. 祖先后代

    1. $parent 或$root
    2. attrs 与 listeners
    3. Provide 与 Inject
  4. Vuex

#attrs 传递数据

子组件:Com.vue,自己没有明确定义 props

<template>
子组件,
</template>

父组件: App.vue

# 通过自定义属性的方式传递给子组件
<Com a=1 b=2></Com>

此时,由于子组件内部没有明确定义 props 来接收,那么此时的 a,b 在子组件内部如何接收? 可以通过 attrs 来接收(不是 props)

#props 传递数据

  • 适用场景:父组件传递数据给子组件
  • 子组件设置props属性,定义接收父组件传递过来的参数
  • 父组件在使用子组件标签中通过字面量来传递值

Children.vue

props:{
    // 字符串形式
 name:String // 接收的类型参数
    // 对象形式
    age:{  
        type:Number, // 接收的类型为数值
        defaule:18,  // 默认值为18
       require:true // age属性必须传递
    }
}

Father.vue组件

<Children name="jack" age=18 />

#$emit 触发自定义事件

  • 适用场景:子组件传递数据给父组件
  • 子组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 父组件绑定监听器获取到子组件传递过来的参数

Chilfen.vue

this.$emit('add', good)

Father.vue

<Children @add="cartAdd($event)" />

#ref

  • 父组件在使用子组件的时候设置ref
  • 父组件通过设置子组件ref来获取数据

父组件

;<Children ref='foo' />

this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据

#EventBus

  • 使用场景:兄弟组件传值
  • 创建一个中央事件总线EventBus
  • 兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 另一个兄弟组件通过$on监听自定义事件

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的功能

Children1.vue

this.$bus.$emit('foo')

Children2.vue

this.$bus.$on('foo', this.handle)

#parentparent 或 root

通过共同祖辈$parent或者$root搭建通信桥连

兄弟组件

this.$parent.on('add',this.add)

另一个兄弟组件

this.$parent.emit('add')

#attrsattrs 与 listeners

  • 适用场景:祖先传递数据给子孙
  • 设置批量向下传属性$attrs和 $listeners
  • 包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
  • 可以通过 v-bind="$attrs" 传⼊内部组件
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo"/>
// 给Grandson隔代传值,communication/index.vue
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2>

// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>

// Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">
{{msg}}
</div>

#provide 与 inject

  • 在祖先组件定义provide属性,返回传递的值
  • 在后代组件通过inject接收组件传递过来的值

祖先组件

provide(){
    return {
        foo:'foo'
    }
}

后代组件

inject: ['foo'] // 获取到祖先组件传递过来的值

#vuex

  • 适用场景: 复杂关系的组件数据传递
  • Vuex作用相当于一个用来存储共享变量的容器 
  • state用来存放共享变量的地方
  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
  • mutations用来存放修改state的方法。
  • actions也是用来存放修改 state 的方法,不过action是在mutations的基础上进行。常用来做一些异步操作

#小结

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

5.computed、methods、watch、watchEffect有什么区别?

//1.watch默认第一次不加载,computed返回

//2.watch内部可以执行异步操作,computed内部不能执行异步的

//3.watch其他细节参数:deep、immediate

//4.computed是有缓存的,wath没有缓存

//5.computed可以作为属性使用,watch不可以

简版

区别: watch 侦听某一数据的变化从而会触发函数,当数据为对象类型时,对象中的属性值变化时需要使用深度侦听 deep 属性,也可在页面第一次加载时使用立即侦听 immdiate 属性 computed 计算属性是触发函数内部任一依赖项的变化都会重新执行该函数,计算属性有缓存,多次重复使用计算属性时会从缓存中获取返回值,计算属性必须要有 return 关键词

vue 中的 watcher 有三类

  1. render watcher
  2. computed
  3. 自定义 watcher

computed 是一种特殊的 watcher

watch 和 computed 都是以函数为基础的,它们都是通过监听自身依赖的数据在变化时触发相关的函数去实现自身数据的变动。

#不同点

#运行时机不同

  1. computed 是在 HTML,DOM 加载后马上执行的,如赋值;(属性将被混入到 Vue 实例)
  2. watch 它用于观察 Vue 实例上的数据变动,一般情况下是依赖项的值变化之后再执行,当然可以设置立刻执行

#计算属性有缓存

#代码内容不同

watcher 可以写任意的逻辑代码;而计算属性必须是同步的计算,并返回

-----------------------------

1. watch 的使用

语法

javascript
复制代码
import { watch } from "vue" 
watch( name , ( curVal , preVal )=>{ //业务处理  }, options ) ;

共有三个参数,分别为:
	name:需要帧听的属性;
	(curVal,preVal)=>{ //业务处理 } 箭头函数,是监听到的最新值和本次修改之前的值,此处进行逻辑处理。
	options :配置项,对监听器的配置,如:是否深度监听。

1.1 监听 ref 定义的响应式数据

javascript
复制代码
<template>
  <div>
    <div>值:{{count}}</div>
    <button @click="add">改变值</button>
  </div>
</template>

<script>
import { ref, watch } from 'vue';
export default {
  setup(){
    const count = ref(0);
    const add = () => {
      count.value ++
    };
    watch(count,(newVal,oldVal) => {
      console.log('值改变了',newVal,oldVal)
    })
    return {
      count,
      add,
    }
  }
}
</script>

在这里插入图片描述

1.2 监听 reactive 定义的响应式数据

javascript
复制代码
<template>
  <div>
    <div>{{obj.name}}</div>
    <div>{{obj.age}}</div>
    <button @click="changeName">改变值</button>
  </div>
</template>

<script>
import { reactive, watch } from 'vue';
export default {
  setup(){
    const obj = reactive({
      name:'zs',
      age:14
    });
    const changeName = () => {
      obj.name = 'ls';
    };
    watch(obj,(newVal,oldVal) => {
      console.log('值改变了',newVal,oldVal)
    })
    return {
      obj,
      changeName,
    }
  }
}
</script>

在这里插入图片描述

1.3 监听多个响应式数据数据

javascript
复制代码
<template>
  <div>
    <div>{{obj.name}}</div>
    <div>{{obj.age}}</div>
    <div>{{count}}</div>
    <button @click="changeName">改变值</button>
  </div>
</template>

<script>
import { reactive, ref, watch } from 'vue';
export default {
  setup(){
    const count = ref(0);
    const obj = reactive({
      name:'zs',
      age:14
    });
    const changeName = () => {
      obj.name = 'ls';
    };
    watch([count,obj],() => {
      console.log('监听的多个数据改变了')
    })
    return {
      obj,
      count,
      changeName,
    }
  }
}
</script>

在这里插入图片描述

1.4 监听对象中某个属性的变化

javascript
复制代码
<template>
  <div>
    <div>{{obj.name}}</div>
    <div>{{obj.age}}</div>
    <button @click="changeName">改变值</button>
  </div>
</template>

<script>
import { reactive, watch } from 'vue';
export default {
  setup(){
    const obj = reactive({
      name:'zs',
      age:14
    });
    const changeName = () => {
      obj.name = 'ls';
    };
    watch(() => obj.name,() => {
      console.log('监听的obj.name改变了')
    })
    return {
      obj,
      changeName,
    }
  }
}
</script>

在这里插入图片描述

1.5 深度监听(deep)、默认执行(immediate)

javascript
复制代码
<template>
  <div>
    <div>{{obj.brand.name}}</div>
    <button @click="changeBrandName">改变值</button>
  </div>
</template>

<script>
import { reactive, ref, watch } from 'vue';
export default {
  setup(){
    const obj = reactive({
      name:'zs',
      age:14,
      brand:{
        id:1,
        name:'宝马'
      }
    });
    const changeBrandName = () => {
      obj.brand.name = '奔驰';
    };
    watch(() => obj.brand,() => {
      console.log('监听的obj.brand.name改变了')
    },{
      deep:true,
      immediate:true,
    })
    return {
      obj,
      changeBrandName,
    }
  }
}
</script>

在这里插入图片描述

2. watchEffect 的使用

watchEffect 也是一个帧听器,是一个副作用函数。 它会监听引用数据类型的所有属性,不需要具体到某个属性,一旦运行就会立即监听,组件卸载的时候会停止监听。

javascript
复制代码
<template>
  <div>
    <input type="text" v-model="obj.name"> 
  </div>
</template>

<script>
import { reactive, watchEffect } from 'vue';
export default {
  setup(){
    let obj = reactive({
      name:'zs'
    });
    watchEffect(() => {
      console.log('name:',obj.name)
    })

    return {
      obj
    }
  }
}
</script>

在这里插入图片描述

停止侦听

当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。 在一些情况下,也可以显式调用返回值以停止侦听:

javascript
复制代码
<template>
  <div>
    <input type="text" v-model="obj.name"> 
    <button @click="stopWatchEffect">停止监听</button>
  </div>
</template>

<script>
import { reactive, watchEffect } from 'vue';
export default {
  setup(){
    let obj = reactive({
      name:'zs'
    });
    const stop = watchEffect(() => {
      console.log('name:',obj.name)
    })
    const stopWatchEffect = () => {
      console.log('停止监听')
      stop();
    }

    return {
      obj,
      stopWatchEffect,
    }
  }
}
</script>

在这里插入图片描述

清除副作用

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (场景:有一个页码组件里面有5个页码,点击就会异步请求数据。于是做一个监听,监听当前页码,只要有变化就请求一次。问题:如果点击的比较快,从1到5全点了一遍,那么会有5个请求,最终页面会显示第几页的内容?第5页?那是假定请求第5页的ajax响应的最晚,事实呢?并不一定。于是这就会导致错乱。还有一个问题,连续快速点5次页码,等于我并不想看前4页的内容,那么是不是前4次的请求都属于带宽浪费?这也不好。

于是官方就给出了一种解决办法: 侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。 当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时;
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
javascript
复制代码
watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})

首先,异步操作必须是能中止的异步操作,对于定时器来讲中止定时器很容易,clearInterval之类的就可以,但对于ajax来讲,需要借助ajax库(比如axios)提供的中止ajax办法来中止ajax。 现在我写一个能直接运行的范例演示一下中止异步操作: 先搭建一个最简Node服务器,3000端口的:

javascript
复制代码
const http = require('http');

const server = http.createServer((req, res) => {
  res.setHeader('Access-Control-Allow-Origin', "*");
  res.setHeader('Access-Control-Allow-Credentials', true);
  res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS');
  res.writeHead(200, { 'Content-Type': 'application/json'});
});

server.listen(3000, () => {
  console.log('Server is running...');
});

server.on('request', (req, res) => {
  setTimeout(() => {
    if (/\d.json/.test(req.url)) {
      const data = {
        content: '我是返回的内容,来自' + req.url
      }
      res.end(JSON.stringify(data));
    }
  }, Math.random() * 3000);
});
javascript
复制代码
<template>
  <div>
    <div>content: {{ content }}</div>
    <button @click="changePageNumber">第{{ pageNumber }}页</button>
  </div>
</template>

<script>
import axios from 'axios';
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    let pageNumber = ref(1);
    let content = ref('');

    const changePageNumber = () => {
      pageNumber.value++;
    }

    watchEffect((onInvalidate) => {
      // const CancelToken = axios.CancelToken;
      // const source = CancelToken.source();
      // onInvalidate(() => {
      //   source.cancel();
      // });
      axios.get(`http://localhost:3000/${pageNumber.value}.json`, {
          // cancelToken: source.token,
      }).then((response) => {
        content.value = response.data.content;
      }).catch(function (err) {
        if (axios.isCancel(err)) {
          console.log('Request canceled', err.message);
        }
      });
    });
    return {
      pageNumber,
      content,
      changePageNumber,
    };
  },
};
</script>

上面注释掉的代码先保持注释,然后经过多次疯狂点击之后,得到这个结果,显然,内容错乱了:

在这里插入图片描述

现在取消注释,重新多次疯狂点击,得到的结果就正确了:

在这里插入图片描述

除了最后一个请求,上面那些请求有2种结局:

  • 一种是响应的太快,来不及取消的请求,这种请求会返回200,不过既然它响应太快,没有任何一次后续 ajax 能够来得及取消它,说明任何一次后续请求开始之前,它就已经结束了,那么它一定会被后续某些请求所覆盖,所以这类请求的 content 会显示一瞬间,然后被后续的请求覆盖,绝对不会比后面的请求还晚。
  • 另一种就是红色的那些被取消的请求,因为响应的慢,所以被取消掉了。

所以最终结果一定是正确的,而且节省了很多带宽,也节省了系统开销。

副作用刷新时机

Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick”中多个状态改变导致的不必要的重复调用。

同一个“tick”的意思是,Vue的内部机制会以最科学的计算规则将视图刷新请求合并成一个一个的"tick",每个“tick”刷新一次视图,如:a=1; b=2; 只会触发一次视图刷新。$nextTick的Tick就是指这个。

如 watchEffect 监听了2个变量 count 和 count2,当我调用countAdd,你觉得监听器会调用2次? 当然不会,Vue会合并成1次去执行。 代码如下,console.log只会执行一次:

javascript
复制代码
<template>
  <div>
    <div>{{count}} {{count2}}</div>
    <button @click="countAdd">增加</button>
  </div>
</template>

<script>
import { ref,watchEffect } from 'vue';

export default {
  setup(){
    let count = ref(0);
    let count2 = ref(10);
    const countAdd = () => {
      count.value++;
      count2.value++;
    }
    watchEffect(() => {
      console.log(count.value,count2.value)
    })
    return{
      count,
      count2,
      countAdd
    }
  }
}
</script>

在核心的具体实现中,组件的 update 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件update前执行。

所谓组件的 update 函数是 Vue 内置的用来更新DOM的函数,它也是副作用,上文已经提到过。 这时候有一个问题,就是默认下,Vue会先执行组件DOM update,还是先执行监听器?

javascript
复制代码
<template>
  <div>
    <div id="value">{{count}}</div> 
    <button @click="countAdd">增加</button>
  </div>
</template>

<script>
import { ref,watchEffect } from 'vue';

export default {
  setup(){
    let count = ref(0);
    const countAdd = () => {
      count.value++;
    }
    watchEffect(() => {
      console.log(count.value)
      console.log(document.querySelector('#value') && document.querySelector('#value').innerText)
    })
    return{
      count,
      countAdd
    }
  }
}
</script>

点击若干次(比如2次)按钮,得到的结果是:

在这里插入图片描述

为什么点之前按钮的innerText打印null? 因为事实就是默认先执行监听器,然后更新DOM,此时DOM还未生成,当然是null。 当第1和2次点击完,会发现:document.querySelector('#value').innerText 获取到的总是点击之前DOM的内容。 这也说明,默认Vue先执行监听器,所以取到了上一次的内容,然后执行组件 update。

Vue 2其实也是这种机制,Vue 2使用 this.nextTick()去获取组件更新完成之后的DOM,在watchEffect里就不需要用this. nextTick() 去获取组件更新完成之后的 DOM,在 watchEffect 里就不需要用this.nextTick()去获取组件更新完成之后的DOM,在watchEffect里就不需要用this.nextTick()(也没法用),有一个办法能获取组件更新完成之后的DOM,就是使用:

javascript
复制代码
// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'
  }
)

现在设上 flush 配置项,重新进入组件,再看看:

在这里插入图片描述

所以结论是,如果要操作“更新之后的DOM”,就要配置 flush: 'post'。

javascript
复制代码
如果要操作“更新之后的DOM ”,就要配置 flush: 'post'。
flush 取值:
	pre (默认)
	post (在组件更新后触发,这样你就可以访问更新的 DOM。这也将推迟副作用的初始运行,直到组件的首次渲染完成。)
	sync (与watch一样使其为每个更改都强制触发侦听器,然而,这是低效的,应该很少需要)

侦听器调试

onTrack 和 onTrigger 选项可用于调试侦听器的行为。

  • onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
  • onTrigger 将在依赖项变更导致副作用被触发时被调用。

这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。 建议在以下回调中编写 debugger 语句来检查依赖关系:

javascript
复制代码
watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrigger(e) {
      debugger
    }
  }
)

onTrack 和 onTrigger 只能在开发模式下工作。

3. 总结

watch 特点

watch 监听函数可以添加配置项,也可以配置为空,配置项为空的情况下,watch的特点为:

  • 有惰性:运行的时候,不会立即执行;
  • 更加具体:需要添加监听的属性;
  • 可访问属性之前的值:回调函数内会返回最新值和修改之前的值;
  • 可配置:配置项可补充 watch 特点上的不足: immediate:配置 watch 属性是否立即执行,值为 true 时,一旦运行就会立即执行,值为 false 时,保持惰性。 deep:配置 watch 是否深度监听,值为 true 时,可以监听对象所有属性,值为 false 时保持更加具体特性,必须指定到具体的属性上。

watchEffect 特点

  • 非惰性:一旦运行就会立即执行;
  • 更加抽象:使用时不需要具体指定监听的谁,回调函数内直接使用就可以;
  • 不可访问之前的值:只能访问当前最新的值,访问不到修改之前的值;

Vue 3 watch 与 Vue 2 watch 对比

  • Vue 3 watch 与 Vue 2 的实例方法 vm.watch(也就是this. watch(也就是 this.watch(也就是this. watch )的基本用法差不多,只不过程序员大多使用 watch 配置项,可能对 $watch 实例方法不太熟。实例方法的一个优势是更灵活,第一个参数可以接受一个函数,等于是接受了一个 getter 函数。
javascript
复制代码
<template>
  <div>
    <button @click="r++">{{ r }}</button>
  </div>
</template>

<script>
import { ref, watch } from 'vue';
export default {
  setup() {
    let r = ref(1);
    let s = ref(10);
    watch(
      () => r.value + s.value,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      }
    );
    return {
      r,
      s,
    };
  },
};
</script>
  • Vue 3 watch增加了同时监听多个变量的能力,用数组表达要监听的变量。回调参数是这种结构:[newR, newS, newT], [oldR, oldS, oldT],不要理解成其他错误的结构。
javascript
复制代码
<template>
  <div>
    <button @click="r++">{{ r }}</button>
  </div>
</template>

<script>
import { ref, watch } from 'vue';
export default {
  setup() {
    let r = ref(1);
    let s = ref(10);
    let t = ref(100);
    watch(
      [r, s, t],
      ([newR, newS, newT], [oldR, oldS, oldT]) => {
        console.log([newR, newS, newT], [oldR, oldS, oldT]);
      }
    );
    return {
      r,
    };
  },
};
</script>
  • 被监听的变量必须是:A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.也就是说,可以是getter/effect函数、ref、Proxy以及它们的数组。绝对不可以是纯对象或基本数据。
  • Vue 3的深度监听还有没有?当然有,而且默认就是,无需声明。当然,前提是深层 property 也是响应式的。如果深层 property 无响应式,那么即便写上 { deep: true } 也没用。

Props和data优先级谁高?

在Vue中,Props和Data都是用来定义组件的数据属性的,但它们有不同的作用和使用场景。

Props是组件的输入属性,用于从父组件传递数据给子组件。Props定义在子组件中,用于接收父组件传递的数据。在子组件内部,可以通过this.$props访问传递进来的Props数据。

Data是组件的内部状态,用于在组件内部存储和管理数据。Data定义在组件的data选项中,可以在组件的模板、计算属性、方法等内部使用。

从优先级的角度来看,Props的优先级高于Data。当一个组件同时定义了同名的Prop和Data属性时,Vue会优先使用Prop属性的值。如果Prop属性未定义或者为空,Vue会回退使用Data属性的值。

因此,在Vue中,如果需要在组件中使用从父组件传递过来的数据,应该优先使用Props来定义属性,以遵循Vue的推荐实践和最佳实践。如果需要在组件内部存储和管理一些私有数据,可以使用Data来定义属性。

vuex的原理

vuex分别有五个功能: 1.state就是数据管理的仓库 2.mutations是做同步的,可以在这里修改state仓库里面的数据 3.actions是做异步修改state仓库数据的,dispath 4.getter数据在源于vuex中的state仓库中的Getter来完成 5.moddule模块导出

Vuex的底层原理是基于vue的响应式原理和集中式状态管理,通过Store,再actions通过commit调用mutations里面的方法,mutations再修改state状态

commit接收两个参数,第一个是mutations里面的方法,另外一个是传递的值。actions不直接更改状态,不是通过commit调用mutations里面的方法,从而修改状态

Vue如何设置代理?

配置代理服务器介绍

跨域问题

1、什么是跨域问题?

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都会导致跨域问题。即前端接口去调用不在同一个域内的后端服务器而产生的问题。

2、如何解决跨域问题? --- 代理服务器

代理服务器的主要思想是通过建立一个端口号和前端相同的代理服务器进行中转,从而解决跨域问题。因为代理服务器与前端处于同一个域中,不会产生跨域问题;而且代理服务器与服务器之间的通信是后端之间的通信,不会产生跨域问题。

具体流程如下图所示,红色框代表浏览器,粉色框代表代理服务器,蓝色框代表后端的服务器。

image.png

3、如何实现代理服务器?-- 用vue-cli来实现

vue-cli配置官方文档

vue-cli配置代理的两种方法

方法一:在Vue.config.js中添加如下配置:

js
复制代码
devServer:{
    proxy:"http://localhost:5000"
}

说明:

1、优点:配置简单,请求资源时直接发给前端(8080)即可

2、缺点:不能配置多个代理,不能灵活的控制请求是否走代理

3、工作方式:若按照上述配置代理,当请求了不存在的资源时,那么该请求就会转发给服务器(有限匹配前端资源)

方法二:编写vue.config.js配置具体代理规则

js
复制代码
module.exports = {
    devServer: {
        proxy: {
            '/api1': { // 匹配所有以'/api1' 开头的请求路径
                target: 'http://localhost:5000', // 代理目标的基础路径
                changeOrigin: true,
                pathRewrite: {'^/api1':''}
            },
            '/api2': { // 匹配所有以'/api2' 开头的请求路径
                target: 'http://localhost:5001',// 代理目标的基础路径
                changeOrigin: true,
                pathRewrite: {'^/api2':''}
            },
        }
    }
}

/*
    changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
    changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
    changeOrigin默认值为true
*/

说明:

1、优点:可以配置多个代理,并且可以灵活的控制请求是否走代理

2、缺点:配置略微繁琐,请求资源时必须加前缀。

案例准备

1、服务器文件

说明:主要包含server1.js,server2.js文件

链接:pan.baidu.com/s/1t4uvyPdI…

提取码:euny

2、安装axios

js
复制代码
npm i axios

案例一:

1、需求:使用axios接收server1.js的服务

2、实现:

App.vue

js
复制代码
<template>
	<div>
		<button @click="getStudents">获取学生信息</button>
	</div>
</template>

<script>
        import axios from 'axios'
	export default {
		name:'App',
		methods:{
			getStudents(){
				//注意:开启代理服务器后,get中的端口号要改为前端所在的端口号,即8080
				axios.get('http://localhost:8080/students').then(
					response => {
						console.log('请求成功了',response.data)
					},
					error => {
						console.log('请求失败了',error.message)
					}
				)
			},
		},
	}
</script>

vue.config.js(这里要先关闭vue,写完后在重启,不然配置更改不会生效)

js
复制代码
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  // 关闭语法检查
  lintOnSave: false,
  // 开启代理服务器,注意:这里的端口号写后端的端口号(方式一)
  devServer: {
    proxy: 'http://localhost:5000'
  },
})

3、效果:

image.png

点击按钮后控制台输出

image.png

案例二:

1、需求:使用axios接收server1.js和server2.js的服务

2、实现:

  • App.vue
js
复制代码
<template>
	<div>
		<button @click="getStudents">获取学生信息</button>
		<button @click="getCars">获取汽车信息</button>
	</div>
</template>

<script>
        import axios from 'axios'
	export default {
		name:'App',
		methods:{
			getStudents(){
				//注意:采用了写法二,要加上前缀 /atguigu
				axios.get('http://localhost:8080/atguigu/students').then(
					response => {
						console.log('请求成功了',response.data)
					},
					error => {
						console.log('请求失败了',error.message)
					}
				)
			},
			getCars(){
				//注意:采用了写法二,要加上前缀 /demo
				axios.get('http://localhost:8080/demo/cars').then(
					response => {
						console.log('请求成功了',response.data)
					},
					error => {
						console.log('请求失败了',error.message)
					}
				)
			},
		},
	}
</script>
  • vue.config.js
js
复制代码
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  // 关闭语法检查
  lintOnSave: false,
  // 开启代理服务器,注意:这里的端口号写后端的端口号(方式一)
  // devServer: {
  //   proxy: 'http://localhost:5000'
  // },

  // 开启代理服务器(方式二)
  devServer: {
    proxy: {
      // /atguigu 是请求的前缀
      '/atguigu': {
        target: 'http://localhost:5000',
        //重写路径,把所有路径中包含/atguigu的路径替换为空字符串
        pathRewrite: {'^/atguigu':''}, 
        // 用于支持websocket
        ws: true,
        // 用于控制请求头中的host值
        changeOrigin: true
      },
      '/demo': {
        target: 'http://localhost:5001',
        //重写路径,把所有路径中包含/atguigu的路径替换为空字符串
        pathRewrite: {'^/demo':''}, 
        // 用于支持websocket
        ws: true,
        // 用于控制请求头中的host值
        changeOrigin: true
      },
    }
  }
})

3、效果:

  • 网页显示:

image.png

  • 控制台输出:

image.png