岁岁年,碎碎念

Vue Doc 随手记 ——三-五节

2023.08.02     967

室温超导体新闻这两天热火朝天。

室温超导体是指在常规的室温条件下(通常是接近或等于常温,即约 20 摄氏度)能够表现出超导现象的材料。超导现象是指在某些材料中,在特定的低温条件下,电阻突然变为零,并且磁场被完全排斥的现象。传统的超导材料需要极低的温度(接近绝对零度,约 -273.15 摄氏度)才能表现出超导行为,这对于应用和实际应用来说非常不便。

三-五节

  1. 响应式基础
  2. 计算属性
  3. Class 与 Style 绑定

响应式基础

ref

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      // 在 JavaScript 中需要 .value
      count.value++
    }

    // 不要忘记同时暴露 increment 函数
    return {
      count,
      increment
    }
  }
}

<button @click="increment">
  {{ count }}
</button>

在 setup() 函数中手动暴露大量的状态和方法非常繁琐。可以通过使用单文件组件 (SFC) 来避免这种情况。可以使用 <script setup> 简化代码。

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>

为什么需要 ref?

使用 ref 可以将普通的 JavaScript 数据转换为 Vue.js 中的响应式数据,从而保证数据的更新能够正确地触发视图的更新,并方便地在组件中访问和修改数据。

// 伪代码,不是真正的实现,类似 iOS 的 KVO 的实现
const myRef = {
  _value: 0,
  get value() {
    track()
    return this._value
  },
  set value(newValue) {
    this._value = newValue
    trigger()
  }
}

DOM 更新时机:当你修改了响应式状态时,DOM 会被自动更新。 注意,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。

要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:

import { nextTick } from 'vue'
async function increment() {
  count.value++
  await nextTick()
  // 现在 DOM 已经更新了
}

reactive

reactive() 将使对象本身具有响应性

响应式对象是 JavaScript 代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。

JavaScript 代理(Proxy)是 ES6(ECMAScript 2015)引入的一种特性,它允许你创建一个代理对象,用于拦截对目标对象的访问和操作。代理对象可以完全自定义目标对象的行为,并提供了一种强大的机制来拦截和定制对象的操作。

代理对象是由 Proxy 构造函数创建的,它接受两个参数:目标对象(target)和一个处理程序对象(handler)。目标对象是被代理的对象,而处理程序对象包含了一组拦截器(handler traps),这些拦截器用于拦截对目标对象的各种操作。

代理对象拦截了对目标对象的各种操作,包括属性访问、属性赋值、函数调用、对象的拷贝等。你可以通过在处理程序对象中提供对应的拦截器来定制代理对象的行为。

下面是一个简单的示例,展示了如何创建一个代理对象:

// 目标对象
const targetObject = {
  name: 'John',
  age: 30,
};

// 处理程序对象
const handler = {
  get: function(target, prop) {
    console.log(`Getting property "${prop}"`);
    return target[prop];
  },
  set: function(target, prop, value) {
    console.log(`Setting property "${prop}" to "${value}"`);
    target[prop] = value;
  },
};

// 创建代理对象
const proxyObject = new Proxy(targetObject, handler);

// 使用代理对象
proxyObject.name; // Output: Getting property "name", Result: "John"
proxyObject.age = 35; // Output: Setting property "age" to "35"

在上面的示例中,我们创建了一个代理对象 proxyObject,它代理了目标对象 targetObject。在处理程序对象 >handler 中,我们定义了 getset 拦截器,用于拦截对属性的访问和赋值操作。

当我们访问代理对象的属性时,拦截器中的 get 方法会被调用,并输出相应的日志。当我们对代理对象的属性进行赋值时,拦截器中的 set 方法会被调用,并输出相应的日志。

代理对象的强大之处在于,你可以在拦截器中实现自定义的逻辑,从而实现更高级的功能,比如数据验证、缓存、日志记录等。代理是 JavaScript 中一种非常有用的特性,它在实际开发中常用于构建拦截器、数据绑定和数据监听等功能。

ref 和 reactive 区别是什么?

