Vue 3教程 - Composition API


什么是Composition API

在旧版本的VUE中,使用Options API,这样对一个数据属性的声明(data),初始化(mounted),衍生(computed),被分散到不同的方法中。在这种方法中,并没有通过逻辑进行分组。这样,当项目规模变大时,代码就变得难以维护。

Composition API和React Hooks的设计理念非常类似。通过Composition API,可以:

  • 将逻辑集中到setup函数中
  • 更加容易的创建可重用的逻辑/函数

Composition API并不是强制性的,而是一个可选的特性。通常在规模较大的项目中,推荐使用Composition API。

setup是所有Composition API表演的舞台

JavaScript
export default {
  setup() {
    // data

    // methods

    // computed

    // lifecycle hooks
  }

}

简单示例

markup
<template>
  <div>
    <p>My id is {{ id }} and my name is {{ name }}
    <button @click="register">Click me</button>  
  </div>  
</template>

<script>
export default {
  setup() {
    let id = '9901';
    let name = 'George';
    const register = () => {
      console.log('Registering...');
    }

    return { id, name, register };
  }
}
</script>  

需要注意,在上面setup中定义的id/name不是reactive的,也就说,当其值发生变化时,UI并不会自动更新。而在传统的data()方法中,所有属性值都是reactive的。

强烈建议不要把Vue 2中的data/methods/computed等配置和Vue 3 setup中的配置混用。

  • 一旦混用,在Vue 2中可以读取Vue 3 setup中的属性,方法。但在Vue 3的setup中,无法读取Vue 2中的data/methods/computed等。
  • 如果两者冲突,则以Vue 3为主
  • 不能在setup前添加async修饰符
  • setup会在beforeCreate之前运行,且只执行一次。同时当时的this为undefined。

关于setup

  • setup发生于beforeCreate声明周期钩子之前
  • 在setup中this的值为undefined
  • setup只接受两个参数:props和context(包含attrs, emit, slots)

slot

在Vue3中,推荐这种方式:

markup
<Test>
  <template v-slot:slot1>
    <div>test</div>
  </template>  
</Test>  

在setup中返回渲染函数

如果在setup中返回一个渲染函数,则可以自定义渲染内容,模板中内容将被忽略。

markup
<script>
  import {h} from 'vue'
  export default {
    name: 'App',
    setup() {
      return () => h('h2', '逻思编程')
    }
  }
</script>  

setup中可以接受两个参数:props, context

使用ref

Vue中的ref和React中的useRef非常类似,通过ref,可以使得setup中的变量reactive。 其原理是通过ref创建了一个引用对象

  • 对于基本类型,其底层实现方式是通过Object.defineProperty()中的gettter/setter实现的。
  • 对于对象类型,其底层实现是借助于Vue 3中的reactive函数。
markup
<template>
  <p>Name: {{ name }}</p>
  <input type="text" v-model="name" />
  <button @click="handleClick">Click me</button>
</template>

<script>
import { ref } from "vue";

export default {
  name: "App",
  setup() {
    const name = ref("George");

    const handleClick = () => {
      console.log(name, name.value);
      name.value = "Paul";
    };

    return { name, handleClick };
  },
};
</script>

需要注意的是,在返回ref variable之前,无法引用其值及对应的属性。这是因为尚未将其和对应的DOM元素绑定。另外,ref对应的变量链接到对应的DOM元素,访问其值的方式为:name.value。

对于上面文本框的绑定,也可以通过这种方式:

markup
<input type="text" v-model="name" />

注意:在对象上使用ref的时候,需要通过如下方式获取其值/赋值:

Javascript
const student = ref({
  id: "9901",
  name: "Paul"
})

// ...
student.value.id = '9902';

reactive

reactive用于对象类型的响应式数据。

ref vs reactive:

  • 在ref中既可以使用基本类型,也可以使用对象类型。reactive中不能使用基本类型(primitive types)
  • reactive通过Proxy实现响应式。但ref通过Object.defineProperty的getter/setter来实现数据劫持/响应式的。

建议对于复杂对象,使用reactive;一般对象则使用ref就足够了。

在使用reactive的时候,无需通过value属性来访问对应的值。

在Vue3中,通过使用Proxy来捕获对象中属性的变化,并使用Reflect对源对象的属性进行操作。

JavaScript
<template>
  <p>Name: {{ student.name }}</p>
  <input type="text" v-model="student.name" />
  <button @click="handleClick">Click me</button>
</template>

<script>
import { reactive } from "vue";

export default {
  name: "App",
  setup() {
    const student = reactive({ id: "9901", name: "George" });

    const handleClick = () => {
      console.log(student);
      student.name = "Paul";
    };

    return { student, handleClick };
  },
};
</script>

