vue

Vue3 基础 – 计算属性 & 侦听器 & 样式绑定 & 模板引用

内容纲要

版权归作者 ©刘龙宾 所有,本文章未经作者允许,禁止私自转载!

1. setup 函数 & 组合式 API

在前面的课程中,我们曾强调过:使用 data() 函数声明响应式的数据、在 methods 节点下声明事件处理器,都是 Vue2 中的旧语法(选项式API)。这种旧语法的存在,主要是为了防止断崖式升级造成的老用户流失。

在 Vue3 中,推出了组合式 API 的新概念,它可以极大提高功能模块的封装性复用性。因此,官方推荐所有用户优先使用组合式 API。

在今后的开发中,为了维护老项目,我们可能会不得不用到选项式 API。除此之外,在开发新项目时,推荐大家优先使用组合式 API。

1.1 setup 函数

setup() 函数是 Vue3 中组合式 API 的入口,它的语法格式如下:

const { createApp } = Vue

const app = createApp({
  setup() {
    // 给模板使用的数据、事件处理器等,都要通过这个 return 的对象暴露给模板
    return {}
  }
})

app.mount('#app')

例如,在 setup return 的对象中,声明一个数据 name

const { createApp } = Vue

const app = createApp({
  setup() {
    // 给模板使用的数据、事件处理器等,都要通过这个 return 的对象暴露给模板
    return {
      name: 'liulongbin'
    }
  }
})

app.mount('#app')

紧接着,在模板中即可使用名为 name 的数据了:

<!-- 模板 -->
<div id="app">
  <h1>{{ name }}</h1>
</div>

注意:刚才定义的 name,并非是响应式的数据,所以基于 app._instance.proxy.name 修改它的值后,不会触发视图的重新渲染。

1.2 组合式 API

组合式 API 是 Vue3 提供的一系列函数。例如,可以使用 reactive()ref() 这两个函数,来声明响应式的数据。

1. reactive 函数

reactive 函数用来定义响应式的数据对象,它的使用步骤如下:

// 1. 从 Vue 上解构出 reactive 函数
const { createApp, reactive } = Vue

const app = createApp({
  setup() {
    // 2. 调用 reactive 函数,定义响应式的数据
    const user = reactive({ name: 'zs' })

    // 3. 把响应式的数据,挂载到 return 的对象上,供模板使用
    return {
      user
    }
  }
})

app.mount('#app')

接下来,就可以在模板中使用响应式的数据 user 啦:

<!-- 模板 -->
<div id="app">
  <h1>{{ user.name }}</h1>
</div>

2. 修改 reactive 数据

如果想要点击按钮,修改 user.name 的值,我们可以在 setup 中定义事件的处理函数,并挂载到 return 的对象上,供模板使用:

setup() {
  const user = reactive({ name: 'liulongbin' })

  // 1. 定义处理函数
  const changeUserName = () => {
    user.name = 'escook'
  }

  return {
    user,
    // 2. 把处理函数,暴露给模板使用
    changeUserName
  }
}

最后,在模板中,为 <button> 按钮绑定点击事件:

<!-- 模板 -->
<div id="app">
  <h1>{{ user.name }}</h1>
  <!-- 3. 绑定事件处理器 -->
  <button @click="changeUserName">按钮</button>
</div>

reactive 函数的缺点:只能为引用类型的数据创建响应式数据对象,无法将值类型的数据转化为响应式数据。

3. ref 函数

ref 函数用来将值类型的数据转化为响应式数据,它的基本用法如下:

// 1. 从 Vue 中结构出 ref 函数
const { createApp, ref } = Vue

const app = createApp({
  setup() {
    // 2. 调用 ref 函数,创建响应式数据 count,其初始值为 0
    const count = ref(0)

    return {
      // 3. 把响应式数据 count,暴露给模板使用
      count
    }
  }
})

app.mount('#app')

紧接着,就可以在模板中使用 count 了:

<!-- 模板 -->
<div id="app">
  <h1>{{ count }}</h1>
</div>

4. 修改 ref 数据

如果想要点击按钮,修改 count 的值,则需要在 setup 函数中定义事件的处理函数,并挂载到 return 的对象上,供模板使用:

setup() {
  const count = ref(0)

  // 1. 定义处理函数(注意:在 js 中必须使用 .value 来访问 ref 声明的响应式数据)
  const increase = () => {
    count.value++
  }

  return {
    count,
    // 2. 把处理函数,暴露给模板使用
    increase
  }
}

最后,在模板中,为 <button> 按钮绑定点击事件:

