Composition API

Vue3Composition API

这个API仅仅是Vue3.0中新增的API,我们依然可以使用Options API,

# 新增API

下面要介绍三个新增API

  • createApp:创建vue对象
  • setUp:Composition API的入口
  • reactive:创建响应式对象

# createApp

createApp

createApp的作用是可以创建一个vue对象,可以接受一个选项作为参数,参数中可以设置属性data,computed,mounted、created、methods等

createApp返回一个app,即Vue对象,和2.x的不同

不同的点:

  1. 成员比Vue2少很多
  2. 而且都没有使用$开头,说明未来基本不用给这个对象新增成员
  3. mount相当于$mount,unmount类似$destroy方法

首先在安装了Vue3.0的项目中,创建html















 
 












 




<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>createApp</title>
</head>
<body>
  <div id="app">
    x: {{ position.x }} <br/>
    y: {{ position.y }}
  </div>
  <script type="module">
    import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'
    const app = createApp({
      // data这里统一了函数的写法,不支持对象的写法
      data () {
        return {
          position: {
            x: 0,
            y: 0
          }
        }
      }
    })
    console.log(app)
    // 调用vue对象的mount方法,这个mount方法类似于$mount,把实例挂载到指定的位置
    app.mount('#app')
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# setup

setup

两个参数:
第一个参数是 props,是响应式对象,其不能被解构
第二个参数是 context 是个对象,有三个成员 attrs、emit、slots

返回值:
setup 返回一个对象,这个对象可以使用到模板、computed、methods及生命周期钩子函数里

执行时机: 在props被解析完毕,在组件实例被创建之前执行的。

注意事项:

  1. 无法通过this获取组件实例,因为组件实例还未被创建,这个时候this是undefined,
  2. 无法访问组件中的data、methods、computed。

setup属性是composition API的入口。









 
 
 
 
 
 
 
 
 
 
 
 
 
 
 







<body>
  <div id="app">
    x: {{ position.x }} <br/>
    y: {{ position.y }}
  </div>
  <script type="module">
    import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'
    const app = createApp({
      setup () {
        // 普通对象,并不是响应式对象
        const position = {
          x: 0,
          y: 0
        }
        // 将position放入setup返回的对象里,这个对象可以使用到模板、computed、methods及生命周期钩子函数里
        return {
          position
        }
      },
      mounted () {
        // 因为这里组件实例已经创建完毕,所以可以通过this访问到position
        this.position.x = 100
      }
    })
    console.log(app)

    app.mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

上面的例子,运行之后发现position.x并没有修改,因为position并不是响应式数据,如果想要将position弄成响应式数据,那么需要使用reactive函数。

# reactive

reactive

参数是对象,返回一个响应式对象

功能:将对象转化成响应式对象,并且该对象的嵌套属性也应该成为响应式对象

设置成响应式数据,setup中还可以使用data,但是在Vue3.0中还有一个API可以让我们用reactive创建响应式对象。












 
 
 
 














<body>
  <div id="app">
    x: {{ position.x }} <br/>
    y: {{ position.y }}
  </div>
  <script type="module">
    // 在使用reactive之前,需要先导入API
    import { createApp, reactive } from './node_modules/vue/dist/vue.esm-browser.js'
    const app = createApp({
      setup () {
        // 这里的使用和observable作为函数名是避免和另一个函数库rxjs重名出现混淆。
        const position = reactive({
          x: 0,
          y: 0
        })
        return {
          position
        }
      },
      mounted () {
        this.position.x = 100
      }
    })
    console.log(app)

    app.mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

这样就可以看到position.x的值进行了改变。

# 生命周期钩子函数

案例中注册mousemove事件可以在mounted中实现,但是最终的目标是获取鼠标的整个逻辑封装到一个函数中,让任何组件都可以重用,这个使用就不适合用mounted选项,其实我们在setup中也可以使用生命周期的钩子函数。

可以看看setup里面对应的钩子函数名称,就是在前面加了on,首字母大写。