在 Vue.js 3.x 中,refreactive 是两种不同的方法用于声明响应式数据,它们之间有一些区别:

  1. 数据结构
  • refref 函数用于将普通的 JavaScript 值转换为一个响应式对象。ref 函数将传入的值包装在一个特殊的对象中,并返回这个对象。当你在模板中使用 ref 声明的数据时,不需要使用 .value,Vue.js 会自动进行解包。

  • reactivereactive 函数用于将普通的 JavaScript 对象转换为一个响应式对象。reactive 函数返回一个代理对象,可以拦截对目标对象的访问和操作。当你在模板中使用 reactive 声明的数据时,需要使用 .value 来解包。

  1. 使用方式
  • ref:通常用于声明基本类型的响应式数据,例如数字、字符串等。
import { ref } from 'vue';

const count = ref(0); // 声明响应式数字
const message = ref('Hello'); // 声明响应式字符串
  • reactive:通常用于声明复杂类型的响应式数据,例如对象和数组。
import { reactive } from 'vue';

const user = reactive({ name: 'John', age: 30 }); // 声明响应式对象
const list = reactive([1, 2, 3]); // 声明响应式数组
  1. 访问方式
  • ref:在模板中访问 ref 声明的数据时,不需要使用 .value,Vue.js 会自动进行解包。
<!-- 在模板中直接使用 ref 声明的数据 -->
<p>{{ count }}</p>
<button @click="count++">Increment</button>
  • reactive:在模板中访问 reactive 声明的数据时,需要使用 .value 来解包。
<!-- 在模板中使用 reactive 声明的数据需要使用 .value 进行解包 -->
<p>{{ user.name }}</p>
<button @click="user.age++">Increment Age</button>
  1. 实现原理: ref 和 reactive 都使用了 ES6 的 Proxy 对象来实现响应式数据,区别在于 ref 包装的是基本类型的值并使用了特殊的 Ref 类型对象,而 reactive 则直接创建一个普通对象的代理。

总结:refreactive 都用于声明响应式数据,ref 适用于简单类型的数据,而 reactive 适用于复杂类型的数据。在模板中使用 ref 声明的数据时,不需要使用 .value 进行解包,而在使用 reactive 声明的数据时,需要使用 .value 进行解包。

Reactive Proxy vs. Original

const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

const proxy = reactive({})
const raw = {}
proxy.nested = raw
// 响应式对象内的嵌套对象依然是代理
console.log(proxy.nested === raw) // false

计算属性

计算属性(Computed Properties)主要用于两个问题:

  1. 复杂逻辑的封装:当在模板中需要根据一些数据的状态或属性进行复杂的计算时,直接在模板中写这些计算逻辑会导致模板变得复杂难以维护。使用计算属性可以将这些复杂的计算逻辑封装在一个函数中,让模板保持简洁和易读。

  2. 缓存计算结果:计算属性会对其依赖的响应式数据进行追踪,只有当依赖的数据发生变化时,计算属性才会重新计算。这意味着计算属性的结果会被缓存,如果依赖的数据没有发生变化,计算属性不会重新执行计算,而是直接返回缓存的结果。这样可以避免不必要的计算,提高性能。

下面是一个示例来展示计算属性的用法和好处:

<template>
  <div>
    <p>Total Price: {{ totalPrice }}</p>
    <p>Discounted Price: {{ discountedPrice }}</p>
  </div>
</template>

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

export default {
  setup() {
    const quantity = ref(5);
    const price = ref(10);
    const discount = ref(0.1);

    // 计算属性 totalPrice
    const totalPrice = computed(() => quantity.value * price.value);

    // 计算属性 discountedPrice
    const discountedPrice = computed(() => totalPrice.value * (1 - discount.value));

    return {
      quantity,
      price,
      discount,
      totalPrice,
      discountedPrice,
    };
  },
};
</script>

在上面的示例中,我们使用计算属性 totalPricediscountedPrice 来计算总价和折扣后的价格。计算属性 totalPrice 依赖于 quantityprice,而 discountedPrice 依赖于 totalPricediscount。当 quantitypricediscount 发生变化时,计算属性会重新计算并更新视图。而如果这些依赖没有变化,计算属性会返回之前缓存的计算结果,避免重复计算。

通过使用计算属性,你可以将复杂的计算逻辑封装在一个地方,使模板保持简洁和可读性,并通过缓存计算结果提高性能。这使得你的代码更易于维护和优化。

可写计算属性(Writable Computed Properties)

<template>
  <div>
    <input v-model="fullName" />
    <p>First Name: {{ firstName }}</p>
    <p>Last Name: {{ lastName }}</p>
  </div>