<!-- 模板 -->
<div id="app">
  <h1>{{ count }}</h1>
  <!-- 3. 绑定事件处理器 -->
  <button @click="increase">+1</button>
</div>

注意:使用 ref 声明的响应式数据,在 js 中必须使用 .value 进行访问。在模板中不必使用 .value 进行访问,因为在模板中使用时会自动对 ref 数据进行解包。

5. 将对象赋值给 ref

ref 函数除了接收值类型的数据之外,还可以将对象类型的数据转化为响应式数据。例如:

setup() {
  // 1. 把对象赋值给 ref 作为初始值
  const info = ref({ name: 'liulongbin', age: 18 })

  // 2. 定义处理函数
  const changeInfo = () => {
    // 通过 info.value 分别为每个属性赋值
    info.value.name = 'escook'
    info.value.age = 22

    // 直接给 info.value 赋新值
    // info.value = { name: 'escook', age: 22 }
  }

  // 3. 向模板暴露数据
  return {
    info,
    changeInfo
  }
}

在模板中,直接使用 info 即可,因为模板中会对 ref 数据自动解包:

<!-- 模板 -->
<div id="app">
  <h1>{{ info.name }}今年{{info.age}}岁了。</h1>
  <button @click="changeInfo">修改</button>
</div>

注意:如果 ref() 处理的是对象类型的数据,则内部会调用 reactive() 函数将对象数据转为响应式数据,然后挂载为 .value 属性。

最佳实践:尽量使用 ref() 来定义响应式数据,因为它既可以处理值类型数据,又可以处理对象类型的数据。

2. 计算属性

计算属性可以避免程序员在模板中编写大段的 JavaScript 表达式,从而提高代码的整洁度和可维护性。

2.1 基于模板中的表达式统计商品信息

  1. 定义购物车中的商品数据如下:

    const { createApp, ref } = Vue
    
    const app = createApp({
     setup() {
       // 购物车中的商品数据
       const goods = ref([
         { id: 1, name: '红烧肉盖饭', price: 26, count: 1 },
         { id: 2, name: '小酥肉', price: 12, count: 1 },
         { id: 3, name: '冰镇可乐', price: 6, count: 1 }
       ])
    
       return {
         goods
       }
     }
    })
    
    app.mount('#app')
  2. 在模板中渲染购物车中的商品数据,并基于模板中的表达式计算商品的数量、总价、运费:

    <div id="app">
     <ul>
       <li v-for="item in goods" :key="item.id">
         <p>商品名称:{{item.name}}</p>
         <p>单价:{{ item.price }}</p>
         <p>购买数量:{{ item.count }}</p>
       </li>
     </ul>
    
     <hr>
     <div>
       <p>总计:</p>
       <p>商品数量:{{ goods.reduce((total, item) => total += item.count, 0) }}</p>
       <p>商品总价:{{ goods.reduce((total, item) => total += item.price * item.count, 0) }}元</p>
       <p>配送费:{{ goods.reduce((total, item) => total += item.price * item.count, 0) >= 50 ? '免配送费' : '5元' }}</p>
     </div>
    </div>
  3. 编写样式,为商品添加分割线:

    li+li {
     border-top: 1px solid #efefef;
    }
  4. 在步骤2中,我们发现模板中掺杂了较为复杂的表达式;而且在商品总价配送费中,两次用到了商品的总价,但每次都需要重新计算总价,无法实现数据的复用。

    基于这样的原因,我们可以利用 computed 计算属性,把模板中的表达式运算,封装到 JS 中,从而达到简化模板实现数据复用的目的。

2.2 计算属性的基本语法

  1. Vue 对象上解构出 computed 函数,它用来定义计算属性:

    const { computed } = Vue
  2. 调用 computed 函数,同时向 computed 中传递一个箭头函数作为参数:

    const res = computed(() => {
     return 计算的结果
    })

    而且,在箭头函数中,必须 return 一个计算的结果。如果忘记了 return,则是一个无效的计算属性。

  3. computed 函数的返回值,是一个 ref 的响应式对象,可通过 .value 访问其值。如果在模板中访问,会自动进行解包:

    <div>{{ res }}</div>

2.3 基于计算属性实现加法运算

  1. 通过 ref 定义响应式数据 n1n2,通过 computed 定义计算属性 result

    const { createApp, ref, computed } = Vue
    const app = createApp({
     setup() {
       // 响应式 ref 数据
       const n1 = ref(0)
       const n2 = ref(0)
    
       // 计算属性,依赖于 n1 和 n2 的变化,会自动运算并返回最新的结果
       const result = computed(() => n1.value + n2.value)
    
       return {
         n1,
         n2,
         result
       }
     }
    })
    
    app.mount('#app')
  2. 在模板中使用计算属性:

    <div id="app">
     <input type="number" v-model="n1">
     <span>+</span>
     <input type="number" v-model="n2">
     <span>=</span>
     <span>{{ result }}</span>
    </div>

    注意:当计算属性依赖的响应式数据发生变化时,会自动对计算属性重新求值。

