读完这篇,Vue3 用到飞起~

在过去的一年新项目开发中,我都采用了 Vue 3+TypeScript 的技术栈。从去年刚了解Vue3和TypsScript 时的懵懵懂懂;到开发时使用时的别扭,随时想转过头去写Vue2;再到后来熟悉和适应它们,而到现在我已经离不开它们了。回头看这一年来的使用经历,我觉得有必要沉淀一下,因为在这当中有很多的疑惑是一直困扰我的,相信大家也会有类似的问题。

下面是内容会结合官方文档进行展开,对于 Vue 3的一些原理性的东西这里不会去做分析。

前排提示:内容很干,请备好茶水,读完一定会有收获。

目标人群:使用Vue3 、TypeScript开发过项目的开发者。

响应式 API⭐️

在 Vue 3 的 响应式 API 中有两对 API,它们的功能相近,看起来能取代对方。而对于它们的选择,每个开发者可能都会有自己的想法。

ref 还是 reactive?

响应式基础是捕获值的改变,Vue 3 使用了 Proxy 实现了值的代理,使值的读写能够进行被监听,进而触发副作用。 Vue 3基于这个原理,提供了两个 API 来获取响应式对象,那么它们有什么区别,而我们应该选择哪个呢?

javascript 复制代码
// 类型  
function ref<T>(value: T): Ref<UnwrapRef<T>>  
interface Ref<T> {  
  value: T  
}  
// 示例  
const count = ref(0)  
console.log(count.value) // 0  
count.value++  
console.log(count.value) // 1  
// 类型  
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>  
// 示例  
const obj = reactive({ count: 0 })  
obj.count++

这两个API 相同点就是功能相同,都能将值转换为一个可修改的响应式对象。ref接受任意的一个值,返回的响应式对象需要通过 .value访问;reactive一个接受一个对象,返回的响应式对象能够直接访问。它们的主要区别有两点:

  1. 入参类型不同。

  2. 返回的响应式对象使用方式不同,ref 返回的响应式对象使用较为繁琐。

  3. ref能作为组件引用。

就第一点区别来说,ref的能力更加强大,非对象类型的数据只能用 ref,而第二点区别来说, reactive 的返回值使用起来更加方便。

如果让我来选择的话,我会选择只用 ref 。 对我而言, ref 需要通过 value 属性访问到对象反而是优点,它会明确的告诉我,当前正在用的对象是一个响应式对象而不是普通对象,而且computed属性也是徐彤通过value属性进行访问的,使用 ref 行为更为统一。更何况 ref 的适用面更广。(至于说会忘记 使用 value属性来访问,麻烦开启TS检查。)

Vue 3 实验提供响应式语法糖,这样由编译器来为ref对象的访问添加 .value 。不过现在已经废弃了,原因大家猜!

watch 还是 watchEffect?

Vue 3提供了一个新的API -- watchEffect ,用来进行丰富依赖追踪。那么这个新的API 它和watch有什么区别呢?什么时候用watch什么时候用watchEffect呢?

watch接受一个数据源和回调函数,数据源发生变化会触发回调函数;而 watchEffect 接受一个回调函数,不需要指定数据源,自动并收集回调函数里的所有依赖,同时创建监听器会立即执行一次回调。所以它们的区别主要有两点:

  1. 依赖收集方式不同。

  2. 回调是否会立即执行。

watch的功能比 watchEffect更加强大:

• 懒执行副作用;

• 更加明确是应该由哪个状态触发侦听器重新执行;

• 可以访问所侦听状态的前一个值和当前值。

js 复制代码
HTML  
<template>  
  {{ refValue }}  
</template>  
  
<script setup lang="ts">  
import { ref, watch, watchEffect } from "vue";  
const refValue = ref(0);  
watch(refValue, (value) => {  
  console.log("watch", value);  
});  
watchEffect(async () => {  
  console.log("watchEffect", refValue.value);  
});  
refValue.value = 1;  
</script>

上述代码会输出:

Bash watchEffect 0 watch 1 watchEffect 1

watchEffect 的依赖关系可能不会那么明确。我们需要通读代码,通过检查有没有读取响应式数据的值,判断它是否会被监听。例如,下面这段代码,refValue1会被当做依赖收集,而refValue2不是回调函数的依赖。

js 复制代码
<template>  
  {{ refValue1 }}  
</template>  
  
