背景
创作背景纯粹是因为本人喜欢面向对象风格,并不是说其他风格不好,无意挑起风格流之争,不喜欢的道友求放过,我的目标是兼容现有的vue3的项目,也就是说可以在现有的项目中也可以使用。
使用
1. model
-
自定义双向数据绑定
-
原生写法
<!-- CustomInput.vue --> <script setup> defineProps(['modelValue']) defineEmits(['update:modelValue']) </script> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template>
现在
v-model
可以在这个组件上正常工作了:<CustomInput v-model="searchText" />
-
面向对象写法
@Component({ template: ` <input :value="model" @input="modelChange" /> `, }) export default class OOPDemo { @Model() model!: string; modelChange = (e: InputEvent) => { this.model = e.target.value } }
使用跟原生方式使用是一样的
<CustomInput v-model="searchText" />
-
-
v-model
的参数默认情况下,
v-model
在组件上都是使用modelValue
作为 prop,并以update:modelValue
作为对应的事件。我们可以通过给v-model
指定一个参数来更改这些名字:<MyComponent v-model:title="bookTitle" />
-
原生写法
<!-- MyComponent.vue --> <script setup> defineProps(['title']) defineEmits(['update:title']) </script> <template> <input type="text" :value="title" @input="$emit('update:title', $event.target.value)" /> </template>
-
面向对象写法
@Component({ template: ` <input :value="model" @input="modelChange" /> `, }) export default class OOPDemo { @Model('title') model!: string; modelChange = (e: InputEvent) => { this.model = e.target.value } }
-
-
多个
v-model
绑定利用刚才在
v-model
参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个v-model
双向绑定。组件上的每一个
v-model
都会同步不同的 prop,而无需额外的选项:<UserName v-model:first-name="first" v-model:last-name="last" />
-
原生写法
<script setup> defineProps({ firstName: String, lastName: String }) defineEmits(['update:firstName', 'update:lastName']) </script> <template> <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" /> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" /> </template>
-
面向对象写法
@Component({ template: ` <input :value="firstName" @input="firstName" /> <input :value="lastName" @input="lastNameChange" /> `, }) export default class OOPDemo { @Model('firstName') firstName!: string; @Model('lastName') lastName!: string; firstNameChange = (e: InputEvent) => { this.firstName = e.target.value } lastNameChange = (e: InputEvent) => { this.lastName = e.target.value } }
-
2. 模板引用
-
访问DOM元素
-
原生写法 为了通过组合式 API 获得该模板引用,我们需要声明一个同名的 ref:
<script setup> import { ref, onMounted } from 'vue' // 声明一个 ref 来存放该元素的引用 // 必须和模板里的 ref 同名 const input = ref(null) onMounted(() => { input.value.focus() }) </script> <template> <input ref="input" /> </template>
-
面向对象写法
@Component({ template: ` <input ref="input" /> `, }) export default class OOPDemo implements LifecycleHook { @ViewChild() input?: HTMLInputElement; onMounted(): void { console.log(this.input) } }
-
-
组件上的ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
-
原生写法
<script setup> import { ref, onMounted } from 'vue' const child = ref(null) onMounted(() => { // child.value 是 <Child /> 组件的实例 }) </script> <template> <Child ref="child" /> </template>
-
面向对象写法
@Component({ template: ` <Child ref="child" /> `, }) export default class OOPDemo implements LifecycleHook { @ViewChild() child?: Child; onMounted(): void { console.log(this.child) } }
-
-
v-for
中的模板引用当在
v-for
中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:-
原生写法
<script setup> import { ref, onMounted } from 'vue' const list = ref([ /* ... */ ]) const itemRefs = ref([]) onMounted(() => console.log(itemRefs.value)) </script> <template> <ul> <li v-for="item in list" ref="itemRefs"> {{ item }} </li> </ul> </template>
-
面向对象写法
@Component({ template: ` <ul> <li v-for="item in list" ref="itemRefs"> {{ item }} </li> </ul> `, }) export default class OOPDemo implements LifecycleHook { @ViewChildren() childs: HTMLInputElement[]; onMounted(): void { console.log(this.childs) } }
-
3. watch
-
基本使用
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用
watch
在每次响应式状态发生变化时触发回调函数:-
原生写法
<script setup> import { ref, watch } from 'vue' const question = ref('') const answer = ref('Questions usually contain a question mark. ;-)') // 可以直接侦听一个 ref watch(question, async (newQuestion, oldQuestion) => { if (newQuestion.indexOf('?') > -1) { answer.value = 'Thinking...' try { const res = await fetch('https://yesno.wtf/api') answer.value = (await res.json()).answer } catch (error) { answer.value = 'Error! Could not reach the API. ' + error } } }) </script> <template> <p> Ask a yes/no question: <input v-model="question" /> </p> <p>{{ answer }}</p> </template>
-
面向对象写法
@Component({ template: ` <p> Ask a yes/no question: <input v-model="question" /> </p> <p>{{ answer }}</p> `, }) export default class OOPDemo implements LifecycleHook { question = ref('') answer = ref('Questions usually contain a question mark. ;-)') @Watch() async questionChange(newQuestion, oldQuestion) { if (newQuestion.indexOf('?') > -1) { this.answer.value = 'Thinking...' try { const res = await fetch('https://yesno.wtf/api') this.answer.value = (await res.json()).answer } catch (error) { this.answer.value = 'Error! Could not reach the API. ' + error } } } }
-
-
监听数据源类型
watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:-
原生写法
const x = ref(0) const y = ref(0) // 单个 ref watch(x, (newX) => { console.log(`x is ${newX}`) }) // getter 函数 watch( () => x.value + y.value, (sum) => { console.log(`sum of x + y is: ${sum}`) } ) // 多个来源组成的数组 watch([x, () => y.value], ([newX, newY]) => { console.log(`x is ${newX} and y is ${newY}`) })
-
面向对象写法
@Component() export default class OOPDemo implements LifecycleHook { x = ref(0) y = ref(0) // getter 函数 数据源 @Watch(context => context.x.value) callbackGetter(newX, oldX) { console.log(`x is ${newX}`) } // 以Change结尾命名的方法名 会自动解析到x属性 也就是这个监听监听的是x的变化 @Watch() xChange(newX, oldX) { console.log(`x is ${newX}`) } // 指定具体属性 会自动解析到x属性 也就是这个监听监听的是x的变化 @Watch('x') xxxCallback(newX, oldX) { console.log(`x is ${newX}`) } // 多个来源组成的数组 @Watch(['x', (context) => context.y.value]) xxxCallback([newX, newY], [oldX, oldY]) { console.log(`x is ${newX} and y is ${newY}`) } }
-
-
即时回调监听器
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。我们可以通过传入
immediate: true
选项来强制侦听器的回调立即执行:-
原生写法
watch(source, (newValue, oldValue) => { // 立即执行,且当 `source` 改变时再次执行 }, { immediate: true })
-
面向对象写法
@Component() export default class OOPDemo implements LifecycleHook { x = ref(0) @Watch({immediate: true}) xChange(newX, oldX) { console.log(`x is ${newX}`) } }
-
-
回调触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明
flush: 'post'
选项:-
原生写法
watch(source, callback, { flush: 'post' })
-
面向对象写法
@Component() export default class OOPDemo implements LifecycleHook { x = ref(0) @Watch({flush: 'post'}) xChange(newX, oldX) { console.log(`x is ${newX}`) } }
-
4. 组合式函数(hooks)
-
组合式函数
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。
相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。
-
鼠标跟踪示例
我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:
// mouse.js import { ref, onMounted, onUnmounted } from 'vue' // 按照惯例,组合式函数名以“use”开头 export function useMouse() { // 被组合式函数封装和管理的状态 const x = ref(0) const y = ref(0) // 组合式函数可以随时更改其状态。 function update(event) { x.value = event.pageX y.value = event.pageY } // 一个组合式函数也可以挂靠在所属组件的生命周期上 // 来启动和卸载副作用 onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) // 通过返回值暴露所管理的状态 return { x, y } }
-
原生写法
<script setup> import { useMouse } from './mouse.js' const { x, y } = useMouse() </script> <template>Mouse position is at: {{ x }}, {{ y }}</template>
-
面向对象写法
@Component({ template: `<template>Mouse position is at: {{ x }}, {{ y }}</template>`}) export default class OOPDemo extends HookReturns<ReturnType<typeof useMouse>> implements LifecycleHook { onSetup() { const {x, y} = useMouse(); return {x, y} } }
-
后续
后续会补充实现的过程 内容比较多 最后可能还会出适配这个插件的Webstrom插件的实现过程(视情况而定)