toRefs

在上面代码中也可以返回id及name,但这样就会丧失reactivity:

JavaScript
setup() {
  const student = reactive({ id: "9901", name: "George" });

  // ...

  return { name: student.name, handleClick };
}

这样当更新name的值时,Vue并不会更新UI。 这时可以使用toRef/toRefs来达到这个目标:

JavaScript
import { reactive, toRef } from 'vue'

setup() {
  const student = reactive({ id: "9901", name: "George" });

  // ...

  return {
    name: toRef(student.name)
  };
}

或者使用toRefs来返回某个对象的所有属性:

JavaScript
return {
  ...toRefs(student)
}

在Composition API中使用方法

markup
<template>
  <div>{{ count }}</div>
  <button @click="increase">+</button>
</template>

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

  export default {
    // ...
    setup() {
      const count = ref(0)
      function increase() {
        count.value++;
      }
      return {
        count, increase
      }
    }
  }
</script>

判断变量类型的几个函数

  • isRef
  • isReactive
  • isReadonly
  • isProxy

computed values

基本用法

JavaScript
import { computed } from 'vue';

export default {
  setup() {
    const fullName = computed(() => {
      return `${lastName}, ${firstName}`;
    })

    return { fullName };
  }
}

使用计算属性过滤信息

再来看一个简单的例子:根据用户输入的关键字过滤学生数据,然后进行显示:

markup
<template>
  <input type="text" v-model="keyword" />
  <div v-for="student in filteredStudents" :key="student.id">
    {{ student.name }}
  </div>
</template>

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

export default {
  name: "App",
  setup() {
    const keyword = ref("");
    const students = ref([
      { id: "9901", name: "George" },
      { id: "9902", name: "Paul" },
      { id: "9903", name: "Lucy" },
    ]);

    const filteredStudents = computed(() => {
      return students.value.filter((student) =>
        student.name.includes(keyword.value)
      );
    });

    return { keyword, students, filteredStudents };
  },
};
</script>

可读写的计算属性

JavaScript
setup() {
  let fullName = computed({
    get() {
      return `${student.firstName} ${student.lastName}`
    },
    set(value) {
      const names = value.split(' ')
      student.firstName = names[0]
      student.lastName = names[1]
    }
  })
}

同时可以将计算属性绑定到reactive对象上:

JavaScript
student.fullName = computed(() => {
  return `${student.firstName} ${student.lastName}`
})

watch & watchEffect

一般用法

watch的作用就是监视某个变量值的变化,比如:

JavaScript
setup() {
  const keyword = ref('');

  const stopWatch = watch(keyword, (newValue, oldValue) => {
    // ...
  })
}

监视两个变量

JavaScript
setup() {
  const username = ref('');
  const password = ref('');

  watch([username, password], (newValues, oldValues) => {
    console.log(newValues[0], oldValues[0])
  })
}

即刻生效的watcher

在前面的例子中,页面首次加载时watcher并不会生效,只有在值发生变化时,才会生效。

如果想要即刻生效的watcher,可以给watch函数添加第三个参数:

JavaScript
watch(keyword, (newValue, oldValue) => {
  // ...
}, {immediate: true})

结束watch

如果想要结束watch,只需要调用watch返回的回调函数stopWatch就可以:

JavaScript
stopWatch();

和reactive的结合使用

JavaScript
import { reactive, toRefs } from 'vue'

export default {
  name: 'WatcherDemo',
  setup() {
    const state = reactive({
      id: '',
      name: ''
    });
    watch(state, function(newValue, oldValue) {
      console(oldValue.id, newValue.id);
      console(oldValue.name, newValue.name);
    })
    // ...
    return {
      ...toRefs(state)
    }
  }
}

可以看到,在上面的例子中,oldValue.id 和 newValue.id的值是相等的。这是默认的行为。

如果想要实现真正的监测:

JavaScript
watch(() => {return {...state}}, function(newValue, oldValue) {
  console(oldValue.id, newValue.id);
  console(oldValue.name, newValue.name);
})

只监测某个属性

JavaScript
watch(()=> state.id, function(newValue, oldValue) {
  console(oldValue, newValue);
})

使用deep watcher监测嵌套属性

需要注意,这里需要一个被监测对象的深拷贝:

JavaScript
import _ from 'lodash';

const state = reactive({
  id: '',
  name: '',
  contact: {
    address: '',
    phone: '',
    email: '',
  }
});
watch(() => _.cloneDeep(state.contact), function(newValue, oldValue) {
  // ...
},
{deep: true});

watchEffect