<script setup lang="ts">  
import { nextTick, ref, watchEffect } from "vue";  
const refValue1 = ref(1);  
const refValue2 = ref(0);  
watchEffect(() => {  
  refValue2.value = refValue1.value * 2;  
  refvalue2.value = obj.a  
});  
</script>

虽然合理的使用watchEffect,能够让代码更加简洁。但是 watchEffect 依赖关系是根据代码推断出来的,对于Reviewer 需要读完整段代码才能确定完整的依赖关系,所以不太建议使用 watchEffect。

函数的两种声明模式⭐️

通过函数声明可以限制函数的入参类型,确定函数的返回类型。 Vue 3的API都提供了两种声明方式:运行时声明或是类型声明,对应的,我们会有两种用法。

运行时声明:函数从参数推到出它的类型。

类型声明:泛型,在函数定义是传入一个类型作为返回类型

可以这样理解 运行时声明,函数根据传入的参数类型推断返回值类型;而类型声明,则由开发者指定泛型,编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。两者在运行效率上没有优劣之分。更多是写法和智能提示的区别,运行时声明不需要指定类型,写法接近JavaScript,能够提供有限的智能提示;类型声明语义化更好、表达更明确,而且编辑器能提供更加完整的智能提示,所以建议使用类型声明。

下面将通过 defineProps 和defineEmits 两个API来介绍这两种类型声明使用的区别。

defineProps

js 复制代码
<script setup lang="ts">  
// 运行时声明 1  
const props = defineProps({  
  foo: { type: String, required: true },  
  bar: Number  
})  
// 运行时声明 2  
const props = defineProps({  
  foo: '',  
  bar: 0  
})  
// 运行不声明 1  
const props = defineProps(['foo', 'bar'])  
// 类型声明  
const props = defineProps<{  
  foo: string  
  bar?: number  
}>()  
</script>

defineProps的用法可以说是非常多了,足足支持有四种完全不一样的写法。其中 运行不声明 1 的写法是完全获取不到类型提示的,所以非常不推荐。运行时声明 2 的写法可能能满足大部分的情景,它能根据传入值的实参类型推断出期望的类型,但是不能做复杂的类型判断。

运行时声明 1 写法是上一种写法的进阶版,它需要开发者提供一个校验选项。这种写法能够提供完整的类型校验,并提供智能提示。

但是有 TypeScript 这样的类型系统为什么不用呢?与 运行时声明 1 相比 类型声明 的写法符合 TypeScript 对类型的定义,没有学习成本,属性的定义也可以抽取成 interfece 进行复用。

综上,对于 defineProps ,推荐使用 类型声明 的写法。

defineProps的类型写法的唯一缺点就是函数不能直接设置默认值,需要借助withDefatult 编译器宏来设置默认值。Vue 也在尝试用新的语法糖来简化写法(响应式语法糖)。

js 复制代码
interface Props {  
  msg?: string  
  labels?: string[]  
}  
const props = withDefaults(defineProps<Props>(), {  
  msg: 'hello',  
  labels: () => ['one', 'two']  
})
事实上defineProps类型声明最终会被编译成defineProps运行时声明 1。

defineEmits

<script setup> 中,defineEmits API可以有两种写法。

js 复制代码
<script setup lang="ts">  
// 运行时  
const emits = defineEmits(['change', 'update'])  
  
// 基于类型  
const emits = defineEmits<{  
  (e: 'change', id: number): void  
  (e: 'update', value: string): void  
}>()  
</script>

defineEmits 比起defineProps就写法就少很多了。结论也显而易见。

运行时写法,入参是事件名,这样定义出来的emits不可能得到提示。而基于类型的写法,需要指明调用时的参数类型,这样在父组件接受事件时,就有足够的提示来推约束处理函数。

所以,推荐使用 类型声明 的写法。

js 复制代码
<template>  
  <li>  
    <span>{{ todoItem.id }}</span>   
    <span>{{ todoItem.message }}</span>   
    <span>{{ todoItem.status }}</span>   
    <button @click="emits('itemDone', todoItem.id)" v-if="todoItem.status === 'pending'" >  
      done  
    </button>  
  </li>  
</template>  
<script setup lang="ts">  
defineProps<{  
  todoItem: {  
    id: number;  
    status: string;  
    message: string;  
  };  
}>();  
const emits = defineEmits<{  
  (e: "itemDone", id: number): void;  
}>();  
</script>

