vue3-ref、动态事件、computed、watch、watchEffect、nextTick、样式穿透、css新特、环境变量

142 阅读8分钟

1)ref等关键函数

ref
支持所有类型
取值、赋值都需要加.value
const man = ref({ name:'menffy' })
man.value.name='bad_guy'
reactive
# 只能作用于引用类型,比如数组、对象、Map、Set
const man = reactive({ name:'menffy' })
man.name='bad_guy' // 取值、赋值直接用

# 不能对整个响应式对象直接赋值,会破坏proxy代理特性,失去响应性
man = {a:'123'} // 错误

# 解决方法:
# 数组使用.push方法 + 解构
const man = reactive([1,2,3])
let res = [3,4,5]
man.push(..res)

# 对象则直接赋值给其中一个属性
const man = reactive({ name:'menffy', like:['sing', 'dance'] })
man.name = 'bad_guy'
man.like = ['run', 'watch'] 
toRef
# 作用于响应式对象。作用于非响应式对象时,毫无用处,值的变化并不会触发视图更新

# 用处:可以将响应式对象的某个属性包装为一个单独的响应式对象,提供给外部使用,而不用暴露整个对象

const man = reactive({ name:'menffy' })
const name = toRef(man, 'name')

# 此时修改name,会触发视图的自动更新
const chaneg = () => {
   name.value = 'bad_guy'
}
toRefs
# 用处:用于对reactive包括的响应式对象进行解构
# 其实就是对响应式对象的每个值都调用了一次toRef,如果直接对响应式对象做解构,拿到的值并不是响应式的

# 原理类似:
const toRefs = <T extends object>(obj:T) => {
 const map:any = {};
 for (let key in obj) {
     map[key]=toRef(obj, key);
 }
 return map;
}

# 例子:
const man = reactive({ name:'menffy', age:12, like:'JK' })

const {name, age, like} = toRefs(man)

# 此时修改name,会触发视图的自动更新
const chaneg = () => {
   name.value = 'bad_guy'
}

toRaw
# 作用:移除响应性
# 用处:当你想修改值,但不想它作为响应式,触发更新时

const man = reactive({ name:'menffy', age:12, like:'JK' })
console.log(man)
console.log(toRaw(man))
其他
# readonly 响应式对象变只读,不可修改
const man = reactive({ name:'menffy', age:12, like:'JK' })
readonly(man) // man参数不再允许修改

# isRef 判断是否为ref响应式对象
const man = ref({ name:'menffy', age:12, like:'JK' })
const flag = isRef(man)? true : false

# shallRef 浅层响应,只响应到.value阶段,不可以和ref同时使用,会被ref影响,导致视图必更新
const test = shallRef({'a':1})
test.value.a = 2 // 值变,视图不更新
test.value = {'a':2} // 值变,视图更新

# shallowReactive 浅层响应,只响应到第一层,不可以和reactive同时使用,会被reactive影响,导致视图必更新
const test = shallowReactive({'a': { 'b': 1 } })
test.a.b = 2 // 值变,视图不更新
test.a = {'b':2} // 值变,视图更新

# triggerRef 强制更新收集的依赖,触发视图更新,用在浅层响应之后,会让浅层响应变深层响应
triggerRef()

2)动态事件切换

  • 使用“[变量]”方式,动态传递事件
  • 使用stop修饰符,阻止冒泡事件,避免父组件click事件被动触发
<template>
    <div @click='我被动触发了'>
      <button @[event].stop='handleClick'>点击我</button>
    </div>
</template>

<script setup lang="ts">
const event = ref('click');
const handleClick = ()=> {console.log('点击了我')};

</script>

3)computed计算属性

<script setup lang="ts">
import { ref, computed } from 'vue';

const firstName = ref('menffy');
const lastName = ref('z');
函数式写法
  • 只支持一个getter函数,不支持修改值
const name = computed<string>(() => `${firstName.value}-${lastName.value}`);
选项式写法
  • 支持传入一个对象,包含get & set函数,可自定义操作
const name = computed<string>({
  get() {
    return `${firstName.value}-${lastName.value}`;
  },
  set(newVal) {
    [firstName.value, lastName.value] = newVal.split('-');
  },
});
</script>

4)watch监听器

watch参数:

  • 第一个位置:监听源
  • 第二个位置:回调函数cb
  • 第三个位置:options配置项,是一个对象
import { watch, ref, reactive } from 'vue';

const a = ref('');

watch(
  a,
  (newVal, oldValue) => {
    console.log(newVal, oldValue);
  },
  {
    immediate: true, //是否立即调用一次,默认false
    deep: true,  // 是否开启深度监听,默认false。当监听的是reactive对象时,自动开启
    flush: 'pre' // 默认pre:组件更新之前调用, sync:同步执行, post:组件更新之后调用
  },
);
监听ref对象
// 基础类型
const a = ref('');

