当前位置: 首页 > news >正文

网帆-网站建设官方店适合个人开网店的平台

网帆-网站建设官方店,适合个人开网店的平台,做网站除了dw,营口网站建设哪家好文章目录组合式API介绍什么是组合式 API#xff1f;为什么要有组合式 API#xff1f;更好的逻辑复用更灵活的代码组织Option ApiOption Api的缺陷Composition Api更好的类型推导更小的生产包体积与选项式 API 的关系取舍组合式 API 是否覆盖了所有场景#xff1f;可以同时使… 文章目录组合式API介绍什么是组合式 API为什么要有组合式 API更好的逻辑复用更灵活的代码组织Option ApiOption Api的缺陷Composition Api更好的类型推导更小的生产包体积与选项式 API 的关系取舍组合式 API 是否覆盖了所有场景可以同时使用两种 API 吗选项式 API 会被废弃吗与 Class API 的关系和 React Hooks 的对比setup函数基本使用访问 PropsSetup 上下文暴露公共属性与渲染函数一起使用响应式核心reactive和ref用reactive声明响应式状态reactive概念reactive的使用reactive的深层响应性reactive 的局限性reactive 响应式丢失的情况响应式代理 vs. 原始对象用ref定义响应式变量ref的概念值类型的ref对象类型的refref的解包ref 在模板中的解包ref 在响应式对象中的解包数组和集合类型的 ref 解包数组声明与赋值的技巧reactive和ref的对比ref的研究reactive研究总结DOM 更新时机响应式工具函数isRef()unref()toRef()和toRefs()toRef()toRefs()toRef()和toRefs()的对比使用toRef()的结果使用toRefs()的结果torefs的妙用在setup函数中的使用在setup语法糖中的使用isProxy()isReactive()isReadonly()计算属性只读计算属性可写计算属性侦听器watch概念基本使用立即监听深层侦听器监听响应式对象监听getter函数watchEffect概念基本使用watch与watchEffect对比回调的触发时机停止侦听器监听数组的研究生命周期钩子注册周期钩子生命周期图示选项式和组合式对比钩子函数详细信息onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmountonUnmountedonErrorCapturedonRenderTrackedonRenderTriggeredonActivatedonDeactivated模板引用访问一个组件或者元素访问多个组件或者元素函数模板引用组件上的 ref侦听模板引用依赖注入Prop 逐级透传问题Provide (提供)概念在组件中使用 Provide应用层 ProvideInject (注入)概念在组件中使用 Inject注入默认值和响应式数据配合使用使用 Symbol 作注入名插槽子组件父组件setup语法糖基本语法响应式使用组件动态组件递归组件命名空间组件使用自定义指令defineProps()defineEmits()defineExpose()useSlots() 和 useAttrs()与普通的 script 一起使用顶层 await限制组合式函数 hook什么是“组合式函数”鼠标跟踪器示例异步状态示例约定和最佳实践命名输入参数返回值副作用使用限制通过抽取组合式函数改善代码结构在选项式 API 中使用组合式函数与其他模式的比较和 Mixin 的对比和无渲染组件的对比和 React Hooks 的对比更多hook的理解组合式API介绍 什么是组合式 API 组合式 API (Composition API) 是一系列 API 的集合使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语涵盖了以下方面的 API 响应式 API例如 ref() 和 reactive()使我们可以直接创建响应式状态、计算属性和侦听器。生命周期钩子例如 onMounted() 和 onUnmounted()使我们可以在组件各个生命周期阶段添加逻辑。依赖注入例如 provide() 和 inject()使我们可以在使用响应式 API 时利用 Vue 的依赖注入系统。 组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本可以使用官方维护的插件 vue/composition-api。在 Vue 3 中组合式 API 基本上都会配合 script setup 语法在单文件组件中使用。下面是一个使用组合式 API 的组件示例 script setup import { ref, onMounted } from vue// 响应式状态 const count ref(0)// 更改状态、触发更新的函数 function increment() {count.value }// 生命周期钩子 onMounted(() {console.log(计数器初始值为 ${count.value}。) }) /scripttemplatebutton clickincrement点击了{{ count }} 次/button /template虽然这套 API 的风格是基于函数的组合但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的而函数式编程通常强调数据不可变。 为什么要有组合式 API 更好的逻辑复用 组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins而组合式 API 解决了 mixins 的所有缺陷。 组合式 API 提供的逻辑复用能力孵化了一些非常棒的社区项目比如 VueUse一个不断成长的工具型组合式函数集合。组合式 API 还为其他第三方状态管理库与 Vue 的响应式系统之间的集成提供了一套简洁清晰的机制例如 RxJS。 更灵活的代码组织 许多用户喜欢选项式 API 的原因是它在默认情况下就能够让人写出有组织的代码大部分代码都自然地被放进了对应的选项里。然而选项式 API 在单个组件的逻辑复杂到一定程度时会面临一些无法忽视的限制。这些限制主要体现在需要处理多个逻辑关注点的组件中这是我们在许多 Vue 2 的实际案例中所观察到的。 我们以 Vue CLI GUI 中的文件浏览器组件为例这个组件承担了以下几个逻辑关注点 追踪当前文件夹的状态展示其内容处理文件夹的相关操作 (打开、关闭和刷新)支持创建新文件夹可以切换到只展示收藏的文件夹可以开启对隐藏文件夹的展示处理当前工作目录中的变更 Option Api 选项式APIOption Api需要在特定的区域datamethodswatchcomputed…编写负责相同功能的代码。如果我们为相同的逻辑关注点标上一种颜色那将会是这样 Option Api的缺陷 你可以看到处理相同逻辑关注点的代码被强制拆分在了不同的选项中位于文件的不同部分。在一个几百行的大组件中要读懂代码中的一个逻辑关注点需要在文件中反复上下滚动这并不理想。 另外如果我们想要将一个逻辑关注点抽取重构到一个可复用的工具函数中需要从文件的多个不同部分找到所需的正确片段。 Composition Api 使用传统的option选项写组件的时候问题随着业务复杂度越来越高代码量会不断的加大由于相关业务的代码需要遵循option的配置写到特定的区域导致后续维护非常的复杂同时代码可复用性不高。 这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外在处理单个业务逻辑时我们必须不断地“跳转”相关代码的选项块。 如果能够将同一个业务逻辑相关代码收集在一起会更好。而这正是组合式 API 使我们能够做到的。而组合式API就是为了解决这个问题而生的。 Composition API字面意思是组合式API它是为了实现基于函数的逻辑复用机制而产生的。主要思想是我们将它们定义为从新的setup函数返回的JavaScript变量而不是将组件的功能(例如method、computed等)定义为对象属性。 使用组合式API我们可以更加优雅的组织我们的代码函数让相关功能的代码更加有序的组织在一起。如果用组合式 APIComposition Api 重构这个组件将会变成下面右边这样 现在与同一个逻辑关注点相关的代码被归为了一组我们无需再为了一个逻辑关注点在不同的选项块间来回滚动切换。此外我们现在可以很轻松地将这一组代码移动到一个外部文件中不再需要为了抽象而重新组织代码大大降低了重构成本这在长期维护的大型项目中非常关键。 更好的类型推导 近几年来越来越多的开发者开始使用 TypeScript 书写更健壮可靠的代码TypeScript 还提供了非常好的 IDE 开发支持。然而选项式 API 是在 2013 年被设计出来的那时并没有把类型推导考虑进去因此我们不得不做了一些复杂到夸张的类型体操才实现了对选项式 API 的类型推导。但尽管做了这么多的努力选项式 API 的类型推导在处理 mixins 和依赖注入类型时依然不甚理想。 因此很多想要搭配 TS 使用 Vue 的开发者采用了由 vue-class-component 提供的 Class API。然而基于 Class 的 API 非常依赖 ES 装饰器在 2019 年我们开始开发 Vue 3 时它仍是一个仅处于 stage 2 的语言功能。我们认为基于一个不稳定的语言提案去设计框架的核心 API 风险实在太大了因此没有继续向 Class API 的方向发展。在那之后装饰器提案果然又发生了很大的变动在 2022 年才终于到达 stage 3。另一个问题是基于 Class 的 API 和选项式 API 在逻辑复用和代码组织方面存在相同的限制。 相比之下组合式 API 主要利用基本的变量和函数它们本身就是类型友好的。用组合式 API 重写的代码可以享受到完整的类型推导不需要书写太多类型标注。大多数时候用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多这也让许多纯 JavaScript 用户也能从 IDE 中享受到部分类型推导功能。 更小的生产包体积 搭配 script setup 使用组合式 API 比等价情况下的选项式 API 更高效对代码压缩也更友好。这是由于 script setup 形式书写的组件模板被编译为了一个内联函数和 script setup 中的代码位于同一作用域。不像选项式 API 需要依赖 this 上下文对象访问属性被编译的模板可以直接访问 script setup 中定义的变量无需一个代码实例从中代理。这对代码压缩更友好因为本地变量的名字可以被压缩但对象的属性名则不能。 与选项式 API 的关系 取舍 一些从选项式 API 迁移来的用户发现他们的组合式 API 代码缺乏组织性并得出了组合式 API 在代码组织方面“更糟糕”的结论。我们建议持有这类观点的用户换个角度思考这个问题。 组合式 API 不像选项式 API 那样会手把手教你该把代码放在哪里。但反过来它却让你可以像编写普通的 JavaScript 那样来编写组件代码。这意味着你能够并且应该在写组合式 API 的代码时也运用上所有普通 JavaScript 代码组织的最佳实践。如果你可以编写组织良好的 JavaScript你也应该有能力编写组织良好的组合式 API 代码。 选项式 API 确实允许你在编写组件代码时“少思考”这是许多用户喜欢它的原因。然而在减少费神思考的同时它也将你锁定在规定的代码组织模式中没有摆脱的余地这会导致在更大规模的项目中难以进行重构或提高代码质量。在这方面组合式 API 提供了更好的长期可维护性。 组合式 API 是否覆盖了所有场景 组合式 API 能够覆盖所有状态逻辑方面的需求。除此之外只需要用到一小部分选项propsemitsname 和 inheritAttrs。如果使用 script setup那么 inheritAttrs 应该是唯一一个需要用额外的 script 块书写的选项了。 如果你在代码中只使用了组合式 API (以及上述必需的选项)那么你可以通过配置编译时标记来去掉 Vue 运行时中针对选项式 API 支持的代码从而减小生产包大概几 kb 左右的体积。注意这个配置也会影响你依赖中的 Vue 组件。 可以同时使用两种 API 吗 可以。你可以在一个选项式 API 的组件中通过 setup() 选项来使用组合式 API。 然而我们只推荐你在一个已经基于选项式 API 开发了很久、但又需要和基于组合式 API 的新代码或是第三方库整合的项目中这样做。 选项式 API 会被废弃吗 不会我们没有任何计划这样做。选项式 API 也是 Vue 不可分割的一部分也有很多开发者喜欢它。我们也意识到组合式 API 更适用于大型的项目而对于中小型项目来说选项式 API 仍然是一个不错的选择。 与 Class API 的关系 我们不再推荐在 Vue 3 中使用 Class API因为组合式 API 提供了很好的 TypeScript 集成并具有额外的逻辑重用和代码组织优势。 和 React Hooks 的对比 组合式 API 提供了和 React Hooks 相同级别的逻辑组织能力但它们之间有着一些重要的区别。 React Hooks 在组件每次更新时都会重新调用。这就产生了一些即使是经验丰富的 React 开发者也会感到困惑的问题。这也带来了一些性能问题并且相当影响开发体验。例如 Hooks 有严格的调用顺序并不可以写在条件分支中。React 组件中定义的变量会被一个钩子函数闭包捕获若开发者传递了错误的依赖数组它会变得“过期”。这导致了 React 开发者非常依赖 ESLint 规则以确保传递了正确的依赖然而这些规则往往不够智能保持正确的代价过高在一些边缘情况时会遇到令人头疼的、不必要的报错信息。昂贵的计算需要使用 useMemo这也需要传入正确的依赖数组。在默认情况下传递给子组件的事件处理函数会导致子组件进行不必要的更新。子组件默认更新并需要显式的调用 useCallback 作优化。这个优化同样需要正确的依赖数组并且几乎在任何时候都需要。忽视这一点会导致默认情况下对应用进行过度渲染并可能在不知不觉中导致性能问题。要解决变量闭包导致的问题再结合并发功能使得很难推理出一段钩子代码是什么时候运行的并且很不好处理需要在多次渲染间保持引用 (通过 useRef) 的可变状态。 相比起来Vue 的组合式 API 仅调用 setup() 或 script setup 的代码一次。这使得代码更符合日常 JavaScript 的直觉不需要担心闭包变量的问题。组合式 API 也并不限制调用顺序还可以有条件地进行调用。Vue 的响应性系统运行时会自动收集计算属性和侦听器的依赖因此无需手动声明依赖。无需手动缓存回调函数来避免不必要的组件更新。Vue 细粒度的响应性系统能够确保在绝大部分情况下组件仅执行必要的更新。对 Vue 开发者来说几乎不怎么需要对子组件更新进行手动优化。 我们承认 React Hooks 的创造性它是组合式 API 的一个主要灵感来源。然而它的设计也确实存在上面提到的问题而 Vue 的响应性模型恰好提供了一种解决这些问题的方法。 setup函数 setup() 钩子是在组件中使用组合式 API 的入口通常只在以下情况下使用 需要在非单文件组件中使用组合式 API 时。需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。 注意 对于结合单文件组件使用的组合式 API推荐通过 script setup 以获得更加简洁及符合人体工程学的语法。 基本使用 组合式API是从 setup() 函数开始的在组件创建之前会自动执行 setup() 函数是组合式API的入口。 直接在 setup() 中声明组件的状态、函数、计算属性等之前写在选项式API中的data、methos、computed等等现在全部写在 setup() 中 需要在在 setup() 的最后位置中返回一个对象该对象应该包含在 setup() 中声明组件的状态、函数、计算属性等该对象会暴露给组件模板和组件实例在组件模板和组件实例上可以直接访问该对象的属性 其它的选项也可以通过组件实例来获取 setup() 暴露的属性 div idapppnum {{num}}/pbutton clickfn绑定fn函数/button /divscriptlet { createApp, onMounted } Vue;const app createApp({// 在选项式API中通过this是可以访问setup函数return出来的属性data() {return {num: 1,};},created() {console.log(created num, this.num);this.fn();},methods: {fn() {console.log(选项式中的 fn函数);},},setup (props) {// 声明组件的状态非响应式let num 2;// 声明组件的函数let fn () {num;console.log(组合式中的 fn函数 num, num);}// 生命周期函数onMounted((){console.log(组合式中的 onMounted , num);});// setup函数返回一个对象该对象的属性可以直接在模板中使用 {{num}}return {num, // 暴露属性fn, // 暴露函数}}});app.mount(#app); /script注意使用了组合式API就不要再使用选项式API 在选项式API配置选项data、methos、computed…中通过this可以访问组合式API setup() 函数中的属性和方法但是不推荐这样做在组合式API setup() 函数中不能使用this访问选项式API配置选项data、methos、computed…中的数据如果在选项式API中添加了属性和方法在组合式API中也添加了同名属性和方法组合式中的数据覆盖了选项式的数据setup() 应该同步地返回一个对象。唯一可以使用 async setup() 的情况是该组件是 Suspense 组件的后裔。 在 setup() 函数中并不含对组件实例的访问权即在 setup() 中访问 this 会是 undefined。如果想要访问组件实例可以使用 getCurrentInstance但是按照组合式API设计的思想我们是不需要访问组件实例的 let { createApp, getCurrentInstance } Vue; const app createApp({setup (props) {let num 2;// 在setup函数或者组合式声明周期函数中使用 getCurrentInstance 获取组件实例然后通过 ctx 属性获取当前上下文// 但是不要为了使用this而是专门使用 getCurrentInstance因为在组合式API中不需要使用this了const instance getCurrentInstance();console.log(instance);console.log(instance.ctx.num);} });访问 Props setup 函数的第一个参数是组件的 props。和标准的组件一致一个 setup 函数的 props 是响应式的并且会在传入新的 props 时同步更新。 export default {props: {title: String},setup(props) {console.log(props.title)} }请注意如果你解构了 props 对象解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。 如果你确实需要解构 props 对象或者需要将某个 prop 传到一个外部函数中并保持响应性那么你可以使用 toRefs() 和 toRef() 这两个工具函数 import { toRefs, toRef } from vueexport default {setup(props) {// 将 props 转为一个其中全是 ref 的对象然后解构const { title } toRefs(props)// title 是一个追踪着 props.title 的 refconsole.log(title.value)// 或者将 props 的单个属性转为一个 refconst title toRef(props, title)} }Setup 上下文 传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值 export default {setup(props, context) {// 透传 Attributes非响应式的对象等价于 $attrsconsole.log(context.attrs)// 插槽非响应式的对象等价于 $slotsconsole.log(context.slots)// 触发事件函数等价于 $emitconsole.log(context.emit)// 暴露公共属性函数console.log(context.expose)} }该上下文对象是非响应式的可以安全地解构 export default {setup(props, { attrs, slots, emit, expose }) {...} }attrs 和 slots 都是有状态的对象它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们并始终通过 attrs.x 或 slots.x 的形式使用其中的属性。此外还需注意和 props 不同attrs 和 slots 的属性都不是响应式的。如果你想要基于 attrs 或 slots 的改变来执行副作用那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。 暴露公共属性 expose 函数用于显式地限制该组件暴露出的属性当父组件通过模板引用访问该组件的实例时将仅能访问 expose 函数暴露出的内容 export default {setup(props, { expose }) {// 让组件实例处于 “关闭状态”// 即不向父组件暴露任何东西expose()const publicCount ref(0)const privateCount ref(0)// 有选择地暴露局部状态expose({ count: publicCount })} }与渲染函数一起使用 setup 也可以返回一个渲染函数此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态 import { h, ref } from vueexport default {setup() {const count ref(0)return () h(div, count.value)} }返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说这样没有问题但如果我们想通过模板引用将这个组件的方法暴露给父组件那就有问题了。 我们可以通过调用 expose() 解决这个问题 import { h, ref } from vueexport default {setup(props, { expose }) {const count ref(0)const increment () count.valueexpose({increment})return () h(div, count.value)} }此时父组件可以通过模板引用来访问这个 increment 方法。 响应式核心reactive和ref 用reactive声明响应式状态 reactive概念 reactive返回一个对象的响应式代理。 响应式转换是“深层”的它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性同时保持响应性。 值得注意的是当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时不会执行 ref 的解包。 若要避免深层响应式转换只想保留对这个对象顶层次访问的响应性请使用 shallowReactive() 作替代。 返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹因此不等于源对象建议只使用响应式代理避免使用原始对象。 reactive的使用 我们可以使用 reactive() 函数创建一个响应式对象或数组 script import { reactive } from vue;export default {setup() {// 组件状态const zhangsan reactive({ name: 张三, age: 12 });const nums reactive([60, 70, 80]);// 组件函数function increment() {zhangsan.age;nums.push(nums[nums.length - 1] 10);}return {zhangsan,nums,increment,};}, }; /scripttemplatebutton clickincrement按钮/buttonp zhangsan.age{{ zhangsan.age }}/ppnums{{ nums }}/p /templatesetup() 函数的简化用法详细内容见后面setup语法糖 在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是我们可以通过使用构建工具来简化该操作。当使用单文件组件SFC时我们可以使用 script setup 来大幅度地简化代码。 script setup import { reactive } from vueconst zhangsan reactive({ name: 张三, age: 12 });function increment() {zhangsan.age } /scripttemplate button clickincrement{{ zhangsan.age }}/button /templatescript setup 中的顶层的导入和变量声明可在同一组件的模板中直接使用。你可以理解为模板中的表达式和 script setup 中的代码处在同一个作用域中。 reactive的深层响应性 在 Vue 中状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组你的改动也能被检测到。 script setupimport { reactive } from vueconst obj reactive({nested: { count: 0 },arr: [foo, bar]})function mutateDeeply() {// 以下更改都会响应式的更新模板内容obj.nested.countobj.arr.push(baz)} /scripttemplatebutton clickmutateDeeply{{ obj }}/button /templatereactive 的局限性 reactive() API 有两条限制 仅对对象类型有效对象、数组和 Map、Set 这样的集合类型而对 string、number 和 boolean 这样的 原始类型 无效。 因为 Vue 的响应式系统是通过属性访问进行追踪的因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象因为这将导致对初始引用的响应性连接丢失 let state reactive({ count: 0 })// 给state重新赋值值一个响应式对象上面的引用 ({ count: 0 }) 将不再被追踪响应性连接已丢失 state reactive({ count: 1 })reactive 响应式丢失的情况 当我们将响应式对象的属性赋值或解构至本地变量时或是将该属性传入一个函数时我们会失去响应性 将响应式对象的属性赋值至本地变量失去响应性 const state reactive({ count: 0 })// n 是一个局部变量n 和 state.count失去响应性连接 let n state.count // 不影响原始的 state改变的仅仅是局部变量n的值将无法跟踪 state.count 的变化 n; console.log(n: , n); // 1 console.log(state.count: , state.count); // 0将响应式对象的属性解构至本地变量失去响应性 const state reactive({ count: 0 }) // count 是一个局部变量count 也和 state.count 失去了响应性连接 let { count } state // 不会影响原始的 state改变的仅仅是局部变量count的值将无法跟踪 state.count 的变化 count console.log(count: , count); // 1 console.log(state.count: , state.count); // 0将响应式对象的属性传入一个函数失去响应式 const state reactive({ count: 6 });function doubleTen(data) {data * 10;return data * 2; }// doubleTen 函数接收一个普通数字并且将无法跟踪 state.count 的变化 console.log(doubleTen(state.count)); // 120 console.log(state.count); // 6响应式代理 vs. 原始对象 响应式对象其实是 JavaScript Proxy其行为表现与一般对象相似。不同之处在于 Vue 能够跟踪对响应式对象属性的访问与更改操作。所以reactive() 返回的是一个原始对象的 Proxy它和原始对象是不相等的 const obj {} const proxyObj reactive(obj)// 代理对象和原始对象不是全等的 console.log(proxyObj obj) // false只有代理对象是响应式的更改原始对象不会触发更新。因此使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本。 为保证访问代理的一致性对同一个原始对象调用 reactive() 会总是返回同样的代理对象而对一个已存在的代理对象调用 reactive() 会返回其本身 // 在同一个对象上调用 reactive() 会返回相同的代理 console.log(reactive(obj) proxyObj) // true// 在一个代理上调用 reactive() 会返回它自己 console.log(reactive(proxyObj) proxyObj) // true这个规则对嵌套对象也适用。依靠深层响应性响应式对象内的嵌套对象依然是代理 const proxyObj reactive({})const obj {} proxyObj.nested objconsole.log(proxyObj.nested obj) // false用ref定义响应式变量 ref的概念 reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref。 ref 接受一个任意类型的内部值返回一个响应式的、可更改的 ref 对象此对象只有一个指向其内部值的属性 .value。 ref 对象是可更改的可以为 .value 赋予新的值。ref 的 .value 属性也是响应式的即所有对 .value 的操作都将被追踪并且写操作会触发与之相关的副作用。 如果将一个对象赋值给 ref那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref它们将被深层地解包。 若要避免这种深层次的转换请使用 shallowRef() 来替代。 值类型的ref 当值为值类型时会用Object.defineProperty() 添加 get() 和 set() 实现响应式 script setupimport { ref } from vueconst count ref(0)console.log(count.value) // 0function increment() {count.valueconsole.log(count.value) // 1 2 3 ...} /scripttemplatebutton clickincrement{{ count }}/button /template对象类型的ref 当值为对象类型时会用 reactive() 自动转换它的 .value实现响应式 script setup import { ref } from vue;// 数组 const nums ref([1, 2, 3]); console.log(nums.value); // Proxy {0: 1, 1: 2, 2: 3}// 对象 const objRef1 ref({ count: 1 }); const objRef2 ref({ count: 2 }); console.log(objRef1.value.count); // 1 console.log(objRef2.value.count); // 2function increment() {nums.value.push(nums.value[nums.value.length - 1] 1);objRef1.value.count;// 一个包含对象类型值的 ref 可以响应式地替换整个对象objRef2.value { count: nums.value.length }; } /scripttemplatebutton clickincrement按钮/buttonpnums: {{ nums }}/ppobjRef1.count: {{ objRef1.count }}/ppobjRef2.count: {{ objRef2.count }}/p /templateref 被传递给函数不会丢失响应性 const count ref(1);// 该函数接收一个 ref需要通过 .value 取值但它会保持响应性 function doubleTen(data) {data.value * 10;return data.value * 2; }console.log(doubleTen(count)); // 20 console.log(count.value); // 10ref 从一般对象上被解构时不会丢失响应性 const obj {foo: ref(1),bar: ref(2), };// 使用解构仍然是响应式的 const { foo, bar } obj// 该函数接收一个 ref需要通过 .value 取值但它会保持响应性 function doubleTen(data) {data.value * 10;return data.value * 2; } console.log(doubleTen(foo)); // 20 console.log(foo.value); // 10简言之ref() 让我们能创造一种对任意值的 “引用”并能够在不丢失响应性的前提下传递这些引用。这个功能很重要因为它经常用于将逻辑提取到 组合函数 中。 ref的解包 ref 在模板中的解包 当 ref 在模板中作为顶层属性被访问时它们会被自动“解包”所以不需要使用 .value script setup import { ref } from vueconst count ref(0)function increment() {count.value } /scripttemplatebutton clickincrement{{ count }} !-- 无需 .value --/button /template请注意仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。 上面的 count 是顶层属性但下面例子的 obj.foo 不是顶层属性 const obj { foo: ref(1) }console.log(obj.foo); // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 1, _value: 1}// 渲染的结果会是一个 [object Object]1因为 obj.foo 是一个 ref 对象 console.log(obj.foo 1); // [object Object]1// 正确用法应该使用 obj.foo.value console.log(obj.foo.value 1); // 2// 或者我们可以通过将 foo 改成顶层属性来解决这个问题 const { foo } obj; // 现在渲染结果将是 2 console.log(foo 1); // 2需要注意的是如果一个 ref 是文本插值即一个 {{ }} 符号计算的最终值它也将被解包。因此在模板中可以单独使用 obj.foo 下面的渲染结果将为 1 {{ obj.foo }}这只是文本插值的一个方便功能相当于 {{ obj.foo.value }}。 ref 在响应式对象中的解包 当一个 ref 被嵌套在一个响应式对象中作为属性被访问或更改时它会自动解包因此会表现得和一般的属性一样 const count ref(0) const state reactive({foo: count, })console.log(state.foo) // 0state.foo 1 console.log(count.value) // 1如果将一个新的 ref 赋值给一个关联了已有 ref 的属性那么它会替换掉旧的 ref const count ref(0) const state reactive({foo: count, })console.log(state.foo) // 0const otherCount ref(2) state.foo otherCount console.log(state.foo) // 2// 原始 count 现在已经和 state.foo 失去联系 console.log(count.value) // 1只有当嵌套在一个深层响应式对象内时才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。 数组和集合类型的 ref 解包 跟响应式对象不同当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时不会进行解包。 const books reactive([ref(Vue 3 Guide)]) // 这里需要 .value console.log(books[0].value)const map reactive(new Map([[count, ref(0)]])) // 这里需要 .value console.log(map.get(count).value)数组声明与赋值的技巧 数组赋值存在的问题使用reactive声明数组给空数组赋值不能直接赋值因为arr [1, 2, 3]; 让arr失去了响应式。 let arr reactive([]); arr [1, 2, 3]; // arr会失去响应式解决方法如下 方法一使用ref声明数组在组件模板中直接使用arr在setup中需要使用arr.value const arr ref([]); arr.value [1, 2, 3];方法二使用reactive声明数组使用的push方法添加新的元素 const arr reactive([]); arr.push(...[1, 2, 3]);方法三创建一个响应式对象对象的属性是数组给该属性直接赋值 const obj reactive({arr: [], }) obj.arr [1, 2, 3];reactive和ref的对比 ref的研究 ref创建一个响应式数据一般来说用于创建简单类型的响应式对象比如String、Number、boolean类型 当我们给ref传递一个值之后如果使用的是基本类型响应式依赖 Object.defineProperty() 的 get() 和 set() 如果ref使用的是引用类型ref函数底层会自动将ref转换成reactive; ref(18) 等价于reactive({value:18}) 需要注意的是ref定义的值在组件模板中使用直接使用所定义的字段但是在setup函数中获取或者修改值需要通过value当然还有一些自动解包的场景 ref也可以创建引用类型对于复杂的对象值是一个被proxy拦截处理过的对象但是里面的属性不是RefImpl类型的对象proxy代理的对象同样被挂载到value上所以可以通过obj.value.key来读取属性这些属性同样也是响应式的更改时可以触发视图的更新 reactive研究 reactive里面参数定义必须是对象或者数组(json/arr)本质将传入的数据包装成proxy对象 基于Es6的Proxy实现通过Reflect反射代理操作源对象相比于reactive定义的浅层次响应式数据对象reactive定义的是更深层次的响应式数据对象 总结 一般来说ref被用来定义简单的字符串或者数值而reactive被用来定义对象数组等 实际上都能用而且ref也可以去定义简单的对象和数组也是具有响应式的不过官方文档中有提到如果将对象分配为ref值则可以通过reactive方法使该对象具有高度的响应式。 DOM 更新时机 当你更改响应式状态后DOM 会自动更新。然而你得注意 DOM 的更新并不是同步的。相反Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改每个组件都只更新一次。 若要等待一个状态改变后的 DOM 更新完成你可以使用 nextTick() 这个全局 API script setupimport { ref, nextTick } from vueconst count ref(0)function increment() {count.valuenextTick(() {// 访问更新后的 DOM})} /scripttemplatebutton clickincrement{{ count }}/button /template响应式工具函数 isRef() 检查某个值是否为 ref。 const count ref(10); const result isRef(count); console.log(result); // trueunref() 如果参数是 ref则返回内部值否则返回参数本身。这是 val isRef(val) ? val.value : val 计算的一个语法糖 const count ref(10); const result unref(count); console.log(result); // 10toRef()和toRefs() toRef() 基于响应式对象上的一个属性创建一个对应的 ref。这样创建的 ref 与其源属性保持同步改变源属性的值将更新 ref 的值反之亦然。 const state reactive({foo: 1,bar: 2 })const fooRef toRef(state, foo)// 更改该 ref 会更新源属性 fooRef.value console.log(state.foo) // 2// 更改源属性也会更新该 ref state.foo console.log(fooRef.value) // 3请注意这不同于 const fooRef ref(state.foo)上面这个 ref 不会和 state.foo 保持同步因为这个 ref() 接收到的是一个纯数值。 toRef() 这个函数在你想把一个 prop 的 ref 传递给一个组合式函数时会很有用 script setup import { toRef } from vueconst props defineProps([foo])// 将 props.foo 转换为 ref然后传入一个组合式函数 useSomeFeature(toRef(props, foo)) /script当 toRef 与组件 props 结合使用时关于禁止对 props 做出更改的限制依然有效。尝试将新的值传递给 ref 等效于尝试直接更改 props这是不允许的。在这种场景下你可能可以考虑使用带有 get 和 set 的 computed 替代。详情请见在组件上使用 v-model 指南。 即使源属性当前不存在toRef() 也会返回一个可用的 ref。这让它在处理可选 props 的时候格外实用相比之下 toRefs 就不会为可选 props 创建对应的 refs。 toRefs() 将一个响应式对象转换为一个普通对象这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。 const state reactive({foo: 1,bar: 2 })const stateAsRefs toRefs(state)// 这个 ref 和源属性已经“链接上了” state.foo console.log(stateAsRefs.foo.value) // 2stateAsRefs.foo.value console.log(state.foo) // 3当从组合式函数中返回响应式对象时toRefs 相当有用。使用它消费者组件可以解构/展开返回的对象而不会失去响应性 function useFeatureX() {const state reactive({foo: 1,bar: 2})// ...基于状态的操作逻辑// 在返回时都转为 refreturn toRefs(state)// 等价于下面的写法return {...toRefs(state)} }// 可以解构而不会失去响应性 const { foo, bar } useFeatureX()toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref请改用 toRef。 toRef()和toRefs()的对比 toRef得到的结果是reactive对象中的某个属性转换为ref变量并与原属性保持同步。如果该属性在原对象上不存在会创建出一个新的ref变量toRefs得到的结果是reactive对象中的所有属性转换为ref变量并与原属性保持同步。只会创建出原对象上存在的属性对应的ref变量不会创建新的ref变量 有以下数据 const zhangsan reactive({name: 张三,age: 18, });使用toRef()的结果 使用toRef()根据zhagnsan对象的属性创建ref变量和原属性保持同步 // 语义上等价于 const ageRef ref(zhangsan.age); 把 zhangsan.age 转换为一个ref变量 // 但是直接使用 ref 创建 ageRefageRef 与 zhangsan.age 的之间的关联将会丢失 const ageRef toRef(zhangsan, age); console.log(ageRef.value: , ageRef.value);// ageRef.value: 18zhangsan.age; console.log(zhangsan.age: , zhangsan.age); // zhangsan.age: 19 console.log(ageRef.value: , ageRef.value); // ageRef.value: 19ageRef.value; console.log(zhangsan.age: , zhangsan.age); // zhangsan.age: 20 console.log(ageRef.value: , ageRef.value); // ageRef.value: 20使用ref根据zhagnsan对象的属性创建ref变量不会和原属性保持同步相当于创建了一个新的属性 const ageRef ref(zhangsan.age); console.log(ageRef.value: , ageRef.value); // ageRef.value: 18zhangsan.age; console.log(zhangsan.age: , zhangsan.age); // zhangsan.age: 19 console.log(ageRef.value: , ageRef.value); // ageRef.value: 18ageRef.value; console.log(zhangsan.age: , zhangsan.age); // zhangsan.age: 19 console.log(ageRef.value: , ageRef.value); // ageRef.value: 19不存在的属性得到新的ref变量 const weightRef toRef(zhangsan, weight); console.log(weightRef.value: , weightRef.value);// weightRef.value: undefined使用toRefs()的结果 使用toRefs根据zhagnsan对象的属性age和name得到ref属性zhangsanRef.name 和 zhangsanRef.age const zhangsanRef toRefs(zhangsan); console.log(zhangsanRef: , zhangsanRef); // {name: ObjectRefImpl, age: ObjectRefImpl} console.log(zhangsanRef.name.value: , zhangsanRef.name.value); // zhangsanRef.name.value: 张三 console.log(zhangsanRef.age.value: , zhangsanRef.age.value); // zhangsanRef.name.value: 18zhangsan.age; console.log(zhangsan.age: , zhangsan.age); // zhangsan.age: 19 console.log(zhangsanRef.age.value: , zhangsanRef.age.value); // zhangsanRef.name.value: 19zhangsanRef.age.value; console.log(zhangsan.age: , zhangsan.age); // zhangsan.age: 20 console.log(zhangsanRef.age.value: , zhangsanRef.age.value); // zhangsanRef.name.value: 20torefs的妙用 在setup函数中的使用 templatepzhangsan.age: {{ zhangsan.age }}/ppage: {{ age }}/p /templatescript import { reactive, toRefs } from vueexport default {setup() {const zhangsan reactive({ name: 张三, age: 12 });return {// 1、直接放回 zhangsan 在模板中可以使用 {{ zhangsan.age }}zhangsan, // 2、通过扩展运算符配合toRefs展开zhangsan对象// 可以在组件的模板中直接使用zhangsan对象的name和age属性 {{ age }}并保证了name和age的响应式...toRefs(zhangsan),// 注意不能直接使用扩展运算符展开zhangsan虽然这样也能在模板中使用name和age属性但是name和age不是响应式的...zhangsan,}} } /script在setup语法糖中的使用 templatepname: {{ name }}/ppage: {{ age }}/p /templatescript setup import { reactive } from vueconst zhangsan reactive({ name: 张三, age: 12 });// 可以在组件的模板中直接使用zhangsan对象的name和age属性 {{ age }}并保证了name和age的响应式 const { name, age} ...toRefs(zhangsan);/scriptisProxy() 检查一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理。 const zhangsan reactive({name: 张三 }); const result isProxy(zhangsan); console.log(result); // trueisReactive() 检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。 const zhangsan reactive({name: 张三 }); const result isReactive(zhangsan); console.log(result); // trueisReadonly() isReadonly() 检查传入的值是否为只读对象。只读对象的属性可以更改但他们不能通过传入的对象直接赋值。 readonly() 接受一个对象 (不论是响应式还是普通的) 或是一个 ref返回一个原值的只读代理。 只读代理是深层的对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同但解包得到的值是只读的。要避免深层级的转换行为请使用 shallowReadonly() 作替代。 通过 readonly() 和 shallowReadonly() 创建的代理都是只读的因为他们是没有 set 函数的 computed() ref。 const result1 isReadonly(zhangsan); console.log(result1); // falseconst zhangsanReadonly readonly(zhangsan); const result2 isReadonly(zhangsanReadonly); console.log(result2); // true计算属性 只读计算属性 计算属性使用 computed() 实现我们在这里定义了一个计算属性 total 和 result script setupimport { ref, computed } from vueconst chinese ref(0);const math ref(0);const english ref(0);// 只读的计算属性const total computed(() chinese.value math.value english.value);const result computed(() {if (total.value 240) {return 优秀;} else if (total.value 180) {return 良好;} else {return 不及格;}}); /scripttemplate语文: input typenumber v-model.numberchinesebr数学: input typenumber v-model.numbermathbr英语: input typenumber v-model.numberenglishbrp总分 {{chinese math english}}/pptotal {{total}}/ppresult {{result}}/p /templatecomputed() 方法期望接收一个 getter 函数返回值为一个计算属性 ref。和其他一般的 ref 类似你可以通过 total.value 访问计算结果。计算属性 ref 也会在模板中自动解包因此在模板表达式中引用时无需添加 .value。 Vue 的计算属性会自动追踪响应式依赖。它会检测到 total 依赖于 chinese、math、english三个属性所以当 chinese、math、english 任何一个属性改变时任何依赖于 total 的绑定都会同时更新。 计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 chinese、math、english三个属性不改变无论多少次访问 total 都会立即返回先前的计算结果而不用重复执行 getter 函数。 可写计算属性 计算属性默认是只读的。当你尝试修改一个计算属性时你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性你可以通过同时提供 getter 和 setter 来创建 script setupimport { ref, computed } from vueconst count ref(1);const bigCount computed({// 获取计算属性的值比如 console.log(bigCount.value) 会执行get函数get () {return count.value * 2;},// 设置计算属性的值比如 bigCount.value 10; 会执行set函数set (value) {count.value value / 2;}}); /scripttemplateinput typenumber v-model.numberbigCount bigCount {{bigCount}} br /template侦听器 计算属性允许我们声明性地计算衍生值。然而在有些情况下我们需要在状态变化时执行一些“副作用”例如更改 DOM或是根据异步操作的结果去修改另一处的状态。 在组合式 API 中我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数。 watch 概念 watch()侦听一个或多个响应式数据源并在数据源变化时调用所给的回调函数。 语法watch(source, callback, options?) 第一个参数是侦听器的源。这个来源可以是以下几种 一个函数返回一个值 一个 ref 一个响应式对象 reactive …或是由以上类型的值组成的数组 第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数 新值 旧值 以及一个用于注册副作用清理的回调函数。 该回调函数会在副作用下一次重新执行前调用可以用来清除无效的副作用例如等待中的异步请求。当侦听多个来源时回调函数接受两个数组分别对应来源数组中的新值和旧值。 第三个可选的参数是一个对象支持以下这些选项 immediate在侦听器创建时立即触发回调。第一次调用时旧值是 undefined。 deep如果源是对象强制深度遍历以便在深层级变更时触发回调。 flush调整回调函数的刷新时机。 onTrack / onTrigger调试侦听器的依赖。 基本使用 script setupimport { ref, watch } from vueconst count ref(1);const zhangsan reactive({name: 张三,age: 18,}); watch(count, (newValue, oldValue) {console.log(count: , newValue, oldValue);});/scripttemplatebutton clickcountcount {{count}}/buttonbutton clickzhangsan.agezhangsan.age {{zhangsan.age}}/button /template监听一个 ref // 当count.value的值发生变化时触发回调函数 watch(count, (newValue, oldValue) {console.log(count: , newValue, oldValue); });count.value;监听一个响应式对象 reactive当直接侦听一个响应式对象时侦听器会自动启用深层模式 // zhangsan对象的任何一个属性发生变化时都会触发回调函数 watch(zhangsan, (newValue, oldValue) {console.log(zhangsan: , newValue, oldValue); });zhangsan.age;监听一个 getter 函数当使用 getter 函数作为源时回调只在此函数的返回值变化时才会触发。 // 当count.value的值发生变化时触发回调函数 watch(() count.value, (newValue, oldValue) {console.log(count.value: , newValue, oldValue); }); count.value;// 当zhangsan.age的值发生变化时触发回调函数 watch(() zhangsan.age, (newValue, oldValue) {console.log(zhangsan.age: , newValue, oldValue); }); zhangsan.age;或是由以上类型的值组成的数组 // 注意多个同步更改只会触发一次侦听器。 watch([count, () zhangsan.age], ([newCount, newAge], [oldCount, oldAge]) {console.log(newCount ,newCount,newZhangsanAge , newAge, oldCount, oldCount, oldZhangsanAge , oldAge); });立即监听 watch() 默认是懒侦听的即仅在侦听源发生变化时才执行回调函数。最初绑定的时候是不会执行的要等到所监听的值发生改变时才执行监听计算。 添加 { immediate: true } 属性实现立即监听。 watch(() count.value,(newVal, oldVal) {// 第一次调用时旧值是 undefinedconsole.log(count.value: , newValue, oldValue);},{ immediate: true }// 立即执行 )深层侦听器 监听响应式对象 直接给 watch() 传入一个响应式对象会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发 监听reactive对象对象中任何一个值发生变化都会触发监听函数注意此处无法正确的获取oldValue因为newValue和oldValue是同一个对象注意强制开启了深度监听deep: false配置无效 const zhangsan reactive({name: 张三,age: 18, }); watch(zhangsan, (newValue, oldValue) {console.log(newValue oldValue); // true},{ deep: false }, // 设置deep无效 );zhangsan.age;监听getter函数 当使用 getter 函数作为源时回调只在此函数的返回值变化时才会触发一个返回响应式对象的 getter 函数 只有在返回不同的对象时才会触发回调如果只是改变响应式对象中的某个属性的值不会触发回调 const zhangsan reactive({name: 张三,age: 18,friend: {age: 20,} }); watch(() zhangsan.friend,() {// 仅当 zhangsan.friend 被替换时触发console.log(zhangsan.friend: , newValue, oldValue);} );zhangsan.friend { age: 21 }; // 会触发watch监听newValue 此处和 oldValue 是不相等的 zhangsan.friend.age; // 不会触发watch监听如果你想让回调在深层级变更时也能触发你需要使用 { deep: true } 强制侦听器进入深层级模式。 监听深度嵌套对象或数组中的属性变化时需要 deep 选项设置为 true强制转成深层侦听器在深层级模式时如果回调函数由于深层级的变更而被触发那么新值和旧值将是同一个对象 给上面这个例子显式地加上 deep 选项 watch(() zhangsan.friend,(newValue, oldValue) {// 注意newValue 此处和 oldValue 是相等的除非 zhangsan.friend 被整个替换了console.log(zhangsan.friend: , newValue, oldValue);}, { deep: true }, // 此时设置deep有效 );// 会触发watch监听此时 newValue 此处和 oldValue 是相等的 zhangsan.friend.age; // 会触发watch监听此时 newValue 此处和 oldValue 是不相等的 zhangsan.friend { age: 21 }; 谨慎使用 深度侦听需要遍历被侦听对象中的所有嵌套的属性当用于大型数据结构时开销很大。因此请只在必要时才使用它并且要留意性能。 watchEffect 概念 watchEffect立即运行一个函数同时响应式地追踪其依赖并在依赖更改时重新执行。 第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数用来注册清理回调。清理回调会在该副作用下一次执行前被调用可以用来清理无效的副作用例如等待中的异步请求 。 第二个参数是一个可选的选项可以用来调整副作用的刷新时机或调试副作用的依赖。 默认情况下侦听器将在组件渲染之前执行。设置 flush: post 将会使侦听器延迟到组件渲染之后再执行。在某些特殊情况下 (例如要使缓存失效)可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: sync 来实现。然而该设置应谨慎使用因为如果有多个属性同时更新这将导致一些性能和数据一致性的问题。 返回值是一个用来停止该副作用的函数。 基本使用 watch() 是懒执行的仅当数据源变化时才会执行回调。但在某些场景中我们希望在创建侦听器时立即执行一遍回调。 举例来说我们想请求一些初始数据然后在相关状态更改时重新请求数据。我们可以这样写 const url ref(https://...); const data ref(null);async function getData() {const response await axios(url.value);data.value await response.data; }// 立即获取 getData() // ...再侦听 url 变化 watch(url, getData)我们可以用 watchEffect 函数 来简化上面的代码。watchEffect() 会立即执行一遍回调函数如果这时函数产生了副作用Vue 会自动追踪副作用的依赖关系自动分析出响应源。上面的例子可以重写为 const url ref(https://...); const data ref(null);watchEffect(async () {const response await axios(url.value);data.value await response.datal; })这个例子中回调会立即执行。在执行期间它会自动追踪 url.value 作为依赖和计算属性的行为类似。每当 url.value 变化时回调会再次执行。 TIP watchEffect 仅会在其同步执行期间才追踪依赖。在使用异步回调时只有在第一个 await 正常工作前访问到的属性才会被追踪。 watch与watchEffect对比 watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式 watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖因此我们能更加精确地控制回调函数的触发时机。watchEffect则会在副作用发生期间追踪依赖。它会在同步执行过程中自动追踪所有能访问到的响应式属性。这更方便而且代码往往更简洁但有时其响应性依赖关系会不那么明确。 与 watchEffect() 相比watch() 使我们可以 懒执行副作用更加明确是应该由哪个状态触发侦听器重新执行可以访问所侦听状态的前一个值和当前值。 const count ref(1); const zhangsan reactive({name: 张三,age: 18, }); // 懒执行副作用初始运行不会执行当count.value的值发生变到时候才会执行 // 响应性依赖关系十分明确更加明确是应该由状态count触发侦听器重新执行 // 可以访问所侦听状态的前一个值和当前值 newValue 和 oldValue watch(count, (newValue, oldValue) {console.log(count: , newValue, oldValue);// zhangsan.age 的值发生变化不会执行执行副作用let age zhangsan.age;console.log(watch, age); });// 立即执行一次副作用初始运行执行一次 // 响应性依赖关系不那么明确每当count.value 或者 zhangsan.age 的值发生变化时执行一次 watchEffect(() {let val count.value;let age zhangsan.age;console.log(watchEffect, val, age); });回调的触发时机 当你更改了响应式状态它可能会同时触发 Vue 组件更新和侦听器回调。 默认情况下用户创建的侦听器回调都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。 如果想在侦听器回调中能访问被 Vue 更新之后的 DOM你需要指明 flush: post 选项 watch(count, (newValue, oldValue) {// 可以访问更新之后的DOMconsole.log(count: , newValue, oldValue);}, {flush: post} );watchEffect(() {// 可以访问更新之后的DOMconsole.log(watchEffect, count.value);},{ flush: post } );后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect() import { watchPostEffect } from vue// watchPostEffect是 watchEffect 带有 flush: post 选项的别名。 watchPostEffect(() {/* 在 Vue 更新后执行 */ })停止侦听器 在 setup() 或 script setup 中用同步语句创建的侦听器会自动绑定到宿主组件实例上并且会在宿主组件卸载时自动停止。因此在大多数情况下你无需关心怎么停止一个侦听器。 一个关键点是侦听器必须用同步语句创建如果用异步回调创建一个侦听器那么它不会绑定到当前组件上你必须手动停止它以防内存泄漏。如下方这个例子 script setup import { watchEffect } from vue// 它会自动停止 watchEffect(() {})// ...这个则不会 setTimeout(() {watchEffect(() {}) }, 100) /script要手动停止一个侦听器请调用 watch 或 watchEffect 返回的函数 const unwatch watch(() {})// ...当该侦听器不再需要时 unwatch()const unwatch watchEffect(() {})// ...当该侦听器不再需要时 unwatch()注意需要异步创建侦听器的情况很少请尽可能选择同步创建。如果需要等待一些异步数据你可以使用条件式的侦听逻辑 // 需要异步请求得到的数据 const data ref(null)watchEffect(() {if (data.value) {// 数据加载后执行某些操作...} })监听数组的研究 方法一使用ref声明数组在组件模板中直接使用arr在setup中需要使用arr.value 使用push等改变数组的元素 arr.value.push(6); 监听 ref 数组watch(arr, callback)当使用 push、splice等方法改变数组元素内容需要加上 { deep: true } 选项才能触发watch但此时nowValue和oldVlue输出的值是一样的 使用getter函数监听数组 watch(() arr.value, callback)需要加上 { deep: true } 选项才能触发watch此时nowValue和oldVlue输出的值是一样的 使用getter函数配合扩展运算符监听一个新的数组 watch(() [...arr.value], callback)不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的 推荐 给数组赋值新的对象 arr.value [11, 22, 33]; 监听 ref 数组不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的使用getter函数监听数组 watch(() arr.value, callback)不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的使用getter函数配合扩展运算符监听一个新的数组 watch(() [...arr.value], callback)不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的 const arr ref([1, 2, 3, 4, 5]); nextTick(() {arr.value.push(6);arr.value [11, 22, 33]; });watch(arr,(n, o) {console.log(arr1: , n o);},{ deep: true } );watch(() arr.value,(n, o) {console.log(arr2: , n o); },{ deep: true } );watch(() [...arr.value],(n, o) {console.log(arr3: , n o); } );方法二使用reactive声明数组使用的push方法添加新的元素 使用push等改变数组的元素 arr.push(6); 监听 reactive 数组 watch(arr, callback)当使用 push、splice等方法改变数组元素内容不需要加上 { deep: true } 选项也能触发watch但此时nowValue和oldVlue输出的值是一样的 使用getter函数配合扩展运算符监听一个新的数组 watch(() [...arr], callback)不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的 推荐 const arr reactive([]); nextTick(() {arr.push(6); });watch(arr,(n, o) {console.log(arr1: , n, o, n o); // true} );watch(() [...arr],(n, o) {console.log(arr3: , n, o, n o); // false} );方法三创建一个响应式对象对象的属性是数组给该属性直接赋值 使用push等改变数组的元素 obj.arr.push(6); 直接监听 reactive对象 watch(obj, callback)当使用 push、splice等方法改变数组元素内容不需要加上 { deep: true } 选项也能触发watch但此时nowValue和oldVlue输出的值是一样的 推荐使用扩展运算符 [...obj.arr]使用getter函数监听一个新的数组 watch(() [...obj.arr], callback)不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的 推荐 给数组赋值新的对象 obj.arr [11, 22, 33]; 直接监听 reactive对象 watch(obj, callback)当使用 push、splice等方法改变数组元素内容不需要加上 { deep: true } 选项也能触发watch但此时nowValue和oldVlue输出的值是不一样的直接使用getter函数监听数组 watch(() obj.arr, callback)不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的使用getter函数配合扩展运算符监听一个新的数组 watch(() [...obj.arr], callback)不需要加上 { deep: true } 选项也能触发watch此时nowValue和oldVlue输出的值是不一样的 const obj reactive({arr: [1, 2, 3], });nextTick(() {// obj.arr.push(6);obj.arr [11, 22, 33]; });script setup import { reactive, ref, watch,nextTick } from vue; // 引入子组件const obj reactive({arr: [1, 2, 3], });watch(obj,(n, o) {console.log(arr1: , n, o, n o); // true}, );watch(() obj.arr,(n, o) {console.log(arr2: , n, o, n o); // true}, );watch(() [...obj.arr],(n, o) {console.log(arr3: , n, o, n o); // false}, );nextTick(() {// obj.arr.push(6);obj.arr [11, 22, 33]; });/script生命周期钩子 每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤比如设置好数据侦听编译模板挂载实例到 DOM以及在数据改变时更新 DOM。在此过程中它也会运行被称为生命周期钩子的函数让开发者有机会在特定阶段运行自己的代码。 注册周期钩子 组合式API通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。 举例来说onMounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码 script setup import { onMounted } from vueonMounted(() {console.log(the component is now mounted.) }) /script还有其他一些钩子会在实例生命周期的不同阶段被调用最常用的是 onMounted、onUpdated 和 onUnmounted。 当调用 onMounted 时Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如请不要这样做 setTimeout(() {onMounted(() {// 异步注册时当前组件实例已丢失// 这将不会正常工作}) }, 100)注意这并不意味着对 onMounted 的调用必须放在 setup() 或 script setup 内的词法上下文中。onMounted() 也可以在一个外部函数中调用只要调用栈是同步的且最终起源自 setup() 就可以。 生命周期图示 下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容但以后它将是一个有用的参考。 选项式和组合式对比 在组合式API中因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的所以不需要显式地定义它们。换句话说在beforeCreate 和 created钩子中编写的任何代码都应该直接在 setup 函数中编写。 Option APIComposition API setup中beforeCreate不需要created不需要beforeMountonBeforeMountmountedonMountedbeforeUpdateonBeforeUpdateupdatedonUpdatedbeforeUnmountonBeforeUnmountunmountedonUnmountederrorCapturedonErrorCapturedrenderTrackedonRenderTrackedrenderTriggeredonRenderTriggeredactivatedonActivateddeactivatedonDeactivated 钩子函数详细信息 onBeforeMount 注册一个钩子在组件被挂载之前被调用。 当这个钩子被调用时组件已经完成了其响应式状态的设置但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。 onMounted 注册一个回调函数在组件挂载完成后执行。 组件在以下情况下被视为已挂载 其所有同步子组件都已经被挂载 (不包含异步组件或 Suspense 树内的组件)。其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时才可以保证组件 DOM 树也在文档中。 这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用。 通过模板引用访问一个元素 script setup import { ref, onMounted } from vueconst el ref()onMounted(() {el.value // div }) /scripttemplatediv refel/div /templateonBeforeUpdate 注册一个钩子在组件即将因为响应式状态变更而更新其 DOM 树之前调用。 这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。 onUpdated 注册一个回调函数在组件因为响应式状态变更而更新其 DOM 树之后调用。 父组件的更新钩子将在其子组件的更新钩子之后调用。 这个钩子会在组件的任意 DOM 更新后被调用这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM请使用 nextTick() 作为替代。 WARNING 不要在 updated 钩子中更改组件的状态这可能会导致无限的更新循环 onBeforeUnmount 注册一个钩子在组件实例被卸载之前调用。 当这个钩子被调用时组件实例依然还保有全部的功能。 onUnmounted 注册一个回调函数在组件实例被卸载之后调用。 一个组件在以下情况下被视为已卸载 其所有子组件都已经被卸载。所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。 可以在这个钩子中手动清理一些副作用例如计时器、DOM 事件监听器或者与服务器的连接。 script setup import { onMounted, onUnmounted } from vuelet intervalId onMounted(() {intervalId setInterval(() {// ...}) })onUnmounted(() clearInterval(intervalId)) /scriptonErrorCaptured 注册一个钩子在捕获了后代组件传递的错误时调用。 错误可以从以下几个来源中捕获 组件渲染事件处理器生命周期钩子setup() 函数侦听器自定义指令钩子过渡钩子 这个钩子带有三个实参错误对象、触发该错误的组件实例以及一个说明错误来源类型的信息字符串。 你可以在 errorCaptured() 中更改组件状态来为用户显示一个错误状态。注意不要让错误状态再次渲染导致本次错误的内容否则组件会陷入无限循环。 这个钩子可以通过返回 false 来阻止错误继续向上传递。请看下方的传递细节介绍。 错误传递规则 默认情况下所有的错误都会被发送到应用级的 app.config.errorHandler (前提是这个函数已经定义)这样这些错误都能在一个统一的地方报告给分析服务。如果组件的继承链或组件链上存在多个 errorCaptured 钩子对于同一个错误这些钩子会被按从底至上的顺序一一调用。这个过程被称为“向上传递”类似于原生 DOM 事件的冒泡机制。如果 errorCaptured 钩子本身抛出了一个错误那么这个错误和原来捕获到的错误都将被发送到 app.config.errorHandler。errorCaptured 钩子可以通过返回 false 来阻止错误继续向上传递。即表示“这个错误已经被处理了应当被忽略”它将阻止其他的 errorCaptured 钩子或 app.config.errorHandler 因这个错误而被调用。 onRenderTracked 注册一个调试钩子当组件渲染过程中追踪到响应式依赖时调用。 这个钩子仅在开发模式下可用且在服务器端渲染期间不会被调用。 onRenderTriggered 注册一个调试钩子当响应式依赖的变更触发了组件渲染时调用。 这个钩子仅在开发模式下可用且在服务器端渲染期间不会被调用。 onActivated 注册一个回调函数若组件实例是 KeepAlive 缓存树的一部分当组件被插入到 DOM 中时调用。 onDeactivated 注册一个回调函数若组件实例是 KeepAlive 缓存树的一部分当组件从 DOM 中被移除时调用。 模板引用 虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作但在某些情况下我们仍然需要直接访问底层 DOM 元素。要实现这一点我们可以使用特殊的 ref attribute input refinputRefref 是一个特殊的 attribute和 v-for 中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后获得对它的直接引用。这可能很有用比如说在组件挂载时将焦点设置到一个 input 元素上或在一个元素上初始化一个第三方库。 ref 用于注册元素或子组件的引用。 使用选项式 API引用将被注册在组件的 this.$refs 对象里使用组合式 API引用将存储在与名字匹配的 ref 里如果用于普通 DOM 元素引用将是元素本身如果用于子组件引用将是子组件的实例。ref 可以接收一个函数值用于对存储引用位置的完全控制 关于 ref 注册时机的重要说明因为 ref 本身是作为渲染函数的结果来创建的必须等待组件挂载后才能对它进行访问。 this.$refs 也是非响应式的因此你不应该尝试在模板中使用它来进行数据绑定。 访问一个组件或者元素 为了通过组合式 API 获得该模板引用我们需要声明一个同名的 ref你只可以在组件挂载后才能访问模板引用在 onMounted 或者 nextTick 中都可以如果你想在模板中的表达式上访问 inputRef在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢 script setupimport { ref, onMounted } from vue// 声明一个 ref 来存放该元素的引用// 必须和模板里的 ref 同名const inputRef ref(null);const childRef ref(null);// 组件渲染后才能访问 inputRef.value否则 inputRef.value 还没有被赋值onMounted(() {inputRef.value.focus()});// 或者在 nextTick 中访问nextTick(() {console.log(inputRef.value);console.log(childRef.value);}); /scripttemplateinput refinputRef /child refchildRef/ /template如果不使用 script setup需确保从 setup() 返回 ref export default {setup() {const inputRef ref(null)// ...return {inputRef}} }访问多个组件或者元素 需要 v3.2.25 及以上版本 当在 v-for 中使用模板引用时对应的 ref 中包含的值是一个数组它将在元素被挂载后包含对应整个列表的所有元素 这种情况仅适用于 v-for 循环数是固定的情况因为如果 v-for 循环数 在初始化之后发生改变那么就会导致 childRefs 再一次重复添加childRefs 中会出现重复的子组件实例 script setup import { ref, onMounted } from vueconst list ref([1, 2, 3, 4, 5]);// 子组件或者元素实例数组 const itemRefs ref([]);onMounted(() console.log(itemRefs.value)); /scripttemplateulli v-foritem in list refitemRefs{{ item }}/li/ul /template应该注意的是ref 数组并不保证与源数组相同的顺序。 函数模板引用 除了使用字符串值作名字ref attribute 还可以绑定为一个函数会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数 input :ref(el) { /* 将 el 赋值给一个数据属性或 ref 变量 */ }注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时函数也会被调用一次此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。 script setup import { ref, onMounted } from vue;// 声明一个 ref 来存放该元素的引用 const input1 ref(null); const input2 ref(null);function setInput(el){input2.value el; }onMounted(() {input1.value.focus(); }); /scripttemplate!-- 1、使用内联函数 --input :refel (input1 el) /!-- 2、使用组件函数 --input :refsetInput / /template组件上的 ref 模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例 script setup import { ref, onMounted } from vue import Child from ./Child.vueconst child ref(null)onMounted(() {// child.value 是 Child / 组件的实例 }) /scripttemplateChild refchild / /template如果一个子组件使用的是选项式 API 或没有使用 script setup被引用的组件实例和该子组件的 this 完全一致这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易当然也因此应该只在绝对需要时才使用组件引用。大多数情况下你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。 有一个例外的情况使用了 script setup 的组件是默认私有的一个父组件无法访问到一个使用了 script setup 的子组件中的任何东西除非子组件在其中通过 defineExpose 宏显式暴露 script setup import { ref } from vueconst a 1 const b ref(2)defineExpose({a,b }) /script当父组件通过模板引用获取到了该组件的实例时得到的实例类型为 { a: number, b: number } (ref 都会自动解包和一般的实例一样)。 侦听模板引用 在声明周期钩子 onMounted 中可以访问模板引用如果需要侦听一个模板引用 ref 的变化确保考虑到其值为 null 的情况 watchEffect(() {if (inputRef.value) {inputRef.value.focus()} else {// 此时还未挂载或此元素已经被卸载例如通过 v-if 控制} })watchEffect() 在 DOM 挂载或更新之前运行副作用所以当侦听器运行时模板引用还未被更新。因此使用模板引用的侦听器应该用 { flush: post } 选项来定义这将在 DOM 更新后运行副作用确保模板引用与 DOM 保持同步并引用正确的元素。 watchEffect(() {inputRef.value.focus();console.log(inputRef.value: , inputRef.value);},{ flush: post } );或者直接使用watchEffect((){ }, { flush: post }) 的别名函数 watchPostEffect(): watchPostEffect(() {inputRef.value.focus();console.log(inputRef.value: , inputRef.value); });依赖注入 Prop 逐级透传问题 通常情况下当我们需要从父组件向子组件传递数据时会使用 props。想象一下这样的结构有一些多层级嵌套的组件形成了一颗巨大的组件树而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下如果仅使用 props 则必须将其沿着组件链逐级传递下去这会非常麻烦 注意虽然这里的 Footer 组件可能根本不关心这些 props但为了使 DeepChild 能访问到它们仍然需要定义并向下传递。如果组件链路非常长可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”显然是我们希望尽量避免的情况。 provide 和 inject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件会作为依赖提供者。任何后代的组件树无论层级有多深都可以注入由父组件提供给整条链路的依赖。 Provide (提供) 概念 provide() 函数提供一个值可以被后代组件注入。 provide(注入名, 值)接收两个参数 第一个参数是要注入的 key被称为注入名 注入名可以是一个字符串或是一个 Symbol 后代组件会用注入名来查找期望注入的值inject(注入名) 一个组件可以多次调用 provide()使用不同的注入名注入不同的依赖值。 第二个参数是提供的值 提供的值可以是任意类型包括普通变量响应式的状态ref、reactive函数等提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。 与注册生命周期钩子的 API 类似provide() 必须在组件的 setup() 阶段同步调用。 在组件中使用 Provide 在script setup中使用给子孙组件注入数据 script setup import { provide } from vue// 1、非响应式的 provide let num 6; provide(num, num); provide(message, hello!);// 2、响应式的provide // 为了增加 provide 值和 inject 值之间的响应性我们可以在 provide 值时使用 ref 或 reactive。 const count ref(10); const zhangsan reactive({ name: 张三, age: 18 }); provide(count, count); provide(zhangsan, zhangsan);/script如果不使用 script setup请确保 provide() 是在 setup() 同步调用的 import { provide } from vueexport default {setup() {provide(message, hello!)} }应用层 Provide 除了在一个组件中提供依赖我们还可以在整个应用层面提供依赖 import { createApp } from vueconst app createApp({})app.provide(message, hello!)在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用因为插件一般都不会使用组件形式来提供值。 Inject (注入) 概念 inject() 注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。 第一个参数是注入的 key。 Vue 会遍历父组件链通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值inject() 将返回 undefined除非提供了一个默认值。 第二个参数是可选的即在没有匹配到 key 时使用的默认值。 它也可以是一个工厂函数用来返回某些创建起来比较复杂的值。 第三个参数是可选的如果默认值本身就是一个函数那么你必须将 false 作为第三个参数传入表明这个函数就是默认值而不是一个工厂函数。 与注册生命周期钩子的 API 类似inject() 必须在组件的 setup() 阶段同步调用。 在组件中使用 Inject 在script setup中使用注入祖先组件提供的数据 script setup import { inject, toRefs} from vue// 注入非响应式数据 const num inject(num); const message inject(message);// 注入ref const count inject(count);// 注入reactive const { age } toRefs(inject(zhangsan)); /script注入数据的注意事项 如果提供的值是非响应式数据可以直接使用 如果提供的值是一个 ref注入进来的会是该 ref 对象而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。 如果提供的值是一个reactive注入进来的也是该reactive对象注入方组件能够通过 reactive 对象保持了和供给方的响应性链接。如果想解构该对象需要使用 toRefs 同样的如果没有使用 script setupinject() 需要在 setup() 内同步调用 import { inject } from vueexport default {setup() {const message inject(message)return { message }} }注入默认值 默认情况下inject 假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供则会抛出一个运行时警告。 如果在注入一个值时不要求必须有提供者那么我们应该声明一个默认值和 props 类似 // 如果没有祖先组件提供 message // value 会是 这是默认值 const value inject(message, 这是默认值)在一些场景中默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用我们可以使用工厂函数来创建默认值 const value inject(key, () new ExpensiveClass())和响应式数据配合使用 当提供 / 注入响应式的数据时建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内使其更容易维护。 有的时候我们可能需要在注入方组件中更改数据。在这种情况下我们推荐在供给方组件内声明并提供一个更改数据的方法函数 !-- 在供给方组件内 -- script setup import { provide, ref } from vueconst location ref(North Pole)function updateLocation() {location.value South Pole }provide(location, {location,updateLocation }) /script!-- 在注入方组件 -- script setup import { inject } from vueconst { location, updateLocation } inject(location) /scripttemplatebutton clickupdateLocation{{ location }}/button /template最后如果你想确保提供的数据不能被注入方的组件更改你可以使用 readonly()来包装提供的值。 script setup import { ref, provide, readonly } from vueconst count ref(0) provide(read-only-count, readonly(count)) /script使用 Symbol 作注入名 至此我们已经了解了如何使用字符串作为注入名。但如果你正在构建大型的应用包含非常多的依赖提供或者你正在编写提供给其他开发者使用的组件库建议最好使用 Symbol 来作为注入名以避免潜在的冲突。 我们通常推荐在一个单独的文件中导出这些注入名 Symbol // keys.js export const myInjectionKey Symbol()// 在供给方组件中 import { provide } from vue import { myInjectionKey } from ./keys.jsprovide(myInjectionKey, { /*要提供的数据 */ });// 注入方组件 import { inject } from vue import { myInjectionKey } from ./keys.jsconst injected inject(myInjectionKey)插槽 子组件 script setup import { useSlots, reactive } from vue; const zhangsan reactive({name: 张三,age: 18, });const slots useSlots(); // 匿名插槽使用情况 console.log(slots.default: , slots.default()); // 具名插槽使用情况 console.log(slots.title: , slots.title()); /scripttemplate!-- 匿名插槽 --slot /!-- 具名插槽 --slot nametitle /!-- 作用域插槽 --slot namefooter :scopezhangsan / /template父组件 script setup // 引入子组件 import Child from ./Child.vue; /scripttemplateChild!-- 匿名插槽 --span我是默认插槽/span!-- 具名插槽 --template #titleh1我是具名插槽/h1h1我是具名插槽/h1h1我是具名插槽/h1/template!-- 作用域插槽 --template #footer{ scope }footer作用域插槽——姓名{{ scope.name }}年龄{{ scope.age }}/footer/template/Child /templatesetup语法糖 script setup 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 script 语法它具有更多优势 更少的样板内容更简洁的代码。能够使用纯 TypeScript 声明 props 和自定义事件。更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数避免了渲染上下文代理对象)。更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。 基本语法 要启用该语法需要在 script 代码块上添加 setup attribute script setup console.log(hello script setup) /script里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 script 只在组件被首次引入的时候执行一次不同script setup 中的代码会在每次组件实例被创建的时候执行。 当使用 script setup 的时候任何在 script setup 声明的顶层的绑定 (包括变量函数声明以及 import 导入的内容) 都能在模板中直接使用 script setup// 变量 const msg Hello!// 函数 function log() {console.log(msg) } /scripttemplatebutton clicklog{{ msg }}/button /templateimport 导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的 helper 函数而不需要通过 methods 选项来暴露它 script setup import { capitalize } from ./helpers /scripttemplatediv{{ capitalize(hello) }}/div /template响应式 响应式状态需要明确使用响应式 API 来创建。和 setup() 函数的返回值一样ref 在模板中使用的时候会自动解包 script setup import { ref, reactive } from vue;// 变量 const msg Hello! const count ref(10); const zhangsan reactive({ name: 张三, age: 18 });/scripttemplatebutton clickcount{{ count }}/button{{zhangsan.age}} /template使用组件 script setup 范围里的值也能被直接作为自定义组件的标签名使用 script setup import MyComponent from ./MyComponent.vue /scripttemplateMyComponent / /template这里 MyComponent 应当被理解为像是在引用一个变量。如果你使用过 JSX此处的心智模型是类似的。其 kebab-case 格式的 my-component 同样能在模板中使用——不过我们强烈建议使用 PascalCase 格式以保持一致性。同时这也有助于区分原生的自定义元素。 动态组件 由于组件是通过变量引用而不是基于字符串组件名注册的在 script setup 中要使用动态组件的时候应该使用动态的 :is 来绑定 script setup import Foo from ./Foo.vue import Bar from ./Bar.vue /scripttemplatecomponent :isFoo /component :issomeCondition ? Foo : Bar / /template请注意组件是如何在三元表达式中被当做变量使用的。 递归组件 一个单文件组件可以通过它的文件名被其自己所引用。例如名为 FooBar.vue 的组件可以在其模板中用 FooBar/ 引用它自己。 请注意这种方式相比于导入的组件优先级更低。如果有具名的导入和组件自身推导的名字冲突了可以为导入的组件添加别名 import { FooBar as FooBarChild } from ./components命名空间组件 可以使用带 . 的组件标签例如 Foo.Bar 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用 script setup import * as Form from ./form-components /scripttemplateForm.InputForm.Labellabel/Form.Label/Form.Input /template使用自定义指令 全局注册的自定义指令将正常工作。本地的自定义指令在 script setup 中不需要显式注册但他们必须遵循 vNameOfDirective 这样的命名规范 script setup const vMyDirective {beforeMount: (el) {// 在元素上做些操作} } /script templateh1 v-my-directiveThis is a Heading/h1 /template如果指令是从别处导入的可以通过重命名来使其符合命名规范 script setup import { myDirective as vMyDirective } from ./MyDirective.js /scriptdefineProps() 在 script setup 中使用 defineProps 来代替 props 选项实现父组件向子组件传值 defineProps 不需要导入可以直接在 script setup 中使用 defineProps 接收与 props 选项相同的值 defineProps 在选项传入后会提供恰当的类型推导。 传入到 defineProps 的选项会从 setup 中提升到模块的作用域。因此传入的选项不能引用在 setup 作用域中声明的局部变量。这样做会引起编译错误。但是它可以引用导入的绑定因为它们也在模块作用域内。 父组件 script setup // 引入子组件 import child from ./child.vue import { ref } from vue;const count ref(10); /scripttemplatechild :countcount/ /template子组件 script setup import { toRefs } from vue; // 声明props const props defineProps([ count ]); // 或者 const props defineProps({count: Number, });// 1、在setup内解构 props 需要使用toRefs或者使用props.count // 2、在组件模板中可以使用props.count也可以直接使用count //const { count } toRefs(props); //console.log(count: , count);/scripttemplatepprops.count:{{ props.count }}/ppcount:{{ count }}/p /templatedefineEmits() 在 script setup 中使用 defineEmits 来代替 emits 选项实现子组件发射自定义事件 defineEmits 不需要导入可以直接在 script setup 中使用 defineEmits 接收与 emits 选项相同的值。 defineEmits 在选项传入后会提供恰当的类型推导。 传入到 defineEmits 的选项会从 setup 中提升到模块的作用域。因此传入的选项不能引用在 setup 作用域中声明的局部变量。这样做会引起编译错误。但是它可以引用导入的绑定因为它们也在模块作用域内。 父组件 script setup // 引入子组件 import child from ./child.vue import { ref } from vue;const count ref(0); /scripttemplatecount: {{count}}child customClickcount $event/ /template子组件 script setup import { ref } from vue;const num ref(6);// 发射事件 const emit defineEmits([customClick]); const btnClick () {emit(customClick, num.value); };/scripttemplatebutton clickbtnClick发射事件/buttonbutton click$emit(customClick, num)发射事件/button /templatedefineExpose() 在标准组件写法里子组件的数据都是默认隐式暴露给父组件的但使用 script setup 的组件所有数据只是默认 return 给组件的模板使用不会暴露到组件外所以父组件是无法直接通过挂载 ref 变量获取子组件的数据 或者 $parent 链获取到父组件的数据。 如果要调用组件的数据需要先在组件显示的暴露出来才能够正确的拿到这个操作就是由 defineExpose 来完成。 defineEmits 不需要导入可以直接在 script setup 中使用通过 defineExpose 来显式指定在 script setup 组件中要暴露出去的属性 子组件 script setup import { reactive, ref, toRefs } from vue;const num 1; const count ref(2); const zhangsan reactive({ name: 张三 });// 将方法、变量暴露给父组件使用父组件才可通过ref API拿到子组件暴露的数据 defineExpose({num,count,// 解构 zhangsan...toRefs(zhangsan),// 声明方法changeName() {zhangsan.name 小三;}, }); /scripttemplatespan{{ zhangsan.name }}/span /template父组件 script setup import { nextTick, ref } from vue; // 引入子组件 import child from ./child.vue;// 子组件ref const childRef ref();// 当父组件通过模板引用的方式获取到当前组件的实例获取到的实例的ref 会和在普通实例中一样被自动解包 nextTick(() {// 获取子组件属性console.log(childRef.value.num); // 1console.log(childRef.value.count); // 2 count自动解包console.log(childRef.value.name); // 张三 name自动解包// 执行子组件方法childRef.value.changeName();console.log(childRef.value.name); // 小三 }); /scripttemplatechild refchildRef / /templateuseSlots() 和 useAttrs() 在 script setup 使用 slots 和 attrs 的情况应该是相对来说较为罕见的因为可以在模板中直接通过 $slots 和 $attrs 来访问它们。在你的确需要使用它们的罕见场景中可以分别用 useSlots 和 useAttrs 两个辅助函数 script setup import { useSlots, useAttrs } from vueconst slots useSlots() const attrs useAttrs() /scriptuseSlots 和 useAttrs 是真实的运行时函数它的返回与 setupContext.slots 和 setupContext.attrs 等价。它们同样也能在普通的组合式 API 中使用。 与普通的 script 一起使用 script setup 可以和普通的 script 一起使用。普通的 script 在有这些需要的情况下或许会被使用到 声明无法在 script setup 中声明的选项例如 inheritAttrs 或插件的自定义选项。声明模块的具名导出 (named exports)。运行只需要在模块作用域执行一次的副作用或是创建单例对象。 script // 普通 script, 在模块作用域下执行 (仅一次) runSideEffectOnce()// 声明额外的选项 export default {inheritAttrs: false,customOptions: {} } /scriptscript setup // 在 setup() 作用域中执行 (对每个实例皆如此) /script顶层 await script setup 中可以使用顶层 await。结果代码会被编译成 async setup() script setup const post await fetch(/api/post/1).then((r) r.json()) /script另外await 的表达式会自动编译成在 await 之后保留当前组件实例上下文的格式。 注意 async setup() 必须与 Suspense 内置组件组合使用Suspense 目前还是处于实验阶段的特性会在将来的版本中稳定。 限制 由于模块执行语义的差异script setup 中的代码依赖单文件组件的上下文。当将其移动到外部的 .js 或者 .ts 文件中的时候对于开发者和工具来说都会感到混乱。因此script setup 不能和 src attribute 一起使用。 组合式函数 hook 什么是“组合式函数” 在 Vue 应用的概念中“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。 当构建前端应用时我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多比如你可能已经用过的 lodash 或是 date-fns。 相比之下有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。 鼠标跟踪器示例 如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能它会是这样的 script setup import { ref, onMounted, onUnmounted } from vueconst x ref(0) const y ref(0)function update(event) {x.value event.pageXy.value event.pageY }onMounted(() window.addEventListener(mousemove, update)) onUnmounted(() window.removeEventListener(mousemove, update)) /scripttemplateMouse position is at: {{ x }}, {{ y }}/template但是如果我们想在多个组件中复用这个相同的逻辑呢我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中 // mouse.js import { ref, onMounted, onUnmounted } from vue// 按照惯例组合式函数名以“use”开头 export function useMouse() {// 被组合式函数封装和管理的状态const x ref(0)const y ref(0)// 组合式函数可以随时更改其状态。function update(event) {x.value event.pageXy.value event.pageY}// 一个组合式函数也可以挂靠在所属组件的生命周期上// 来启动和卸载副作用onMounted(() window.addEventListener(mousemove, update))onUnmounted(() window.removeEventListener(mousemove, update))// 通过返回值暴露所管理的状态return { x, y } }下面是它在组件中使用的方式 script setup import { useMouse } from ./mouse.jsconst { x, y } useMouse() /scripttemplateMouse position is at: {{ x }}, {{ y }}/template如你所见核心逻辑完全一致我们做的只是把它移到一个外部函数中去并返回需要暴露的状态。和在组件中一样你也可以在组合式函数中使用所有的组合式 API。现在useMouse() 的功能可以在任何组件中轻易复用了。 更酷的是你还可以嵌套多个组合式函数一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。 举例来说我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中 // event.js import { onMounted, onUnmounted } from vueexport function useEventListener(target, event, callback) {// 如果你想的话// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素onMounted(() target.addEventListener(event, callback))onUnmounted(() target.removeEventListener(event, callback)) }有了它之前的 useMouse() 组合式函数可以被简化为 // mouse.js import { ref } from vue import { useEventListener } from ./eventexport function useMouse() {const x ref(0)const y ref(0)useEventListener(window, mousemove, (event) {x.value event.pageXy.value event.pageY})return { x, y } }异步状态示例 useMouse() 组合式函数没有接收任何参数因此让我们再来看一个需要接收一个参数的组合式函数示例。在做异步数据请求时我们常常需要处理不同的状态加载中、加载成功和加载失败。 script setup import { ref } from vueconst data ref(null) const error ref(null)fetch(...).then((res) res.json()).then((json) (data.value json)).catch((err) (error.value err)) /scripttemplatediv v-iferrorOops! Error encountered: {{ error.message }}/divdiv v-else-ifdataData loaded:pre{{ data }}/pre/divdiv v-elseLoading.../div /template如果在每个需要获取数据的组件中都要重复这种模式那就太繁琐了。让我们把它抽取成一个组合式函数 // fetch.js import { ref } from vueexport function useFetch(url) {const data ref(null)const error ref(null)fetch(url).then((res) res.json()).then((json) (data.value json)).catch((err) (error.value err))return { data, error } }现在我们在组件里只需要 script setup import { useFetch } from ./fetch.jsconst { data, error } useFetch(...) /scriptuseFetch() 接收一个静态的 URL 字符串作为输入所以它只执行一次请求然后就完成了。但如果我们想让它在每次 URL 变化时都重新请求呢那我们可以让它同时允许接收 ref 作为参数 // fetch.js import { ref, isRef, unref, watchEffect } from vueexport function useFetch(url) {const data ref(null)const error ref(null)function doFetch() {// 在请求之前重设状态...data.value nullerror.value null// unref() 解包可能为 ref 的值fetch(unref(url)).then((res) res.json()).then((json) (data.value json)).catch((err) (error.value err))}if (isRef(url)) {// 若输入的 URL 是一个 ref那么启动一个响应式的请求watchEffect(doFetch)} else {// 否则只请求一次// 避免监听器的额外开销doFetch()}return { data, error } }这个版本的 useFetch() 现在同时可以接收静态的 URL 字符串和 URL 字符串的 ref。当通过 isRef() 检测到 URL 是一个动态 ref 时它会使用 watchEffect() 启动一个响应式的 effect。该 effect 会立刻执行一次并在此过程中将 URL 的 ref 作为依赖进行跟踪。当 URL 的 ref 发生改变时数据就会被重置并重新请求。 这里是一个升级版的 useFetch()出于演示目的我们人为地设置了延迟和随机报错。 约定和最佳实践 命名 组合式函数约定用驼峰命名法命名并以“use”作为开头。 输入参数 尽管其响应性不依赖 ref组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用你最好在处理输入参数时兼容 ref 而不只是原始的值。unref() 工具函数会对此非常有帮助 import { unref } from vuefunction useFeature(maybeRef) {// 若 maybeRef 确实是一个 ref它的 .value 会被返回// 否则maybeRef 会被原样返回const value unref(maybeRef) }如果你的组合式函数在接收 ref 为参数时会产生响应式 effect请确保使用 watch() 显式地监听此 ref或者在 watchEffect() 中调用 unref() 来进行正确的追踪。 返回值 你可能已经注意到了我们一直在组合式函数中使用 ref() 而不是 reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象这样该对象在组件中被解构为 ref 之后仍可以保持响应性 // x 和 y 是两个 ref const { x, y } useMouse()从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反ref 则可以维持这一响应性连接。 如果你更希望以对象属性的形式来使用组合式函数中返回的状态你可以将返回的对象用 reactive() 包装一次这样其中的 ref 会被自动解包例如 const mouse reactive(useMouse()) // mouse.x 链接到了原来的 x ref console.log(mouse.x)Mouse position is at: {{ mouse.x }}, {{ mouse.y }}副作用 在组合式函数中的确可以执行副作用 (例如添加 DOM 事件监听器或者请求数据)但请注意以下规则 如果你的应用用到了服务端渲染 (SSR)请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用例如onMounted()。这些钩子仅会在浏览器中被调用因此可以确保能访问到 DOM。确保在 onUnmounted() 时清理副作用。举例来说如果一个组合式函数设置了一个事件监听器它就应该在 onUnmounted() 中被移除 (就像我们在 useMouse() 示例中看到的一样)。当然也可以像之前的 useEventListener() 示例那样使用一个组合式函数来自动帮你做这些事。 使用限制 组合式函数在 script setup 或 setup() 钩子中应始终被同步地调用。在某些场景下你也可以在像 onMounted() 这样的生命周期钩子中使用他们。 这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例只有能确认当前组件实例才能够 将生命周期钩子注册到该组件实例上将计算属性和监听器注册到该组件实例上以便在该组件被卸载时停止监听避免内存泄漏。 TIP script setup 是唯一在调用 await 之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。 通过抽取组合式函数改善代码结构 抽取组合式函数不仅是为了复用也是为了代码组织。随着组件复杂度的增高你可能会最终发现组件多得难以查询和理解。组合式 API 会给予你足够的灵活性让你可以基于逻辑问题将组件代码拆分成更小的函数 vue script setup import { useFeatureA } from ./featureA.js import { useFeatureB } from ./featureB.js import { useFeatureC } from ./featureC.jsconst { foo, bar } useFeatureA() const { baz } useFeatureB(foo) const { qux } useFeatureC(baz) /script在某种程度上你可以将这些提取出的组合式函数看作是可以相互通信的组件范围内的服务。 在选项式 API 中使用组合式函数 如果你正在使用选项式 API组合式函数必须在 setup() 中调用。且其返回的绑定必须在 setup() 中返回以便暴露给 this 及其模板 js import { useMouse } from ./mouse.js import { useFetch } from ./fetch.jsexport default {setup() {const { x, y } useMouse()const { data, error } useFetch(...)return { x, y, data, error }},mounted() {// setup() 暴露的属性可以在通过 this 访问到console.log(this.x)}// ...其他选项 }与其他模式的比较 和 Mixin 的对比 Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板 不清晰的数据来源当使用了多个 mixin 时实例上的数据属性来自哪个 mixin 变得不清晰这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref 解构模式的理由让属性的来源在消费组件时一目了然。命名空间冲突多个来自不同作者的 mixin 可能会注册相同的属性名造成命名冲突。若使用组合式函数你可以通过在解构变量时对变量进行重命名来避免相同的键名。隐式的跨 mixin 交流多个 mixin 需要依赖共享的属性名来进行相互作用这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入像普通函数那样。 基于上述理由我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。 和无渲染组件的对比 在组件插槽一章中我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。 组合式函数相对于无渲染组件的主要优势是组合式函数不会产生额外的组件实例开销。当在整个应用中使用时由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。 我们推荐在纯逻辑复用时使用组合式函数在需要同时复用逻辑和视图布局时使用无渲染组件。 和 React Hooks 的对比 如果你有 React 的开发经验你可能注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部分灵感正来自于 React hooksVue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而Vue 的组合式函数是基于 Vue 细粒度的响应性系统这和 React hooks 的执行模型有本质上的不同。 更多hook的理解 Vue3中的Hook函数对比mixinVue3必学技巧-自定义Hooks-让写Vue3更畅快浅谈为啥vue和react都选择了Hooksvueuse:我不许身为vuer的前端,你的工具集只有lodash!
http://www.dnsts.com.cn/news/144043.html

