- 主题:对比 vue,reactjs 就是个笑话!
没研究过,不了解
【 在 littleSram (littleSram) 的大作中提到: 】
: 学习了
: ng2和react比较呢?
--
FROM 122.57.163.*
我就说我这个例子不好,果然带来了一些误会。
首先,useEffect 产生的是 side effect,通过某些 state 值从获取数据。那么正常情况下,你所使用的 value 值应该是从外部的 counter 来的,而不可能是在 setCounter 中再取一遍当前值。这里面的逻辑是,当产生外部作用的时候,控件的状态是 A,等异步执行完之后,控件的状态已经变成 B 了,这时再用 A 计算出来的结果去更新 B 状态,那么这个操作很大概率是错的。
其次,我的例子也是偷懒了,写成
const value = ref(...) vs const [value, setValue] = useState(...)
const step = ... vs const [step, setStep] = ...
可能更合适一些。
另外,按照 react 的设计,如果用 counter 的数据结构,使用 useReducer 更合理一些。这种情况下,具体用原来 value 还是增加后的 value,取决于 reducer/dispatch 怎么设计的。而 vue 的话,如果想用原本的 value 值,你会发现没那么容易了。
最后,react 里由于所有的函数都是完全正常的 js,多个 useEffect 是按照顺序执行的。在 vue 里面,watcher 的执行顺序就有有魔法了,没记错的话,watch 的是 computed 的话,那么会在 data/reactive wachters 运行完成之后才执行。
useState 里面的 setter,如果用函数形式的话,非异步的执行是合理的。如果是复杂数据结构的话,可以根据 useReducer 和多次 dispatch 来控制想要的执行结果。还是老话,每一步都是 newState = reduce(oldState, action),没有意外也很容易写 unittest。
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: @eGust 试了一下,有意思。
: const [counter, setCounter] = useState({ value: 1, step: 1 })
: useEffect(() => {
: ...................
--
FROM 122.57.163.*
你还是没理解我的意思,这么举例吧:有 props P,computed C,data D 三个状态,有两个分别监听 P 和 C。C 依赖于 P,当 P 变化的时候 C 也有可能产生变化。
// w1
watch(P, () => {
D = f1(P, D); // D: d1 -> d2
})
// w2
watch(C, () => {
D = await f2(D, ...);
})
在 w1 里面,D 的值发生了变化 d1 -> d2,而在 w2 触发时,由于 D 的值已经变化了,想取得原始值 d1 是非常困难的。而取得 d1 的值是完全合理的选择,因为在 C 的状态发生变化的时候,D 的值还是 d1。
换成等价的 react 操作
// e1
useEffect(() => {
setD(f1(D, P)) // D: d1 -> d2
}, [P])
// e2
useEffect(async () => {
setD(await f2(D, P))
}, [P, ...]) // 相当于 C = computed(P, ...)
在 e2 发生的时候,D 的值仍然是 d1,进行 setD(async (d2) => f2(d2, P)) 这种操作是非法的。而对于
d3 = await f2(D, P) // D === d1
setD((d2) => d3)
这种操作,是在用基于 D === d1 的状态求出来的结果 d3,去更新 D === d2 的状态,很可能并不是想要的结果,而是引入了一个 bug。那么解决的办法就是 reducer
D = reduce({ D: d2 }: State, { P, D: d3, oldD: d1 }: Action)
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: 明白你的意思了,你是说,前面例子中,如果用户需要从当前的外部counter取值,也应该能做到?vue也可以啊,watch(someRef, (v, oldV) => ... ) 这里就可以取到someRef的当前值。(顺便还附赠可以一起取到旧值。react hooks下useEffect里是取不到旧值的,官方也没有相关a
: watch(aRef, (v) => aRef.value = v+1)
: useEffect(() => setA(a+1))
: ...................
--
修改:eGust FROM 101.98.83.*
FROM 101.98.83.*
为啥调试麻烦?是 pure function 是 function 的子集,immutable 是变量的子集,为啥调试超集没问题,子集反倒出问题了?
【 在 chaobill (若我离去,后会无期) 的大作中提到: 】
: fp 的一大问题是调试麻烦
--
FROM 101.98.83.*
不然为啥 facebook 要搞 react,为啥微软要跟?都是吃饱了撑的?
【 在 chaobill (若我离去,后会无期) 的大作中提到: 】
: 如果不支持这种功能的语言是怎么解决这些问题的?
: 抛事件,事件乱飞?
--
FROM 101.98.83.*
你的流行是怎么定义的?react 现在是最流行的前端库,js 是现在最大的社区,怎么就不流行了?
【 在 chaobill (若我离去,后会无期) 的大作中提到: 】
: 那么为什么 fp 不流行呢?
--
FROM 101.98.83.*
computed 不重要,await 是为了强调这是一个带副作用的操作,也是 useEffect 应该使用的场景。重点的变化是状态的变化,P 发生变化的时候 d1,w1 里直接让状态变化成 d2,触发 w2 时的状态是 d1、但执行的状态是 d2。
react 这边不管 e1 还是 e2,触发的时的状态是 d1,执行时的状态也是 d1。
把控件的更新过程看做一个周期的话,react 整个周期里 state 是一致的。useEffect 的应用场景是副作用,所以如果在 useEffect 里面更新了状态,是会触发重绘的。vue 在一个周期内,state 是不断变化的,所以流程不像 react 那么简单。
另外,useEffect 是在 dom 更新后执行,watcher 是在更新 dom 之前执行,这俩东西不论是原理还是触发时机都是不一样的。你前面偏偏要把 useEffect 弄成 watcher 的样子用,所以我不得不设置一些障碍避免对 setCounter 不合理的使用方式。
reducer 到底是要 d1、d2、d3 的结果也不重要,因为这是一个纯函数,给定确定的参数就能得到相同的结果。想要什么结果都是很容易写测试的,以后不管怎么改 useEffect 都不影响结果的正确性。vue 这边因为一个更新周期里 state 是不断变化的,computed 的计算变了,watcher 的逻辑变了甚至只是调整了一下 watchers 的顺序,bug 就又回来了。
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: 还是没特别明白。。你这个例子里面computed很关键吗?await很关键吗?我不觉得这个例子里面“取得的d1的值是完全合理的选择”,因为就这个例子的逻辑而言,如果取得的是d1,那相当于w2把w1的效果给覆盖掉了,一般来说会是一个bug,和你下面react那里说的那个bug是一个道
: let prevD
: watch([C,D], ([c2,d2], [c1,d1]) => { // 同时 watch c 和 d
: ...................
--
修改:eGust FROM 101.98.83.*
FROM 101.98.83.*
我再尝试解释最后一遍,你要是再看不懂我也真没辙了,vue 的实例代码在这里:
https://epuxm.csb.app/
setup(props) {
const propP = toRef(props, "p");
const dataD = ref(1);
const computedC = computed(() => Number(propP.value));
watch(propP, (newP, oldP) => {
console.debug("w1", { D: dataD.value, newP, oldP });
dataD.value += Number(newP);
});
watch(computedC, async (newC, oldC) => {
console.debug("w2", { D: dataD.value, newC, oldC });
dataD.value = await remoteCall(dataD.value, newC);
});
return { D: dataD };
},
w2 是 async 操作,如果你想写成 useEffect 的形式的话
setD(async (upToDateD) => {
const newD = await remoteCall(upToDateD, p);
return newD;
});
并不是一个合法的操作。
同理在 react 里面
const Foo = ({ p }) => {
const [d, setD] = useState(...);
const c = useMemo(() => bar(p), [p]);
// e1
useEffect(() => {
const newD = f(d, p);
setD(newD);
}, [p])
// e2
useEffect(async () => {
const newD = await remoteCall(d, p);
setD(newD);
}, [c]);
}
如果想用 vue 来实现 e2 的话,就会变的非常复杂。但 e2 的操作可能会产生 bug,更合理的写法是
const [d, dispatch] = useReducer(reducer, ...)
useEffect(() => {
const newD = ...;
dispatch({ type: 'e1', payload: { newD } })
}, ...);
useEffect(async () => {
const newD = await ...;
dispatch({ type: 'e2', payload: { newD, oldD: d } })
}, ...);
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: 再贴一下我之前实验的结果:
: watch(aRef, (v) => aRef.value = v+1) // 触发时候aRef的值固定在了v参数中
: useEffect(() => setA(a+1)) // 触发时候a的值通过闭包被固定
: ...................
--
FROM 122.57.163.*
几年前在版上吹 react 的时候,我表达过我的意思,oo 是工程师搞出来的东西,而 fp 是学院派出身,是有理论背景的。ng1 就算是工程师搞出来的最高成就了,而 react 一出则是彻底革了 ui 技术的命。
前面我说 react 用了俩 fp 概念其实不准确,实际上只有 pure function 一个核心概念,而 immutable 是由于 js 语言本身没带这能力才引入的。由于纯函数是个特别强的约束,表达能力反倒是极其有限的。而且正是由于这个特性,导致不得不对 side effect 进行特殊的考虑。这两天还看到 reddit 上有人问,为啥 random 是 side effect,其实答案也很简单,因为它不是 pure function 了。要对 random 进行特殊处理是违反直觉的,但正是因为有了纯函数的约束,才简化了问题。如果状态错了,那么只要改 reducer 就可以了,反之那就是 jsx 映射错东西了。这样的好处就是,state 的转换过程可以单独测试,ui 也可以单独测试,解耦了业务逻辑和 ui。而业务逻辑中,纯函数的部分可以单独测试,副作用的部分需要单独考虑,但不影响纯函数的正确性。要么纯函数写错了,要么副作用处理错了,界限也非常清晰。
useEffect、useMemo、useReducer 等很多 api 都是先有了第三方库的生态,得到了社区的公认,react 又反过来引入的。比如复杂的状态管理也是经历了从 flux 到 redux 的过程,而 redux 为了解决 side effect 又引入了其它第三方的扩展。同理 redux-thunk 是工程师搞出来的东西,而 redux-saga 是以论文为基础搞出来的学院派的东西。特点也是一样的,thunk 看起来很简单好用,saga 则又引入了一堆的概念,学起来特别晦涩。但两者相比我自然还是推崇 saga,因为从理论上已经证明了有效性,工程师搞出来的东西就是摸着石头过河,你不知道什么时候会出问题。
回过头来看 vue,并不是说 react 的实践就不适用,就像很多人学了 rust 之后反过来也影响了写 c++ 或者其它语言代码,自然而然的就意识到潜在的问题。vue 里也可以尽量写纯函数,遇到 side effect 的时候多留点儿心,也一样可以写出容易测试的代码。但这个就是写代码的人的造化了,代码质量可能会参差不齐。而 react 那边是有个底线在的,不排除有复制粘贴凑合把代码跑通的可能性,但总体上只要有搞懂了的人把关,那项目的质量是差不到哪里去的。
比如我们的 vue 项目,一开始我是企图学 redux connect 解耦控件本身和 vuex 的。但别人不遵守,再加上赶进度,很快就变成想测试控件就不得不带上 vuex 了。你可以说是我们的工程质量问题,人员水平问题,我不反对。但如果上了 react + redux,那是几乎不可能开这个口子的。当然话说回来,由于除了我以外的人都是现学的,上了 react 的话也几乎不可能用相同的速度上线的。
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: 应该是看懂了。说说我的理解哈。
: 你最后提到的useReduce应该和咱们的话题没有太多直接关系,不涉及react和vue的能力对比问题。我理解useReduce就是把原本分散在各处的setState(v=>foo(v))逻辑都起了个名儿,集中在了一起,方便管理。
: 关键问题在于对e2这种逻辑的表达能力:
: ...................
--
FROM 101.98.83.*
redux 本来就是 flux 的特殊情况,同样单向的数据流动,属于 react 生态环境的一部分。不然为啥 jquery 的时候没诞生 redux,最多只搞出了饱受诟病的 ng1?1985年微软就搞出来 excel 了,为啥直到 react 大家才意识到 declarative ui 才是正确的?
后面的不管 vue 还是 svelte,本质上都在用更符合人直觉的方式来组织数据,或者说,使用 procedural programming 替代 react 的 fp。我还是那句老话,数据的单向流动简化了逻辑,减少了业务逻辑的复杂度。算法和数据结构等等的核心思想,都是把复杂的问题简单化,而在这点上 fp 是做到了极致的。你要看不出来的话我也真没啥办法解释,就跟很多人不觉得 declarative ui 不比传统的 imperative ui 有优势一样,也一样举出前者各种的毛病,后者的问题也一样可以解释成,缺乏良好的架构约束。
另外,我不了解 ng2+,了解仅限于 rxjs 火了的很大一部分原因是由于它。如果设计相当于是 multiple stores 的话,那我可以肯定是比 single store 更糟糕的。因为又回到了起点,没有 single source of truth 了。在多个 stores 之间发生基于同一个源的数据交换的时候,是很难确定哪个 store 里的数据才是应该更新的。虽然可能更符合人的直觉,但比单源系统复杂多了。
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: 你说的react-redux整体方案的可维护性优势是存在的,但是我觉得你夸大了react的作用。这个优势主要是redux带来的。我们可以看到大量的react项目,业务逻辑也都是写在组件里面的,在组件里搞一大堆数据存取和计算,然后在组件里setState。这种玩法照样不可能带来任何好的
: vue生态在这方面基本是没什么建树的。ng在这方面的做法我觉得反而更靠谱些,就是引导大家把业务逻辑集中到service里,然后把可测性目标聚焦在service的设计上。不像redux,service不是全局单例的,是每一个组件都可以new的(其实是通过di系统注入的)。这就更合理一些。
: 我认为试图利用react vue ng这样的框架或者ui库来推行好的架构,本身就不彻底。前端在这方面和后端没啥区别,好的做法依然是靠架构规范来保证,而不是靠库来保证。
: ...................
--
FROM 122.57.163.*