抛开框架细枝末节的差异,以及开玩笑式的语法区别,本系列文章会将 Angular/Vue/React 联合在一起进行阐述,只重点讲述 三大框架都通用 的核心开发理念
MVVM 模式,中介模式的一种,构建原系统到目标系统的一个中介模型,建立起两系统交互的桥梁
时代在变化,对于编程来说,一个很明显的趋势就是:
无论什么模式实现,都有向适配器发展的趋势
因为工具的发展会越来越完善,封装会越来越彻底,由于应用开发越来越复杂,因此工具的心智负担也需要相应地降低,以便提升开发效率
前端广泛使用的中介模式 MVVM 也是被替代者中的一员
以 vue 为例,不论框架以什么模式实现,当用户使用时,将框架视为适配器是最优解:
在 Vue2 中,当我们进行组件开发时,我们需要复写以下结构:
// MVVM
<script>
export default {
data(){
return {}
},
computed:{
// 数据映射
},
watch:{
// io 源
},
methods:{
// 方法
},
// 生命周期
updated(){
},
created(){
},
mounted(){
},
beforeDestroy(){
}
// ...
}
</script>
很多人在这里遇到了学习上的困难,比如:
- computed/watch 什么意思?有什么区别?
- 组件中生命周期的调用顺序是什么?组件间生命周期调用顺序又是什么?
- 为什么部分情况下需要 nextTick 进行变更?
- 如果数据需要跨组件进行访问,该怎么实现?怎么弥合不同组件间的生命周期?
- 如何保持组件状态?为何采用 keep-alive 需要替换生命周期?
- 如何变更嵌套的 data?
…
这些问题,放置在 React/ng 中,就是类似”如何实现 keep-alive, 为何有 changed after checked 错误?多组件共享服务为何有意外变更?等等等……”
如果一个东西使用起来困难或复杂,优先考虑工具的问题
因为程序员最伟大的特质,就是 ——
懒
请你想想,遇到这些问题的根源是什么?
没错,根源就是 MVVM,或者说 ——
MVVM 被暴露给了用户
- 生命周期是什么? 是 M,V ,VM 之间不断同步的回调
- mount 子到父,update 父到子?因为变检和patch顺序不同
- 为什么跨组件的数据访问,会是个问题? 因为逻辑被 MVVM 打断,不同组件间调度彼此隔离,仅通过 props 绑定
- 为什么组件状态保持是个问题? 因为没有方便的方案,只能将 M,VM 都缓存下来
…
归根到底,生命周期,跨组件逻辑复用,组件调度等等问题,都理应是框架处理的问题!
框架将本该他处理的问题交给你,你当然会觉得难受,当然复用出现困难
而现在,你只需要写你的代码逻辑,框架,只是一个帮你响应视图的工具 ——
只有 M -> V , 不必考虑 VM
<script setup>
// 跨组件 状态逻辑 (注意,是状态逻辑,不单单是状态)
// 又称 依赖注入
const someOtherLogic = inject(someToken)
export const someMethod = ()=>{
someOtherLogic.callSomeFunc()
}
export const {xxx} = someOtherLogic
export const local = ref({xxx:'xxx'})
// 响应式io
watch([local], (res)=>{
// in
})
const newLocal = computed(()=>local)
</script>
我们来看看消失了什么?
- props
- 生命周期
- 跨组件相关工具的必要性
这些东西的消失,带来的后果就是 ——
组件(前端意义上的组件),只是一个处理视图和相关数据绑定的地方,并不承担具体的逻辑(setup 只是个绑定函数的地方)
props 不再变得必要,因为有个更好的处理组件间状态逻辑共享的方案 —— 依赖注入
换言之,你只需要写你的代码逻辑,不必担心框架会对你的代码进行怎样的解析
父组件得告诉子组件,你只是我的一部分 —— Key绑定
props 的作用是什么?父子组件交互?
不对,因为单纯只处理父子组件交互的工具,不具备完善的复用性(中间再加一层组建封装,就没得玩了)
所以,只采用依赖注入,可以实现所有的跨组件逻辑么?
先别急着下结论,我们来看看依赖注入经常会遇到的问题 ——
迭代分形
// 父组件
<script setup>
export const data = ref([1,2,3,4,5])
provide('parent',data)
</script>
<template>
<ChildCompo v-for="(item,key) in data" :key="key"/>
</template>
// 子组件
<script setup>
const parent = inject('parent')
</script>
子组件拿到的,只是父组件的全部数据,如何知道自己是 v-for 生成的实例中的哪一个?
很简单,只需要:
// 子组件
<script setup>
const key = getCurrentInstance()?.vnode?.key
const parent = inject('parent')
const currentData = parent.value[key]
</script>
这样,就能将当前组件实例,与父组件中声明的列表实例项匹配起来
然而,getCurrentInstance 是一个限制颇多,且不符合使用习惯的 api,因此,这个时候可以捡起 props,用更优雅的方式定义:
// 父组件
<script setup>
export const data = ref([1,2,3,4,5])
provide('parent',data)
</script>
<template>
<ChildCompo v-for="(item,key) in data" :key="key" :index="key"/>
</template>
// 子组件
<script setup>
const props = defineProps(['index'])
const parent = inject('parent')
const currentData = parent.value[props.index]
</script>
这里需要注意的是:
- :key=”xxx” 是给框架使用的迭代标识, :index 是组件使用的迭代标识
- 注意:从祖先组件获取的 ref/reactive ,只能通过其进行赋值
const parentData = inject('parent')
parentData.xxx.xxx.xxx = xxx
生效
const xxx = parent.xxx.xxx; xxx.xxx = xxx
不生效或无法监听
永远记住,你操作的是 proxy
迭代分形是一个非常重要的概念,甚至代码结构都需要按照是否出现分形进行划分,后面的文章会从 DDD 的角度,专门讨论 aggregation划分 的问题
但是,如果我们把 parentData[key] 的取值,也看成逻辑,显然,这个逻辑应该放置在父组件更好一些(父组件取值更自然)
那我们封装一个很巧的组件,避开这种硬邦邦的分形绑定
// Mapping.vue
<script setup name="Mapping">
const props = defineProps(['data','prividerKey'])
provide(props.providerKey, props.data)
</script>
<template>
<slot></slot>
</template>
// 直接在父组件:
<script setup>
import Mapping from './Mapping.vue'
export const data = ref([1,2,3,4,5])
provide('parent',data)
</script>
<template>
<Mapping v-for="item in data" :key="key" :providerKey="dataItem" :data="item">
<ChildCompo />
</div>
</template>
这种方式,暂且称之为 “Key绑定注入组件”
眼尖的朋友们肯定会发现,这种做法其实已经在 UI 库中广泛出现,比如 Antd React:
<Form
{...layout}
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item {...tailLayout} name="remember" valuePropName="checked">
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
用一个 key 绑定并 privide (或者拥有相同职能)的组件,就能将集合数据的一部分,投射给子组件,看并且绕过生硬的 props 数据传递
有了这些技巧,你的代码,就可以 ——
完全脱离组件编程
// someSetup.js
function someSetup(){
return {
// ...
}
}
// SomeComponent.vue
<script>
import someSetup from './someSetup.js'
export default defineComponent({setup:someSetup})
</script>
<template>
</template>
也就是说,状态逻辑真正地写在 js 文件中
这一点 React 和 Angular 也是一样的(逻辑不在组件中):
// react
// useSomeService.ts
useSomeService(){
return {/* ... */}
}
function Component(){
const someService = useSomeService()
return /* ... */
}
// Angular
// some.service.ts
@injectable()
export default class SomeService{
// ...
}
// 组件内
constructor(public someService: SomeService){}
关于 Ng 特殊的全类型依赖注入会在 SOA 主题下专门讨论
甚至在 Angular 的代码风格要求中,白纸黑字地写着:
坚持在组件中只包含与视图相关的逻辑。所有其它逻辑都应该放到服务中。
逻辑代码,是你的逻辑代码
没有生命周期,没有拆分复用的负担,你就好好写你的代码就行了
框架重要么?框架不重要!
作为一个适配器,已经不具备切换的学习成本了