Vue与React响应式区别
希望该博客可以为从vue转react的开发人员提供帮助。
先浅浅的说一下vue如何实现响应式
在Vue2中使用的时Object.defineProperty()方法为对象去遍历并添加每一个属性的set,get方法。从而在数据更新时同步更新页面。并且重写了数组的方法(push,pop,splice,sort等)去实现数组的响应式。
// 源数据
const sourceData = {
a: '111'
}
// 模拟页面值
let text = ''
// 响应式数据
const vm = {}
// 添加响应式
Object.defineProperty(vm, 'a', {
get: () => {
console.log('vm.a 被读取了', sourceData.a);
return sourceData.a
},
set: (newValue) => {
console.log('vm.a 设置了新值', newValue);
// 拦截了属性设置值新增的处理
text = newValue
sourceData.a = newValue
}
})
// 模拟初始化渲染
text = vm.a
console.log('当前文本值1', text)
// 模拟数据更新,触发响应式
vm.a = '222'
console.log('当前文本值2', text)
问题就来了,只劫持了获取和设置的操作,如果我们添加属性,或者删除属性,则不会触发响应。 而数组使用下标修改元素也无法触发响应式。
[Vue文档](深入响应式原理 — Vue.js (vuejs.org))中在注意事项的原文如下
对于对象
Vue 无法检测 property 的添加或移除。
由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 `data` 对象上存在才能让 Vue 将它转换为响应式的。
对于数组
Vue 不能检测以下数组的变动:
1. 当你利用索引直接设置一个数组项时,例如:`vm.items[indexOfItem] = newValue`
2. 当你修改数组的长度时,例如:`vm.items.length = newLength`
官方也提供了Vue.set的方法去弥补这一缺陷。或者使用响应式的方法去修改。
而且,当对于复杂的状态,需要进行深度遍历才可以实现每一个属性的响应式。
所以会导致初始化时间长,会出现首屏加载慢等问题。 这个是不可避免的。
针对上面的问题,在vue3中实现响应式改为了Proxy。
// 添加响应式函数
function reactive(obj) {
const observed = new Proxy(obj, {
get:(target, property, receiver) => {
console.log(`获取${property}:${target[property]}`);
return target[property]
},
set:(target, property, value, receiver) => {
console.log(`设置${property}:${value}`);
return target[property] = value
},
deleteProperty: (target, property) => {
console.log(`删除${property}`);
delete target[property]
}
})
return observed
}
// 获取响应式状态
const state = reactive({
a: 1
})
// 模拟读取
state.a
// 模拟状态更新
state.b = 2
console.log(state);
// 模拟删除状态属性
delete state.a
console.log(state);
可以拦截对对象的各种操作,get,set,delete等。并且不需要遍历每一个属性,而是针对对象整体进行操作拦截。
但是Proxy是es6的新特性,所以对于某个浏览器的兼容性不是很好。
使用
因为响应式是针对原始数据,所以在vue中直接修改属性,才会触发响应式,从而更新页面。
let state = reactive({a:1})
state.a = 2 // 触发响应式
const { a } = state
a = 2 // 丢失a的响应式
state = {a:1} // 丢失整个状态的响应式
而向上面给状态替换了整个对象, 或者使用结构,则会丢失响应性连接。
记住!直接修改状态属性的操作在vue中很正确。
我们再来浅浅的了解一下react的数据(状态)驱动
先推荐一下react官方文档,写的很详细,跟着快速入门一两页就可以上手(你值得拥有)
我没有开发过类组件,之后的例子都以函数式组件举例
首先需要介绍一下react的状态
useState 是一个React Hook,它允许你在组件中添加状态变量,状态变化会引起组件重新渲染。
// func component
const MyComp = () => {
const [count, setCount] = useState(0)
console.log(111)
const handleClick = () => {
setCount(count + 1)
}
return (
<button onClick={handleClick}>{ count }</button>
)
}
count是当前状态,setCount是修改状态的方法。
初始化时控制台会输出log 111,并且按钮的显示的是0
当我们点击按钮时调用修改方法去修改当前的状态,引起组件重新渲染。
何为重新渲染? 从头到尾再次执行该函数,创建新的状态,打出新的log,创建新的函数,并返回生成的新的按钮。
而这时,按钮里数字变成了1。
注意!状态的变化会引起组件重新渲染。
那如何判断当前状态是否变化呢?如果写的是setCount(count) 或 setCount(0) 还会重新渲染吗?
答案是不会。
js中数据类型分为两种:基本数据类型(Number,String等),引用数据类型(Object,Array等) 区别则是常说的栈中存值还是地址。
在react中使用的是 Object.is() 方法来判断状态是否是相同值。其对于引用数据类型的判断则是地址是否发生改变。
我们了解了这个原理,再继续看下面的例子
const MyComp = () => {
const [obj, setObj] = useState({ count: 0 })
console.log(111)
const handleClick = () => {
obj.count = obj.count + 1
setObj(obj)
}
return (
<button onClick={handleClick}>{ obj.count }</button>
)
}
这时候我们点击按钮,数字会不会变化?
不会啊!地址没有变化,没有引起渲染,页面不会更新,没有响应式啦!
所以正确的写法是
setObj({ count: obj.count + 1 })
如果属性再多一点,考虑一下结构
setObj({
...obj
count: obj.count + 1
})
注意!有没有想到什么?
是的,在vue中,直接修改状态某个属性的值会触发响应式。
而在react中必须调用修改状态的方法,对于引用数据类型必须更新成一个新的数据(改地址)。
开发时的坑
本来写了很多东西,但是自认不会比react文档说的更全更仔细,所以都删了,贴几个链接.
留了两张图方便理解状态快照,生命周期以及具体代码的执行顺序.
个人觉得这个很重要,自己写的代码不知道执行顺序靠蒙是会被笑话的.


