Open stormqx opened 4 years ago
React 和 Vue 都使用了 Virtual DOM 技术。但他们在 侦查变化 和 更新 等方面使用了不同的策略,我们曾经在virtual dom diff算法原理概述中详细的讨论过差异点。这里主要聊聊 React 的调停策略和日常编码中需要注意的点。
从信息获取的角度来看,
举个具体的例子,当一个用户触发了一个点击事件,React并不知道它会引起多少改变。所以只能在内存中渲染整个Virtual DOM,然后在对Virtual DOM进行diff,在将差异点 patch 到真实DOM中。虽然减少了真实DOM操作,但是每次的 virtual DOM 计算也是一笔不小的开销。所以React 提供了 shouldComponentUpdate
方法、PureComponent
和 Memo Hooks
等手段,允许开发者自行跳过整个渲染过程。他们的底层原理其实是一致的,
以 shouldComponentUpdate
为例子。当 C1 组件发生变化时,其下面对应的所有子组件都会进入 Render
流程(如果不做干预)。如果我们已知,C2和C7组件是不需要Render
的,直接 shouldComponentUpdate
函数返回false,对应的组件及其子组件就不会Render
。C1、C3、C6和C8组件由于未对shouldComponentUpdate
做处理,Virtual DOM会重新渲染然后diff,在比对了VDOM后,其中C8组件不用改变真实DOM。C1、C3和C6的VDOM有变化,需要改变真实DOM。
Vue 能做到局部更新vdom,是因为在组件初始化时进行了依赖收集,这种方法是一种tradeoff,它能保证开发者不需要做过多的性能优化就可以收获不错的性能,但由于依赖收集的存在,所以在初始化时会引入更多的时间,尤其是对于数据量复杂且首屏敏感的应用来说,会有很大的影响。
React的做法就相当于给用户提供了性能优化的手段,将 vue 中依赖收集的步骤分摊到每个组件,每次render
环节中去。强大的灵活性也对应着开发者要做更多的性能优化相关的工作。
当然上述两种的vdom方法并不是最高效的。从前面描述的信息获取角度来讲,两者对于新旧dom的比较,都需要进行diff算法做patch处理。当然还有其他处理方式,Svelte
的编译风格是将模板编译为命令式 (imperative) 的原生 DOM 操作,省掉了vdom的diff/patch操作。
不谈具体背景,直接讨论方案孰优孰劣是不切实际的。更合理的事,结合自身业务需求场景做合适的选择。
Q:为什么必须在函数组件顶部作用域调用Hooks API? A:
memoizedState
属性,在类组件中,调用setState()
时更新 memoizedState
即可。但是在函数组件中,memoizedState
被设计成一个链表(Hooks对象)。dispatchAction
函数是更新 state 的关键,它会生成一个update
挂载到Hooks队列中,并提交一个React更新调度,后续的工作和类组件一致。update
的 state)useState
更新数据和setState
不同,setState
会对 state 进行 merge 操作。useState
则是直接覆盖。useState
传入的是具体 state 不同,useEffect
传入的是一个callBack函数。与useState
最大的不同是执行时机,useEffect
callback 是在 组件被渲染为真实DOM后执行。useEffect
调用也会在当前 Fiber 节点的Hooks链中追加一个 Hook 并返回,它的memoizedState
存放的是一个 effect
对象,effect
对象最终会被挂载到 Fiber 节点的updateQueue
队列。(当Fiber节点都渲染到页面上后,就会开始执行Fiber节点中的updateQueue中所保存的函数)useEffect
可以接受第二个参数deps
,用户在re-render时判断是否重新执行callback,deps
必须按照实际依赖传入,不能少传或者多传。deps
数据项应该是mutable,比较也是浅比较,传入对象、函数无意义。使用时,应尽可能都传deps
。const useReducer = (reducer, initState, initFn) => {
const [state, setState] = useState(initFn ? initFn(initState) : initState);
const dispatch = useCallback((action) => setState(prev => reducer(prev, action))), [reducer]);
return useMemo(() => [state, dispatch), [state, dispatch]);
}
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T
由于JavaScript的特殊性,当函数签名被作为deps传入useEffect时,还是会引起re-render(即使函数体没有改变)。这种现象在类组件里面也存在:
// 当Parent组件re-render时,Child组件也会re-render
class Parent extends Component {
render() {
const someFn = () => {}; // re-render时,someFn函数会重新实例化
return (
<>
<Child someFn={someFn} />
<Other />
</>
);
}
}
class Child extends Component {
componentShouldUpdate(prevProps, nextProps) {
return prevProps.someFn !== nextProps.someFn; // 函数比较将永远返回false
}
}
function component:
function App() {
const [count, setCount] = useState(0);
const [list, setList] = useState([]);
const fetchData = async () => {
setTimeout(() => {
setList(initList);
}, 3000);
};
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<>
<div>click {count} times</div>
<button onClick={() => setCount(count + 1)}>Add count</button>
<List list={list} />
</>
);
}
解决方法:
useEffect
内部(比如需要传递给子组件),可以使用 useCallback
API 包裹函数。 useCallback
本质是对函数进行依赖分析,依赖变更时才重新执行。简单理解: useCallback(fn, deps) === useMemo(() => fn, deps)
useRef
Hook返回一个ref对象的可变引用,但useRef的用途比ref更广泛,它可以存储任意javascript值而不仅仅是DOM引用。
推崇组合(组合大于继承)
React有十分强大的组合模式,react也推崇了多种组合的方式来实现组件复用性和可扩展性。和 vue 的 slot 概念一致,Vue为了增强 slot 的能力,引入了很多概念和语法糖(Slots / Named Slots / Scoped Slots),但都能从 react 上找到对应实现。 React 的灵活性更高,相对给用户带来的心智负担小。 例如:
React Router 也使用了 render props,利用它可以解决横切关注点(Cross-Cutting Concerns)。但使用 render props 有一些注意事项:
render
时都重新生成,后两者浅比较 props 总会返回 false,这会抵消了后两者的效果。为了绕过这个问题,可以定义一个实例方法。