предисловие
С момента появления React Hooks была постоянная критика, многие говорят, что это приносит умственную нагрузку, потому что характеристики, зависящие от порядка выполнения useState/useEffect, непредсказуемы по сравнению с традиционным методом написания Class. Напротив, вVue3 Composition API RFCВ , мы увидели, что Vue3 официально описал CompositionAPI как лучшее решение, основанное на существующей «реактивной» ментальной модели, что заставило нас почувствовать, что мы можем быстро инвестировать в разработку Composition API без какого-либо переключения ментальной модели. Но после того, как я попробовал это некоторое время, я обнаружил, что это не так, нам все еще нужны некоторые умственные изменения, чтобы адаптироваться к новому API Compsition.
Настройка ловушки
простая ловушка
Давайте посмотрим на простой пример Vue2:
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
addCount() {
this.count += 1
}
}
};
</script>
В ментальной модели Vue2 мы всегда возвращаем объект в данных, нам все равно, является ли значение объекта простым типом или ссылочным типом, потому что они могут хорошо обрабатываться реактивной системой, как и в приведенном выше примере. Однако, если мы начнем использовать Composition API, не внося никаких изменений в ментальную модель, мы сможем легко написать такой код:
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
import { reactive } from '@vue/runtime-dom'
export default {
setup() {
const data = reactive({
count: 0
})
function addCount() {
data.count += 1
}
return {
count: data.count,
addCount
}
}
};
</script>
На самом деле этот код работает некорректно, при нажатии на кнопку представление не реагирует на изменение данных. Причина в том, что мы сначала взяли количество в данных, а затем объединили его с этим.$data, но как только количество было извлечено, это простой простой тип данных, и ответ потерян.
сложная ловушка
Чем сложнее структура данных, тем легче нам попасть в ловушку.Здесь мы извлекаем часть бизнес-логики в кастомные хуки следующим образом:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_name',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 异步获取数据
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
return data
}
Затем, как обычно, используем в бизнес-компоненте:
// App.vue
<template>
<div>
{{name}}
{{role}}
{{list}}
</div>
</template>
<script>
import useSomeData from './useSomeData'
export default {
setup() {
const { userInfo, projectList } = useSomeData()
return {
name: userInfo.name // 响应式断掉
role: userInfo.role, // 响应式断掉
list: projectList // 响应式还是断掉
}
}
}
</script>
Мы видим, что независимо от того, что мы возьмем из реактивных данных (простой или ссылочный тип), это приведет к тому, что реактивные данные сломаются и, следовательно, не смогут обновить представление.
Корень всех этих проблем:установка будет выполнена только один раз.
Переход на новую ментальную модель
- Всегда помните, что установка будет выполнена только один раз.
- Никогда не используйте простые типы напрямую
- Деструктуризация может быть рискованной, лучше использовать саму ссылку, чем ее деструктировать.
- Разрушение можно сделать безопасным с помощью некоторых средств
Используйте новые ментальные модели для решения проблем
Простая ловушка: никогда не используйте простые типы напрямую
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
import { reactive, ref } from '@vue/runtime-dom'
export default {
setup() {
const count = ref(0) // 在这里使用ref包裹一层引用容器
function addCount() {
count.value += 1
}
return {
count,
addCount
}
}
};
</script>
Подводные камни сложности. Сценарий 1. Деструктуризация может быть рискованной. Лучше использовать саму ссылку, чем ее деструктировать.
// useSomeData.js
...
// App.vue
<template>
<div>
{{someData.userInfo.name}}
{{someData.userInfo.role}}
{{someData.projectList}}
</div>
</template>
<script>
import useSomeData from './useSomeData'
export default {
setup() {
const someData = useSomeData()
return {
someData
}
}
}
</script>
Сложная ловушка — решение 2: деструктурирование можно сделать безопасным с помощью вычислений
// useSomeData.js
import { reactive, onMounted, computed } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_user',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 异步获取数据
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
const userName = computed(() => data.userInfo.name)
const userRole = computed(() => data.userinfo.role)
const projectList = computed(() => data.projectList)
return {
userName,
userRole,
projectList
}
}
// App.vue
export default {
setup() {
const { userName, userRole, projectList } = useSomeData()
return {
name: userName // 是计算属性,响应式不会断掉
role: userRole, // 是计算属性,响应式不会断掉
list: projectList // 是计算属性,响应式不会断掉
}
}
}
Сложная ловушка - схема 3: схема 2 требует написания некоторых дополнительных вычисляемых свойств, что более хлопотно, мы также можем сделать деструктурирование безопасным через toRefs
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_user',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 异步获取数据
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
// 使用toRefs
return toRefs(data)
}
// App.vue
export default {
setup() {
// 现在userInfo和projectList都已经被ref包裹了一层
// 这层包裹会在template中自动解开
const { userInfo, projectList } = useSomeData()
return {
name: userInfo.value.name, // ???好了吗
role: userInfo.value.role, // ???好了吗
list: projectList // ???好了吗
}
}
}
Как вы думаете, это будет хорошо? На самом деле естьловушка в ловушке: projectList — это хорошо, но имя и роль все еще находятся в отзывчивом отключенном состоянии, потому что toRefs будут только «поверхностно» обернуты, фактически результат, возвращаемый useSomeData, таков:
const someData = useSomeData()
↓
{
userInfo: {
value: {
name: '...', // 依然是简单类型,没有被包裹
role: '...' // 依然是简单类型,没有被包裹
}
},
projectList: {
value: [...]
}
}
Поэтому, если наш useSomeData хочет добиться реальной безопасности деструктурирования через toRefs, его нужно написать так:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
...
// 让每一层级都套一层ref
return toRefs({
projectList: data.projectList,
userInfo: toRefs(data.userInfo)
})
}
Предложение: при использовании пользовательских хуков для возврата данных, если уровень данных относительно прост, вы можете напрямую использовать toRefs для переноса; если уровень данных сложный, рекомендуется использовать вычисление.
обойти ловушку
Вышеупомянутая операция на самом деле является стандартным способом официального использования CompositionAPI VUE, потому что CompositionAPI полностью разработан в соответствии с программой установки. Тем не менее, нельзя отрицать, что это приносит большую нагрузку на ум, потому что мы должны обратить внимание на данные ответа, которые не могут быть расшифрованы, иначе легко приспособиться к яме.
На самом деле все эти проблемы вызваны тем, что установка будет выполнена только один раз, так есть ли способ ее решить? Да, вы можете использовать JSX или h, чтобы обойти проблему, связанную с тем, что установка выполняется только один раз:
Или этот кастомный хук с угрозами безопасности:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_name',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 异步获取数据
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
return data
}
Используйте JSX или h
import useSomeData from './useSomeData'
export default {
setup() {
const someData = useSomeData()
return () => {
const { userInfo: { name, role }, projectList } = someData
return (
<div>
{ name }
{ role }
{ projectList }
</div>
)
}
}
}
При использовании JSX или h программа установки должна вернуть функцию, которая на самом деле является функцией рендеринга, которая будет повторно выполняться при изменении данных, поэтому нам нужно только поместить логику деструктурирования в функцию рендеринга, а затем программе установки нужно только to be будет выполнен один раз.
постскриптум
Нам могут понадобиться некоторые соглашения, чтобы ограничить использование пользовательских хуков. Но официалы его не дали, что заставит нас писать разные зацепки и лазейки. На данный момент «не разбирать» — самый безопасный способ.
Я специально спрашивал yyx босса по этому вопросу(#1739), большой парень дал "конвенцию", то есть использовать "деконструкцию" как можно меньше. Я тоже беспомощен. На самом деле, я надеюсь, что официальный может дать инструмент, который позволит нам уменьшить вероятность ошибок в кастомных хуках. (toRefs на самом деле такой инструмент, но он не решает всех проблем)