watch(a, (newVal, oldValue) => {
  console.log(newVal, oldValue);
});

// 引用类型,需要开启深度监听。并且注意“返回的新值和旧值是一样的”
const d = ref({
  foo: {
    bar: {
      name: 'menffy',
    },
  },
});

watch(
  d,
  (newVal, oldValue) => {
    console.log(newVal, oldValue);  
  },
  {
    deep: true,
  },
);

监听reactive对象
const c = reactive({ t: 1 });

// 默认开启了深度监听选项
watch(c, (newVal, oldValue) => {
  console.log(newVal, oldValue);
});
监听数组
const a = ref('');
const b = ref('');
const c = reactive({ t: 1 });

// newVal, oldValue返回的也是数组,位置与监听对象的位置一一对应
watch([a, b, c], (newVal, oldValue) => {
  console.log(newVal, oldValue);
});
监听函数
const c = reactive({ t: 1 });

// 监听reactive对象内某一个属性时,直接监听是不行的,拿到的是字符串,不是响应式对象
// 但可以通过函数返回的方式监听
watch(
  () => c.t,
  (newVal, oldValue) => {
    console.log(newVal, oldValue);
  },
);

5)watchEffect高级监听器

  • 只能监听响应式对象!!!
  • watchEffect是非惰性的,进入页面会自动触发一次,而不是等值变化的时候才触发(类似watch方法开启了immediate选项的效果)
<template>
  <div>
    <span ref="ipt2">message1: <input v-model="message1" type="text" /></span>
    <span id="ipt">message2: <input v-model="message2" type="text" /></span>
    <span>message3: <input v-model="message3" type="text" /></span>
    <t-button @click="stopWatch">点我停止监听</t-button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>
<script setup lang="ts">
import { ref, watchEffect } from 'vue';

const ipt2 = ref();
const message1 = ref('飞机');
const message2 = ref('火车');
const message3 = ref('');

const showAlert = (value) => {
  console.log(`3 message3.value值变化了:${value},所以我也被运行了`);
};

// 直接把需要监听的对象放进去,在回调函数里做逻辑处理。赋值或调用其他函数
const stop = watchEffect(
  (beforeEffect) => {
  
    // 如果不加下面的flush: 'post'选项,此处的两个dom元素都会在一开始获取不到,分别为undefined和null
    // 紧接着等组件和dom加载完了,dom元素ipt2.value有值了又会自动运行一次。效果看起来就是第一次进入页面时,会连续运行2次watchEffect
    console.log('ref=ipt2的dom元素', ipt2.value);
    
    // 注意这里的ele就算后续有值了也不会触发watchEffect,因为它不是响应式对象
    const ele = document.querySelector('#ipt');
    console.log('id=ipt的dom元素', ele);

    console.log('2', message1.value);
    message3.value = message1.value;
    showAlert(message3.value);
    console.log('4', message2.value);
    
    // watchEffect里的前置动作
    beforeEffect(() => {
      console.log('1 不管我在代码里的哪个位置被调用,反正在做其他事前会先执行我!!可以做防抖、清理数据等前置动作');
    });

  },
  {
    flush: 'post', // 在组件更新、dom加载完成之后才调用watchEffect,建议用这个
    onTrigger(e) {
      debugger; // 可以帮助调试,打断点
    },
  },
);

// 返回一个stop函数,调用后可以停止监听
const stopWatch = () => stop();

</script>

<style scoped></style>

6)nextTick

  • 在vue中,dom更新是异步的,数据更新是同步的
  • 我们的写的逻辑代码基本都是同步的。vue的执行顺序是,先同步后异步(先宏任务后微任务)
  • nextTick方法就是用来解决需要先更新dom再继续执行同步代码问题的。比如操作dom的时候发现读取的数据还是上次的
  • nextTick的原理就是把我们的任务放到一个promise里去运行,变成微任务
写法一 回调函数
<script setup lang="ts">
import { ref, nextTick } from 'vue';

const box = ref<HTMLDivElement>();
const dataList = ref([]);

const hClick = () => {
  dataList.value.push({
    name: 'menffy',
  });
  // 写法一,接受一个回调函数,在回调函数里写dom更新后需要做的事
  nextTick(() => {
    // 添加数据后,移动下拉滚条到最底部
    box.value.scrollTop = 999999;
  });
};
</script>
写法二 async await模式
<script setup lang="ts">
import { ref, nextTick } from 'vue';

const box = ref<HTMLDivElement>();
const dataList = ref([]);