2.4 基于计算属性改造购物车案例

  1. 定义购物车中的商品数据如下:

    const { createApp, ref } = Vue
    
    const app = createApp({
     setup() {
       // 购物车中的商品数据
       const goods = ref([
         { id: 1, name: '红烧肉盖饭', price: 26, count: 1 },
         { id: 2, name: '小酥肉', price: 12, count: 1 },
         { id: 3, name: '冰镇可乐', price: 6, count: 1 }
       ])
    
       return {
         goods
       }
     }
    })
    
    app.mount('#app')
  2. 分析模板的结构,我们可以得到如下的结论:

    • 商品的数量,是购物车中所有商品的数量总和,依赖于每个商品数量的变化,是一个派生属性
    • 商品的总价,是购物车中所有商品的单价×数量的总和,依赖于每个商品的数量和单价的变化,是一个派生属性
    • 商品的运费,依赖于商品总价的变化而变化,也是一个派生属性

    所以,我们可以基于计算属性,分别计算出以上3个数据的值,代码如下:

    // 1. 导入 computed 函数
    const { createApp, ref, computed } = Vue
    
    const app = createApp({
     setup() {
       // 购物车中的商品数据
       const goods = ref([
         { id: 1, name: '红烧肉盖饭', price: 26, count: 1 },
         { id: 2, name: '小酥肉', price: 12, count: 1 },
         { id: 3, name: '冰镇可乐', price: 6, count: 1 }
       ])
    
       // 2.1 计算属性:商品总数量
       const total = computed(() => goods.value.reduce((t, item) => t += item.count, 0))
       // 2.2 计算属性:商品的总价格
       const amount = computed(() => goods.value.reduce((t, item) => t += item.price * item.count, 0))
       // 2.3 计算属性:运费
       const freight = computed(() => amount.value >= 50 ? '免配送费' : '5元')
    
       return {
         goods,
         // 3. 把计算属性暴露给模板使用
         total,
         amount,
         freight
       }
     }
    })
    
    app.mount('#app')
  3. 在模板中,使用使用这三个计算属性的值即可:

    <div>
     <p>总计:</p>
     <p>商品数量:{{ total }}</p>
     <p>商品总价:{{ amount }}元</p>
     <p>配送费:{{ freight }}</p>
    </div>

    思考:如果在模板中,想要把实际的支付金额(商品总价 + 运费)显示出来,用计算属性该如何实现?

2.5 计算属性的缓存 vs 方法

我们发现,如果把计算属性的业务逻辑封装到 function 中,也能实现相同的结果,例如计算两个数之和的案例:

const { createApp, ref } = Vue
const app = createApp({
  setup() {
    const n1 = ref(0)
    const n2 = ref(0)

    // 1. 定义一个用于计算的方法
    const getResult = () => {
      console.log('触发了 function 的执行')
      return n1.value + n2.value
    }

    return {
      n1,
      n2,
      // 2. 向模板暴露这个方法
      getResult
    }
  }
})

app.mount('#app')

接下来,在模板中调用这个方法,显示计算的结果:

<div id="app">
  <input type="number" v-model="n1">
  <span>+</span>
  <input type="number" v-model="n2">
  <span>=</span>
  <span>{{ getResult() }}</span>
</div>

注意:使用 function 并不会缓存计算的结果。当我们在模板中多次调用这个 function 时,每次使用都会触发的的执行,哪怕每次计算的结果都是一模一样的:

<div id="app">
  <input type="number" v-model="n1">
  <span>+</span>
  <input type="number" v-model="n2">
  <span>=</span>
  <!-- 第1次调用方法 -->
  <span>{{ getResult() }}</span>

  <hr>
  <!-- 第2次调用方法 -->
  <div>{{ getResult() }}</div>
  <!-- 第3次调用方法 -->
  <div>{{ getResult() }}</div>
</div>

所以 function 总是会在每次调用时重新执行计算的过程,不会对计算的结果进行缓存。

而 computed 计算属性就很好的实现了缓存的功能:当计算属性中所依赖的响应式数据没有发生变化时,多次使用计算属性,会立即返回之前计算的结果,这极大的提高了计算的性能。

计算属性的优点:1. 能缓存计算的结果 2. 能简化模板的代码