Options API setup Hook inside
beforeCreate not needed *
created not needed *
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmouned onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

其中setup是在beforeCreate和created两个函数中间执行的,所以并不需要单独领出来。

renderTracked和renderTriggered都是调用render的时候触发的,不同的是,renderTracked是首次调用render的时候也会触发。renderTriggered在首次调用的时候不会触发。

# 案例

跟着上面的案例,我们的目标:

  • 注册鼠标移动的事件,当鼠标移动的时候显示当前鼠标的位置
  • 当组件被卸载的时候,鼠标被移动的事件也要被移除







 














 
 
 


 
 
 















<body>
  <div id="app">
    x: {{ position.x }} <br/>
    y: {{ position.y }}
  </div>
  <script type="module">
    // 导入onMounted, onUnmounted函数
    import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
    const app = createApp({
      setup () {
        const position = reactive({
          x: 0,
          y: 0
        })

        // 接收一个参数,事件对象e,把鼠标的位置记录到position的x和y中
        const update = e ={
          position.x = e.pageX
          position.y = e.pageY
        }

        // onMounted生命周期触发的时候创建侦听器,监听mousemove事件
        onMounted(() ={
          window.addEventListener('mousemove', update)
        })
        
        // onUnmounted生命周期触发的时候移除侦听器,移除mousemove事件处理函数
        onUnmounted(() ={
          window.removeEventListener('mousemove', update)
        })
        
        return {
          position
        }
      },
      mounted () {
        this.position.x = 100
      }
    })
    console.log(app)

    app.mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

这个时候把页面打开,可以看到鼠标移动,x和y的值跟着改变,下面我们要定义一个函数,把功能封装进去








 

 



















 





 














<body>
  <div id="app">
    x: {{ position.x }} <br/>
    y: {{ position.y }}
  </div>
  <script type="module">
    // 导入onMounted, onUnmounted函数
    import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
    // 定义一个函数,将实现逻辑写进去。
    function useMousePosition () {
      const position = reactive({
        x: 0,
        y: 0
      })

      const update = e ={
        position.x = e.pageX
        position.y = e.pageY
      }

      onMounted(() ={
        window.addEventListener('mousemove', update)
      })
      
      onUnmounted(() ={
        window.removeEventListener('mousemove', update)
      })

      // 将position返回,让组件可以使用position
      return position
    }

    const app = createApp({
      setup () {
        // 调用函数,并接收position让组件使用
        const position = useMousePosition()
        return {
          position
        }
      },
      mounted () {
        this.position.x = 100
      }
    })
    console.log(app)

    app.mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 创建响应式数据API

  • reactive
  • toRefs
  • ref

这三个函数都是创建响应式数据的,刚才的鼠标移动案例中,使用reactive有一个小问题,如果在插值表达式里面不想写

{{ position.x }}

而是写

{{ x }}

,那么要对响应式的position进行解构

const {
    x,
    y
} = useMousePosition()
1
2
3
4

这样运行发现,鼠标的值根本无法获得。这是为什么呢?

因为我们创建position响应式对象,就是创建了一个proxy对象,我们调用访问position.x和position.y就是调用proxy的getter拦截收集依赖,当x和y变化之后就调用setter进行拦截触发更新。

当我们进行解构的时候,就相当于定义了一个x和一个y变量去接收position.x和position.y,而基本类型的赋值就是把内存中的内容复制一份,这两个变量和proxy对象无关,重新赋值的时候也不会调用代理对象的setter。

那如果我们就想这么做怎么办?介绍一个新的API —— toRefs

# toRefs

它可以把一个响应式对象的所有属性,也转换成响应式的。

toRefs

参数:
传入的参数必须是一个代理对象,如果position不是代理对象就会警告。

功能:
会遍历代理对象的属性,将其转化成响应式对象,然后再挂载到新创建的对象上面,将新创建的对象返回。

原理:
其内部会对代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的。value属性里面有getter和setter,getter里面返回代理对象对应属性的值,setter中给代理对象的属性赋值,所以返回的每一个属性都是响应式的。
value在插值表达式中可以省略,但是在代码中不能省略。








 








 





