React useEffect 与 Vue watch的区别
他们都是监听对应状态发生改变去做处理.
watch是靠响应式去触发处理函数,处理函数中也是靠响应式去更新视图.
useEffect是判断当前快照对应状态是否发生改变,或者说,当前快照是否是因为依赖项中的状态变化生成的,去做对应的处理,如果处理函数中再次发生状态的变化,则会继续创建快照.渲染视图.
我在刚接触react的时候以为他俩差不多, 但是执行顺序(log输出顺序)和我设想的有出入.去翻文档了解了具体实现后才理解.
保持当前快照状态干净
何为干净?当在当前快照中代码执行到任何时候,去取状态,都是我快照生成时的值.
// 当前状态 obj = { count:0 }
const handleClick = () => {
// 状态发生变化,不干净了
obj.count = obj.count + 1
// 这之后运行的所有同步代码,再取obj.count时都不对了
// 执行了某段函数
handleXXX()
// 虽然用了解构,状态更新了,也会生成新快照,但是那是下辈子的事了不是吗?
setObj({ ...obj })
}
const handleXXX = () => {
// 你以为是0? 但其实是1
console.log(obj.count)
}
调完更新状态方法后,当前快照状态未改变
// 当前状态 obj = { count:0 }
const handleClick = () => {
// 状态更新了,之后会生成新快照,但是那是下辈子的事了不是吗?
setObj({ count: obj.count + 1 })
// 这辈子还没过完,好好珍惜.
// 执行了某段函数
handleXXX()
}
const handleXXX = () => {
// 你以为是1? 但其实是0
console.log(obj.count)
}
当前快照中,多次修改同一个状态
const MyComp = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
return (
<button onClick={handleClick}>{ count }</button>
)
}
点击按钮后更新状态,但是在下一快照中,count = 1而不是 3
因为react的任务调度会合并同一快照对相同状态的多次变化,只留最后一个.
如果确实有需要可以这么写 setCount(value => value + 1)
当前快照中,多次修改不同状态
const MyComp = () => {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
const handleClick = () => {
setCount1(count1 + 1)
setCount2(count2 + 1)
}
return (
<button onClick={handleClick}>
{
`${count1} --- ${count2}`
}
</button>
)
}
点击按钮后,只会渲染一次,生成一个快照.
也是归功于react的任务调度,当前快照的同步状态更改,会统一更新到下一个快照
但是,前提时同步更新.下面的情况,点击按钮一次则会渲染两次.任务调度不会等异步操作结束.
const handleClick = () => {
setCount1(count1 + 1)
setTimeout(() => {
setCount2(count2 + 1)
}, 1000)
}
但是有个好玩的现象,顺带考一考你有没有理解,在1s内点击按钮10次, count1 是几? count2又是几?
思考
因为只要组件中不管定义了几个状态,只要有一个状态变化,组件都会重新渲染,并且!该组件的子组件也会重新渲染.
想一想,如果一个组件有二十个子组件,每一个子组件都会影响父组件中对应的状态.当其中某一个子组件更新状态时,会发生什么?
所有组件都会重新渲染! 组件都很小时还好. 但是组件递归很深呢? 如果里面还有大量的数据处理呢?如果更新很频繁呢?
是一个很恐怖的事情!
所以希望在开发时尽量做到
- 避免同一个组件中有大量的状态定义.有大量的状态定义时,说明当前组件很大,业务逻辑很多.这时候需要考虑一下当前组件能不能再细分功能组件,每一个模块实现自己完整的功能,自己维护自己的状态,减少耦合度
- 避免组件传入大量的props.同上,props也是状态的一种.能在内部定义就在内部定义.
- 常量不要定义在组件内部. 因为每次渲染都会重新定义,浪费时间.我习惯放在同层的constants.js中
- 与状态没有关系的函数,尽量定义在组件外部.可以抽出去放在同层的utils.js中.
- 定义要清晰,尽量做到状态统一定义在最上面,之后是定义的函数,而不是函数中间随机穿插状态定义.给后人留个路吧!
- 大量的业务代码中可以写写注释,注释不嫌多啊.
如果能做到,会发现项目中会少很多几百上千行的文件,后期维护也会方便很多.当你几个月后看到了自己或者别人写的上千行的组件,会疯掉的.内部业务代码深度耦合,拆也不好拆,改也不好改
以上所有的内容都只是在了解了useState,useEffect两个hook后就可以实现的.也是必须掌握的.掌握后就可以快乐的写代码啦.
进阶
当我们做到了上面前两点之后,如果还是有性能的瓶颈。还是想减少渲染,减少数据处理的话。
那么可以学习一下react提供的这几个hook.
- useMemo. 直接返回函数值.大量的数据处理可以考率,他只会在依赖项变化时重新计算.
- Memo. 减少组件的重新渲染.
- useCallback. 减少函数的多次定义,可以配合Memo减少组件渲染次数.
我看小思哥前段时间发了一篇博客讲了这几个hook,在此不细说了,(前端性能优化-react渲染)
性能优化的成功案例,嘿嘿嘿,也是本人写的.(React项目 数据展示页面的性能优化)
效果最明显的一个cpu占用率从普遍100%到30%-40%
总结
- 对比了Vue与React.
- 响应式的区别.
- 状态更新渲染的区别.
- useEffect和watch的区别.
- 列举了react开发时状态更新的坑.
- 提出了react开发时希望可以做到的规范.
- 进阶的hook
希望该博客可以对vue转react的开发提供帮助.
再次推荐React官方文档,前两年更新后的文档很棒.
啊还有, 那个题的答案是count1 = 10, count2 = 1. 有没有答对呢?