watchEffect会自动检测在回调函数中使用到的变量并对其进行监测。watchEffect感觉更加智能化,其实它和computed函数有些类似,只不过computed需要通过返回值来体现最新的数据,而watchEffect则是强调当关联数据发生变化时,另一个过程将被调用。

JavaScript
setup() {
  const keyword = ref('');

  watchEffect(() => {
    // 由于下面使用了keyword,因此才会对其进行监测
    console.log(keyword.value);
  })
}

使用provide/inject

基本用法

In parent component:

JavaScript
import { provide } from 'vue';

export default {
  setup() {
    provide('c_authenticated', true);
  }
}

In child component:

JavaScript
import { inject } from 'vue';

export default {
  setup() {
    // 如果没有组件provide c_authenticated,则默认值为false
    const authenticated = inject('c_authenticated', false);

    return {
      authenticated
    }
  }
}

和ref/reactive的结合使用

父组件:

JavaScript
import { provide, ref, reactive, toRefs } from 'vue'

export default {
  setup() {
    const count = ref(0);
    const state = reactive({
      id: '9901',
      name: 'Paul'
    });

    provide('c_count', count);
    provide('c_student', state);

    return {
      count, 
      ...toRefs(state)
    }
  }
}

子组件:

JavaScript
import { inject, toRefs } from 'vue';

export default {
  setup() {
    const count = inject('c_count', 0);
    const student = inject('c_student', {});

    return {
      count,
      ...toRefs(student)
    }
  }
}

provide/inject操作数据的方法

父组件:

JavaScript
setup() {
  function increase() {
    count.value += 1;
  }

  provide('increase', increase);

  return {
    count,
    increase
  }
}

子组件:

JavaScript
setup() {
  const increase = inject('increase');

  return {
    //...
    increase
  }
}

使用Template refs

JavaScript
import { ref, onMounted } from 'vue';

export default {
  name 'Refs test',
  setup() {
    const nameRef = ref(null);

    onMounted(() => {
      nameRef.value.focus();
    });

    return {
      nameRef,
    };
  }
}

使用props从父组件向子组件传递信息

要想在Composition API中使用属性props,只需要在定义setup的时候传入props参数就可以:

JavaScript
setup(props) {
  const snippet = computed(() => {
    return props.product.summary.substring(0, 100) + '...';
  })
}

使用context从子组件向父组件传递信息

在Options API中,我们可以使用$this.emit,但在composition API中,就可以在子组件中接受context作为参数,然后访问其对应属性了:

子组件:

JavaScript
setup(props, context) {
  function sendEvent() {
    context.emit('registered', studentId);
  }

  return {
    studentId,
    sendEvent
  }
}
emits: ['registered']

上面的context中又包含emit, props, slots, attrs等属性。

父组件:

markup
<template>
  <Student @registered="registered" />
</template>

<script>
export default {
  setup() {
    function registered(studentId) {
      console.log(studentId);
    }
  }
}
</script>  

使用生命周期钩子

需要注意的是,在composition API中,对应的方法名称稍有改变:

Options API Composition API
beforeCreate NOT NEEDED
created NOT NEEDED
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
JavaScript
import {
  onMounted,
  onUnmounted,
  onUpdated
} from 'vue'

export default {
  name: 'Lifecycle Test',

  setup(props) {
    onMounted(() => {
      console.log("onMounted");
    })
    onUnmounted(() => {
      console.log("onUnmounted");
    })
    onUpdated(() => {
      console.log("onUpdated");
    })
  }
}

构建可重用组件Composable component

创建组件

首先创建一个目录composables,然后在其中添加可重用的组件,比如useStudentLoader.js。注意之类的命令规范:useXXX。

JavaScript
import { ref } from 'vue';

const useStudentLoader = () => {
  const students = ref([]);
  const error = ref(null);

  const loadData = async () => {
    try {
      let result = await fetch(YOUR_URL);
      if(!result.ok) {
        throw Error('Error while loading data');
      }
      students.value = await result.json();
    } catch (err) {
      error.value = err.message;
      console.log(error.value);
    }
  }

  return { students, error, loadData };
}

export default useStudentLoader;

在上面的定义中,也可以传给useStudentLoader参数:

JavaScript
const useStudentLoader = (initialStudentList) => {
  const students = ref(initialStudentList);
  // ...
}  

在Vue组件中使用useStudentLoader组件

JavaScript
import useStudentLoader from '../composable/useStudentLoader';

export default {
  setup() {
    const { students, error, loadData } = useStudentLoader();
    loadData();

    return { students, error, loadData };
  }
}

文章作者: 逻思
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 逻思 !