相关文章:

  • 国内做的比较好的旅游网站手机wap网站免费建站
  • seo网站排名厂商定制专业建设思路
  • 为什么网站突然打不开行业网站建设教程
  • 给朋友网站做宣传怎么写wordpress图片站优化
  • 信息手机网站模板做网站推广哪些
  • 网站开发背景知识微网站设计与开发是什么
  • 网站的功能规范厦门外贸网站建设
  • 检测网站是否被墙wordpress自定义页
  • 做响应网站的素材网站南昌百度推广公司
  • 国外有哪些做建筑材料的网站亚马逊外贸网站如何做
  • 文具网站建设策划书做财经类新闻的网站
  • 建基建设集团网站做门户网站难吗
  • 个人网站建设方案模板网络营销的策划方案
  • 网站建设公司优惠中男女做暧视频网站免费
  • 荷城网站制作公司最新黑帽seo培训
  • 免费php网站模板下载啥是网络推广
  • 微网站方案萧涵 wordpress
  • 东莞企业网站推广网站开发企业标准
  • 网站开发的职位要求新手学做网站 pdf 下载
  • 双语网站建设定制开发学历提升文案
  • 网站备案关闭嘉兴网站建设制作
  • 一 网站建设管理基本情况wordpress默认邮件在哪里设置
  • 做网站编辑累不累wordpress算前端
  • 做网站卖得出去吗wordpress 懒人图库
  • 大连英文网站建设杭州产品设计公司有哪些
  • 企业网站设计代码百度 营销推广怎么做
  • 南安市城乡住房建设局网站手机网站 微信链接
  • 云南省住房和城乡建设厅网站首页个人简历怎么写简短又吸引人
  • 速度啊网站广告代运营公司
  • 网站设计部西安网站制作哪家好