总览 Vue3 的单向数据流

尽信官网,不如那啥。

vue的版本一直在不断更新,内部实现方式也是不断的优化,官网也在不断更新。

既然一切皆在不停地发展,那么我们呢?等着官网更新还是有自己的思考?

我觉得我们要走在官网的前面,而不是等官网更新后,才知道原来可以这么实现。。。

我习惯先给大家一个整体的概念,然后再介绍各个细节。

脑图版

先整理一下和单向数据流有关的信息,做个脑图:

大纲版

列个大纲看看:

  • 自动版

    • v-model、emit(defineModel):组成无障碍通道,实现父子组件之间的值类型的响应性。
    • pinia.$state、pinia.$patch:状态管理提供的方法。
    • props + reactive:直接改 reactive,争议比较大
    • 注入 + reactive:直接改 reactive,一般可以忍受
  • 手动版
    • 注入 + reactive + function:官网建议通过 function 改 reactive,而不是直接改 reactive。
    • 状态管理的getter、mutation、action:状态管理,其实也涉及到了单向数据流。
  • props是否可以直接改?(从代码的角度来分析)
    • 值类型:不可改,否则响应性就崩了。
    • 引用类型:地址不可改,但是属性可以改。对于引用类型,其实都是通过 reactive 实现响应性的。
  • 有无意义的角度 (这是一个挨骂的话题
    • 有意义的方式:实现响应性的唯一方式,或者有记录(timeline)、有验证、限制等。
    • 无意义的方式:没有上面说的功能,还自认为是严格遵守规矩。
  • 限制的是谁?
    • 发起者:如果是限制子组件不能发起修改的话,那么任何方式都应该不能被允许,emit 也不行。
    • 方式(手段):如果只是限制一些方式的话,那么为啥 emit 可以,reactive 就不能直接改?有啥区别呢?
      • 二者都没有做记录(timeline),
      • 没有做任何限制、验证。

画个表格对比一下:

再来看看各种方式的对比:

方式 实现手段 有无记录 有无限制、验证 官网意见 适合场景
v-model + emit 抛出事件 可以 以前的方式
v-model + defineModel 抛出事件 推荐 V3.4 推荐的方式
props + reactive 代理,set 不推荐 适合传递引用类型
注入 + reactive 代理,set 不建议直接改reactive 适合多层级的组件结构
注入 + reactive + function 调用指定的函数 可以有 可以有 推荐方式 适合特殊需求
pinia.$patch、$state 代理,set等 timeline
pinia 的 getter、 action 调用指定的函数 timeline 可以有

这样应该有一个明确的总体感觉了吧。

props 的单向数据流

为啥弄得这么复杂?还不是因为两点:

  • vue 自带响应性,主要是 reactive有点太“逆天”。
  • composition API,可以把响应性分离出来单独使用。

如果没有 reactive,那么也就不会这么乱糟糟的了,让我们细细道来。

props 本身是单向的

https://cn.vuejs.org/guide/components/props.html#one-way-data-flow

官网里关于 props 的单向数据流是这样描述的:

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。

这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

整理一下重点:

  • props 本身是单向的,只能接收父组件传入的数据,本身不具有改变父组件数据的能力。
  • 父组件的(响应性)数据如果变化,会通知 props 进行更新。
  • props.xxxx ,自带响应性。
  • props 不具有修改父组件数据的能力,这样就避免了父组件的数据被意外修改而受到影响。
  • 否则,数据流向 会混乱,导致难以理解

其实 props 本来就是单向的,用于子组件接收父组件传入的数据,完全没有让子组件修改父组件里的数据的功能。

那么为何还要强调单向数据流呢?原因有二:引用类型reactive

props可以设置两种数据类型:

  • 值类型(数字、字符串等),用于简单情况,比如 input、select 的值等。
  • 引用类型(对象、数组等),用于复杂情况,比如表单、验证信息、查询条件等。

现在,仅从代码的角度看看 props 在什么情况可以改、不可以改。

  • 值类型,那是肯定不能直接改,直接改就破坏了响应性,父子组件的数据也对应不上。
  • 引用类型,又分为两种情况:改地址、改属性。
    • 改地址,那当然也是不行滴!同上,地址换了怎么找到你家?
    • 如果传入的是普通对象,虽然可以改属性,但是没有响应性;
    • 如果传入的是 reactive 的话,那就可以改其属性了,因为 reactive 自带响应性。

那么问题来了:

  • reactive 在父组件可以改,不会难以理解。
  • reactive 通过依赖注入的方式给子组件,虽然官网不建议直接改,但是就问问你,你会不会直接改?
  • reactive 通过 props 的方式给子组件,为啥一改就混乱而难以理解了呢?
  • 【重点】单向数据流,限制的是发起者,还是“渠道”?

所以重点就是这个 reactive !如果没有他,props 即使直接改了,也无法保证响应性,从而被我们所抛弃,也就不用纠结和争论了。

那么 reactive 到底是怎么回事?大家先不要着急,先看看官网允许的情况,然后再对比思考。那谁不是说了吗,没有对比就没有那啥。。。

为什么会混乱?想到了一种可能性:父组件定义了一个 reactive 的数据,然后通过 props 传递个多个子组件,然后某个子组件里面还有很多子子组件,也传入了这个数据。

某个时候发现状态异常变更,那么问题来了:到底是谁改了状态?(后续跟进)

emit 怎么可以改了?

emit 本意是子组件向父组件抛出一个事件,然后 vue 内部提供了一种方式(update:XXXXX),可以实现子组件修改父组件的需求。

<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>

update:XXX 可以视为内部标识,会特殊处理这个 emit。

好了,这里不讨论具体是如何实现了,而是要讨论一下,不是说好的单向数据流,子组件不能改父组件的吗?不是说改了会导致混乱而难以理解吗?

官方的说法:emit 并不是直接修改,而是通过向父组件抛出一个事件,父组件响应这个事件来实现的。所以,不是直接改,并没有破坏单向数据流。

这个说法嘛,确实很官方。只是从结果来看,还是子组件发起了状态的变更,那么问题来了,如果是上面的那种情况,可以方便获知是谁改了状态吗?(似乎也会导致混乱和难以理解吧)

那么问题来了:单向数据流,是限制发起者,还是手段

  • 如果限制的是发起者的话,那么 emit 也不行,因为也是在子组件发起的,啥时候改,怎么改都是由子组件决定,emit只是一个无障碍通道的起始端,另一端是 v-model。
  • 如果限制手段的话,那么不同的手段到底有啥区别?为啥 emit 可以,reactive 就不可以?

不要钻牛角尖了,其实是有一个很实际的需求:

  • 父子组件之间要保持响应性
  • 子组件有“直接”改的要求

举个例子,各种 UI库 都有 xx-input 组件,外面用 v-model 绑定一个变量,然后 xx-input 里面必须可以修改传入的变量,而且要保持响应性对吧,否则咋办?

v-model + emit 就是解决这个实际需求的。(解决问题,给大家带来方便,然后才会选择vue,其余其他的嘛。。。)

当然,可以使用 ref,但是 ref 的本体是一个class,属于引用类型,如果传入 ref 本体的话,相当于传入一个对象给子组件。这个咋算?

vue 现在的做法是,template 会默认把 ref.value 传给子组件,而不是 ref 本体,这样传入的还是基础类型。

所以,这是实现父子组件之间,值类型的响应性的唯一方法。

defineModel,是直接改?

https://cn.vuejs.org/guide/components/v-model.html

defineModel 是 vue3.4 推出来的语法糖(稳定版),内部依然使用了 emit 的方式,所以可以视为和 emit 等效。

官网示例代码:

<!-- Child.vue -->
<script setup>
const model = defineModel() function update() {
model.value++
}
</script> <template>
<div>Parent bound v-model is: {{ model }}</div>
</template>

官方的示例代码,特意展示了一下可以在子组件“直接改”的特点。

看过内部实现代码的都知道,其内部有一个内部变量,然后返回的是一个customerRef(官方说是ref),所以我们不是直接改 props,而是改 ref.value,然后内部通过 set 拦截,调用 emit 向父组件提交申请。

如果对内部原理感兴趣可以看这里:

依赖注入(provide/inject)也有单向数据流?

https://cn.vuejs.org/guide/components/provide-inject.html#working-with-reactivity

父子组件之间传值,就不得不说说依赖注入,那么是否存在“单向数据流”的问题呢?那也是必然应该存在呀,只是官网没有直接明确说。

注意:依赖注入只负责传递数据,并不负责响应性。

官网的意思,是让我们在父组件实现状态的变更,然后把状态和负责状态变更的函数一起传给(注入到)子组件,子组件不要直接改状态,而是通过调用 【父组件传入的函数】 来变更状态。

官网原文:

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:

官网推荐的方式是这样的:

<!-- 在供给方组件内 -- > 父组件
<script setup>
import { provide, ref } from 'vue' // 数据、状态
const location = ref('North Pole') // 变更状态的函数
function updateLocation() {
location.value = 'South Pole'
} // 提供数据和操作方法(function)
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 --> 子组件
<script setup>
import { inject } from 'vue' // 被注入(得到)状态和方法
const { location, updateLocation } = inject('location')
</script> <template>
<!--调用函数修改状态-->
<button @click="updateLocation">{{ location }}</button>
</template>

看着是不是有点眼熟?这让我想起了 react 的 useState。

其实想一想,为啥非得学 react?react 的特点就是:不能变。所以当需要变更的时候,必须调用专门的 hooks 来处理。

但是 vue 的特点就是响应性呀,和 react 恰恰相反。

当然了,自己写一个函数也是有好处的,比如:


const 张三 = reactive({name:'zs',age:20}) const setAge = (age) => {
if (age < 0) {
// 年龄不能是负数
}
// 其他验证
// 通过验证,赋值
张三.age = age
// 还可以做记录(timeline)
}

这样就不能瞎改年龄了。或者根据出生日期自动计算年龄。

不是说不能自己写函数,而是说这个函数要有点意义。

状态管理也涉及单向数据流吗?

props 和注入说完了,那么就来到了状态管理,这里以 pinia 为例。

状态管理也涉及单向数据流吗?那当然是必须滴呀,否则 Vuex 的时候,为啥总强调要通过 mutation 去变更状态,而不要直接去改状态?

$state 是直接改吗?

那么 pinia 为什么提供了 $state 用于“直接”改状态呢?这还得看看源码:

  • pinia.mjs 1541 行
    Object.defineProperty(store, '$state', {
get: () => ((process.env.NODE_ENV !== 'production') && hot ? hotState.value : pinia.state.value[$id]),
set: (state) => {
/* istanbul ignore if */
if ((process.env.NODE_ENV !== 'production') && hot) {
throw new Error('cannot set hotState');
}
$patch(($state) => {
assign($state, state);
});
},
});

不太会TypeScript,所以我们来看看编译后的代码,是不是有点眼熟。

虽然表面上看是直接修改,但是却被 set 给拦截了,实际上是通过 $patch 和 Object.assign 实现的赋值操作。

这个和 defineModel 有点类似,表面上看直接改,其实都是间接修改。

而 $patch 里面还有一些操作,比如做记录(timeline)。

store.xxx 是直接修改吗?

可能你会说,$state 并不是状态自己的属性,当然不算直接修改了,那么我们来试试直接修改状态。

通过测试我们可以发现:

  • 可以直接改状态
  • 可以产生记录(timeline)

那么是怎么实现的呢?

  • 其实 pinia 的状态(store)也是 reactive。

    pinia.mis:1436行
    const store = reactive((process.env.NODE_ENV !== 'production') || USE_DEVTOOLS
? assign({
_hmrPayload,
_customProperties: markRaw(new Set()), // devtools custom properties
}, partialStore
// must be added later
// setupStore
)
: partialStore);
  • 然后对 reactive 进行了监听

    pinia.mis:1409行
    const partialStore = {
_p: pinia,
// _s: scope,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {
const removeSubscription = addSubscription(subscriptions, callback, options.detached, () => stopWatcher());
const stopWatcher = scope.run(() => watch(() => pinia.state.value[$id], (state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback({
storeId: $id,
type: MutationType.direct,
events: debuggerEvents,
}, state);
}
}, assign({}, $subscribeOptions, options)));
return removeSubscription;
},
$dispose,
};

这里的第10行,用 watch 对状态的属性进行了监听,然后写记录(timeline)。

pinia 不仅没有阻止我们直接改属性,还很贴心的做了记录。

pinia 的 timeline

以前就一直对这个 timeline 非常好奇,想知道记录的是什么,但是奈何各种原因总是看不到,现在vue 推出了,终于看到了。

这里的记录非常详细,有状态名称、动作、属性名称、新旧值、触发时间等等信息,只是有个小问题,到底是谁改了状态? 没发现有定位代码位置的功能。

reactive 怎么算?

好了,终于到了比较有争议的 reactive 了,大家有没有等着急?

首先 reactive 的本质是 Proxy,而 Proxy 是代理,这个想必大家都知道,所以我们可以设置这样的代码:


const 张三 = {
name:'zhangsan',
age:20
} const 张三的代理 = reactive(张三) const setAge = (age) => {
if (age < 0) {
// 年龄不能是负数
}
// 其他验证 // 通过验证后才能赋值
张三的代理.age = age
}

平时大家都是一步成,现在分成了两步,是不是就很明确了呢。

张三是一个普通的对象,没有响应性,张三的代理是 reactive 有响应性,是张三的代理。

所以,我们传递给子组件的是张三的代理,并不是张三本尊。

既然子组件根本就得不到张三的本尊,那么又何来直接修改呢?

如果说通过 emit 是间接修改(抛出事件),那么通过 reactive 也是通过代理间接修改的。

虽然一个是事件,一个是代理,但是有啥本质区别呢?事件是函数,Proxy 里的 set 也是函数呀。

同样都是没有记录(timeline)、判断、验证、限制,想怎么改就怎么改。

如果你还不理解,可以看看这个演化过程。

阶段一:参考官网里面依赖注入的推荐方式

// 阶段一:按照官网里面注入的推荐方式
const person = reactive({
name:'zhangsan',
age:20
}) const setAge = (age) => {
person.age = age
} // 通过 props 或者 依赖注入,把 proxyPerson 传给子组件,
const proxyPerson = reactive({
// 使用 readonly 变成只读形式,只能通过 setAge 修改。
person: readonly(person),
setAge
})

这样子组件只能使用 setAge 修改,代理套上 readonly 之后,通过代理的修改方式都给堵死了,是严格遵守单向数据流了吧。

阶段二:充血实体类,把数据和方法合在一起

// 阶段二:充血实体类,把数据和方法合在一起
const person2 = {
name:'zhangsan',
_age:20, // 内部成员,相当于“本尊”
// set 拦截,其实也是一个函数,类似于代理。
set age(age) { // 拦截设置属性
// 可以做验证
this._age = age
},
get age(){ // 拦截读取属性
return this._age
}
} // 给子组件用
const proxyPerson2 = reactive(person2) // 子组件
// 表名上看是通过属性修改,但是实际上被 set 拦截了,调用的是一个函数
proxyPerson2.age = 30

在父组件里面把数据和变更方法合并,也是符合官网的建议对吧。

那么看看阶段二是不是有点眼熟?如果你熟悉 Proxy 和 reactive 内部原理的话,这不就是 reactive 内部代码的一小部分吗?

既然 reactive 都自带了这种功能,那么我们又何必自己手撸?

当然 reactive 也有点小问题,没有内置记录,不过我们可以用 watch 的 onTrigger 做记录,详细看下面:

给 Pinia 加一个定位代码的功能(支持 reactive)

小结

  • v-model + emit

    目的是实现父子组件之间,值类型数据的响应性,如果不用 emit 的话,如何实现?

  • defineModel

    语法糖(宏),封装复杂的代码,让我们使用起来更方便。

  • 状态管理

    pinia 提供了 timeline,弥补了 reactive 的不足,方便我们调试代码,提供 $state 方便我们直接赋值。

    给 Pinia 加一个定位代码的功能(支持 reactive)

  • reactive

    我觉得可以直接改,因为本身就是一个代理(Proxy),直接用就好了。

    如果外面再套一个 Proxy 有何意义呢?当然了,如果可以加上 timeline,或者是判断、验证等,那么就有意义了。

  • 数据 + 方法

    可以在方法里面做一些操作,比如验证、判断等,那么就有意义,如果是个“空”函数,除了赋值啥都没做,那么有何意义呢?

【vue3】详解单向数据流,大家千万不用为了某某而某某了。的更多相关文章

  1. 从Flux到Redux详解单项数据流

    从Flux到Redux是状态管理工具的演变过程,但两者还是有细微的区别的.但是最核心的都还是观察者模式的应用. 一.Flux 1. Flux的处理逻辑 通俗来讲,应用的状态被放到了store中,组件是 ...

  2. Python 单向队列Queue模块详解

    Python 单向队列Queue模块详解 单向队列Queue,先进先出 '''A multi-producer, multi-consumer queue.''' try: import thread ...

  3. 《TCP/IP详解卷1:协议》第19章 TCP的交互数据流-读书笔记

    章节回顾: <TCP/IP详解卷1:协议>第1章 概述-读书笔记 <TCP/IP详解卷1:协议>第2章 链路层-读书笔记 <TCP/IP详解卷1:协议>第3章 IP ...

  4. Python 双向队列Deque、单向队列Queue 模块使用详解

    Python 双向队列Deque 模块使用详解 创建双向队列Deque序列 双向队列Deque提供了类似list的操作方法: #!/usr/bin/python3 import collections ...

  5. 新手入门:史上最全Web端即时通讯技术原理详解

    前言 有关IM(InstantMessaging)聊天应用(如:微信,QQ).消息推送技术(如:现今移动端APP标配的消息推送模块)等即时通讯应用场景下,大多数都是桌面应用程序或者native应用较为 ...

  6. Web端即时通讯技术原理详解

    前言 有关IM(InstantMessaging)聊天应用(如:微信,QQ).消息推送技术(如:现今移动端APP标配的消息推送模块)等即时通讯应用场景下,大多数都是桌面应用程序或者native应用较为 ...

  7. Linux串口编程详解(转)

    串口本身,标准和硬件 † 串口是计算机上的串行通讯的物理接口.计算机历史上,串口曾经被广泛用于连接计算机和终端设备和各种外部设备.虽然以太网接口和USB接口也是以一个串行流进行数据传送的,但是串口连接 ...

  8. 图文详解互联网根基之HTTP

    这是本人对<图解HTTP>和<HTTP权威指南>阅读后总结的大家常用的.重要的知识点,前端.后端同学居家必备! 一.概述 HTTP是Hyper Text Transfer Pr ...

  9. Observable详解

    Observable详解 rxjs angular2 在介绍 Observable 之前,我们要先了解两个设计模式: Observer Pattern - (观察者模式) Iterator Patte ...

  10. vue和react全面对比(详解)

    vue和react对比(详解) 放两张图镇压小妖怪 本文先讲共同之处, 再分析区别 大纲在此: 共同点: a.都使用虚拟dom b.提供了响应式和组件化的视图组件 c.注意力集中保持在核心库,而将其他 ...

随机推荐

  1. CH57x/CH58x/CH59x获取从机广播信息

    有时需要通过主机设备(MCU非手机)获取从设备的广播信息例如广播包,MAC地址,扫描应答包等 以下的程序片段及功能实现是在WCH的CH59X的observer例程上实现的: 1.获取广播包 所有的函数 ...

  2. pandas基础--缺失数据处理

    pandas含有是数据分析工作变得更快更简单的高级数据结构和操作工具,是基于numpy构建的. 本章节的代码引入pandas约定为:import pandas as pd,另外import numpy ...

  3. [吐槽]困扰了1周的API调用失败问题的原因是使用了加密DNS

    参考API的官方文档使用postman测试了一下,导入了百度提供的postman环境配置文件,粘贴提供的预处理代码后直接发起请求,响应里提示 "signature is empty" ...

  4. CF1815

    CF1815 Div. 1 确实难,Virtual Contest 上只完成了两道题,想出来了三道题. A. Ian and Array Sorting 秒切题--考虑将前 \(n - 1\) 个数变 ...

  5. 算法金 | AI 基石,无处不在的朴素贝叶斯算法

    大侠幸会,在下全网同名「算法金」 0 基础转 AI 上岸,多个算法赛 Top 「日更万日,让更多人享受智能乐趣」 历史上,许多杰出人才在他们有生之年默默无闻, 却在逝世后被人们广泛追忆和崇拜. 18世 ...

  6. 由于找不到 XINPUT1_3.dll,无法继续执行代码。重新安装程序可能会解决此问题。

    ---------------------------EpicGamesLauncher.exe - 系统错误---------------------------由于找不到 XINPUT1_3.dl ...

  7. rust程序设计(6)枚举与模式匹配

    rust中的枚举有什么用?枚举可以嵌入类型的好处是什么 你可以在同一个枚举中既有单个值,也有元组或结构体. 枚举的每个变体可以拥有不同数量和类型的关联数据. 这增加了类型的灵活性和表达力,使你能够更精 ...

  8. 1. Elasticsearch 入门安装与部署

    引言 Elasticsearch是一个基于Lucene的搜索服务器.它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口.Elasticsearch是用Java语言开发的,并作为 ...

  9. TRL(Transformer Reinforcement Learning) PPO Trainer 学习笔记

    (1)  PPO Trainer TRL支持PPO Trainer通过RL训练语言模型上的任何奖励信号.奖励信号可以来自手工制作的规则.指标或使用奖励模型的偏好数据.要获得完整的示例,请查看examp ...

  10. 使用AWS Glue进行 ETL 工作

    数据湖 数据湖的产生是为了存储各种各样原始数据的大型仓库.这些数据根据需求,进行存取.处理.分析等.对于存储部分来说,开源版本常见的就是 hdfs.而各大云厂商也提供了各自的存储服务,如 Amazon ...