前端框架React
- --------------------组件基础----------------------
- React事件机制
- 哪些方法会让React重新渲染、render会做什么
- React类组件和函数组件
- React高阶组件,和普通组件的区别,适用场景
- React受控组件和非受控组件
- React有状态组件和无状态组件
- React-Fiber
- --------------------数据管理----------------------
- setState调用的原理 **
- setState是同步还是异步
- this.state和setState
- state和props的区别
- props为什么是只读的
- props改变时更新组件的方法
- --------------------生命周期----------------------
- React的生命周期
- 废弃的生命周期
- constructor
- React发起网络请求应该在哪个生命周期
- React性能优化在哪个生命周期
- --------------------组件通信----------------------
- 父子组件
- 跨级组件
- 非嵌套关系组件
- --------------------虚拟DOM----------------------
- 虚拟DOM
- React Diff 算法
- React的key
- --------------------hooks----------------------
- --------------------redux----------------------
--------------------组件基础----------------------
React事件机制
首先,React中的事件是合成事件,与原生HTML事件是不同的。
原生事件,譬如用addEventListener添加的click事件:
componentDidMount(){
document.body.addEventListener('click',this.handleClickBody,false);
}
React合成事件,React中通过JSX去绑定的事件
定义:React合成事件是React 模拟原生DOM事件所有能力 的一个对象,它根据W3C规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。
render(){
return(
<div>
<button onClick={this.handleClickBotton}}>test</button>
</div>
)
}
区别:
- React合成事件是小驼峰写法;原生HTML事件是全小写
- React合成事件传入的是函数;原生HTML事件传入的是一个字符串
- React合成事件不能用return false来阻止浏览器默认行为,需要明确调用preventDefault()来阻止
事件机制:
- React合成事件是通过事件委托,绑定到document对象上
- 事件触发后,先开始DOM事件流:事件捕获、处于目标、事件冒泡,因此先处理原生HTML事件
- 当冒泡到document对象时,再由dispatchEvent去处理React合成事件
- 最后再执行真正绑定在document对象上的事件
- 因此执行顺序是:原生事件,React合成事件,document对象挂载事件
使用合成事件的目的:
- 抹平浏览器之间的兼容问题
- 可以自定义事件
- 利用事件委托,将React事件代理到document对象,减少内存开销,提高性能
哪些方法会让React重新渲染、render会做什么
一般有三种方法
- setState() 通过setState改变state,则会重新渲染。若传入null是不会重新渲染的
- 父组件重新渲染/props变化
- forceUpdate() 调用forceUpdate进行强制渲染
render会进行diff算法
React类组件和函数组件
- 组件是React中可复用的最小代码片段,无论是类组件还是函数组件,它们都返回要渲染在页面中的元素,最后的呈现效果是完全一致的。
- 类组件和函数组件之间可以互相改写。
- 以前,如果需要使用到继承与生命周期等概念时,主张使用类组件;
- 现在,有了React Hooks,使得生命周期概念逐渐淡化;另外官方也主张组合优于继承的设计概念,继承并不是组件的最佳设计模式,故函数组件完全可以替代类组件。
- 尽管,类组件更容易上手。但React Hooks的推出,函数组件应该是未来的主流(自己做过的项目)
React高阶组件,和普通组件的区别,适用场景
- 高阶组件HOC是复用组件逻辑的一种高级技巧,基于React组合特性的一种设计模式
- 实际上高阶组件就是一个接收组件为参数,返回值为新组件的函数
- 可以实现逻辑复用,又不影响WrappedComponent的内部逻辑
React受控组件和非受控组件
满足以下两个条件的表单元素,叫受控组件
- React的state成为表单元素的唯一数据源
- 渲染表单元素的React组件控制了用户输入过程中表单发生的操作
受控组件与state的具体表现如下:
- 通过初始化state,为表单元素提供默认值
- 当用户输入时,表单的值发生变化,调用onChange事件处理器
- onChange事件处理器可以通过event.target.value拿到表单更新后的值
- 通过setState(),用event.target.value更新state,完成视图的重新渲染,从而完成组件的更新
受控组件的缺陷:
- 受控组件的唯一数据源是state,需要编写对应的事件处理函数去控制表单发生的变化。
- 如果有多个表单,那么就需要编写多个事件处理函数,代码会变得臃肿。所以出现了非受控组件
非受控组件:
- 非受控组件将真实数据储存在DOM节点,而不是state
- 不需要为状态更新编写对应的事件处理函数,而是通过ref从DOM节点对数据现取现用
- 可以减少代码量
React有状态组件和无状态组件
有状态组件
- 是类组件
- 有继承,并且使用生命周期
- 可以使用this接收状态和属性
- 内部使用state去管理自身的状态变化
- 缺点:容易频繁触发生命周期函数,影响性能,加载速度慢
无状态组件
- 可以是类组件也可以是函数组件
- 不继承,也不使用生命周期钩子
- 可以不使用this
- 内部不使用state去管理,仅仅是用于展示
React-Fiber
React Fiber架构 出现的原因
- 在React V15,当我们调用setState()更改state导致页面更新时,React会比对新旧 Virtual DOM Tree,找出需要变更的节点并更新它们。
- 而这个过程是一气呵成不能被打断的(Stack Reconciler),也就使得JS运算任务持续占用主线程,阻塞了浏览器其它事件 。如果时间过长,那么就会一些问题,如:导致用户触发的事件得不到响应、渲染掉帧,从而让用户觉得卡顿。
Fiber架构实际上是 Fiber Reconciler,使得这个执行过程是可中断的,适当地让出CPU控制权,可以让浏览器做以下事情:
- 及时响应用户的交互事件
- 让优先级更高的任务先执行,避免掉帧
--------------------数据管理----------------------
setState调用的原理 **
setState是同步还是异步
setState并不是单纯表现为绝对是同步或者绝对是异步。是同步,还是异步,取决于setState的调用场景。
观察源码,内部是通过isBatchingUpdates来判断setState是先存进state队列(异步代码)还是直接更新(同步代码)
- isBatchingUpdates如果为true,则存进state队列,是异步代码
- isBatchingUpdates如果为false,则直接更新,是同步代码
表现为异步 (多次setState会合并操作、延迟更新):
表现为同步 (在React无法控制的地方)
- 原生事件如 addEventListener、setTimeout、setInterval
设计为异步的好处:
- 性能优化,减少渲染次数 : 如果是同步,每次调用setState时,都会进行一次 vnode Diff 和 DOM更新。频繁执行,显然效率低下。最好则是异步,将多次操作合并成一次操作,批量更新。
关于批量更新:
- 某个属性进行setState,会进入state队列。而对相同属性的多次设置,React只会为其保留最后一次的更新
this.state和setState
- this.state是用来初始化state的
- this.setState是用来修改state的
直接修改this.state,state是不会更新的,页面不会更新。
state和props的区别
state:
- state用于组件去保存、控制和修改组件自身的状态
- 在constructor中用this.state进行初始化
- 在组件内部用this.setState方法进行修改
- 它是组件内部的私有属性,不可以直接通过外部访问和修改
props:
- props是外部传进组件的参数,主要就是父组件向子组件传递参数
- props具有只读性,只能通过外部组件主动传入新的props给子组件去重新渲染
props为什么是只读的
props的只读性
- props是外部传进组件的参数,主要是父组件向子组件传递的参数。
- “props只能从父组件流入子组件”。就像函数式编程中纯函数一般
纯函数:
- 给定相同的输入,获得的一定是相同的输出
- 不会有副作用
- 不对外部产生依赖
props的只读性,只能从父组件流入子组件,保证了相同的输入,组件或者说页面的内容一定是相同的。
props改变时更新组件的方法
- componentWillReceiveProps 已经被废弃
- getDerivedStateFromProps 16.3引入
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
if (type !== prevState.type) {
return {
type,
};
}
return null;
}
getDerivedStateFromProps
- 是一个静态函数,不能通过this去访问
- 通过nextProps和prevState进行判断,是否要将新传入的props映射到state
- 如果传进来的props不影响state,应该在末尾返回个null
--------------------生命周期----------------------
React的生命周期
组件的生命周期可以分为三个大的阶段
- 挂载阶段 Mount,即组件第一次在DOM树中被渲染
- 更新阶段 Update,即组件的状态发生变化,重新更新渲染
- 卸载阶段 Unmount,即组件从DOM树中移除
组件挂载阶段 (只发生一次)
- constructor
- getDerivedStateFromProps
- render
- componentDidMount (发起网络请求、执行依赖DOM的操作、添加订阅)
组件更新阶段 (可发生多次)
- getDerivedStateFromProps
- shouldComponentUpdate (性能优化)
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
组件卸载阶段
废弃的生命周期
- componentWillMount
- componentWillReceiveProps 被getDerivedStateFromProps替代
- componentWillUpdate
① 废弃componentWillMount,因为它完全可以被constructor和componentDidMount代替:
- 如果是要初始化数据,那么constructor对this.state操作即可
- 如果是要发起网络请求,应该放到componentDidMount进行操作
② componentWillReceiveProps
constructor
组件的构造函数,是第一个被执行的。
如果没有显示定义它,则会默认添加一个构造函数。
如果显示定义了构造函数,由于ES6继承机制的不同,必须写 super(props),否则无法拿到this对象
- ES6的继承机制,是先将父类的属性和方法添加到一个空对象上,再将这个对象作为子类的的实例。
- 调用super,实际上就是调用父类的构造函数,从而创建了“空对象”,再为其添加属性和方法,最后作为子类的实例。
- 如果不写super,那父类没有对象可以给子类,子类自然没有this对象。
constructor生命周期通常只做两件事
- 通过this.state去初始化组件的state
- 为事件处理方法绑定this
constructor(props) {
super(props);
this.state = { counter: 0 }
this.handleClick = this.handleClick.bind(this)
}
React发起网络请求应该在哪个生命周期
一个组件的挂载阶段,通常有以下四个生命周期:
constructor -> componentWillMount(废弃) -> render -> componentDidMount
- constructor通常只用于初始化state和为事件处理方法绑定this
- componentWillMount发生在render之前,即使加载了数据,进行setState也不会触发render重新渲染,是无效的
- componentDidMount的代码,是组件完全挂载到网页上才会被执行。完全可以保证数据的加载,另外进行setState也是有效的
所以,发起网络请求、执行依赖DOM的操作、添加订阅消息,都是在componentDidMount阶段执行。
React性能优化在哪个生命周期
以下三种情况会导致组件的重新渲染
- 调用this.setState
- 父组件更新/props变化
- forceUpdate
以下两种情况是值得考虑,是否仍然需要重新渲染组件的
情况一、如果调用setState是以下的情况
this.setState({number: this.state.number})
情况二、如果父组件重新渲染,但是props并没有发生变化
很显然这时候组件重新渲染并没有实际意义,倒是降低了性能。
组件更新阶段:
getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate
可以看到,组件更新阶段,render方法前有一个shouldComponentUpdate方法,可以避免不必要的组件渲染。
shouldComponentUpdate可以进行性能优化
shouldComponentUpdate(nextProps) {
if (this.props.num === nextProps.num) {
return false
}
return true;
}
- 比较this.props和nextProps,比较this.state和nextState
- 如果发生变化,返回true,则需要重新渲染
- 如果没有发生变化,返回false,没有必要重新渲染
注意:
- shouldComponentUpdate的比较是进行浅比较
- 不建议进行深度比较,譬如用JSON.parse(JSON.stringfy())。因为深比较的效率很低,可能比重新渲染组件的效率还低。
--------------------组件通信----------------------
总的来说,通信方式有:
- 父组件通过props向子组件通信
- 子组件通过props+回调函数,向父组件通信
- 使用context
- 使用redux
- 使用发布订阅模式
- 兄弟组件可以通过共同的父组件进行转发
父子组件
父组件向子组件通信:通过props向下,向子组件传送数据
const Child = props =>{
return <p>{props.name}</p>
}
const Parent = ()=>{
return <Child name="react"></Child>
}
子组件向父组件通信:通过props,向子组件传输回调函数
const Child = props =>{
const cb = msg =>{
return ()=>{
props.callback(msg)
}
}
return (
<button onClick={cb("你好!")}>你好</button>
)
}
class Parent extends Component {
callback(msg){
console.log(msg)
}
render(){
return <Child callback={this.callback.bind(this)}></Child>
}
}
跨级组件
父组件向子组件的子组件(“孙子组件”)进行通信
- 使用props逐层往下传递。但是如果层数较深,中间的每一层都需要去传递props,增加复杂度,而且中间层不一定需要用到props
- 使用context。context的设计,是让不管嵌套多深的子组件,都可以访问context中的内容。
非嵌套关系组件
非嵌套关系组件,就是没有包含关系的组件。
如:兄弟组件和非兄弟组件
- 可以使用redux等进行全局状态管理
- 可以使用发布订阅模式
- 如果是兄弟组件,可以通过公共的父节点去转发信息
--------------------虚拟DOM----------------------
虚拟DOM
Real DOM , 即真实DOM。
DOM即 Document Object Modal,文档对象模型,是对文档的结构化表述,并提供了对文档结构的访问方式。
Virtual DOM,本质上是一个JavaScript对象,以JS对象的形式去表示DOM结构
在React当中,有JSX这一特性,即支持在JS中通过XML的方式去声明页面的DOM结构。
const vDom = <h1>Hello World</h1>
const root = document.getElementById('root')
ReactDOM.render(vDom, root)
JSX实际上是一种语法糖,它会被babel编译转化成JS代码:
const vDom = React.createElement(
'h1',
{ className: 'hClass', id: 'hId' },
'hello world'
)
可以看出:
- JSX实际上就是简化调用React.createElement方法
- React.createElement方法返回的是一个JS对象,也就是Virtual DOM
Virtual DOM长什么样:
const VDom = {
key:null,
type:"div",
props:{
children:[
{ type:"span",},
{ type:"span",}
],
className:'red',
onClick: ()=>{}
}
}
Virturl DOM 与 Real DOM的区别
- Virtual DOM不会进行重排重绘;Real DOM会重排重绘
- Virtual DOM的总损耗:Virtual DOM的增删改 + DOM DIFF + 较少的重排重绘
- Real DOM的总损耗:Real DOM的增删改 + 较多的重排重绘
Virtual DOM的优点:
① 减少DOM操作的次数
- 如果手动操作真实DOM,需要一次一次操作DOM,繁琐且容易出错
- 使用Virtual DOM则可以将多次操作合并成一次,避免频繁更新DOM
② 减少DOM操作的范围
- Virtual DOM借助 DOM Diff,不会去更新不需要更新的DOM,只更新发生变化的DOM
- 避免更新不必要的DOM
①和②都可以减少重排重绘,从而提升性能
③ 跨平台
- 可跨平台:Virtual DOM本身是JS对象,它可以变成IOS应用、安卓应用、小程序等,即具有跨平台的能力
React Diff 算法
当虚拟DOM树发生变化后,会根据比较新旧虚拟DOM树的变化,生成patch
patch是一个结构化数组,它记录了最小成本更新DOM的方式。
Diff有三个策略,Tree Diff、Component Diff、Element Diff:
Tree Diff
- 对新旧两棵树进行对比,找出需要更新的节点
- 如果是组件,则看Component Diff
- 如果是标签,则看Element Diff
Component Diff
- 节点是组件,则先比较两棵树的组件类型
- 如果组件类型就不同,则直接删除旧的
- 如果类型相同,则只更新属性,递归进行Tree Diff
Element Diff
- 节点是原生标签,看两棵树的标签名是否相同
- 不相同直接删除
- 相同的话,只更新属性,递归进行Tree Diff
React的key
Key是React用于追踪哪些元素是被修改、添加或者移除的标识,我们需要保证某一个元素的Key在同级元素中具有唯一性。
<div>
<span>1span>
<span>2span>
div>
如果没有设置key,删除掉span 1
diff的比较是:
- div是一样的
- 旧的虚拟DOM树中是,1;新的是 2;进行一次修改,把1改成2
- 新的树中没有2,进行一次删除
所以就有两次操作。
从数组的角度来看
[1,2,3] 删除掉第二项2,变成[1,3]
实际上发生了什么:
- array[0]还是1,没有变化
- array[1]从2变成了3,所以要进行一次修改
- array[2]没有了,所以要进行一次删除
有了key值,则可以准确判断是哪个元素被修改、删除、增加,因此可以减少不必要的DOM操作
总结:
- key得在同级元素中是唯一的
- 避免使用index作为key
- key不能使用随机数(不稳定的)
--------------------hooks----------------------
--------------------redux----------------------