其他

Vue中几乎所有API都支持两种声明模式,并不是所有的API都应该使用类型声明的方式。运行时声明能够简化我们的代码,如果类型可以通过入参明确确定,指定泛型反而是画蛇添足,所以这就需要去分析到时什么时候该用类型声明。

举个例子, toRef这个API,它接受一个reactive 对象和它的一个属性的key,返回类型就是该属性的类型。在这个API中,返回的参数就很明确,且就是和入参强相关,这种情况用运行时声明就没什么问题。

总结了一下,在下列情况下使用类型声明 :

  1. 入参的类型不定,需要约束的。例如 ref ,可以传任意类型作为参数,而当希望它只为某种类型时,就可以提供一个类型来声明。

  2. 需要类型提示的。例如 defineEmits ,使用类型声明能够获得类型提示。

js 复制代码
interface Props {  
  msg?: string;  
  labels?: string[];  
}  
const props = withDefaults(defineProps<Props>(), {  
  msg: "hello",  
  labels: () => ["one", "two"],  
});  
const emits = defineEmits<{  
  (e: "change", id: number): void;  
  (e: "update", value: string): void;  
}>();  
const number = ref<number>(0);  
const numberPlusOne = computed<number>(() => {  
  return number.value + 1;  
});  
interface State {  
  foo: number;  
  bar: number;  
}  
const state = reactive<State>({  
  foo: 1,  
  bar: 2,  
});  
const fooRef = toRef(state, "foo");  
const fooRefs = toRefs(state);  
defineExponse({  
    number  
})

<script setup>

在script块中,我们通过指定setup参数,Vue编译时会自动把这个块编译成setup函数,并将里面定义的函数和属性暴露给模版,不需要手动写返回。

它与setup函数相比,具有更多优势:

• 更少的样板内容,更简洁的代码。

• 能够使用纯 TypeScript 声明 props 和自定义事件。

• 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。

• 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。

所以,选择<script setup>是毋庸置疑的。

setup 异步?

如果我们的一个组件需要在一个异步操作得到响应后才能进行渲染,那我们应该怎么写?

js 复制代码
<template>  
  <span v-if="preLoaded"> suspense: {{ showValue }} </span>  
</template>  
  
<script setup lang="ts">  
import { ref } from "vue";  
const showValue = ref<number>();  
const preLoaded = ref(false);  
async function asyncFun() {  
  await new Promise((resolve: (res: number) => void) => {  
    console.log("async start", Date.now());  
    setTimeout(resolve, 2000, Math.random());  
  }).then((res) => {  
    preLoaded.value = true;  
    showValue.value = res;  
    console.log("async res" + res, Date.now());  
  });  
}  
asyncFun();  
console.log("under asyncFun()");  
</script>

如果在下面还有逻辑是依赖于某个异步的结果的,那么我们就得继续在这个async函数中加代码。而如果直接在setup函数里写await,页面是不会渲染的。

能否直接让setup函数就支持await呢?Vue 3提供了suspense抽象组件:

js 复制代码
<template>  
  <span v-if="preLoaded"> suspense: {{ showValue }} </span>  
</template>  
  
<script setup lang="ts">  
...  
await asyncFun();  
...  
</script>  
  
<script setup lang="ts">  
import SuspenseCompVue from "./components/SuspenseComp.vue";  
</script>  
  
<template>  
  <header>  
      <suspense>  
        <suspense-comp-vue />  
      </suspense>  
  </header>  
</template>

借助 suspense 抽象组件,我们能够让 setup 函数能够阻塞组件的渲染,进行异步操作。

定义组件的name

在vue2的选项式模式中,可以给自定义组件设置一个name属性,它在以下情况下会被使用到:

  1. 组件模板递归地调用自身。(写递归组件时)

  2. 便于调试。(vue-devtools)

  3. keep-alive时,include和exclude 是通过name来匹配的。

Vue 3 的 <setup script> 可以有个同级的 <scrpit>,在这里可以像vue2 一样导出一个对象,它最终会和 <setup script> 返回做合并。

js 复制代码
<script>  
// 普通 <script>, 在模块作用域下执行 (仅一次)  
runSideEffectOnce()  
  
// 声明额外的选项  
export default {  
  name: 'myName',  
  customOptions: {}  
}  
</script>  
  
<script setup>  
/* eslint-disable import/first */  
// 在 setup() 作用域中执行 (对每个实例皆如此)  
</script>