3. 侦听器

侦听器允许我们监视响应式数据的变化,并触发回调函数的执行,在回调函数的形参中,可以接收 newValueoldValue 两个形参,分别代表变化后的新值变化前的旧值

3.1 侦听器的基本语法

  1. Vue 对象上解构出 watch 函数:

    const { watch } = Vue
  2. setup 函数中,调用 watch 函数,声明侦听器:

    watch(要监视的响应式数据, (新值, 旧值) => { /* 回调函数的操作 */ })
  3. 例如:

    const { createApp, ref, watch } = Vue
    
    const app = createApp({
     setup() {
       // 1. 从本地存储中读取初始值
       const initValue = localStorage.getItem('count') || 0
       // 2. 将初始值转为整数后,赋值给 count
       const count = ref(parseInt(initValue))
    
       const add = (step) => {
         count.value += step
       }
    
       // 3. 监听 count 的变化,并把新值存储到本地
       watch(count, (newValue) => {
         // BOM 的 API
         localStorage.setItem('count', newValue)
       })
    
       return {
         count,
         add
       }
     }
    })
    
    app.mount('#app')

    对应的模板为:

    <div id="app">
     <h1>{{ count }}</h1>
     <button @click="add(1)">+1</button>
     <button @click="add(2)">+2</button>
     <button @click="add(3)">+3</button>
    </div>

3.2 停止侦听器

调用 watch 函数会创建一个侦听器,并返回一个终止侦听器的函数,语法格式如下:

const unwatch = watch()

// 当侦听器不再需要时,可调用 unwatch 停止它
unwatch()

我们可以改造一下前面的例子,当 count >= 20 时,我们希望停止侦听器的执行,并且移除本地存储,代码如下:

// 监听 count 的变化,并把新值存储到本地
const unwatch = watch(count, (newValue) => {
  if (newValue >= 20) {
    console.log('数值>=20了')
    unwatch() // 停止侦听器
    localStorage.removeItem('count') // 移除本地存储的数据
    return
  }
  localStorage.setItem('count', newValue)
})

3.3 侦听的数据源类型

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。

1. 侦听 ref 数据源

setup() {
  // 1. 定义 ref 类型的响应式数据
  const x = ref(0)

  // 监听 ref 数据的变化
  watch(x, (newValue, oldValue) => {
    console.log(newValue, oldValue)
  })

  return {
    x
  }
}

2. 侦听 reactive 数据源

setup() {
  // 1. 定义 reactive 类型的响应式数据
  const obj = reactive({ count: 0 })

  // 2. 监听 reactive 数据的变化
  watch(obj, (newValue, oldValue) => {
    // 注意:这里的 newValue 和 oldValue 是变化前后的同一个对象
    // 所以,它们里面的数据完全一样
    console.log(newValue.count, oldValue.count)
    console.log(newValue, oldValue)
  })

  return {
    obj
  }
}

3. 侦听 reactive 中某个属性的变化(属性值为对象)

  1. 定义 reactive 类型的响应式数据 obj,其中 obj.info 是一个对象:

    const obj = reactive({ count: 0, info: { name: 'zs', age: 18 } })
  2. 如果想侦听 obj.info 下每个属性的变化,并得到变化后的新 obj.info 对象,则可以使用 watch 进行如下的侦听:

    watch(obj.info, (newValue, oldValue) => {
     // 注意:这里的 newValue 和 oldValue 是变化前后的同一个对象
     // 所以,它们里面的数据完全一样
     console.log(newValue, oldValue)
    })
  3. 当我们修改 obj.info.name 的值,或修改 obj.info.age 的值,都会触发此侦听器。得到的 newValue 是变化后新的 obj.info 对象。

4. 基于 getter 侦听 reactive 中某个属性的替换操作(属性值为对象)

  1. 定义 reactive 类型的响应式数据 obj,其中 obj.info 是一个对象:

    const obj = reactive({ count: 0, info: { name: 'zs', age: 18 } })
  2. 如果想侦听 obj.info 对象的替换操作,例如,在定时器中把 obj.info 替换为一个新对象:

    setTimeout(() => {
     obj.info = { username: 'ls', gender: '男' }
    }, 1000)
  3. 此时,使用 watch(obj.info, 回调函数) 无法侦听到对象的替换操作。必须使用 getter 才能侦听到对象的替换操作

    watch(() => obj.info, (newValue, oldValue) => {
     // 注意:这里的 newValue 和 oldValue 是变化前后的不同的对象
     // 所以,它们里面的数据是不同的
     console.log(newValue, oldValue)
    })

    注意:所谓的 getter,指的就是通过箭头函数来访问响应式对象中的某个具体属性的取值操作。

5. 基于 getter 侦听 reactive 中某个值类型数据的变化

  1. 定义 reactive 类型的响应式数据 obj,其中 obj.info 是一个对象:

    const obj = reactive({ count: 0, info: { name: 'zs', age: 18 } })
  2. 如果想侦听 obj.count 的变化,则不能使用 watch(obj.count, 回调函数) 的方式进行侦听。因为 obj.count 返回的是具体的值,不是响应式的数据。因此,必须使用 getter 才能侦听到对象中值类型数据的变化:

    // 注意:reactive 中值类型的数据,只能通过 getter 进行侦听
    watch(() => obj.count, (newValue, oldValue) => {
     // 其中,newValue 值变化后的新值;oldValue 是变化前的旧值
     console.log(newValue, oldValue)
    })

6. 侦听多个数据源组成的数组

  1. 定义 refreactive 类型的响应式数据如下:

    // ref 类型的响应式数据
    const x = ref(0)
    // reactive 类型的响应式数据
    const obj = reactive({ count: 0, info: { name: 'zs', age: 18 } })
  2. 调用 watch 声明侦听器时,提供一个要侦听的数据源的数组,当其中任何一个数据发生变化时,都会触发回调函数的执行:

    watch([x, () => obj.count], ([newX, newCount], [oldX, oldCount]) => {
     console.log(newX, newCount)
     console.log(oldX, oldCount)
    })

3.4 深层侦听器

  1. 假设有如下的响应式数据:

    const obj = reactive({ count: 0, info: { name: 'zs', age: 18 } })
  2. 如果定义了 watch(obj.info, 回调函数) 侦听器,则只能侦听到 obj.info.nameobj.info.age 的修改操作,无法侦听到 obj.info 对象的替换操作。

  3. 如果定义了 watch(() => obj.info, 回调函数) 侦听器,则只能侦听到 obj.info 对象的替换操作,无法侦听到某个具体属性的修改操作。也就是说,目前的情况下鱼和熊掌不可兼得。

  4. 如果既想侦听到对象的替换操作,又想侦听到对象下某个具体属性的修改操作。此时可以给 watch 侦听器提供第3个参数,它是一个配置对象,通过 deep: true 选项即可实现我们的目的,语法格式如下:

    watch(() => obj.info, (newValue, oldValue) => {
     // 1. 当触发 obj.info 对象的替换操作时,会触发此回调函数的执行
     // 2. 当修改 obj.info.name 或 obj.info.age 时,也会触发此回调函数的执行
    }, { deep: true })

3.5 即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。

此时,我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

3.6 回调的触发时机

当你更改了响应式状态,它可能会同时触发 Vue 组件更新侦听器回调

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:

watch(source, callback, {
  flush: 'post'
})

例如,在 watch 侦听器中,如果想得到最新的 DOM 内容,则需要添加 flush: post 选项,否则得到的是 DOM 更新前的旧内容:

setup() {
  // 1. 定义 ref 类型的响应式数据
  const count = ref(0)

  // 2. 侦听 count 的变化
  watch(count, () => {
    // 获取 DOM 元素
    const dom = document.querySelector('#title')
    // 打印内部的文本
    console.log(dom.innerText)
  }, { flush: 'post' })

  return {
    count
  }
}

对应的模板结构为:

<div id="app">
  <h1 id="title">count的值是:{{ count }}</h1>
  <button @click="count++">+1</button>
</div>

3.7 watchEffect()

1. watch 侦听器的缺点

watch 侦听器需要手动维护依赖的列表,如果侦听器所依赖的数据很多,则维护依赖列表的成本会很高,例如:

<body>
  <div id="app">
    <input type="text" v-model="p1">
    <input type="text" v-model="p2">
    <input type="text" v-model="p3">
  </div>

  <script src="./vue3.global.js"></script>
  <script>
    const { createApp, ref, watch } = Vue

    const app = createApp({
      setup() {
        const p1 = ref('')
        const p2 = ref('')
        const p3 = ref('')
        const baseUrl = 'https://api.liulongbin.top/v1/common/get'

        watch([p1, p2, p3], () => {
          fetch(`${baseUrl}?params1=${p1.value}&params2=${p2.value}&params3=${p3.value}`)
            .then(res => res.json())
            .then(result => console.log(result.data))
        })

        return {
          p1,
          p2,
          p3
        }
      }
    })

    app.mount('#app')
  </script>
</body>

我们发现,只要 p1, p2, p3 任何一个发生变化,都会触发 watch 的回调函数,而在回调函数中,我们又把这3个数据当做调用接口的参数

在使用 watch 的时候,我们不得不维护一个 [p1, p2, p3] 的数组,从而明确告诉 watch 要侦听哪些数据的变化。如果要侦听的数据项很多,则每次添加或删除了调用接口的参数时,都需要手动维护这个数组中的元素,非常的麻烦。

例如:下面的例子中,忘记了把 p4 添加到侦听器的依赖数组中,最终导致 p4 变化时,无法触发侦听器的执行:

// 2. 但是,却忘记了把 p4 添加到侦听的列表中
// 导致的问题是:哪怕修改了 p4 的值,也不会触发侦听器的执行
watch([p1, p2, p3], () => {
  // 1. 在回调函数中,我们把 p1, p2, p3, p4 当做请求参数
  fetch(`${baseUrl}?params1=${p1.value}&params2=${p2.value}&params3=${p3.value}&params4=${p4.value}`)
    .then(res => res.json())
    .then(result => console.log(result.data))
})

2. watchEffect()

为了让用户不必维护侦听器的依赖数组,Vue3 提供了 watchEffect() 函数,它会自动收集响应式数据的依赖关系,例如:

watchEffect(() => {
  // 1. 在回调函数中,我们把 p1, p2, p3, p4 当做请求参数
  fetch(`${baseUrl}?params1=${p1.value}&params2=${p2.value}&params3=${p3.value}&params4=${p4.value}`)
    .then(res => res.json())
    .then(result => console.log(result.data))
})

另外,watchEffect() 中的回调函数会立即执行,因此不需要指定 immediate: true

在第一次执行期间,他会自动收集 p1.value, p2.value, p3.value, p4.value 作为依赖。

每当任何一个响应式数据发生变化时,回调函数会再次执行。

因此 watchEffect 让我们不用再明确传递触发回调执行的依赖项

3. watch() vs watchEffect()

相同点:

watch 和 watchEffect 都能侦听响应式数据的变化,并重新执行给定的回调函数。

不同点:

  • watch 只侦听明确给定的依赖项,从而触发回调函数的重新执行
    • 优点:我们可以根据 watch 的依赖项列表清楚地知道哪些依赖项会触发 watch 回调的执行
    • 缺点:必须手动维护依赖项列表,当依赖项多了以后,维护起来很麻烦
  • watchEffect 会在回调函数第一次执行的时候自动收集响应式数据的依赖关系
    • 优点:省去了手动维护依赖项列表的过程,能让代码更简洁
    • 缺点:响应式依赖关系不明确,无法一眼确定哪些依赖项会触发回调函数的执行

4. 类与样式绑定

4.1 绑定 HTML 的 class

每个 HTML 元素都支持使用 class 属性来美化元素的样式,因此 v-bind 指令同样适用于元素的 class 属性,我们可以为 class 动态绑定类名,从而达到动态修改元素样式的目的。

为了演示如何绑定 HTML 的 class,我们先定义如下的一组 class 样式:

.red {
  color: red; /* 红色文字 */
}

.bgyellow {
  background-color: yellow; /* 黄色背景 */
}

.thin {
  font-weight: 200; /* 文字粗细 200 */
}

.fat {
  font-weight: 900; /* 文字粗细 900 */
}

.big {
  font-size: 100px; /* 文字大小 100px */
}

.small {
  font-size: 12px; /* 文字大小 12px */
}

1. 绑定单个 class

定义 ref 响应式数据 cname 并把它暴露给模板:

setup() {
  // 要绑定给元素的 class 类名
  const cname = ref('')

  // 暴露给模板使用
  return {
    cname
  }
}

在模板中,通过 :class="cname" 为元素动态绑定 class 类名:

<h1 class="big" :class="cname">这是一个大标题</h1>

点击 button 按钮时,给 cname 赋值:

<button @click="cname='red'">红色</button>

注意:在给元素提供 class 的时候,可以把静态 class动态绑定的 class 分别应用于同一个元素。

2. 动态切换单个 class

需求:点击按钮,动态为元素切换 thinfat 的 class 类名。

首先,定义布尔值 flag 并暴露给模板使用:

setup() {
  // 定义 ref 类型的布尔值
  const flag = ref(false)

  // 暴露给模板使用
  return {
    flag
  }
}

其次,实现点击按钮控制布尔值的切换:

<button @click="flag = !flag">Toggle</button>

最后,根据布尔值的状态,为元素动态绑定 class 类名:

<h1 class="big" :class="flag ? 'thin' : 'fat'">这是一个大标题</h1>

注意:这里用到了三元表达式,为了模板内容的简洁,我们也可以把三元表达式封装为 computed 计算属性。

3. 基于计算属性动态切换单个 class

定义 cname 的计算属性:

setup() {
  // 定义 ref 类型的布尔值
  const flag = ref(false)
  // 基于 flag.value 的值动态计算要应用给元素的 class 名
  const cname = computed(() => flag.value ? 'thin' : 'fat')

  // 暴露给模板使用
  return {
    flag,
    cname
  }
}

在模板中把 cname 动态绑定为元素的 class

<div id="app">
  <h1 class="big" :class="cname">这是一个大标题</h1>
  <button @click="flag = !flag">Toggle</button>
</div>

4. 绑定 class 对象

为元素动态绑定 class 对象,可以控制切换元素的多个 class,它的语法格式如下:

<!-- 当“布尔值1”为 true 时,会给元素应用“类名A”,否则会移除元素上的“类名A” -->
<!-- 当“布尔值2”为 true 时,会给元素应用“类名B”,否则会移除元素上的“类名B” -->
<h1 :class="{类名A: 布尔值1, 类名B: 布尔值2}">这是一个大标题</h1>

例如下面的例子:

定义 ref 类型的响应式数据 isRedisBgYellow,它们的默认值为为布尔值 false

setup() {
  // 1. 定义 ref 类型的响应式数据
  const isRed = ref(false)
  const isBgYellow = ref(false)

  // 2. 暴露给模板使用
  return {
    isRed,
    isBgYellow
  }
}

在模板中,点击按钮切换这两个布尔值的状态:

<button @click="isRed = !isRed">Toggle Color</button>
<button @click="isBgYellow = !isBgYellow">Toggle Background Color</button>

在模板中,为 h1 元素绑定 class 对象:

<h1 class="big" :class="{red: isRed, bgyellow: isBgYellow}">这是一个大标题</h1>

5. 把 class 对象封装为 reactive 响应式数据

为了简化模板中的代码,我们还可以把动态绑定的 class 对象,封装为 reactive 类型的响应式数据对象:

setup() {
  // 1. 定义 reactive 类型的响应式数据
  const classObj = reactive({
    red: false,
    bgyellow: false
  })

  // 2. 暴露给模板使用
  return {
    classObj
  }
}

在模板中,为 h1 元素绑定 class 对象:

<h1 class="big" :class="classObj">这是一个大标题</h1>

并且在点击按钮的时候,动态切换 classObj.redclassObj.bgyellow 的布尔值,从而决定是否把 class 类名应用给元素:

<button @click="classObj.red = !classObj.red">Toggle Color</button>
<button @click="classObj.bgyellow = !classObj.bgyellow">Toggle Background Color</button>

6. 把 class 对象封装为 computed 计算属性

除了可以把动态绑定的 class 对象封装为 reactive 类型的响应式数据之外,我们还可以把它封装为 computed 计算属性,这也能简化模板中绑定 class 的代码:

setup() {
  // 1. 定义 ref 类型的响应式数据
  const isRed = ref(false)
  const isBgYellow = ref(false)

  // 2. 定义计算属性
  const classObj = computed(() => ({
    red: isRed.value,
    bgyellow: isBgYellow.value
  }))

  // 3. 暴露给模板使用
  return {
    classObj,
    isRed,
    isBgYellow
  }
}

在模板中,为 h1 元素动态绑定 class 对象:

<h1 class="big" :class="classObj">这是一个大标题</h1>

点击按钮时,动态切换布尔值的状态:

<button @click="isRed = !isRed">Toggle Color</button>
<button @click="isBgYellow = !isBgYellow">Toggle Background Color</button>

7. 绑定 class 数组

我们可以为 class 绑定数组,从而为元素应用多个 class:

<h1 class="big" :class="['thin', 'red', 'bgyellow']">这是一个大标题</h1>

也可以通过三元表达式,动态控制某个 class:

<h1 class="big" :class="['thin', 'red', flag ? 'bgyellow' : '']">这是一个大标题</h1>

还可以把三元表达式改造成对象的形式,按需为元素应用 class:

<h1 class="big" :class="['thin', 'red', {bgyellow: flag}]">这是一个大标题</h1>

为了进一步简化模板中的代码,我们还可以把数组封装为 computed 计算属性:

setup() {
  // 1. ref 类型的布尔值
  const flag = ref(false)
  // 2. 计算属性:要应用给元素的样式数组
  const classArr = computed(() => ['thin', 'red', { bgyellow: flag.value }])

  // 3. 暴露给模板使用
  return {
    flag,
    classArr
  }
}

然后,只需要为元素的 class 绑定这个计算属性即可:

<h1 class="big" :class="classArr">这是一个大标题</h1>

4.2 绑定 style 内联样式

1. 绑定对象

:style 支持绑定 JavaScript 的对象值。例如,定义一个 ref 类型的响应式数据:

setup() {
  // 1. 定义 ref 类型的响应式数据
  const fs = ref(12)

  // 2. 把数据暴露给模板使用
  return {
    fs
  }
}

在模板中为 h1 绑定 style 样式对象:

<div id="app">
  <input type="number" v-model="fs">
  <!-- 动态绑定 style 样式对象 -->
  <h1 :style="{color: 'red', fontSize: fs + 'px'}">这是一个大标题</h1>
</div>

注意:如果是连字符格式的样式属性,推荐写成小驼峰的形式。

2. 绑定计算属性

为了保证模板的简洁,我们可以把绑定给 style 的样式对象,封装为 computed 计算属性:

setup() {
  const fs = ref(12)
  // 1. 定义计算属性
  const styleObj = computed(() => ({ color: 'red', fontSize: fs.value + 'px' }))

  // 2. 把数据暴露给模板使用
  return {
    fs,
    styleObj
  }
}

在模板中,直接使用计算属性:

<div id="app">
  <input type="number" v-model="fs">
  <!-- 动态绑定 style 样式对象 -->
  <h1 :style="styleObj">这是一个大标题</h1>
</div>

3. 绑定数组

我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上。例如,声明如下的两个 style 样式对象:

setup() {
  const fs = ref(12)
  // 1. 定义计算属性
  const styleObj = computed(() => ({ color: 'red', fontSize: fs.value + 'px' }))
  // 2. 定义 style 的样式对象
  const styleObj2 = computed(() => ({ fontWeight: 200, backgroundColor: 'yellow' }))

  // 3. 把数据暴露给模板使用
  return {
    fs,
    styleObj,
    styleObj2
  }
}

styleObjstyleObj2 以数组的形式,绑定为 h1style 样式:

<h1 :style="[styleObj, styleObj2]">这是一个大标题</h1>

5. 模板引用

虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:

<input ref="input">

5.1 访问模板引用

那么,在 JS 中如何才能够访问到模板中的元素呢?这就需要我们使用 ref() 函数声明一个同名的响应式数据:

setup() {
  // 1. 定义同名的 ref 数据
  const input = ref(null)

  watchEffect(() => {
    if (input.value) {
      // 3. 如果 input.value 有值,则调用 DOM 的 focus 函数让其获取焦点
      input.value.focus()
    }
  })

  // 2. 并把 ref 数据暴露给模板
  return {
    input
  }
}

5.2 v-for 中的模板引用

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

v-for 循环生成的 li 添加 ref 属性:

<ul>
  <li v-for="item in list" :key="item.id" ref="liRefs">{{ item.name }} --- {{item.age}}</li>
</ul>

在 JS 中使用 ref() 函数声明同名的数据:

setup() {
  // 要渲染的数据
  const list = ref([
    { id: 1, name: 'zs', age: 18 },
    { id: 2, name: 'liulongbin', age: 19 },
    { id: 3, name: 'escook', age: 22 }
  ])

  // 1. 声明同名的 ref 数据
  const liRefs = ref([])

  watchEffect(() => {
    // 3. 循环每个 DOM 元素,根据索引的奇偶性,为 DOM 元素设置对应的 color 颜色
    liRefs.value.forEach((li, i) => li.style.color = i % 2 === 0 ? 'red' : 'blue')
  })

  return {
    list,
    // 2. 并把它暴露给模板
    liRefs
  }
}

5.3 函数模板引用

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。

例如,在定义响应式数据 flag,用来控制页面上 input 元素的显示和隐藏:

setup() {
  // 布尔值
  const flag = ref(true)
  // 把数据暴露给模板使用
  return {
    flag
  }
}

在模板中,通过点击按钮,切换 flag 的值:

<!-- 点击按钮,切换 flag 的值 -->
<button @click="flag = !flag">Toggle</button>

最后,使用 v-if 指令控制 input 的显示和隐藏,并给 input 添加函数模板引用

<!-- 使用 v-if 指令控制 input 元素的显示和隐藏 -->
<!-- 使用 :ref 绑定一个函数,在函数内可以获取到 DOM 的引用 -->
<input v-if="flag" :ref="(el) => el && el.focus()">

版权归作者 ©刘龙宾 所有,本文章未经作者允许,禁止私自转载!

一个自由の前端程序员

留言

您的电子邮箱地址不会被公开。 必填项已用 * 标注