<body>
  <div id="app">
    x: {{ x }} <br/>
    y: {{ y }}
  </div>
  <script type="module">
    // 导入toRefs函数
    import { createApp, reactive, onMounted, onUnmounted, toRefs } from './node_modules/vue/dist/vue.esm-browser.js'
    // 定义一个函数,将实现逻辑写进去。
    function useMousePosition () {
      const position = reactive({
        x: 0,
        y: 0
      })
      ...
      // 使用toRefs将position中的属性也转化成响应式数据
      return toRefs(position)
    }

    const app = createApp({
      setup () {
        // 解构position
        const {
          x,
          y
        } = useMousePosition()
        return {
          x,
          y
        }
      }
    })
    console.log(app)

    app.mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# ref

它是将普通数据可以转换成响应式数据,和reactive不同的是,reactive是将对象转换成响应式数据,ref是可以将基本类型的数据包装成响应式对象。

ref

参数:
如果是对象,那么函数内部调用reactive会返回一个代理对象;
如果是基本类型的值,那么会创建一个value属性的对象,value属性中会有getter和setter,getter收集依赖,setter触发更新。并返回。

返回值:
如果参数是对象,返回一个代理对象
如果参数是基本类型,返回一个有value属性的对象

通过一个简单的点击按钮+1的案例说明:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>点击按钮自身+1</title>
</head>
<body>
  <!-- 点击按钮自身+1 -->
  <div id="app">
    <button @click="increase">按钮</button>
    <!-- 在模板中使用可以省略value -->
    <span>{{ count }}</span>
  </div>
  <script type="module">
    import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js'

    function useCount () {
      // 如果直接等于0不是响应式的
      // const count = 0
      // 调用ref是响应式数据,返回的是一个对象
      // 这个对象中只有一个属性是value,value的值为0,且有getter和setter,用于拦截访问和赋值
      const count = ref(0)
      return {
        count,
        increase: () ={
          count.value++
        }
      }
    }

    createApp({
      setup () {
        return {
          // 将useCount返回的对象进行解构,直接return
          ...useCount()
        }
      }
    }).mount('#app')
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 用法

  • 第一种用法:watch(() => count.value + 1)

传入一个获取值的函数,函数内部依赖响应式的数据,当依赖的数据发生变化之后会重新执行该函数获取数据。computed函数返回一个不可变的响应式对象,类似于使用ref创建的对象,只有一个value属性,获取计算属性的值要通过value获取,模板中使用计算属性可以省略value。

  • 第二种用法:

传入一个对象,这个对象具有getter和setter,返回一个不可变的响应式对象,当获取值的时候会触发这个值的getter,当设置值的时候会触发这个值的setter

const count = ref(1)
const plusOne = computed({
    get: () => count.value + 1,
    set: val => {
        count.value = val - 1
    }
})
1
2
3
4
5
6
7

# 案例

  • 点击按钮,添加一个代办选项
  • 显示未完成数量



 















 
 
 


 













<body>
  <div id="app">
    <button @click="push">按钮</button>
    <p>未完成: {{ activeCount }}</p>
    <ul>
      <li v-for="todo in todos" :key="text">{{ todo.text }} -- {{ todo.completed }}</li>
    </ul>
  </div>
  <script type="module">
    import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'
    const data = [
      { text: '看书', completed: false },
      { text: '写代码', completed: false },
      { text: '约会', completed: true },
    ]

    createApp({
      setup () {
        const todos = reactive(data)
        const activeCount = computed(() => {
          return todos.filter(item => !item.completed).length
        })

        return {
          activeCount,
          todos,
          push: () => {
            todos.push({
              text: '开会',
              completed: false
            })
          }
        }
      }
    }).mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# watch

和computed类似,我们可以在setup中使用watch函数创建一个侦听器。他的用法和之前的this.$watch和选项中的watch作用是一样的,监听响应式数据的变化,然后执行回调函数。可以获取到监听数据的新值和旧值。