cn.vuejs.org/api/sfc-scr...

inheritAttrs属性用于控制是否启用默认的组件 attribute 透传行为,默认开启。也可以通过这样的方式来进行关闭inheritAttrs的行为。

js 复制代码
<script>  
export default {  
  inheritAttrs: false  
}  
</script>  
  
<script setup>  
  
</script>

自定义组件

defineExponse

我们在ref调用子组件的方法时,希望IDE能够提示组件导出的方法有哪些入参以及返回值是什么,可以这样写。

js 复制代码
    
// app.vue  
<script setup lang="ts">  
import { ref } from "vue";  
import InputValidateVue from "./components/InputValidate.vue";  
const inputValidateRef = ref<InstanceType<typeof InputValidateVue> | null>(null);  
</script>  
  
<template>  
    <InputValidateVue ref="inputValidateRef" />  
</template>
js 复制代码
// inputvalidate.vue  
<template>  
  <input type="text" v-model="inputStr" />  
</template>  
  
<script setup lang="ts">  
import { ref } from "vue";  
  
const inputStr = ref("");  
  
function validate() {  
  const inputStrVal = +inputStr.value;  
  if (Number.isInteger(inputStrVal)) {  
    return true;  
  }  
  return false;  
}  
  
function getInputVal() {  
  return inputStr.value;  
}  
  
defineExpose({  
  validate,  
  getInputVal,  
});  
</script>

在父组件中定义给子组件的 ref 时,指定 InstanceType<typeof InputValidateVue> | null 这样的一个类型,编辑器便能提示 子组件实例呢内置指令和通过 defineExponse 导出的属性和方法。

InstanceType 是 TypeScript提供的一个预设类型,它会返回指定的类的实例类型。而在setup里定义ref 时,组件是还没挂载的,所以还需要指定一个 联合类型 null。www.typescriptlang.org/docs/handbo...

v-model 的使用

v-model是vue提供的一个语法糖,它既能作用于普通表单元素上,又可以作用在组件上。

Vue 2和Vue 3在组件上的v-model是很大区别的:

• 破坏性更新:在自定义组件上使用v-model 时,默认绑定的属性名和事件名变了。

○ prop: value -> modelValue

○ event: input -> update:modelValue

• 破坏性更新:v-bind的 .sync写法和组件的model选项改为用v-model

• 新特性: 同个组件可以绑定多个 v-model

• 新特性: 支持自定义修饰符

可以通过例子看他们的区别。

• 自定义组件上使用v-model

js 复制代码
<!-- vue2 -->  
<ChildComponent v-model="pageTitle" />  
<!-- === -->  
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />  
  
<!-- Vue 3 -->  
<ChildComponent v-model="pageTitle" />  
<!-- would be shorthand for: -->  
<ChildComponent  
  :modelValue="pageTitle"  
  @update:modelValue="pageTitle = $event"  
/>

• v-bind的.sync 写法和 组件的model选项改为用v-model

js 复制代码
<!-- vue2 -->  
<ChildComponent :title.sync="pageTitle" />  
  
<!-- Vue 3 -->  
<ChildComponent v-model:title="pageTitle" />

• 同个组件可以绑定多个 v-model

• 支持自定义修饰符

父组件会将自定义的修饰符通过名为 arg + "Modifiers"属性传到子组件,子组件能够判断属性中是否包含自定义修饰符做对应的操作。

js 复制代码
<script setup>  
const props = defineProps({  
  modelValue: String,  
  modelModifiers: { default: () => ({}) }  
})  
defineEmits(['update:modelValue'])  
console.log(props.modelModifiers) // { capitalize: true }  
</script>  
<template>  
  <input  
    type="text"  
    :value="modelValue"  
    @input="$emit('update:modelValue', $event.target.value)"  
  />  
</template>
Vue 3把v-bind的.sync 写法删除了,所以现在在vue中进行双向绑定只有v-model一种写法。

最佳实践 表单绑定:

有一个表单数据obj,它有 a~d 4个属性,其中 a 属性是不能改变的,b~c属性由 组件A来修改,d属性由组件B来修改,f 属性计算依赖a~c属性。

js 复制代码
<template>  
  <ModelAVue v-if="obj" v-model:b="obj.b" v-model:c="obj.c" /> //只对组件能修改的做绑定  
  <ModelBVue v-if="obj" v-model:d="obj.d" :obj="obj" /> //obj只支持查看  
