来到新公司,接触 react以来,有因为对 react 机制理解不清而造成 过线上问题
因此,我洗心革面,买了本react书,希望能在100天内的时间里,从被 react 拿捏到掌握主动权 也在这个地方记录 读书后的收获和总结。
1. React更新驱动源
1 毋庸置疑:state 和 props 就是驱动 React 应用更新的驱动源
2 函数式组件:每次父组件的render方法被调用时,无论是否使用了props,子组件都会被重新渲染
javascript
function CompC(){
const [State, setState ]= React.useState() return <div>
<CompD />
{ ...} </div>
}
所以 ,当 CompC 改变 State,但是 CompD 并没有绑定属性,那么在 CompD 不做处理的情况下,CompD 还是会更新的。
2. React props
1,props变化可以通过 useEffect 监听
2,可以通过 cloneElement 的第二个参数来隐式注入 props
js
function Son(props){
console.log(props) // {name: "hello,world", mes: "let us learn React !"} return <div> { props.name } </div>
}
function Father(prop){
return React.cloneElement(prop.children,{mes: let us learn React ! })
}
function Index(){
return <Father>
<Son name = "hello,world" />
</Father> }
3. props 衍生出两种非常出色的设计模式:组合模式和 render props 模式
1. 组合模式
经典场景:Tab组件
适合容器化组件场景,通过外层组件包裹内层组件,类似于vue 的slot插槽, 外层组件可以轻松获取内层组件的props,为内层组件添加属性,还可以控制内层组件的渲染,组合模式能直观地反映出父子组件的包含关系
总之,组合模式,父组件对 子组件 的掌控感是杠杠的
2. render props 模式
render props 模式和组合模式类似,区别是用函数形式代替 Children,函数的参数由容器组件提供
4, State 驱动
函数组件中的 useState
State, dispatch \]= useState(initData) 对于 dispatch 的参数,也有两种情况:第一种是非函数情况,此时将作为新的值赋给 State,作为 下一次渲染使用; 第二种是函数的情况,如果 dispatch 的参数为一个函数,这里可以称它为 reducer。reducer 参数是上一次返回最新的 State,返回值作为新的 State。 #### dispatch更新特点 **在函数组件中,dispatch 更新效果和类组件是一样的, 但是 useState 有一点值得注意,就是当调用改变 State 的函数 dispatch 时,在本次函数执行上下文中,是获取不到最新的 State 值的。** **原因很简单,函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明, 所以改变的 State 只有在下一次函数组件执行时才会被更新。** #### 函数组件 useState 注意事项 在 useState 的 dispatchAction 处理逻辑中,会浅比较两次 State,发现 State 相同,不会开启更新调度任务; 两次 State 指向了相同的内存空间,所以默认为 State 相等,就不会发生视图更新了。 解决问题: 把上述的 dispatchState 改成 dispatchState({...State})即可,浅复制了对象,重新申请 了一个内存空间。 #### 函数组件模拟强制更新 const \[, forceUpdate \]= React.useState() forceUpdate({}) #### useState和setState异同 类组件中的 setState 和函数组件中的 useState 有什么异同呢? ##### 相同点: 首先从原理角度出发,setState 和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法。 ##### 不同点: 1,在非 pureComponent 组件模式下,setState 不会浅比较两次 State 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。 但是 useState 中的 dispatchAction 会默认比较两次 State 是否相同,然后决定是否更新组件。 2,setState 有专门监听 State 变化的回调函数 callback,可以获取最新 State; 但是在函数组件中,只能通过 useEffect 来执行 State 变化引起的副作用。 3,setState 在底层处理逻 辑上主要是和老 State 进行合并处理,而 useState 更倾向于重新赋值。 ### 5,主流框架批量更新模式 #### 宏任务 vs 微任务 在浏览器环境下,宏任务的执行并不会影响到浏览器的渲染和响应。 微任务在这种情况下,浏览器直接卡死了,没有了响应 #### 第一种:批量更新的实现,就是基于宏任务和微任务来实现的 等js执行完毕,用一个微任务统一去批量更新队列里面的任务,如果微任务存在兼容性,那么降级成一个宏任务,这里优先采用微任务的原因就是微任务的执行时机要早于下一次宏任务的执行 ```js class Scheduler { constructor() { this.callbacks = [] queueMicrotask(() => { // 用 queueMicrotask 创建一个微任务 this.runTask() }) } addTask(fn){ this.callbacks.push(fn) } runTask() { console.log('----合并更新开始---') while(this.callbacks.length){ const cur = this.callbacks.shift() cur() } console.log('---合并更新结束---') console.log('---开始更新组件--') } } function nextTick (cb) { const scheduler = new Scheduler() cb(scheduler.addTask.bind(scheduler)) } function mockOnClick() { nextTick((add) =>{ add(()=> {console.log('第一次更新')}) console.log('---宏任务逻辑') add(() => { console.log('第二次更新') }) }) } mockOnClick() ``` 总结一下就是: 先把更新任务push callbacks里,然后再创建 微任务,一次性将所有 更新任务执行 #### 第二种:可控任务实现批量更新 以 React 的事件批量更新为例子,比如我们的 onClick、onChange 事件都是被 React 的事件系统处理的。外层用一个统一的处理函数进行拦截。而我们绑定的事件都是在该函数的执行上下文内部被调用的。 在一次点击事件中触发了多次更新,本质上外层在 React 事件系统处理函数的上下文中,这样的情况下,就可以通过一个开关,证明当前更新是可控的,可以做批量处理。接下来 React 用一次就可以了。 ```js let batchEventUpdate = false let callbackQueue = [] function flushSyncCallbackQueue() { console.log('-----执行批量更新-------') while(callbackQueue.length > 0){ const cur = callbackQueue.shift() cur() } console.log( '-----批量更新结束-------') } function wrapEvent(fn){ return function (){ /* 开启批量更新状态 */ batchEventUpdate = true fn() /* 立即执行更新任务 */ flushSyncCallbackQueue() /* 关闭批量更新状态 */ batchEventUpdate = false } } function setState(fn){ // 如果在批量更新状态下,那么批量更新 if(batchEventUpdate){ callbackQueue.push(fn) }else{ // 如果没有在批量更新条件下,那么直接更新 fn() } } function handleClick(){ setState(() =>{ console.log( '---更新 1---' ) }) console.log( '上下文执行' ) setState(() =>{ console.log( '---更新 2---' ) } ) } /* 让 handleClick 变成可控的 */ handleClick = wrapEvent(handleClick) ``` 分析一下核心流程: 本方式的核心就是让 handleClick 通过 wrapEvent 变成可控的。 首先 wrapEvent 类似事件处理函数,在内部通过开关 batchEventUpdate 来判断,是否开启批量状态, 最后通过 flushSyncCallbackQueue 来清空待更新队列。 在批量更新条件下,事件会被放入更新队列中,在非批量更新条件下,立即执行更新任务 ### 6 两种模式下的 State 更新 #### 1 legacy 模式和 concurrent 模式 legacy 模式:所有的更新任务都是同样级别的,而且只要开始更新,那么中途是不能中断 的,但是在每次渲染大量元素节点的场景下就显得很乏力了,如果后面还有很多更新任务,就会阻塞 到任务的执行,给用户的直观反应就是很卡。 concurrent 模式: 这个模式下,可以把更新任务 分为不同的优先级,低优先级的任务可以让高优先级的任务先执行。 并且在这个模式下,更新是可以中断的,一个更新任务中断,会优先执行更为紧迫的任务,然后会恢复中断的任务,这就说明一个渲染过程可能会被执行多次。 #### 2 老版本 legacy 模式下的更新 首先对于传统的 legacy 模式,采用的就是 5.3 小节可控任务实现批量更新的手段。 #### 3 新版本 concurrent 模式下的更新 新版本的 State 更新叫作 Automatic batching (自动批处理),它的具体实现方式类似于 5.2 小节中的更新手段,采用异步任务统一开启更新调度。 #### 4 flushSync 提高优先级 如何提高 State 的更新优先级呢?React-DOM 提供了 flushSync,它可以将回调函数中的 State 更新任务放在一个较高优先级的更新中。 ### 7 外部数据源 #### 1 什么是外部数据源 典型的外部数据源就是 Redux 中的 Store, Redux 是如何把 Store 中的 State,安全地变成组件的 State ```js const store = createStore(reducer, initState) function App ({ selector }) { const [State, setReduxState] = useState({}) useEffect(() => { // 通过useEffect监听订阅 Store 变化 const unSubscribe = store.subscribe(() => { // 先把外部数据源通过 selector 选择器, // 将组件需要的数据映射到 State | props 上 const value = selector(data.getState()) // /* 如果发生变化 */ if (ifHasChange(State, value)){ setReduxState(value) } }) return function () { unSubscribe() } }, [store]) return