[TOC]

computed

计算属性有什么作用

在前面我们讲解过计算属性computed:computed 被推荐用来描述依赖响应式状态的复杂逻辑。
当我们的某些变量的数据是依赖其他响应式状态数据时,我们可以使用计算属性来处理。

换句话说,计算属性会自动追踪响应式依赖,当依赖的状态发生变化,计算属性就会自动更新,并把更新传递出来。

最典型的就是和 vuex 一起使用。
vuex 的数据是响应式的,并且里面的 state 的数据在不断更新。为了让组件能拿到最新的数据,就需要使用 computed 函数跟踪 state 的变化。

computed 里面有副作用函数 effect,当依赖的响应式数据更新,副作用函数就会执行一次拿到最新的数据。这个副作用函数名字怪怪的,其实就是收集依赖的包裹函数。

基本使用

  • 在前面的Options API中,我们是使用 computed 选项来完成的;
  • 在Composition API中,我们可以在 setup 函数中使用 **computed** 方法来编写一个计算属性;

如何使用computed呢?我们知道 options api 中 computed( ) 函数其实是对象的 get 语法糖,所以可以写成函数形式,也可以写成完整的对象形式。composition api 中也一样,也有类似的两种方式。

  • 方式一:接收一个 getter 函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
  • 方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象; ```html
``` ## 缓存 计算属性是基于依赖的,它的值会被缓存,当它依赖的状态发生变化时,才会重新执行getter函数来计算它的值。而方法在每次调用时都要重新跑一遍。
[3. Options-API](https://www.yuque.com/jinwanchisha-beh08/vmcrnr/ghpq1c?view=doc_embed) ## 注意事项 1. **计算函数不应有副作用,应该只用来处理数据。** 计算属性的计算函数应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。换句话说计算属性应该只用来处理数据。
举个例子,不要在计算函数中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此计算函数的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。 2. **避免直接修改计算属性值,应该修改计算属性所依赖的源** 从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。 3. **计算属性无法追踪非响应式数据** 例如想监听页面的宽度变化,直接在computed()中使用document.querySelector("body").offsetWidth,是无法达到预期的,因为获取到的值不是响应式的,只是一个数字。想实时监听页面宽度,可以使用window.onresize的回调,或者使用其他方法(例如定时器,但是要考虑页面的性能)。
在这个例子中,在模板中使用调用方法,也不能实现实时监听宽度变化,因为模板中的方法只在渲染的时候调用一次,随后dom不再重新渲染的话,就获取不到最新的宽度。可以考虑使用nextTick()或者其他方法。 # watchEffect watchEffect 是一个监听器函数,但它有一个特点,能自动监听使用到的可响应式依赖。 - `watchEffect(fn)` 它接收一个监听函数做参数,并且会立即执行一次监听函数,相当于对监听函数中的代码做了一次分析,分析出哪些是可响应式的依赖,然后对他进行自动监听。 ```html ``` ## watchEffect 的停止侦听 watchEffect的返回值是一个函数,调用这个函数就能停止监听。
比如:当 age 达到20的时候就停止侦听: ```javascript setup() { const obj = reactive({name: 'ls', age: 19}) const stop = watchEffect(() => { console.log(obj.age); }) function changeName() { obj.age++ < 25 || stop() } return { obj } } ``` ## watchEffect 清除副作用 什么是清除副作用呢?
比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。
那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用; 副作用应该指的是整个回调函数。 watchEffect 的参数函数中有一个形参 `onInvalidate`,它其实也是个函数,并且接收一个函数作参数。
当副作用(比如网络请求)即将重新执行 或者 侦听器被停止 时会执行该函数,也就是在监听器开始发挥作用前或者一次监听结束后都会执行该函数。
所以我们可以在这个函数中做一些收尾工作,或者说初始化工作,清除副作用。 ```javascript setup() { const obj = reactive({name: 'ls', age: 19}) const stop = watchEffect((hhh) => { // 副作用函数 console.log(obj.age); // 监听到 age 变化,再次发送网络请求,副作用函数,不是这个 const timer = setTimeout(() => { console.log('success'); }, 2000); // 清除副作用 hhh( () => { // 在2s内一直改变 age,success 还没来得及打印,定时器就会被取消 // 相当于疯狂点击发送网络请求,结果还没来,请求就被主动取消,清除副作用 clearTimeout(timer) }) }) function changeName() { obj.age++ } return { obj } } ``` ## setup 中使用 ref 获取 dom 为了获取 dom 元素,我们可以在标签上添加一个 `ref`attribute,然后通过 `this.#refs` 获取 dom 元素。可是 setup 中并没有绑定 this,该怎么获取呢?其实直接使用 `ref()`就行。 ```html ``` 可能会有个疑惑,上面的 element 明明是个 ref 对象,里面的 value 肯定是 123 啊,怎么又变成 dom 元素了,前面学的 ref 使用都是错的?
那是**因为 **`**setup()**`**会在组件挂载前就执行,所以之前使用 ref.value 都没有问题,组件挂载后,dom 元素才会赋值给绑定的 ref 对象。**
这是一个执行时机的问题,那怎么才能拿到被 dom 元素已经赋值的 element 呢?
可以用周期函数,也可以用 `watchEffect` ## watchEffect 的执行时机 为什么 watchEffect 可以?
因为在挂载之前 setup 执行,watchEffect 的副作用函数也会在这时立即执行,扫描代码中的可迭代依赖。之后 dom 挂载,这个时候 element 被赋值,watchEffect 发挥监听的作用监听到了变化,副作用函数再次执行,从而可以拿到被 dom 元素赋值的 element ```html ``` ### 调整执行时机 如果我们希望在第一次的时候就打印出来对应的元素呢?
这个时候我们需要改变副作用函数的执行时机;`watchEffect()`还有第二个参数,
它的默认值是 pre,它控制副作用函数在元素 挂载 之前执行;所以我们会执行两次副作用函数。 - `post`控制为挂载后执行。 ```javascript setup() { const element = ref(123) watchEffect( () => { console.log(element.value); //

hhh

},{ flush: 'post' // 控制在挂载之后执行 }) return { element } } ``` flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。 # watch `watch()`的API完全等同于 options api 的 watch 选项:watch 需要侦听特定的数据源,并在回调函数中执行副作用函数;默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调; 与 watchEffect 的比较,watch 允许我们: - 懒执行副作用(第一次不会直接执行); - 可以更具体的说明是哪些依赖状态发生变化,触发侦听器的执行; - 可以访问侦听状态变化前后的值; ## 侦听单个数据源 watch侦听函数的数据源有两种类型: - **一个getter函数**,就是读取数据源的普通函数:但是该getter函数**必须引用可响应式的对象**(比如reactive或者ref); - newValue 和 oldValue 就是数据源变化前后的普通值 - 直接写入**一个可响应式的对象**,reactive或者ref(比较常用的是ref); - 响应式对象是 reactive - newValue 和 oldValue 也是 reactive 对象 - 如果想要 newValue 和 oldValue 变成普通的值,则可以使用解构 - 响应式对象是 ref - newValue 和 oldValue 都是普通值 > 有一个细节要注意: > 就是如果监听的是对象或数组,那么 newValue 和 oldValue 都指向改变后的对象,他们值是一样的。oldValue 不会保存变更前的对象。**因为传入到 watch 参数函数中的是对数据源对象的一个浅拷贝。** > 一般对对象、数组进行响应式处理的是 reactive 函数,那么 newValue 和 oldValue 都是同一个 reactive 对象。 ```html

<a name="dVocN"></a>
## 侦听多个数据源
<a name="P3alz"></a>
### 对象形式
多个源就是将单个源放到一个数组里,然后再遍历数组进行单个数据源监听,本质和单个源差不多。数据源是个数组,newValue 和 oldValue 也是数组,里面数组元素对应着数据源的值。所以数组元素会很杂。<br />比如数据源有 reactive 对象和 ref 对象,则值的数组中就有 Proxy 对象和普通的值
```html
<template>
  <div>
    <h2 ref="title">{{info.name}}</h2>
    <button @click="changeData">修改数据</button>
  </div>
</template>

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

  export default {
    setup() {
      const info = reactive({name: "why", age: 18});
      const name = ref("why");

      watch([info, name], (newValue, oldValue) => {
        console.log("newValue:", newValue, "oldValue:", oldValue);
        // newValue: [Proxy{kobe}, hhh] oldValue: [Proxy{kobe}, why]
      })

      const changeData = () => {
        info.name = "kobe";
        name.value = 'hhh'
      }

      return {
        changeData,
        info
      }
    }
  }
</script>

侦听响应式对象 - getter 函数形式

如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并对可响应对象进行解构:就和 reactive 对象解构将 newValue 和 oldValue 变普通值一样。

setup() {
  const objproxy = reactive(['a', 'b', 'c'])

  // 注意这里解构不能带上花括号,() => {[...objproxy]},要不然监听函数不执行
  watch( () => [...objproxy], (newValue, oldValue) => {
    console.log("newValue:", newValue, "oldValue:", oldValue);
    // newValue: (3) ['a', 'h', 'c'] oldValue: (3) ['a', 'b', 'c']
  })

  const changeData = () => {
    objproxy[1] = 'h'
  }

  return {
    changeData,
    info
  }
}

watch 的选项

对深层的对象或数组侦听,那么依然可以设置 deep 为 true:也可以传入 immediate 立即执行;

  • 在 watch 函数第三个参数中进行配置 deep 和 immediate,并且默认就是深层监听

    setup() {
    const obj = reactive({
      name: 'zs', 
      friends: { name: 'ls', age: 18}
    })
    
    watch( obj, (newValue, oldValue) => {
      console.log('监听到了变化');
    }, {
      // deep: true,
       immediate: true
    })
    
    const changeData = () => {
      obj.friends.name = 'w5'
    }
    
    return {
      changeData
    }
    }
    

    周期函数

    我们前面说过 setup 可以用来替代 data 、 methods 、 computed 、watch 等等这些选项,也可以替代 生命周
    期钩子。
    可以使用直接导入的 onX 函数注册生命周期钩子;
    composition api 中没有与之对应的 created 和 beforeCreated 钩子,因为 setup 本身执行就是在 created 之前,所以在组件创建之前的代码都可以直接写在 setup 中。
    另外 composition api 中可以定义多个相同时期的钩子。
    image.png ```html

<a name="TQuyy"></a>
# provide \ inject
对于非父子间传值,Composition API也可以替代之前的 Provide 和 Inject 的选项。<br />我们可以通过 provide 方法来提供数据:在 后代组件 中通过 inject 方法来注入需要的属性和对应的值:<br />可以通过 provide 方法来定义每个 Property;<br />provide 可以传入两个参数:

- name:提供的属性名称;
- value:提供的属性值;

inject 也可以传入两个参数,接收的属性和若没有接收到属性下的默认值

传递的数据一般需要进行响应式,所以要进行响应式处理。并且数据传递都要遵循单向数据流原则,使用方只管使用数据,不应该修改数据。所以 provide 时不仅要响应式处理,还要进行只读处理。
```html
<script>
  import { readonly, ref } from '@vue/reactivity'
  import { provide } from '@vue/runtime-core'
  import about from './components/about.vue'
  export default {
    components: { about },
    setup() {
      // 响应式处理数据
      const message = ref('hhh')
      // 单向数据流处理
      provide('message', readonly(message)) 
    }
  }
</script>
---------about.vue--------
<script>
  import { inject } from '@vue/runtime-core'
  export default {
    setup() {
      const msg = inject('message')
      return { msg }
    }
  }
</script>

hook

composition api 解决了 options api 的代码分散并且和各个 option 耦合度极高的问题。现在代码都写在 setup 中,但我们还能进一步的解耦。比如将 setup 中一个个独立的功能抽离出来封装成一个模块。
这个模块称为 hook,一般以 use 开头进行命名。hook 意为钩子,是 react 中的概念,但被 vue 社区广泛借用。

例如:一个计数器案例

<template>
  <div>
    <h2> {{ data }}</h2>
    <button @click="increase">+1</button>
    <button @click="reduce">-1</button>
  </div>
</template>

<script>
import { ref } from '@vue/reactivity'
  export default {
    setup() {
      // setup 中实现计数器
      const data = ref(0)
      const increase = () => { data.value++ }
      const reduce = () => { data.value-- }

      return { data, increase, reduce}
    }
  }
</script>
import { ref } from '@vue/reactivity'

export function useCounter() {
  // 将计数器实现从 setup 中抽离
  const data = ref(0)
  const increase = () => { data.value++ }
  const reduce = () => { data.value-- }

  return { data, increase, reduce}
}
<template>
  <div>
    <h2> {{ data }}</h2>
    <button @click="increase">+1</button>
    <button @click="reduce">-1</button>
  </div>
</template>

<script>
  // 导入 hook
  import { useCounter } from './hooks/useCounter.js'
  export default {
    setup() {
      // 解构获取 hook 中的导出响应式数据和方法,并且可以直接使用
      const {data, increase, reduce } = useCounter()

      return {data, increase, reduce}
      // 也可以利用展开运算符直接将 hook 中的导入在 setup 中导出,但可读性太差不建议使用
      // return { ...useCounter() } 
    }
  }
</script>

一些其他 hook
image.pngimage.png
image.pngimage.png