</template>  
<script lang="ts" setup>  
import ModelAVue from "./ModelA.vue";  
import ModelBVue from "./ModelB.vue";  
import { ref } from "vue";  
const obj = ref<{  
  a: string;  
  b: string;  
  c: string;  
  d: string;  
}>();  
</script>  
// ModelA.vue  
<template>  
  {{ c }}  
</template>  
<script lang="ts" setup>  
defineProps<{  
  b: string;  
  c: string;  
}>();  
defineEmits<{  
  (e: "update:b", data: string): void;  
  (e: "update:c", data: string): void;  
}>();  
</script>  
// ModelB.vue  
<template>  
  {{ d }}  
</template>  
<script lang="ts" setup>  
defineProps<{  
  obj: {  
    a: string;  
    b: string;  
    c: string;  
  };  
  d: string;  
}>();  
defineEmits<{  
  (e: "update:d", data: string): void;  
}>();  
</script>

其他

组合式函数⭐️

Vue 3最显著的特点是,支持组合式(Composition)API。同时,Vue 3也鼓励我们使用组合式函数来抽象一些通用逻辑。

官方文档是这样定义的:"组合式函数"(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。需要注意的是,组合式函数和一般的工具函数是有区别的,组合式函数需要带状态,而工具函数是不带状态的。

约定

命名:组合式函数约定用驼峰命名法命名,并以"use"作为开头。

输入参数:没有要求,可以使ref也可以是普通参数。

返回值:一般是包含多个ref的普通非响应式对象

在日常开发中,我们可能需要在 dom元素上添加原生事件监听器,那么,在改dom元素被卸载时,我们需要回收掉,可以写出如下代码:

js 复制代码
<template>  
  <div id="specialDiv" ref="specialDiv">Click Me</div>  
</template>  
<script lang="ts" setup>  
import { onMounted, onUnmounted, ref } from "vue";  
const specialDiv = ref<HTMLDivElement>();  
function specialDivClickHandler(event: Event) {  
  console.log("specialDiv been click", event.timeStamp);  
}  
function specialDivMouseOverHandle(event: Event) {  
  console.log("specialDiv been mouse over", event.timeStamp);  
}  
onMounted(() => {  
  if (specialDiv.value) {  
    specialDiv.value.addEventListener("click", specialDivClickHandler);  
    specialDiv.value.addEventListener("mouseover", specialDivMouseOverHandle);  
  }  
});  
onUnmounted(() => {  
  if (specialDiv.value) {  
    specialDiv.value.removeEventListener("click", specialDivClickHandler);  
    specialDiv.value.removeEventListener(  
      "mouseover",  
      specialDivMouseOverHandle  
    );  
  }  
});  
</script>

每新增一个原生事件监听器,便要多重复写一次添加和卸载逻辑,能否让代码能自动在回收原生监听器呢?

可以通过组合式函数抽象回收的公共逻辑:

js 复制代码
// useEventListener.ts  
import { onUnmounted } from "vue";  
  
export function useEventListener(  
  target: HTMLDivElement,  
  type: string,  
  listener: (event: Event) => void  
) {  
  target.addEventListener(type, listener);  
  onUnmounted(() => {  
    target.removeEventListener(type, listener);  
  });  
}
js 复制代码
<template>  
  <div id="specialDiv" ref="specialDiv">Click Me</div>  
</template>  
<script lang="ts" setup>  
import { onMounted, ref } from "vue";  
import { useEventListener } from "./useEventListener";  
const specialDiv = ref<HTMLDivElement>();  
  
onMounted(() => {  
  if (specialDiv.value) {  
    useEventListener(specialDiv.value, "click", function (event: Event) {  
      console.log("specialDiv been click", event.timeStamp);  
    });  
    useEventListener(specialDiv.value, "mouseover", function (event: Event) {  
      console.log("specialDiv been mouse over", event.timeStamp);  
    });  
  }  
});  
</script>

vueuse ,这个仓库里实现了很多有用的组合式函数,可以学习或者copy到项目中使用。

在开发的过程中,我们应该多思考、多做提取,减少重复编码,提高开发效率。对于不带状态的逻辑,提取为普通函数,带状态的,提取为组合式函数。

多根节点和class⭐️

在Vue 2编码中,我们在写template时会觉得很繁琐,除了用template标签包裹,还得额外定义一个标签来套住所写的内容,否则编译器会报错。

js 复制代码
<template>  
<div class="header"></div>  
<div class="body"></div>  
<div class="footer"></div>  
</template>  
  
warn:  
 - Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

而在 Vue 3的 template 里可以直接写多个标签。

js 复制代码
<template>  
  <li v-for="item of list" :key="item">{{ item }}</li>  
  <button @click="handleClick">Click</button>  
</template>  
<script lang="ts" setup>  
defineProps<{  
  list: number[];  
}>();  
function handleClick() {  
  console.log("clicked");  
}  
</script>
js 复制代码
<script setup lang="ts">  
import MultiRootVue from "./components/MultiRoot.vue";  
</script>  
  
<template>  
  <header>  
    <ul>  
      <MultiRootVue :list="[1, 2, 3, 4]"  class="wrapper" />  
    </div>  
  </header>  
</template>  
  
  
<style>  
.wrapper {  
  color: red;  
}  
</style>

还是报了warn,同时 wrapper 的样式也没有生效。这是因为 class 属性是一个 "非props"属性,如果不做特殊处理的话,它会被当成原生属性透传到子组件的唯一根元素上,而 Vue 3 的多根template让编译器不知道该将这些属性挂载在哪个元素上,所以直接就进行告警。

"透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class style id 。当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。

所以,我们该如何将这些属性进行显性绑定呢?Vue 3提供了模版变量 $attrs 和 API useAttrs() 帮助我们获到当前所有透传的属性。只要将这些属性显性绑定到能接受这些属性的元素上就能消除警告。

如果绑定的元素是自定义组件,还是不能消费掉这个属性,那么还是会告警。

js 复制代码
<template>  
  <li :class="$attrs.class" v-for="item of list" :key="item">{{ item }}</li>  
</template>
js 复制代码
<template>  
  <li v-bind="$attrs" v-for="item of list" :key="item">{{ item }}</li>  
</template>  
// **v-bind不指定属性则会解构对象并绑定所有的属性**

我们也可以通过设置inheritAttrs属性来关闭透传行为来消除警告,但是这样无异于掩耳盗铃,因为属性还是被丢弃了。

js 复制代码
export default {  
  inheritAttrs: false  
}

对于多根节点,我们需要注意透传属性要做进行显性绑定。 有时我们会发现在父组件使用v-show控制子组件的显示隐藏不生效,而 v-if 却能隐藏元素,这就和透传行为相关。

或许Vue未来可以做点优化,多根节点的父节点存在透传属性时,批量设置透传属性。

总结

Vue 3相比于Vue 2的变动是非常巨大的。推出组合式API、全面拥抱TypeScript、setup函数,甚至对于相同的问题提供了几套开发选型来选择,像选项式API和组合式API、watch和watchEffect、运行时声明和类型声明;当然还有许多附加的新的 API。

这些改变或颠覆了让开发者看花了眼,也产生了巨大的压力和畏难心理,所以会有人道Vue 2 就已经足够了。曾经我也是这样认为的,但是一眨眼,抬头望去早已变了天。

希望以上总总会对你有帮助。

当然,上述只是我一些浅略的拙见,如有疏漏之处烦请见谅。

相关推荐
qq. 28040339845 小时前
CSS层叠顺序
前端·css
喝拿铁写前端5 小时前
SmartField AI:让每个字段都找到归属!
前端·算法
猫猫不是喵喵.5 小时前
vue 路由
前端·javascript·vue.js
烛阴6 小时前
JavaScript Import/Export:告别混乱,拥抱模块化!
前端·javascript
bin91536 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例12,TableView16_12 拖拽动画示例
前端·javascript·vue.js·ecmascript·deepseek
GISer_Jing6 小时前
[Html]overflow: auto 失效原因,flex 1却未设置min-height &overflow的几个属性以及应用场景
前端·html
程序员黄同学6 小时前
解释 Webpack 中的模块打包机制,如何配置 Webpack 进行项目构建?
前端·webpack·node.js
拉不动的猪6 小时前
vue自定义“权限控制”指令
前端·javascript·vue.js
再学一点就睡7 小时前
浏览器页面渲染机制深度解析:从构建 DOM 到 transform 高效渲染的底层逻辑
前端·css
拉不动的猪7 小时前
刷刷题48 (setState常规问答)
前端·react.js·面试