Watch

参数:

  1. 要监听的数据(可以是一个ref,也可以是reactive返回的对象,还可以是数组。)
  2. 监听到数据变化后执行的函数,这个函数有两个参数分别是新值和旧值。
  3. 选项对象,deep (深度监听)和 immediate(立即执行)

返回值: 取消监听的函数

# 案例

监听文本框的内容,内容更改就发送接口获取答案。












 







 

















<body>
  <div id="app">
    <p>
      请问一个 yes/no 的问题:
      <input v-model="question">
    </p>
    <p>{{ answer }}</p>
  </div>

  <script type="module">
    // https://www.yesno.wtf/api
    import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'
   
    createApp({
      setup () {
        const question = ref('')
        const answer = ref('')

        // 第一个参数vue2.x这里传的是字符串,现在可以是ref返回的对象
        watch(question, async (newValue, oldValue) => {
          // fetch返回的是promise对象
          const response = await fetch('https://www.yesno.wtf/api')
          // json返回的也是一个promise对象
          const data = await response.json()
          // 这里获取值要用value
          answer.value = data.answer
        })

        return {
          question,
          answer
        }
      }
    }).mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# watchEffect

vue3.0中新增函数,是watch函数的简化版本,也用来监视数据的变化。内部实现和watch调用的是同一个函数,不同的是watch没有第二个回调函数的参数。

watchEffect

参数:
WatchEffect接收一个函数作为参数,监听函数内响应式数据的变化。它会立即执行一次这个函数,当数据变化之后会重新运行该函数。

返回值:
返回一个取消监听的函数




 





 







 
 
 


 











<body>
  <div id="app">
    <button @click="increase">increase</button>
    <button @click="stop">stop</button>
    <br>
    {{ count }}
  </div>

  <script type="module">
    import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js'
   
    createApp({
      setup () {
        // count是一个响应式数据,初始值是0
        const count = ref(0)
        // watchEffect接收一个函数参数,数据变化的执行执行该函数
        // 返回stop,stop是停止监听的函数
        const stop = watchEffect(() => {
          console.log(count.value)
        })

        return {
          count,
          stop,
          // 增加,数值+1
          increase: () => {
            count.value++
          }
        }
      }
    }).mount('#app')
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

打开浏览器可以看到,每点击一次increase,count的值就+1,然后在控制台中也会打印count的数值。再点击stop之后,点击increase的时候,count的数值还在增加,但是控制台中不会再打印count的值了。

# 自定义指令

Vue2.x和Vue3.0的自定义指令不同,主要是自定义指令的钩子函数被重命名,Vue3.0中把钩子函数的名称和组件钩子函数的名称保持一致了,这样很容易理解,但是自定义指令钩子函数和组件钩子函数的执行方式是很不一样的。

// Vue2.x
Vue.directive('editingFocus', {
    bind(el, binging, vnode, prevVnode) {},
    inserted() {},
    update() {}, // remove
    componentUpdated() {},
    unbind() {}
})
1
2
3
4
5
6
7
8

Vue3.0有三组,挂载mount,更新update和卸载unmount。

// Vue3.0
app.directive('editingFocus', {
    // 挂载到DOM树
    beforeMount(el, binging, vnode, prevVnode) {},
    mounted() {},
    beforeUpdate() {}, // new
    updated() {}, 
    beforeUnmount() {}, // new
    unmounted() {}
})
1
2
3
4
5
6
7
8
9
10

而函数简写的方式两者并没有什么不同:

  • Vue2.x是在bind和update的时候执行
    • el 参数是指令绑定的那个元素
    • binding 参数可以获取到指令对应的值,通过binding.value来获取
// Vue2.x
Vue.directive('editingFocus', (el, binding) => {
    binding.value && el.focus()
})
1
2
3
4
  • Vue3.0是在mounted和updated的时候执行。
// Vue3.0
app.directive('editingFocus', (el, binding) => {
    binding.value && el.focus()
})
1
2
3
4
更新时间: 2022-01-10 09:52