</template>

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

export default {
  setup() {
    const fullName = ref('');
    
    // 可写计算属性
    const firstName = computed({
      get: () => fullName.value.split(' ')[0],
      set: (value) => {
        const lastName = fullName.value.split(' ')[1];
        fullName.value = value + ' ' + lastName;
      },
    });

    const lastName = computed(() => fullName.value.split(' ')[1]);

    return {
      fullName,
      firstName,
      lastName,
    };
  },
};
</script>

计算属性最佳实践​

  1. Getter 不应有副作用​

    计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。

  2. 避免直接修改计算属性值​

    从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

Class 与 Style 绑定

Vue 专门为 class 和 style 的 v-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

绑定 HTML class

绑定对象

<div :class="{ active: isActive }"></div>

const isActive = ref(true)
const hasError = ref(false)
<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>
# 渲染结果为
<div class="static active"></div>

# 直接绑定一个对象
const classObject = reactive({
  active: true,
  'text-danger': false
})
<div :class="classObject"></div>

# 绑定一个返回对象的计算属性
const isActive = ref(true)
const error = ref(null)

const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))
<div :class="classObject"></div>

绑定数组

const activeClass = ref('active')
const errorClass = ref('text-danger')
<div :class="[activeClass, errorClass]"></div>

# 三元表达式
<div :class="[isActive ? activeClass : '', errorClass]"></div>

<div :class="[{ active: isActive }, errorClass]"></div>

在组件上使用

对于只有一个根元素的组件,当你使用了 class attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。

<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>

<!-- 在使用组件时 -->
<MyComponent class="baz boo" />

# 渲染出的 HTML 为:
<p class="foo bar baz boo">Hi!</p>

Class 的绑定也是同样的:

<MyComponent :class="{ active: isActive }" />

<p class="foo bar active">Hi!</p>

如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来实现指定:

<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>

<MyComponent class="baz" />

# 渲染出的 HTML 为:
<p class="baz">Hi!</p>
<span>This is a child component</span>

绑定内联样式

绑定对象

const activeColor = ref('red')
const fontSize = ref(30)
# camelCase
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

# kebab-cased
<div :style="{ 'font-size': fontSize + 'px' }"></div>


const styleObject = reactive({
  color: 'red',
  fontSize: '13px'
})
<div :style="styleObject"></div>

绑定数组

<div :style="[baseStyles, overridingStyles]"></div>

自动前缀

当你在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。

什么是浏览器特殊前缀

浏览器特殊前缀(Browser Vendor Prefix),也称为私有前缀(Private Prefix),是一种在 CSS 属性名称前面添加特定浏览器厂商前缀的技术。这些前缀用于在浏览器支持某些实验性或尚未正式纳入 CSS 规范的 CSS 属性时进行区分,以便开发者能够在不同浏览器中实验和应用新的 CSS 特性。

由于 CSS 规范在发展过程中,一些新特性可能尚未得到所有浏览器的广泛支持,或者不同浏览器对同一属性的实现方式可能存在差异。为了解决这些问题,浏览器厂商(如Chrome、Firefox、Safari、Edge等)会在实验性或尚未稳定的 CSS 属性名称前添加特定的前缀,以区分不同浏览器的实现。

常见的浏览器特殊前缀包括:

  • -webkit-:用于 Safari 和 Chrome 浏览器。
  • -moz-:用于 Firefox 浏览器。
  • -ms-:用于 Microsoft Edge 浏览器。
  • -o-:用于 Opera 浏览器。

例如,-webkit-border-radius 是 Safari 和 Chrome 中用于设置元素边框圆角的属性,而 -moz-border-radius 则是 Firefox 中的对应属性。在这种情况下,开发者需要使用多个前缀来支持不同浏览器,例如:

.my-element {
  -webkit-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
}

然而,随着 CSS 规范的不断发展和浏览器对新特性的支持日益完善,许多实验性属性已经得到了标准化,并且浏览器厂商逐渐减少了对特殊前缀的依赖。现在在使用 CSS 属性时,通常只需要写标准的属性名即可,而无需添加浏览器特殊前缀。不过,在开发过程中,为了确保网站的兼容性,仍然建议使用 CSS 厂商前缀进行一定程度的属性设置。

样式多值

对一个样式属性提供多个 (不同前缀的) 值

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex。