总览 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. docker ——网络配置和管理

    docker网络基础 了解docker网络 两种docker网络 单主机与多主机的docker网络 网络驱动 网络驱动 介绍 bridge 桥接网络,这是默认的网络驱动程序 host 主机网络 ove ...

  2. Redux之combineReducers方法

    Redux 提供了一个combineReducers方法,用于 Reducer 的拆分.你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer. import { ...

  3. JavaScript防抖与节流笔记

    JavaScript防抖与节流 概念 防抖(debounce)与节流(throttle)是两个相似但有本质区别的两个概念,但两个概念的存在都是为了控制在特定条件下函数最大的执行次数.这在例如将函数执行 ...

  4. Java扫描文件目录大小及递归扫描

    #Java扫描文件目录大小及递归扫描 package com.example.core.mydemo; import org.apache.commons.io.FileUtils; import j ...

  5. SVG <pattern> 标签的用法和应用场景

    通过使用 <pattern> 标签,可以在 SVG 图像内部定义可重复使用的任意图案.这些图案可以通过 fill 属性或 stroke 属性进行引用. 使用场景 例如我们要在 <sv ...

  6. ansible v2.9.9离线安装脚本

    链接:https://pan.baidu.com/s/18uxyWWyJ39i1mJJ1hb8zww?pwd=QWSC 提取码:QWSC

  7. 架构师必知的11种API性能优化方法

    前言 接口性能优化是后端开发人员经常碰到的一道面试题,因为它是一个跟开发语言无关的公共问题. 这个问题既可以很简单,也可以相当复杂. 有时候,只需要添加一个索引就能解决. 有时候,代码需要进行重构. ...

  8. 05-CentOS防火墙

    概述 CentOS中的防火墙有很多,如SELinux.Firewall.TCP Wrappers.iptables/netfilter. 每种防火墙都有各自擅长的地方. 这里主要讲两种:SELinux ...

  9. 【论文阅读】TRO2022: A Two-Stage Optimization-Based Motion Planner for Safe Urban Driving

    TRO2022: A Two-Stage Optimization-Based Motion Planner for Safe Urban Driving Summary: 探讨planning过程中 ...

  10. 最新最全的BMS/EMS/PCS六大国产“储能方案”,不信你全都看过!

    作为国内领先的嵌入式产品平台提供商,创龙科技在"能源电力"行业拥有超过1000家客户,接下来就让小编向大家分享创龙科技推出的BMS/EMS/PCS"六大储能方案" ...