const hClick = async () => {
  dataList.value.push({
    name: 'menffy',
  });
  // 写法二,async await模式,在这句话后面写dom更新后需要做的事,这句话后面的代码都变成异步的了
  await nextTick();
  // 添加数据后,移动下拉滚条到最底部
  box.value.scrollTop = 999999;
};
</script>

7)样式穿透

<template>
  <div>
    <div>我是index</div>
    <hr />
    <t-button @click="hClick">点我</t-button>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>
<script setup lang="ts">
</script>

<style scoped lang="less">

// 样式穿透,可以修改第三方组件的样式
:deep(.t-button__text) {
  background-color: red;
}

</style>

8)css新特性

一、插槽内容样式设置
父组件
<template>
  <div>
    <div>我是index</div>
    <div ref="box">
      <c-vue><div class="setClass">我是私人定制插槽</div></c-vue>
    </div>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>

<script setup lang="ts">
import CVue from './C.vue';

</script>

<style scoped lang="less"></style>
子组件-C组件
<template>
  <div style="border: 1px solid black; height: 500px; background-color: #3dc9b0">
    我是C
    <slot />
  </div>
</template>

<script setup lang="ts"></script>

<style scoped>
:slotted(.setClass) {
  background-color: #0d429a;
}
</style>

image.png

二、全局样式修改
<style scoped>
:global(div) {
  background-color: pink;
}
</style>
三、动态css
<template>
  <div>
    <div>我是index</div>
    <div class="box">
      <div class="setClass">我是私人定制插槽</div>
    </div>
  </div>
</template>

<script lang="ts">
export default {
  name: 'ApplyAWSIndex',
};
</script>
<script setup lang="ts">
import { ref } from 'vue';

const css = ref('pink');

const css2 = ref({
  backgroundColor: '#0d429a',
});

setTimeout(() => {
  css2.value.backgroundColor = 'red';
}, 2000);


</script>

<style scoped lang="less">
.box {
  border: 3px solid black;
  height: 50px;
  
  // 通过绑定参数实现
  .setClass {
    color: v-bind(css);
    background-color: v-bind('css2.backgroundColor');
  }
  
}
</style>

image.png

image.png

四、 css module

image.png

image.png

image.png

9) 编译宏

defineEmits写法:
const emit = defineEmits(['send']);

const emit = defineEmits<{
  (e: 'inited1'): void;
  (e: 'inited', value: any): void;
}>();

const emit = defineEmits<{
  send1;
  send: [name: string];
  'update:modelValue': [name: string];
}>();
defineOptions写法:

image.png

defineSlots

主要是给插槽做类型限制的

defineProps

image.png

image.png

10) 环境变量

一、创建自定义的环境变量文件
  • 文件.env开头,后面接.development(这个随意)
  • .env应该是默认选择配置

image.png

二、自定义环境变量要用'VITE_'开头
VITE_SOME_KEY=456
# 打包路径
VITE_BASE_URL = ./
# 自定义环境变量
VITE_TEST=asd
三、模板/项目 内使用
console.log(import.meta.env);

// DEV: npm run dev的时候为true,也就是测试环境
// MODE: 当前运行的环境
// PROD: npm run build的时候为true,也就是生产环境

image.png

四、系统读取环境变量,使之生效
  • mode参数后的名字要跟文件名的后缀对应上
  • 生产环境默认读取production,所以可以创建.env.production文件

image.png

五、vite.config.ts使用环境变量方式
import { ConfigEnv, UserConfig, loadEnv } from 'vite';
import { viteMockServe } from 'vite-plugin-mock';
import createVuePlugin from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import svgLoader from 'vite-svg-loader';

import path from 'path';

const CWD = process.cwd();

// https://vitejs.dev/config/
export default ({ mode }: ConfigEnv): UserConfig => {

  // 这里读取环境变量,注意这里使用了loadEnv函数,这个是关键
  // 开发环境下,mode就是'development',与前述对应
  const { VITE_BASE_URL } = loadEnv(mode, CWD);
  
  return {
    base: VITE_BASE_URL,
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
      },
    },

    plugins: [
      createVuePlugin(),
      vueJsx(),
      viteMockServe({
        mockPath: 'mock',
        localEnabled: mode === 'mock',
      }),
      svgLoader(),
    ],

    server: {
      port: 3002,
      host: '0.0.0.0',
      proxy: {
        '/web_api': {
          target: 'http://9.135.92.235',
          // target: 'http://127.0.0.1:5000',
          rewrite: (path) => path.replace(/^/web_api/, '/erp_api_menffy/'),
          changeOrigin: true,
        },
      },
    },
  };
};