概述
本文是学习 react 以后的一些知识点梳理,旨在能帮助自己随时复习。总结梳理的不一定全面到位准确,仅供参考。
知识点梳理
-
react render & react commit
一个组件从 template 到最后的 dom 节点 要经历两个过程: react render 和 react commit。
react render,即 渲染阶段,主要的工作为:
- 执行 createElement 方法,构建组件 template 对应的 react element;
- 基于 react element,通过 diff 算法,生成 new fiber node tree;
- 如果是新增的 fiber node,生成对应的 dom 节点;
- 收集副作用;
react commit,即 提交阶段,主要为处理 react render 阶段 收集的 副作用,如 dom 元素的增、删、更新、移动,生命周期方法的执行等。
每次 react render 阶段 都是从 容器节点(id=app对应的dom节点) 开始,不需要更新, 直接跳过,然后处理子节点;需要更新,则做更新处理。
-
副作用
副作用,可以理解为修改 state,导致 虚拟节点树发生更新,虚拟树更新完成 以后的额外操作。
常见的 副作用,主要如下:
- dom 节点的增、删、更新、移动;
- 类组件生命周期方法的执行;
- 函数组件 effect、layoutEffect的执行和卸载;
- ref 的挂载、卸载;
副作用 的执行顺序:
-
dom 操作前,触发 getSnapshotBeforeUpdate;
-
执行 dom 操作,即增、删、更新、移动(先删除,后增、更新、移动);
-
执行 dom 操作之后的副作用;
类组件挂载,触发 componentDidMount;
类组件更新,触发 componentDidUpdate;
函数组件挂载,触发 layoutEffect;
函数组件更新,卸载上一次的 layoutEffect,触发本次的 layoutEffect;
类组件卸载,触发 componentWillUnmount;
函数组件卸载,卸载上一次的 effect、layoutEffct;
ref 挂载及卸载;
-
nextTick中,卸载上一次的 effect,触发本次的 effect
同一类型的副作用,子组件的先执行。
-
diff 算法
react 每次更新时都需要更新 fiber node tree。
diff 算法用于确定在更新 fiber node tree 时,是否可以 复用 old fiber node,而不是新建一个 new fiber node。
复用 old fiber node, 意味着最后进行的是 dom 节点更新(update)、移动(move); 而 新建 new fiber node,意味着最后进行的是 dom 新增(create)、删除(delete)操作。
diff 算法执行时会对比 old fiber node 和 new react element 的 key 和 元素类型。如果 key 和 元素类型 都相等, old fiber node 就可复用。old fiber nodes 中没被复用的要删除 - dom 删除(delete),复用的要更新 - dom 更新(update)及移动(move),new react elements 中没有对应的 old fiber node 的要新建 - dom 新增(create)。
diff 算法对比 同层的 old fiber nodes 的 new react elements 时,分如下情况:
-
new react element 只有一个
此时,遍历 old fiber nodes,找到可以复用的 old fiber node。如果有,复用 old fiber node,剩下的 old fiber node 删除;如果没有,删除所有的 old fiber node,根据 new react element 重新构建一个 new fiber node。
-
new react element 有多个
按序遍历 old fiber nodes 和 new react elements,判断相应的 old fiber node 是否可以复用,如果不可复用,立即停止遍历。
如果 old fiber nodes 先遍历完成,根据剩下的 new react elements 构建 new fiber nodes。
如果 new react elements 先遍历完成,删除剩下的 old fiber nodes。
如果由于 old fiber node 不可复用,导致遍历停止,old fiber nodes 和 new react elements 都没有处理完毕,此时会基于剩下的 old fiber nodes 建立一个 map,key 为 old fiber node 的 key 或者 index,value 为 old fiber node。
遍历剩余 new react element,根据 new react element 的 key 或者 index,寻找 map 中对应的 old fiber node。 如果能找到且元素类型相同,复用 old fiber node 并且删除 map 匹配的 old fiber node; 如果能找到但元素类型不匹配,构建 new fiber node;如果不能匹配,构建 new fiber node。
new react element 处理完毕以后,删除剩余的 old fiber nodes。
在 react commit 阶段,会根据 new fiber nodes 进行 dom 操作。
实际的 dom 操作顺序是: 先删除该删除的 dom 节点,然后依次处理 new fiber nodes 对应的 dom 节点。如果 dom 节点需要更新,直接更新;如果 dom 节点需要新增或者移动,找到剩余 new fiber nodes 中对应的第一个只需要更新的 dom 节点,然后在其前面插入,如果找不到,通过 appendChild 的方式添加到父节点中。
-
-
对比 vue diff 算法
react 是先进行 sameNode 比较,再决定是否构建新的 virtual node。而 vue 是构建 new virtual node, 再进行 sameNode 比较。
react 只要 key 和 元素类型相同,就认为 old virtual node 可以复用;而 vue 也需要 key 和 元素类型相同, 但元素类型必须严格相同(如果是 input 元素,需要 type 类型也相同),才认为 old virtual node 可以复用。
diff 算法比较时, react 是头比到尾;而 vue 是从两端比较到中间。
react 的 dom 元素删除先处理,而 vue 的 dom 删除最后处理。
react 中 dom 元素移动时,只能向后移动; 而 vue 中 dom 元素移动时可以向前向后移动。
vue 中 dom 元素向后移动时, 通过 insertBefore 移动到 oldEndIndex 的第一个兄弟元素之前(如果没有, 添加到元素列表最后); 向前移动时, 通过 insertBefore 移动到 oldStartIndex 之前。
vue 中 dom 元素新增时, 如果 old virtual nodes 没有处理完毕,通过 inserBefore 插入到 oldStartIndex 之前;如果 old virtual nodes 处理完毕, 通过 insetBefore 添加到 newEndIndex 之前。
-
react、react-dom、redux、react-redux、react-router 的作用
react 应用中 react 和 react-dom 需配合使用。
react 负责为类组件构建组件实例、提供 createElement 方法构建 react element,react-dom 负责更新 DOM 和 react element 保持一致。
在 react 应用中,如果要使用公共状态,需引入 redux 和 react-redux。redux 提供了一种通用的状态管理机制,如果需要引入 react 应用, 需配合 react-redux 一起使用。
在 react 应用中,如果要使用路由控制,需引入 react-router。
-
React.Fragment
通过 React.Fragment, 我们可以避免添加无用 dom 节点的情况。
在 react render 阶段,如果遇到 React.Fragment, 会直接渲染 Fragment 的子节点,并将构建的 child fiber node 挂到 Fragment 对应的 parent fiber node 的 child 属性下。
-
JSX
JSX 仅仅是 React.createElement 方法的语法糖。在实际的项目中, JSX 会在打包过程中转化为 React.createElment 格式。
在执行 render 方法时,会调用 React.createElement, 返回 react element。
-
setState 的响应式更新
类组件的响应式更新是通过 setState 触发的。
执行 setState 时,会构建一个 update 对象,添加到类组件 fiber node 的 updateQueue 中, 然后进行 react render 和 react commit 操作。 类组件实例的 state 属性在 react render 阶段更新。
不同的应用场景, react render、 react commit 操作的时机不同。
在 componentDidMount 中直接使用 setState, state 的更新是异步的,需要等到 componetnDidMount 中的同步代码全部执行完毕之后,才会进行 react render,更新 state。
componentDidMount 中多次直接使用 setState, 只会触发一次 render。
在事件 callback 中使用 setState, state 的更新也是异步的,需要等到 callback 中的同步代码全部执行完毕之后, 才会更新 state, 情况和 componentDidMount 一致。
事件 callback 中多次直接使用 setState, 只会触发一次 render。
在 ajax、setTimeout、promise 等回调中使用 setState, state 的更新是同步的。即执行 setState 以后, 立即处理对应的更新, 获取最新的 state, 然后执行 render 方法。
异步回调中使用一次 setState, 就会触发一次 render。
批量执行 setState 方法时,可以传入普通对象和函数。 传入的值的类型不同,会导致更新以后的 state 也不相同。 如果传入的是对象, react 会依次将传入的对象和上一次 commit 以后的 state 合并,然后更新 state;如果传入的是函数,会先传入上一次 commit 以后的 state,返回值和 上一次 commit 以后的 state 合并, 然后传入下一个函数。等最后一个函数执行完毕以后,返回值和 上一次 commit 以后的 state。
-
类组件在 mount 阶段和 update 阶段,分别经历了什么过程?
挂载阶段(mount),即组件 template 对应的 dom 节点添加到页面的过程。更新阶段(update),即更新组件的 state,对页面上组件对应的 dom 节点做局部更新。
类组件(class component)在挂载阶段(mount)经历的过程为:
-
执行类组件构造函数构建类组件实例;
-
将类组件实例和组件标签对应的 fiber node 建立一一对应关系;
-
执行类组件实例的 render 方法生成对应的 react element tree;
-
根据 react element tree 生成 fiber node tree;
-
根据 fiber node tree 生成 dom 树并添加到页面中;
类组件(class component) 在更新阶段(update)经历的过程为:
-
执行组件实例的 setState 方法触发 state 的更新;
-
根据类组件标签对应的 fiber node 获取对应的组件实例对象;
-
构建每一次更新对应的 update 对象。
类组件实例每次执行 setState 方法是时都会构建一个 update 对象。update.payload 的值为调用 setState 时传入的值。
批量执行 setState 方法时,会构建多个 update 对象, 下一个 update 对象会挂在上一个 update 对象的 next 属性上。
-
react render 阶段处理类组件 updateQueue 收集的 update 对象, 获取更新以后的 state。
遍历 updateQueue 收集的 update 对象,使用 update.payload 的值 - 普通对象或者函数的返回值,和原来的 state 做合并处理, 返回最后的 state,并更新组件实例的 state。
-
重新执行类组件实例的 render 方法,生成对应的 react element tree。
-
根据 react element tree 生成新的 fiber node tree;
-
对比新旧 fiber node tree, 对页面上的 dom 树做局部更新;
-
-
函数组件在 mount 阶段和 update 阶段分别经历了什么过程?
函数组件(function component) 在挂载阶段(mount)经历的过程为:
-
执行函数组件。
-
如果使用了 useState hook,返回 useState 生成的 state、setState。
在每一次 useState 的执行过程中,都会生成一个 hook 对象。hook.memoizedState 存储组件的 state,hook.queue 用于更新组件的 state,hook.queue.dispatch 用于触发 state 的更新。
函数组件中有多个 useState,会生成多个 hook 对象, 每一个 hook 对象会挂载前一个 hook 对象的 next 属性上。
一个函数组件对应多个 hook, 一个 hook 对应一个 state、一个 queue。
挂载阶段(mount)使用的 useState 和更新阶段(update)的 useState 不是同一个 useState。
useState 方法会返回 hook.memoizedState、hook.queue.dispatch 作为函数式组件的 state、 setState
-
使用 useState 返回的 state、setState, 生成 template 对应的 react element tree。
-
将 react element tree 转化为 fiber node tree。
-
根据 fiber node tree 生成 dom 树并添加到页面中。
函数组件(function mount) 在更新阶段(update)经历的过程为:
-
执行挂载阶段(mount) useState 生成的 setState 来触发 state 的更新。
-
构建每一次更新对应的 update 对象。
执行一次 setState 方法, 就会生成一个 update 对象。 如果批量执行 setState 方法, 会构建多个 update 对象, 每一个 update 对象都会挂在前一个 update 对象的 next 属性上。 构建的 update 对象会收集到 hook.queue 中。
update.eagerState 会保存调用 setState 时传入的值 - 普通对象或者函数的返回值。
-
react render 阶段重新执行函数组件,再次执行 useState 方法。
此时执行的 useState 方法和挂载阶段(mount)提供的 useState 方法不同, 用于返回更新以后的 state。
useState 在执行过程中会遍历 hook.queue 中收集的 update 对象,一次使用 update.eagerState 的值替换 hook.memoizedState,然后返回 hook.memoizedState、hook.queue.dispatch 作为函数式组件的 state、 setState。
函数式组件执行 setState 时,不会做 state 合并, 只会替换。
-
使用 useState 返回的 state、setState, 生成 template 对应的 react element tree。
-
将 react element tree 转化为 fiber node tree。
-
对比新旧 fiber node tree, 对页面上的 dom 树做局部更新。
-
-
react 组件 props 的只读性
不管是函数组件还是类组件, 传入的 props 都不能修改。
组件实例的 props 也不可以修改。
不能修改的意思是:不能向 props 添加新的属性,不能删除已有属性,不能修改 props 中 已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。
如果 props 的某个属性是一个对象,那么修改这个属性对象的某个属性,是可以的, 不过不建议这么做。
props的只读性, 是通过 Object.freeze 方法实现的。
在渲染过程中, template 会先转化为 react element tree,每一个 template 标签对应的一个 react element。 标签上的每一个属性,都会存储到 react element 的 props 属性中。 构建 react element 的时候, 会通过 Object.freeze 方法将 element、element.props 冻结。
在执行 函数组件方法 或者 类组件构造函数 时, 传入的就是对应的 react element 对象的 props 属性。由于 props 属性已经被冻结, 所以不可以修改。如果强行修改, 严格模式下会报错。
react 只将 element 和 element.props 冻结。 如果 element.props 的某个属性是一个对象, 那么属性对象中的属性没有被冻结,可以正常修改。
注意:所有组件都要像纯函数一样保证他们的 props 不被修改。
-
react 怎么区分函数组件和class组件?
函数组件 和 class 组件 都是一个 function, react是怎么区分两者呢?
class 组件继承自 React.Component。在 React.Component.prototype 上, 有一个isReactComponent 属性,值为 {}。
如果 componet.prototype.isReactComponent 属性值为 {}, 那么当前组件是一个class 组件。
如果 component.prototype.isReactComponent 的值为 undefined, 那么需要根据组件的返回值来判断是什么类型的组件。如果返回值是一个对象,且包含render方法, 那么会当做一个class 组件, 此时会抛出警告,否则作为 函数组件。
-
react hooks - useState 的使用
通过 hook - useState, 我们可以为函数组件添加状态 - state 以及对应的 dispatch(用于修改 state)。
函数式组件是一个函数,不管是在初次挂载还是更新的时候,都会执行这个函数,返回相应的 react element tree。 在执行函数的时候, 如果使用了 useState, 那么在初次挂载或者更新的时候, 都会执行 useState。
不同的阶段,执行的 useState 方法不同。 挂载阶段,执行 useState, 返回初始化的 state 和 dispatch; 更新阶段,执行 useState, 返回的是更新以后的 state 和 dispatch。
-
挂载(mount)阶段
挂载阶段,react 会提供一个 useState 方法,主要工作是创建一个 hook 对象。
hook 对象有几个关键属性:memoizedState、queue、next。 memoizedState 用于保存函数式组件的 state, 值为调用 useState 时传入的值; queue 用于函数式组件的更新; next 指向下一个 hook 对象。
只要执行一次 useState 方法, 就会生成一个 hook 对象。 批量执行 useState 方法时, 会生成多个 hook 对象。 每一个 hook 对象会挂在上一个 hook 对象的 next 属性上,最后一个 hook 对象的 next 属性为 null。 第一个 hook 对象会挂到函数式组件标签对应的 fiber node 的 memoizedState 属性上。 通过函数式组件相应的 fiber node 的 memoizedState 属性, 我们即可访问 useState 方法生成的 hook 对象。
hook 对象构建完成以后, useState 方法会返回 [hook.memoizedState, hook.queue.dispatch] 作为 函数式组件的 state 及 setState。
hook.queue.dispatch 是一个闭包, 它会把相应的 hook、fiber node 保存起来。当我们执行 hook.queue.dispatch 时, 可以访问相应的 hook 及 fiber node。
接下来,函数式组件会使用 useState 方法返回的 state、setState, 生成 template 对应的 react element tree, 然后生成 fiber node tree, 最后生成相应的 dom tree, 添加到页面中。
-
更新 (update) 阶段
当我们需要更新 state 时, 我们会主动调用 mount 阶段 useState 返回的 dispatch 方法来更新 state。
dispatch 方法的主要工作是构建更新操作相应的 update 对象。
只要执行一次 dispatch 方法, 就会构建一个 update 对象。update 对象通过 action 属性来保存执行 dispatch 方法时传入的值。 action 属性的值可以是函数,也可以是一个普通对象。
当我们批量执行 dispatch 方法时,会构建多个 update 对象。 此时多个 update 对象会通过 next 属性关联起来。 update 对象的 next 属性指向 下一个 update 对象。
构建的最后一个 update 对象, 会挂载 hook.queue 的 last 属性上,它的 next 属性会指向 第一个 update 对象。 通过 hook.queue.last.next 和 update.next, 即可访问本次更新生成的所有 update 对象。
react render 阶段, 会再次调用函数式组件方法,重新执行 useState 方法。不过此时 react 会提供一个新的 useState 方法,用于获取更新以后的 state。在 useState 执行过程中,也会生成一个 hook 对象。 这个 hook 对象会对挂载阶段对应的 hook 做浅拷贝,即同样的 useState 操作, mount 阶段和 update 阶段生成的 hook 对象虽然不是同一个对象, 但是属性完全相同。
只要执行一个 useState 方法, 就会构建一个 hook 对象。
接下来,会处理 hook.queue 收集的 update 对象,依次使用 update.action 的值来更新 hook.memoizedState, 然后返回 [hook.memoizedState, hook.queue.dispatch ] 作为新的 state 和 setState, 然后使用新的state、setState,生成新的 react element tree,将 react element tree 转化为 fiber node tree, 然后对比新旧 fiber node tree, 对 dom 节点做局部更新。
-
-
执行 useState 提供的 dispatch 方法, 是否立即可以触发 state 的更新?
执行 useState 提供的 dispatch ,不一定可以立即触发 state 的更新。
首先,不能在函数式组件中直接使用 useState 提供的 dispatch 更新 state, 否则会陷入一个死循环。
在事件 callback 中使用 dispatch, state 的更新也是异步的,需要等到 callback 中的同步代码全部执行完毕之后, 才会更新 state。
事件 callback 中多次直接使用 dispatch, 函数式组件只会执行一次。
在 ajax、setTimeout、promise 等回调中使用 dispatch, state 的更新是同步的。即执行 dispatch 以后, 立即处理对应的更新, 获取最新的 state, 然后执行函数式组件。
异步回调中使用一次 setState, 就会执行一次函数式组件。
-
为什么不可以在函数式组件中直接使用 useState 方法返回的 setState?
使用 setState 修改 state 时, 会触发函数式组件函数的执行。 函数式组件再次执行, 又会导致 setState 方法的重新执行, 这样就会陷入一个死循环。
不要在函数式组件中直接使用 useState 方法返回的 setState。
-
为什么不可以在类组件的 render 方法中使用 setState?
使用 setState 修改 state 时, 会触发类组件实例的 render 方法执行。 render 方法执行时, 又会导致 setState 方法的重新执行, 这样就会陷入一个 死循环。
不要在类组件的 render 方法中 直接 使用 useState 方法返回的 setState。
-
函数式组件触发 setState 时,传入普通对象和函数有什么区别?
和类组件触发 setState 的情况一样。
-
类组件和函数式组件使用 setState 更新时,有什么区别?
类组件 使用 setState 更新 state 时, 做的是 合并操作。即将 setState 方法时传入的数据(普通对象或者函数的返回值),合并到上次的 state。
而 函数组件 使用 setState 时,做的是 替换操作。即将 setState 方法时传入的数据(普通对象或者函数的返回值),替换上次的 state。
-
为什么在 componentDidMount 里面直接使用 setState, 用户却不会看到中间状态?
浏览器并不会在发生 dom 操作的时候立即渲染。相反,会收集多次 dom 操作一次性渲染。
浏览器会以一定的频率每隔一段时间进行一次渲染。 在这个时间段内, 如果发生多次 dom 操作,只会修改 dom 树结构。等到可以渲染的时候, 将最新的 dom 树结果渲染出来。
js 代码执行和浏览器渲染是互斥的, 即 js 代码执行的时候不能进行浏览器渲染, 浏览器渲染的时候不能执行js代码。
当执行 componentDidMount 的时候, template 对应的 dom 节点已经挂载到了容器dom节点上。 但此时由于 js 代码还未执行完毕, 所以不会进行浏览器渲染。 在 componentDidMount 里面直接使用 setState, 会导致组件重新执行 render 方法, 更新对应的dom节点。 等到 componentDidMount 执行完毕, 浏览器开始渲染, 将最新的dom树渲染出来, 因此用户看不到中间状态。
debugger 模式下, 用户可以看到中间状态,而使用alert的时候, 用户不会看到中间状态。
即 debugger 不会阻塞浏览器渲染, alert 会阻塞浏览器渲染。
-
一个组件,从 template 标签到最后的 dom 节点, 经历了什么过程?
一个组件(类组件或者函数组件),从 template 标签到最后的 dom 节点, 经历的大致流程如下:
-
父组件实例或者 ReactDom 执行 render 方法(或者函数式组件方法执行)时, 组件从 template 标签转化为 react element。
reactElement.props 会存储 template 标签上的所有属性,如果 ref、value、onClick 等。
-
基于 react element, 构建对应的 fiber node。
-
执行类组件实例的 render 方法或者函数式组件方法, 将组件内部的 template 转化为 react element。
-
基于步骤3生成的 react element, 将组件 template 转化为 fiber node tree。
-
基于步骤4生成的 fiber node tree, 将组件 template 转化为 dom 节点。
-
react render 阶段完成, 进入 react commit 阶段;
-
将组件对应的 dom 节点添加到 dom 树中;
-
-
useState 和 useReducer 的区别?
useState 和 useReducer 都可以用来构建函数式组件的 state 和 setState。不同是, 使用 useState 的时候, 只需要传递初始state,而使用 useReducer 的时候, 需要传递一个 reducer 和 初始 state。
当我们使用 useReducer 来构建 state、 setState 时, 和 useState 过程基本相同。 都会构建一个 hook 对象, hook.memoizedState 的值为初始 state, hook.queue 用于更新 state, hook.queue.dispatch 即 setState 用于触发 state 的更新。唯一不同的时,hook.lastRenderedReducer 属性。 如果使用 useReducer,lastRenderedReducer 的值为调用 useReducer 传入的 reducer;如果使用的是 useState, lastRenderedReducer 的值为 react 提供的默认 reducer - basicStateReducer。
basicStateReducer 如下:
function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }当我们执行 setState 方法触发 state 的更新时, 会构建一个 update 对象, update.eagerState 的值为此次更新以后 state。 react 会使用 hook.lastRenderedReducer 来计算 eagerState 的值。计算的时候,会传入两个参数, 第一个参数为当前 state, 第二个参数为执行 setState 方法时传入的值。
如果 setState 是由 useState 构建, 那么使用 basicStateReducer 计算 state,计算过程如上; 如果 setState 是有 useReducer 构建, 那么就会使用用户自定义的 reducer 计算 state。
useState 实际上就是使用 basicStateReducer 的 useReducer, 如下:
const [count, setCount] = useState(0) // 等价于 function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; } const [count, setCount] = useReducer(basicStateReducer, 0) -
useReducer
使用 useReducer 构建 state、 setState 时, 除了传入 reducer、initState 以外, 还可以传入一个 init 方法, 构建最后的 state, 用法如下:
const [state, setState] = useReducer(reducer, initState, init)在 挂载阶段(mount), 如果没有传入 init, 会使用 initState 作为函数式组件的 state; 如果传入了 init, 会使用 init 的返回值作为函数式组件的 state。 执行 init 方法时,传入 initState。
注意: init 只在挂载阶段(mount)有用, 更新阶段(update)无用。
-
useEffect 的使用
通过 hook - useEffect, 我们可以注册一个 effect。当浏览器渲染 完成以后,执行注册的 effect。
函数式组件 挂载或者更新的时候,都会执行函数式组件方法,然后执行 useEffect 来注册 effect。 因此,只要函数式组件方法执行了, effect 就会注册, 浏览器渲染工作完成以后就会触发 effect。
-
挂载阶段(mount)
挂载阶段执行 useEffect(effect, deps) 时,会构建一个 hook 对象 和 effect 对象。 其中,effect.create 对应传入的 effect,组件完成浏览器渲染以后触发; effect.deps 对应传入的 deps,是 effect 触发的条件。effect 对象会添加到 hook 对象的 memoizedState 属性上。
只要执行一次 useEffect, 就会构建一个 hook 对象 和 effect 对象。 当我们在函数式组件中批量执行 useEffect 时, 会构建多个 hook 对象和 effect 对象。新的 hook 对象和 effect 对象会分别添加到上一个 hook 对象和 effect 对象的 next 属性上。这样就生成了一个 hook 对象列表和 effect 对象列表。
挂载阶段生成的 effect 列表会添加到函数式组件对应的 fiber node 的 updateQueue 属性上。
当浏览器完成渲染工作以后, 会遍历 updateQueue 中收集的 effect 对象, 依次触发 effect.create 属性对应的方法。 effect.create 方法的返回值会挂载 effect.destory 属性上,用于清除 effect.create 方法引起的副作用。 effect.destory 会在下一次函数式组件更新时触发。
-
更新阶段(update)
更新阶段,函数式组件方法会再次执行,useEffect 方法会再次被触发。更新阶段,会为函数式组件对应的标签,构建一个 new fiber node。执行 useEffect 时,也会像 mount 阶段一样构建一个 hook 对象列表和 effect 对象列表。生成的 hook 对象和 effect 对象和更新前的 hook 对象和 effect 对象的属性一样。
更新阶段生成的 effect 列表会添加到函数式组件对应的 new fiber node 的 updateQueue 属性上。
当浏览器完成渲染工作以后, 会遍历 updateQueue 中收集的 effect 对象, 先触发 effect.destory, 清除上一次触发 effect 时造成的副作用, 然后再触发 effect.create 方法。
-
-
条件 effect
默认情况下, useEffect 注册的 effect, 函数式组件挂载(mount)和更新(update)时都会触发。
我们可以在调用 useEffect, 传入第二个参数, 使得 effect 在满足某些条件的时候才会触发,如下:
// useEffect(effect, deps) useEffect(effect, [count, props.id])在上面示例中, mount 阶段, effect 肯定会执行的。在 update 阶段, 如果 count、 props.id 的值都没有变化, effect 不会触发。 只要 deps 数组中有一个值发生了变化, effect 都会触发。
在更新阶段, useEffect 会生成一个新的 effect 对象。 如果用户定义了 deps, 会先比较 新旧 deps 是否相同(通过 Object.is 判断)。 如果不相同, 则把 effect 对象添加到 updateQueue 中; 如果相同, 直接忽略。
当浏览器完成渲染工作以后, 会遍历 updateQueue 中收集的 effect 对象, 先触发 effect.destory, 清楚上一次触发 effect 时造成的副作用, 然后再触发 effect.create 方法。
-
effect 的异步执行
与 componentDidMount、componentDidUpdate 不同的,effect 会在浏览器完成渲染工作以后才会触发。 在 react commit 阶段,会为 effect 构建一个任务, 添加到异步任务队列中,等到浏览器完成渲染工作以后, 才会处理异步任务队列, 触发 effect。
react 通过 messageChanel、setTimeout、requestAnimationFrame 实现 异步任务队列。
-
useEffect 和 useLayoutEffect 的区别
useLayoutEffect 的用法和 useEffect 一模一样。唯一的不同是 useLayoutEffect 注册的 effect 在浏览器渲染工作之前触发。
layoutEffect 和 componentDidMount、componetDidUpdate 的触发时机相同, 都是在浏览器渲染工作之前触发。
-
useContext 的用法
通过 useContext 方法,函数式组件可以接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。
调用了 useContext 的组件总会在上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 值变化时重新渲染。
当使用 useContext 时, 除了会读取 Context 的值外, 还会构建一个 contextItem 对象添加到当前组件对应的 fiber node 的 dependencies 中。
当父组件更新导致 Context 的值发生变化时, 就先会检查 Context.Provider 中的子组件是否使用了 Context(判断 fiberNode.dependencies 中是否有 Context)。 如果有子组件使用了 Context,就会被强制更新, 函数式组件方法就会执行。
函数式组件作为子组件使用时, 父组件更新,子组件默认会更新。 如果函数式组件被 React.memo 包裹时, 如果父组件传入的 props 没有更新, 子组件不会更新的。使用 Context 以后,即使父组件传入的 props 没有更新, 子组件也会强制更新。
-
Context.consumer 的使用
除了通过类组件的 contextType 和函数式组件的 useContext,我们还可以通过 Context.Consumer 实现父组件 Context 更新时, 使用 Context 的子组件触发更新。
使用原理为: 使用 Context.Consumer 时, 会构建一个 contextItem 对象添加到当前组件对应的 fiber node 的 dependencies 中。
当父组件更新导致 Context 的值发生变化时, 就先会检查 Context.Provider 中的子组件是否使用了 Context(判断 fiberNode.dependencies 中是否有 Context)。 如果有子组件使用了 Context,子组件就会被强制更新。
此时, Context.Consumer 会强制更新, 重新生成子元素, 相应的使用 context 的子组件会重新渲染。
-
useMemo 的用法
useMemo 可用于定义一个类似于 vue 计算属性的值, 用法如下:
const obj = React.useMemo(compute, deps)定义的计算属性,默认情况下,函数式组件每次更新的时候都会重新计算。如果定义的时候, 定义了依赖的值,那么函数式组件更新的时候,如果计算属性依赖的值没有变化, 计算属性不需要重新计算。
挂载阶段(mount)执行 React.useMemo 方法时,会构建一个 hook 对象,compute 方法的返回值 value 和 deps 会缓存到 hook 对象的 memoizedState 属性上。
更新阶段(update)执行 React.useMemo 方法时,如果有 deps, 会比较更新前后 deps 是否有变化。 deps 中只要有一个依赖项发生变化, 计算属性就会重新计算。 如果 deps 没有变化, 会使用上一次的计算结果。 如果没有定义依赖项 deps, 计算属性会重新计算。
没有依赖项, 函数式组件每次更新时计算属性都会重新计算。 设置依赖项以后, 依赖项发生变化(通过 Object.is 来判断), 计算属性才会重新计算, 否则使用上一次更新时缓存的结果。
useMemo 不可靠,你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。
-
React.useCallback 的用法
useCallback 可用于返回一个缓存的 function, 用法如下:
const func = React.useCallback(callback, deps)如果没有定义 deps, 函数式组件每次更新的时候, func 和 callback 一样。 如果定义了 deps,且 deps 没有变化, 则 func 为缓存的函数,与传入的 callback 可能不一样; 如果 deps 发生了变化, func 和 callback 一样。
挂载阶段(mount)执行 React.useCallback 方法时,会构建一个 hook 对象,传入的 callback、deps 会缓存到 hook 对象的 memoizedState 属性中。
更新阶段(update)执行 React.useCallback 方法时, 如果有 deps, 会比较更新前后 deps 是否有变化。 deps 中只要有一个依赖项发生变化(通过 Object.is 来判断), 返回传入的 callback。 如果 deps 没有变化, 会使用缓存的 callback。 如果没有定义依赖项 deps, 会返回传入的 callback。
-
父组件更新的时候, 是否每一个子组件都会更新?
默认情况下, 父组件更新的时候, 子组件都会更新。即如果子组件是类组件(非纯组件且不提供 shouldComponentUpdate 方法), 子组件实例的 render 方法会执行; 如果子组件是函数式组件, 组件方法会再次执行。
如果子组件是类组件且为纯组件, 子组件的 props、state 没有变化, render 方法不会执行。
如果子组件是类组件且提供了 shouldComponentUpdate 方法, 如果 shouldComponentUpdate 方法返回 false, render 方法不执行; 返回 true, render 方法执行。
如果子组件是使用 React.memo 返回的高阶组件, props 没有变化, 子组件不会重新渲染。
-
组件强制更新
-
类组件实例主动调用 forceUpdate;
-
使用 Context 的值发生变化;
-
shouldComponentUpdate 返回 true;
-
函数组件不使用 React.memo;
-
-
refs & dom
React.createRef 方法可以用来构建一个 ref 对象。通过 ref.current, 我们可以访问组件 dom 节点和子类组件实例对象。
使用 refs 时, 我们需先使用 React.createRef 方法构建一个 ref 对象, 此时 ref.current 的值为 null。
类组件渲染的时候, 会先执行 render 方法, 构建 react element tree。 此时如果模板标签有 ref 属性, 那么对应的 react element 对象的 ref 属性的值为我们通过 React.createRef 构建的 ref 对象。 通过 react element 生成的 fiber node 的 ref 属性值也为我们通过 React.createRef 构建的 ref 对象。
fiber node 的 ref 属性的赋值过程在 react commit 阶段。 赋值的时候, fiber node 对应的 dom 节点已经添加到浏览器 dom 树结构中。
如果 fiber node 对应的普通 dom 标签, 那么相应的 dom 元素会赋值给 fiber node 的 ref.current 属性,即给 React.createRef 构建的 ref 对象的 current 属性赋值。
如果 fiber node 对应的是类组件标签, 那么相应的类组件实例会赋值给 fiber node 的 ref.current 属性,即给 React.createRef 构建的 ref 对象的 current 属性赋值。
ref.current 的赋值操作发生的时候, react render 阶段生成的 dom 节点已经添加到浏览器 dom 树中。
子组件的 ref 先赋值, 父组件的 ref 后赋值。
使用 refs 的 dom 节点或者组件在删除时会卸载 refs, 即将 ref 设置为 null。
ref 不是 props 属性。
-
回调 refs
我们也可以通过回调 refs 来实现访问组件 dom 节点和子类组件实例对象, 如下:
// 组件实例通过 refElement 属性访问 dom 节点 <div ref={e => this.refElement = e} ></div> // 父组件通过 childComp 属性访问子类组件实例 <Component ref={e => this.childComp = e} />使用回调 refs 以后, 相应的 fiber node 的 ref 属性是一个函数。在 react commit 阶段, 如果 fiber node 有 ref 属性且是一个函数, 会触发函数的执行, 传入的参数为 ref 属性所在的 dom 节点或者组件对应的实例。
回调 refs 执行的时候, react render 阶段生成的 dom 节点已经添加到浏览器 dom 树中。
子组件的回调 refs 先执行, 父组件的回调 refs 后执行。
使用 refs 的 dom 节点或者组件在删除时会卸载 refs, 即将 ref 设置为 null。
通过 回调 refs, 父组件可以直接获取子组件的某个 dom 节点, 如下:
// 父组件: < Component inputRef={e => this.input = e} /> // 子组件: <input ref={props.inputRef} /> -
React.useRef 的用法
React.useRef 的用法和 React.createRef 完全一样。
通过 React.useRef 可以构建一个 ref 对象, 在 react commit 阶段, 使用 ref 属性所在的 dom 节点或者组件实例为 ref.current 赋值。
挂载阶段和更新阶段, 使用的 ref 对象是同一个。
如果使用 React.createRef, 在更新阶段,会重新构建一个 ref 对象。
-
ref 转发
ref 转发的用法如下:
// 子组件 const component = React.forwardRef((props, ref) => { return ( <div ref={ref}></div> ) }) // 父组件 <component ref={this}ref 转发, 实际上就是父组件和子组件共用一个父组件的 ref 对象。
在 react commit 阶段,如果 ref 是一个对象,将 dom 节点赋值到子组件 fiber node 的 ref.current 属性时, 实际是给父组件的 ref 对象的 current 属性; 如果 ref 是一个函数,触发 ref 函数的执行,将 dom 节点作为传入参数。
ref 转发适用于函数式组件, 不适用与类组件。
转发的 ref 可以是一个 ref 对象(有 current 属性), 也可以是一个函数。
-
useImperativeHandle 的用法
useImperativeHandle 一般配合 farwards 一起使用,可用于向父组件的 ref 暴露自定义的值, 用法如下:
// 子组件 const Child = React.farwards((props, ref) => { const refInput = React.useRef() React.useImperativeHandle(ref, () => { return { focus: () => { refInput.current.focus() } } }) return < input ref={refInput} / > }) // 父组件 function Parent(props) { const refInput = React.useRef() React.useLayoutEffect(() => { refInput.current.focus() }) return < Child ref={refInput} / > }在上面的示例中, useImperativeHandle 会将 callback 的返回值添加到 Parent 的 refInput 对象的 current 属性上, 这样我们就可以使用 refInput.current.focus 来操作子组件中的 dom 节点。
useImperativeHandle 用法与 useLayoutEffect 一样, 会注册一个 effect,在 react commit 阶段, 触发注册的 effect, 执行 effect 对应的 callback。 此时如果转发的 ref 是一个对象, callback 的返回值会添加到 ref 的 current 属性上; 如果转发的 ref 是一个函数, 执行 ref 函数,callback 的返回值会作为 ref 的一个参数。
-
父组件访问子组件 dom 节点的方式
父组件访问子组件 dom 节点的方式:
-
子组件是类组件
- refs
// 子组件 class Child extends React.Component { constructor(props) { super(props) this.refDom = React.createRef() } render() { return ( <div ref= {this.refDom}></div> ) } } // 父组件 function Parent(props) { const refDom = React.useRef() return ( <Child ref={refDom} /> ) // 通过 refDom.current.refDom.current 访问子组件dom节点 }- refs 回调
// 子组件 class Child extends React.Component { constructor(props) { super(props) } render() { return ( <div ref= {this.props.getDom}></div> ) } } // 父组件 function Parent(props) { const refDom = React.useRef() return ( <Child getDom={e => refDom = e} /> ) // 通过 refDom 访问子组件dom节点 } -
子组件是函数式组件
- refs 回调
// 子组件 function Child (props) { return ( <div ref= {props.getDom}></div> ) } // 父组件 function Parent(props) { const refDom = React.useRef() return ( <Child getDom={e => refDom = e} /> ) // 通过 refDom 访问子组件dom节点 }- ref 转发
// 子组件 const Child = React.forwardRef((props, ref) => { return ( <div ref={ref}></div> ) }) // 父组件 function Parent(props) { const refDom = React.useRef() return ( <Child ref={refDom} /> ) // 通过 refDom.current 访问子组件dom节点 }-
ref 转发 + useImperativeHandle
// 子组件 const Child = React.farwards((props, ref) => { const refInput = React.useRef() React.useImperativeHandle(ref, () => { return { focus: () => { refInput.current.focus() } } }) return <input ref={refInput} /> }) // 父组件 function Parent(props) { const refInput = React.useRef() React.useLayoutEffect(() => { refInput.current.focus() }) return <Child ref={refInput} /> }
-
-
状态提升
状态提升: 多个子组件共用同一个 state 时, 可以把这个 state 提升到父组件,然后通过 props 供子组件使用。 父组件会定义一个更新 state 的方法, 通过 props 传递给子组件供子组件调用。 子组件触发父组件传递的方法, 即可通知父组件更新 state。
状态提升和 vue 的自定义事件一样。
-
类组件生命周期方法
-
错误边界
-
高阶组件 - HOC
高阶组件,实质是一个纯函数,传入一个组件, 返回一个新组件,不改变传入的组件。
使用高阶组件时, 应该注意以下关键点:
-
高阶组件是一个纯函数,不要对传入的组件进行修改。应该提供一个容器组件包裹传入的组件,在容器组件上做对应的修改。
-
如果传入的源组件有静态方法, 应将源组件的静态方法复制到容器组件。
-
不要在 render 方法和函数式组件方法中使用 HOC。
在 render 或者函数式组件方法中使用 HOC, 父组件每次更新时,会先卸载更新前的 HOC, 删除对应的 dom 节点, 然后挂载新的 HOC,将对应的 dom 节点添加到 dom 树中,而不是对子组件做局部更新, 还造成不必要的子组件重新渲染。
可以这么理解,在 render 方法中使用 HOC, 父组件每次更新时, HOC 组件的 componentWillUnmount、 componentDidMount 都会触发, componentDidUpdate 永远不会触发。
-
通过 refs 转发和 props 将 HOC 上的 ref 传递到源组件。
方式如下:
function HocComponent(Component) { class Component1 extends React.Component { ... render() { return <Component ref={props.forwardRef} /> } } return React.forwardRef ((props, ref) => { return <Component1 forwardRef= {ref} /> }) }传递方式为: 先通过 refs 转发将 HOC 组件标签上的 ref 传递给容器组件,然后容器组件通过 props 将 ref 传递给 源组件。
-
-
Portals
通过 Portals,我们将子节点添加到父组件以外的指定 dom 节点。
具体用法如下:
ReactDOM.createPortal(child, container)其中, container 为指定的 dom 节点, child 为任何可渲染的 react element。
Portals 中的子节点会在 react commit 节点添加到指定的 dom 节点中。
在 react 应用中, fiber node 树结构和 template 结构是匹配的。使用 Portals 后, 会导致实际 dom 树结构和 fiber node 树结构不匹配的情况。
使用 Portals 以后, Protals 中子节点的事件会被非父节点的 dom 节点捕获。可以这么理解, 事件冒泡(捕获), 是按照 fiber node 树进行的, 即 parent fiber node 可以捕获 child fiber node 的事件。
具体的实现流程如下:
-
挂载阶段,parent fiber node 如果需要绑定事件, 会给 document 设置一个代理事件;
-
子节点触发事件,被 document 捕获;
-
document 捕获触发的事件以后, 根据 event object, 找到触发事件的 dom 节点;
-
找到触发事件的 dom 节点的对应的 fiber node;
-
以 child fiber node 为起点, 找到注册相应事件的所有 parent fiber node, 依次触发事件对应的 callback;
-
-
render props
render props, 即父组件传递给子组件的的 prop 是一个函数,子组件执行这个函数,返回一个 react element。
使用 render props时, 如果组件是一个 pure component, 则应该将 render props 定义为静态属性,否则 pure component 无效。
-
受控组件&非受控组件
受控组件: 组件中表单元素的值与组件的 state(props) 绑定, 通过组件 state 获取表单元素的值。
非受控组件: 组件中表单元素的值与组件的 state(props) 没有关系,直接通过表单元素获取表单元素的值。 非受控组件一般需配合 refs 使用。
-
React.Context 的基本用法
context 提供了一个无需为每层组件添加 props, 就可以在组件树中进行数据传递的方法。
使用 context 时, 我们需要先通过 React.createContext 方法构建一个 Context 对象。Context 对象有一个属性 _currentValue,react 会通过这个属性在组件树中进行数据传递。
React.Context 的使用流程, 挂载阶段(mount)和更新阶段(update)各不相同。
-
挂载阶段(mount)
在父组件中, 会通过 Context.Provider 修改 Context._currentValue 的值。
子组件中, Context 对象会通过 contextType 属性挂到组件上。在构建组件实例的时候, 会读取 Context._currentValue 的值给组件实例的 context 属性赋值。这样,子组件执行 render 方法的时候,可以通过组件实例使用 context 属性。
在子组件获取 context 的过程中, 会构建一个 contextItem 对象, 添加到子组件对应的 fiber node 的 dependencies 中。
-
更新阶段(update)
当父组件发生更新时, 会判断 Context 的值是否发生变化。 如果发生变化, 就会检查 Context.Provider 中的子组件是否使用了 Context。 如果子组件中使用了 Context, 就会为子组件构建一个 update 对象(标注为需要强制更新), 添加到子组件的 updateQueue 中。
在更新子组件时, 由于子组件的 updateQueue 中有一个标注为强制更新的 update 对象, 子组件会被标注为需要强制更新。使用 Context 的值更新组件实例的 context 属性后, 重新执行 render 方法。(子组件可能是一个纯组件,如果 props、state 没有变化, 就不会执行 render 方法。 此时,必须对子组件强制更新,执行 render 方法, 否则父组件更新 Context 对子组件是没有意义的)。
使用 context 在组件树中进行数据传递的原理: 父组件和子组件共用一个 Context 对象, 父组件使用 Context.provider 修改 Context._currentValue, 子组件实例构建或者更新时使用 Context._currentValue 作为 context 属性的值, 并强制执行 render 方法。
-
-
React.memo 的用法
我们可以通过 React.memo 实现类似 React.PureComponent 的功能。
一个函数组件作为子组件, 如果父组件发生更新, 函数组件默认会更新,即函数式组件方法会重新执行一次。这种情况下, 即使子组件从父组件接收的 props 没有变化, 子组件还是会更新。
我们可以通过 React.memo 方法包装函数式组件, 当函数式组件接收的props没有变化, 子组件不会更新。
在挂载阶段, 会为 React.memo 构建一个 fiber node,child fiber node 为函数式组件对应的 template 生成的 fiber node。
当父组件更新需要 React.memo 对应的 fiber node 时, 会使用 react 提供的 shallowEqual 方法对 new fiber node 和 old fiber node 的 props 对象做一个浅层比较, 如果新旧 props 不相等, 需要重新执行子组件函数式组件方法, 更新子组件; 如果新旧 props 相等, 则不需要更新子组件。
使用 React.memo 时,我们还可以传入一个用户自定义的 compare 方法, 替换 react 提供的 shallowEqual 方法。当 compare 方法返回 false 时, 需要更新子组件, 当 compare 返回 true 时, 不需要更新子组件。
-
React.PureComponent 的用法
我们可以通过继承 React.PureComponent 来构建一个纯组件。
纯组件默认会有一个 shouldComponentUpdate 方法, 来决定组件是否需要更新。更新时, 会对新旧props、新旧 state 做浅层比较。如果相同, 则不需要更新;不同,需要更新。
props、state都是对象,做浅层比较的流程为:
- 先判断新旧props(state) 是否是同一个对象, 如果是, 直接返回 true;
- 如果不是同一个对象, 比较新旧props(state) 对象的属性个数是否相同, 如果不相等, 直接返回 false;
- 如果对象属性个数相等, 就比较属性名是否相同, 有一个不相同, 返回 false;
- 如果属性名都相同, 就比较属性值, 如果有一个属性值不相同, 返回 false;
由于是浅层比较, props、state 的数据结果不要太复杂, 以免比较不准确。
如果不需要更新, componentWillMount 生命周期函数不会触发。
如果纯组件需要强制更新, 则 shouldComponentUpdate 方法不会触发。
-
React.createElement 的用法
React.createElement 用于创建并返回指定类型的 react element。
react 应用中的 template 到最后的 dom 节点, 要经历的过程为: template -> generate code -> react element tree -> fiber node tree (虚拟 dom 树) -> dom 树。 其中, generate code 为 React.createElement(type, props, [...children]), 会通过 babel 处理 template 生成。
对比 vue, vue 应用中的 template 到最后的 dom 节点, 要经历的过程为: template -> render -> vnode tree(虚拟 dom 树) -> dom 树。 vue 会使用一个 compiler 将 template 转化为一个 render 方法。
// JSX const itemList = [1, 2, 3, 4, 5].map(item => { return <div> {item} </div> }) const stringList = ['a', 'b', 'c', 'd'] const obj = { value: '123' } const objList = [obj] const value = '1' var a = ReactDOM.render( <div value={value}> {itemList}{stringList}{objList}{obj}123 </div>, document.getElementById('app') ) // babel 处理以后的代码 const itemList = [1, 2, 3, 4, 5].map(item => { return React.createElement("div", null, " ", item, " "); }); const stringList = ['a', 'b', 'c', 'd']; const obj = { value: '123' }; const objList = [obj]; const value = '1'; var a = ReactDOM.render(React.createElement("div", { value: value }, itemList, stringList, objList, obj, "123"), document.getElementById('app'));React.createElement 执行的时候,需要传入至少两个参数。其中, 第一个参数为 type, 为 react element 的类型; 第二个参数为 props, 为对应 template 标签上的属性; 第二个参数之后的参数 - child,对应当前 template 标签的子元素,数量没有限制。
type 参数的类型为 string | function | object, 不同的类型,代表不同类型的 template 标签, 如下:
- string - template 标签为 html dom 节点;
- function - template 标签为用户自定义组件;
- object - React.Fragment、 React.memo、 React.farwards 等;
props 参数的类型为对象,值为 template 标签上的全部属性;
child 参数的类型为: number | [number] | string | [string] | react element | [react element]
child 参数的类型不能是普通对象或者元素有普通对象的数组,否则会报异常。
-
React.lazy & React.Suspense
React.lazy 和 React.Suspense 配合一起使用可以实现组件的懒加载, 具体用法如下:
const Component1 = React.lazy(() => import('./component1.js')) const Component2 = React.lazy(() => import('./component2.js')) class App extends React.Component { render() { return ( <React.Suspense fallback={<div>Loading...</div>}> <Component1 /> <Component2 /> </React.Suspense> ) } } export default App使用 React.Suspense 的使用,必须有 fallback 属性, 否则报错。
组件懒加载是基于 webpack 等打包工具实现。在使用动态加载组件时, 会构建一个 promise 对象。 在 promise 对象初始化的时候, 会通过动态构建 script 元素的方法, 从服务端获取懒加载模块对应的文件。 懒加载文件加载并执行完毕以后, 会将 promise 对象的状态置为 resolved, 然后触发相应的 onFullfilled。 在 onFullfilled 中, 会中相应的处理, 比如说使用加载完成的组件。
react 组件懒加载也是基于 webpack 等打包工具实现的, 整个流程大致如下:
-
在初次渲染的时候,遇到懒加载组件, 构建相应的 promise 对象, 从服务端获取懒加载模块对应的文件。
此时, promise 对象的状态为 pending。
React.Suspense 中有多少个懒加载组件, 就构建多少个 promise 对象。
-
完成初次渲染。
初次渲染时,会将 React.Susupense 中 fallback 对应的 template 渲染为 dom 节点。
此时浏览器会显示 fallback 提供的节点。
-
第一个懒加载模块加载完成以后, 相应的 promise 对象的状态变为 resolved,触发注册的 onFullfilled。
在 onFullfilled 中, react 会通过 MessageChanel 触发 react 异步更新。
-
等所有的懒加载模块加载完成以后, 开始 react 更新。
此时, 所有懒加载组件都已经获取完毕。经过 react render、 react commit 阶段以后, 所有的 template 都渲染为最终的 dom 的节点。
-
-
react 异步任务调度
setTimeout、 MessageChanel、requestAnimationFrame、requestIdleCallback
-
浅比较
继承 React.PureComponent 的 类组件 或者 React.memo 返回的组件都可以称之为 纯组件。
类纯组件,执行 setState 触发更新以后,会对新旧 state 和 新旧 props 进行浅比较。如果新旧 state 和 新旧 props 都相同, 类纯组件不需要重新渲染。
React.memo 返回的 纯组件 在触发更新以后,会对新旧 props 进行浅比较。如果新旧 props 相同,纯组件不需要重新渲染。
react 提供的浅比较方法 - shallowEqual 如下:
function shallowEqual(objA, objB) { // 使用 Object.is 判断两个值是否相等, 如果相等, 返回true if (is$1(objA, objB)) { return true; } // 如果 objA 不是对象, 或者为 null, 返回 false // 如果 objB 不是对象, 或者值为 null, 返回 false if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } // objA 对象的属性 var keysA = Object.keys(objA); // objB 对象的属性 var keysB = Object.keys(objB); // 如果属性值的数量不同, 直接返回false if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. for (var i = 0; i < keysA.length; i++) { // 通过 Object.is 比较第一层属性,不相同,即返回 false if (!hasOwnProperty$2.call(objB, keysA[i]) || !is$1(objA[keysA[i]], objB[keysA[i]])) { return false; } } return true; } function is(x, y) { return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y; } // 用于判断两个值是否相等 var is$1 = typeof Object.is === 'function' ? Object.is : is;ShallowEqual 是基于 Object.is 进行比较的。
Object.is() 判断两个值是否相同。如果下列任何一项成立,则两个值相同:
- 两个值都是 undefined
- 两个值都是 null
- 两个值都是 true 或者都是 false
- 两个值是由相同个数的字符按照相同的顺序组成的字符串
- 两个值指向同一个对象
- 两个值都是数字并且
- 都是正零 +0
- 都是负零 -0
- 都是 NaN
- 都是除零和 NaN 外的其它同一个数字
这种相等性判断逻辑和传统的 == 运算不同,== 运算符会对它两边的操作数做隐式类型转换(如果它们类型不同),然后才进行相等性比较,(所以才会有类似 "" == false 等于 true 的现象),但 Object.is 不会做这种类型转换。
这与 === 运算符的判定方式也不一样。=== 运算符(和== 运算符)将数字值 -0 和 +0 视为相等,并认为 Number.NaN 不等于 NaN。
var a = {name: '123'} var ObjA = {a: a} // {a: {name: '123'}} var ObjB = {a: {name: '123'}} // {a: {name: '123'}} shallowEqual(ObjA, ObjB) // false使用 shallowEqual 比较两个对象时, 如果属性值是一个对象, 那么这个对象必须是同一个引用, 否则返回 false。
-
redux 的使用
-
react-redux 的使用
-
redux 和 vuex 的异同
-
react-router 的使用
-
react router 和 vue router 的异同
其他
-
react 应用挂载的 dom 节点,一般为 id=app 所在的元素, 会作为 react 应用的 container 节点。将 react 应用挂载到 container 之前,会先把 container 中原来的 dom 节点移除掉。
-
函数组件, 未使用 hooks 之前, 也称之为 无状态组件。