- 主题:对比 vue,reactjs 就是个笑话!
我咋觉得react也一样呢?复刻一下这个例子:
const [{ value, step }, setCounter] = useState(counter);
useEffect(() => {
console.debug("watcher A", { ...counter });
setCounter(old => ({ value: old.value + old.step, step: old.step })) // 1
}, [step]);
useEffect(() => {
console.debug("watcher B", { ...counter });
setCounter(old => ({ value: old.value + old.step, step: old.step })) // 2
}, [step]);
跑的时候 1 和 2 两个 setCounter 也是顺序运行的,它们各自取到的 old.value 也不一样吧?
我个人理解,useState useEffect 本质就是为了造成纯组件函数的“假象”,把生存周期是跨越多次渲染而一直存在的 state 变量、effect 函数,都存到了用户看不见的闭包里,因为 react 函数组件只有一层 render 函数,正常来看的话没有造闭包的地儿:
...; // state 和 effect 真实创建于框架代码里,也就是组件 render 函数体之外
function Component () {
...; // render 通过 useState useEffect 取出作用域外的闭包变量
}
而 vue 是把这种变量显式地放在 setup 函数形成的闭包里。
setup() {
...; // 构造跨多次渲染的闭包变量
return () => {
// render 直接通过原生js作用域机制获取外面的闭包变量
}
}
所以本质都是闭包变量,只是闭包的位置不同而已吧?
除非说 react 内部在上述同一批次内多次调用 setCount 的时候,故意都是传入初始的
count,而不是前面 setCount 修改过的 count。。。如果真是这样,我觉得反而不符合用户意图啊?
稍后有空试一下。
【 在 eGust (十年) 的大作中提到: 】
: 标 题: Re: 对比 vue,reactjs 就是个笑话!
: 发信站: 水木社区 (Wed Jun 30 16:51:15 2021), 站内
:
: 重点并不在 watch 上,declarative ui 是 model -> view 的映射。理论上只要给定了
一个确定的状态,state 也好 data 也好,都能生成一个确定的 ui。
:
: react 里面 newState = reduce(oldState, action),这是一个非常确定的状态。举一个
counter.value += Number(counter.step);
}, [step]);
【 在 eGust (十年) 的大作中提到: 】
: 标 题: Re: 对比 vue,reactjs 就是个笑话!
: 发信站: 水木社区 (Wed Jun 30 16:51:15 2021), 站内
:
: 重点并不在 watch 上,declarative ui 是 model -> view 的映射。理论上只要给定了一个确定的状态,state 也好 data 也好,都能生成一个确定的 ui。
:
: react 里面 newState = reduce(oldState, action),这是一个非常确定的状态。举一个不太恰当的例子:
: import { reactive, watch, toRef } from "vue";
:
: export const counter = reactive({
: value: 1,
: step: 1
: });
:
: const step = toRef(counter, "step");
:
: watch(step, () => {
: console.debug("watcher A", { ...counter });
: counter.value += Number(counter.step);
: });
:
: watch(step, () => {
: console.debug("watcher B", { ...counter });
: counter.value += Number(counter.step);
: });
:
: 如果 step 的值改变了:1 -> 2,那么你会看到的输出是
: watcher A { value: 1, step: 2 }
: watcher B { value: 3, step: 2 }
:
: 如果把这个实现换成 react 的话,由于 value 是从
: const [{ value, step }, setCounter] = useState(counter);
:
: 里取出来的,所以不管有几个 useEffect,调用了几次 setCounter,在所有的 effect 函数里面,value 的值都会是 1。
:
: 这个例子本身很不好,实际中的情况是,多个 watchers 监听各自的 props/computed,但它们都会根据变化来和 data 本身的值来更新 data,于是就产生了不确定性。但是一时间不太容易构造一个合适的例子,临时只能想出这个示意一下,不知道解释明白了没……
:
: 【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: : 下面这一段,watcher的复杂性,和useEffect有何不同呢?useEffect如何解决“触发顺序”之类的问题呢?
:
:
: --
:
: ※ 修改:·eGust 于 Jun 30 17:00:22 2021 修改本文·[FROM: 101.98.83.*]
: ※ 来源:·水木社区 mysmth.net·[FROM: 101.98.83.*]
--
修改:eGust FROM 101.98.83.*
FROM 123.120.168.*
useState useEffect useMemo 的原理非常重要,不是实现细节。不懂具体怎么实现的,只背api,分分钟掉到沟里,基本没法干活
【 在 tgfbeta (右旋肉碱) 的大作中提到: 】
: 这是实现细节,不重要
: 正所谓闭包是穷人的对象,对象是穷人的闭包
: Closures are poor man's objects. Objects are poor man's closures.
: ...................
--
FROM 123.120.168.*
要不是做多平台, web 下 直接 js 基本就够了。
切图仔最喜欢的就是调用而不是自己研究了
【 在 adoal (阿豆) 的大作中提到: 】
: GUI开发本来就是复杂的,你看看Windows GUI程序的开发,
: 从裸写SDK到MFC,.NET下从WinForm到WPF,里面的方法论
: 演变都是通过引入复杂的设计来解决现实中项目做大了一定
: ...................
--
FROM 124.240.15.*
@eGust 试了一下,有意思。
const [counter, setCounter] = useState({ value: 1, step: 1 })
useEffect(() => {
console.log('A', { ...counter })
setCounter((old) => {
console.log('AA', { ...old })
return { value: old.value + old.step, step: old.step }
})
}, [counter.step])
useEffect(() => {
console.log('B', { ...counter })
setCounter((old) => {
console.log('BB', { ...old })
return { value: old.value + old.step, step: old.step }
})
}, [counter.step])
step 1->2 时,外层 A B 处拿到的两个 counter 是一样的,如你所说。
但是,setCounter内层 AA BB 处拿到的两个 old 是不一样的,反而和 vue 类似,拿到的一个是没加过 step 的,一个是加过的。
所以 react 最终页面效果还是value加了2,和vue的最终效果一样。
看来一个渲染批次之内的多个 setCount 传入多个 old => new 这样的 reducer 函数的时候,是 lazy evaluation,把两个 setCount 压到最后顺序执行了。这个最终效果和你说的有偏差,react 的这种写法,一样有你说的调用顺序的“微妙”问题。也就是说,那两个
setCount,和 vue 的两个 watch 一样,都有先后次序的问题。
想想也应该是这样啊,用户写了对同一个内存对象的两次累加修改,在单线程模式下,最后的本质肯定还是一先一后执行的啊,最终肯定得加两次啊。哪怕并发模式,将来有了
cocurrent,正确的逻辑也得是加锁,最终还是要加两次呀。
只要加两次,就肯定无法避免先后次序问题。咱们这个例子只是加一,顺序无所谓。碰上顺序有关系的,反而是 cocurrent 比较麻烦,vue 和 non-cocurrent react 还好,不管怎么 lazy evalute,最终应该还是按照代码里的真实顺序来调用的,次序虽然不一定直观,但应该是稳定的。
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: 标 题: Re: 对比 vue,reactjs 就是个笑话!
: 发信站: 水木社区 (Wed Jun 30 21:52:25 2021), 站内
:
: 我咋觉得react也一样呢?复刻一下这个例子:
:
: const [{ value, step }, setCounter] = useState(counter);
:
: useEffect(() => {
: console.debug("watcher A", { ...counter });
: setCounter(old => ({ value: old.value + old.step, step: old.step })) // 1
: }, [step]);
:
: useEffect(() => {
: console.debug("watcher B", { ...counter });
: setCounter(old => ({ value: old.value + old.step, step: old.step })) // 2
: }, [step]);
:
: 跑的时候 1 和 2 两个 setCounter 也是顺序运行的,它们各自取到的 old.value 也不一样吧?
:
: 我个人理解,useState useEffect 本质就是为了造成纯组件函数的“假象”,把生存周期是跨越多次渲染而一直存在的 state 变量、effect 函数,都存到了用户看不见的闭包里,因为 react 函数组件只有一层 render 函数,正常来看的话没有造闭包的地儿:
:
: ...; // state 和 effect 真实创建于框架代码里,也就是组件 render 函数体之外
:
: function Component () {
: ...; // render 通过 useState useEffect 取出作用域外的闭包变量
: }
:
: 而 vue 是把这种变量显式地放在 setup 函数形成的闭包里。
:
: setup() {
: ...; // 构造跨多次渲染的闭包变量
: return () => {
: // render 直接通过原生js作用域机制获取外面的闭包变量
: }
: }
:
: 所以本质都是闭包变量,只是闭包的位置不同而已吧?
:
: 除非说 react 内部在上述同一批次内多次调用 setCount 的时候,故意都是传入初始的
: count,而不是前面 setCount 修改过的 count。。。如果真是这样,我觉得反而不符合用户意图啊?
:
: 稍后有空试一下。
:
:
:
:
: 【 在 eGust (十年) 的大作中提到: 】
: : 标 题: Re: 对比 vue,reactjs 就是个笑话!
: : 发信站: 水木社区 (Wed Jun 30 16:51:15 2021), 站内
: :
: : 重点并不在 watch 上,declarative ui 是 model -> view 的映射。理论上只要给定了
: 一个确定的状态,state 也好 data 也好,都能生成一个确定的 ui。
: :
: : react 里面 newState = reduce(oldState, action),这是一个非常确定的状态。举一个
: counter.value += Number(counter.step);
: }, [step]);
:
:
: 【 在 eGust (十年) 的大作中提到: 】
: : 标 题: Re: 对比 vue,reactjs 就是个笑话!
: : 发信站: 水木社区 (Wed Jun 30 16:51:15 2021), 站内
: :
: : 重点并不在 watch 上,declarative ui 是 model -> view 的映射。理论上只要给定了一个确定的状态,state 也好 data 也好,都能生成一个确定的 ui。
: :
: : react 里面 newState = reduce(oldState, action),这是一个非常确定的状态。举一个不太恰当的例子:
: : import { reactive, watch, toRef } from "vue";
: :
: : export const counter = reactive({
: : value: 1,
: : step: 1
: : });
: :
: : const step = toRef(counter, "step");
: :
: : watch(step, () => {
: : console.debug("watcher A", { ...counter });
: : counter.value += Number(counter.step);
: : });
: :
: : watch(step, () => {
: : console.debug("watcher B", { ...counter });
: : counter.value += Number(counter.step);
: : });
: :
: : 如果 step 的值改变了:1 -> 2,那么你会看到的输出是
: : watcher A { value: 1, step: 2 }
: : watcher B { value: 3, step: 2 }
: :
: : 如果把这个实现换成 react 的话,由于 value 是从
: : const [{ value, step }, setCounter] = useState(counter);
: :
: : 里取出来的,所以不管有几个 useEffect,调用了几次 setCounter,在所有的 effect 函数里面,value 的值都会是 1。
: :
: : 这个例子本身很不好,实际中的情况是,多个 watchers 监听各自的 props/computed,但它们都会根据变化来和 data 本身的值来更新 data,于是就产生了不确定性。但是一时间不太容易构造一个合适的例子,临时只能想出这个示意一下,不知道解释明白了没……
: :
: : 【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: : : 下面这一段,watcher的复杂性,和useEffect有何不同呢?useEffect如何解决“触发顺序”之类的问题呢?
: :
: :
: : --
: :
: : ※ 修改:·eGust 于 Jun 30 17:00:22 2021 修改本文·[FROM: 101.98.83.*]
: : ※ 来源:·水木社区 mysmth.net·[FROM: 101.98.83.*]
:
:
: --
:
: ※ 修改:·beep 于 Jun 30 22:18:46 2021 修改本文·[FROM: 123.120.168.*]
: ※ 来源:·水木社区 mysmth.net·[FROM: 123.120.168.*]
--
修改:eGust FROM 101.98.83.*
FROM 123.120.168.*
【 在 eGust (十年) 的大作中提到: 】
: 标 题: Re: 对比 vue,reactjs 就是个笑话!
: 发信站: 水木社区 (Wed Jun 30 09:19:44 2021), 站内
:
: react 其实只用了俩核心 fp 概念,一个是 pure function,另外一个是 immutable。延伸出来就是数据的单向流动,hooks 的 getter/setter 设计跟 input/select 完全是一样的。这种设计的结果就是,react 里的 state 就是 single source of truth。而每次由当前的 state 和一个参数(action)生成一个新的 state 的过程也非常简单,由于是纯函数,给定确定的 state 和 action 每次都能保证产生相同的结果。这其实也是 fp 的优势,非常容易写 unittest。
:
所以现在回复一下你这个第二段,我觉得讲得不对,和上面第一段也没有因果关系。
web ui肯定是个有大量可变状态和副作用的系统,这是避免不了的。你第一段说 react 能够用纯函数和不可变性来写业务逻辑,其实只是它自己把所有的可变性都给集中在框架内部的处理环节了,留给用户的部分迫使用户写成纯函数。
对于用户来说,这有得有失。很多业务逻辑其实用 OOP 风格写起来很合适,但是非要写成一组一组的纯函数,一方面写起来有点麻烦(幸亏现在有 immer),另一方面,就是那个典型的问题,性能低下。人家本来就是冯诺依曼结构的机器,都是cpu加可变存储单元,结果你非要每个变更都把整个状态树都重做一遍,然后再全树 diff,reconcile,中间做了无数无用功。react 的源码比 vue 复杂无数倍,接下来的 cocurrent mode 肯定还要更复杂。从宏观设计上看,在 UI 这么典型的 OO 领域,在 js 这种这么方便做 proxy 的语言里,非要硬拗 FP,我不觉得这是个好的设计决策。
得到的好处呢?其实就是可测性好。。。但 OOP 风格的模型也不见得可测性不好,只要依赖反转做到位,坚持面向接口编程,万物都可以 mock。mock 这东西,FP 测试也避免不了的。
然后说到下面第二段,watch 并不是因为 vue 采用了双向数据绑定才不得不引入的。这个东西和双向数据绑定毫无关系。而且我认为所谓双向数据绑定,是个普遍的误解。vue 和
ng 和 react 一样,都是组件树自上而下单向数据流,只是在 form 组件这一个小地方,
vue 和 ng 提供了一个语法糖,把一个向下的参数传递,和一个向上的事件传递,允许用户写成一个 v-model 或者 [(ng-model)] 简写,仅此而已。如果 react 愿意,分分钟也可以这么做。在数据流的结构这方面,三大框架是非常一致的,组件树的数据肯定是从上往下,然后允许子树上新增局部数据,继续往下传。如果有往上传的需求,都是通过事件来做。
回到 watch。上面说过,UI 系统不可能避免副作用,副作用有很多种,前端框架 react vue 本身只是帮你集中处理了 DOM 变化的副作用,但还有很多很多其他的副作用,是必须用类似 watch 的东西来写的。react 从前有 componentWillReceiveProps,现在有 useEffect,其实就是和 watch 一样的东西。现在大家写 react hooks,一个很大的吐槽就是如你下面所说,useEffect 里面的逻辑套逻辑,你触发我,我触发它,层次多了就乱了。和你说的问题是一样的。这也不是前端框架独有的问题。凡是 pub-sub 结构的体系,都是一样的问题。
倒是因为 useEffect 还需要用户手动写依赖项,导致 effect 嵌套的时候,会频繁遗漏依赖项,导致更多 bug。react 官方说,今后会提供一个静态分析工具,自动给用户填 effect 依赖。。。
最后,咱们刚才做实验的 watcher 触发顺序问题,也是一样的。react 只是把真实修改
state 的地方集中到它内部来执行了,该有顺序问题还是会有顺序问题,毕竟还是在一个
cpu 核心上跑的代码。
: ng1/vue 的双向数据绑定表面上看起来非常直观,vue 的 data 看起来也是跟 state 一样的概念。但这种设计却不得不引入 watch,而它又其实引发了许多问题。首先 watcher 里面可能会再更新 data,而又可能触发其它的 watcher,这就导致了数据的更新流程非常复杂,不像 newState = reduce(oldState, action) 那么简单。其次,多个 watchers 监视同一个数据的变动也是很难避免的,那么它们触发的顺序就变成了一件非常微妙的事情,很可能由于触发顺序的不同而产生不同的结果。另外,watcher 里面依赖了哪些外部的变量也是非常难预测的,导致的结果就完全相同 data 初始结果,同样的事件可能导致不同的最终值。所以如果没设计好的话,很容易把 unittest 的依赖关系搞的非常复杂,很难实现。
:
: 总体上,我是更倾向 react + hooks 的,因为 fp 的条条框框在那里绕不过去,迫使项目的设计差不了,用起来是个先苦后甜的过程。vue 容易上手,副作用就是项目上设计起来非常容易放飞自我,最后的坑不比 jquery 的项目少。
:
: 【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: : class组件,和hooks组件,各有各的复杂,都很麻烦。本质上是因为react采取了一个奇葩的响应式方案,就是对状态根对象做简单的引用比较。为了这个,弄出一堆fp不可变的东西来,不管咋弄心智负担都不小。
: : 这方面我还是觉得vue mobx这种proxy式的响应式方案比较靠谱,也切合现在业务逻辑一般都用class oop来写的实际情况。proxy式的响应方案,虽然也要注意不能丢失引用,但是规则还是比较简单的,只要记住永远要用a.b就可以,不能没有中间那个点。
: : 前面有人提到被toRef toRaw这些搞晕了,其实这些都是处理特殊情况的边缘api。正常只要记住上面的规则,会用ref reactive,知道computed返回的是个ref,就可以干活了。
: : ...................
:
: --
:
: ※ 来源:·水木社区 mysmth.net·[FROM: 115.188.133.*]
--
修改:beep FROM 123.120.168.*
FROM 123.120.168.*
我认为 现在的体系就是往没必要的复杂里搞。
为什么要用函数式呢?
事件响应不就已经很足够了么。
如同 cisc 和 risc 的区别
【 在 adoal (阿豆) 的大作中提到: 】
: GUI开发本来就是复杂的,你看看Windows GUI程序的开发,
: 从裸写SDK到MFC,.NET下从WinForm到WPF,里面的方法论
: 演变都是通过引入复杂的设计来解决现实中项目做大了一定
: ...................
--
FROM 124.240.15.*
react 团队的 dan 写博客解释为什么 react 不采用 proxy 式的方案,说,因为把变量都转成 proxy 要花时间,不利于首屏渲染速度。。。这也太牵强了。。
【 在 beep (菜M.喵星耗子) 的大作中提到: 】
: 所以现在回复一下你这个第二段,我觉得讲得不对,和上面第一段也没有因果关系。
: web ui肯定是个有大量可变状态和副作用的系统,这是避免不了的。你第一段说 react 能够用纯函数和不可变性来写业务逻辑,其实只是它自己把所有的可变性都给集中在框架内部的处理环节了,留给用户的部分迫使用户写成纯函数。
: 对于用户来说,这有得有失。很多业务逻辑其实用 OOP 风格写起来很合适,但是非要写成一组一组的纯函数,一方面写起来有点麻烦(幸亏现在有 immer),另一方面,就是那个典型的问题,性能低下。人家本来就是冯诺依曼结构的机器,都是cpu加可变存储单元,结果你非要每个变
: ...................
--
FROM 123.120.168.*
因为 vue2 赶上了一个好时代。
vue2 代替了angular ,后面 angular 就没人看了。
就像 laravel 比 zend framework 还重。那样
PHP 不是一种好语言,但是他赶上了个好时代。 laravel 走向了邪路
nodejs 本来可以替代 php 的,可惜就是太做了。一直是歪的
【 在 gang2k (枕头) 的大作中提到: 】
: 为啥国内用angular的这么少?
: 发自「今日水木 on iPhone 12」
--
FROM 124.240.15.*
fp 的一大问题是调试麻烦
【 在 eGust (十年) 的大作中提到: 】
: 所以你只是因为已经习惯 class、生命周期的模型了,但实际上 hooks 对初学者来说更友好。自始至终都是 fp,跟用什么语言关系不大,不需要再掺进来 oo;hooks 是更加抽象的封装,只要记使用场景就好了,不需要了解内部是怎样实现的。
--
FROM 124.240.15.*
如果不支持这种功能的语言是怎么解决这些问题的?
抛事件,事件乱飞?
【 在 eGust (十年) 的大作中提到: 】
: 重点并不在 watch 上,declarative ui 是 model -> view 的映射。理论上只要给定了一个确定的状态,state 也好 data 也好,都能生成一个确定的 ui。
: react 里面 newState = reduce(oldState, action),这是一个非常确定的状态。举一个不太恰当的例子:
: import { reactive, watch, toRef } from "vue";
: ...................
--
FROM 124.240.15.*