React进阶学习记录
Vue、React 乃至 Angular 在代码编写层面越发相似,且在设计层面都在朝着WebComponents标准靠近。在展望未来的前端框架时,它们往往会逐渐趋同(标准化)。
在环境无法提供优质的成长环境时,谨记自己创造途径:深挖一个优质的前端框架吃透它的原理,跟这开发团队学习框架的思想,编码规范和学习设计模式。
学习React的必要性:
- 大厂
- 面试时React相关问题的区分度高
- 用框架锻炼个人思维和代码思想
架构设计哲学上:
- 数据驱动视图
- 组件化
- 函数式编程
- 面向对象
- fiber
底层技术选型上:
- jsx
- 虚拟DOM
周边生态上:
- 状态管理
- 前端路由
其他方面:
- 状态管理机制
- 事件系统
- 在前端引入hooks思想
React知识体系庞大,精密复杂的底层原理与长长的知识链路。
大厂的React面试是最有效用导向的一个学习依据,将大厂面试的逻辑利用充分,将实现面试和应用的双重突破。 贴着源码讲原理并不是死磕源码,源码不等于原理。源码是代码,而原理是逻辑,代码繁杂冗长,而原理可以简洁清晰。在一些场景下,源码可以是校准工具, 但是阅读源码不是抵达原理的唯一途径。
对于体系性较强的知识,创建足够充分的上下文。一些知识难学不是因为它有多么复杂,而是因为理解它需要上下文,如果在正确的上下文中,理解它就是很轻松的事。如果学习上下文是断裂的,那知识点就难以理解。
对于复杂度较高的知识,用现象向原理提问,注意先导知识的学习。
规划:
-
基础夯实
设计React的基本原理和源码
-
核心原理
面向日常开发中的难点,大厂面试压轴难题,框架底层的逻辑和源码设计
-
周边生态
redux,react-router的工作原理和设计思想
-
生产实践
对于一个优秀的前端应用来说,性能和设计模式是永恒的主题,性能决定用户体验,设计模式决定研发效率。
学习的本质是重复,重复的结果是记住。
jsx代码映射为DOM
现在接入JSX语法的框架越来越多。本节的重点是jsx如何转为DOM。
jsx中的三个重点:
- jsx的本质是什么,它和js之间到底是什么关系?
- 为什么要用jsx,不用的后果是什么?
- jsx背后的功能模块是什么,这个功能模块都做了哪些事情?
大多数开发者认为它是模版语法中的一种。
jsx与React底层的联系。本课时的目标是:通过本课时的学习能用自己的话回答上面的三个问题。
jsx的本质
jsx的本质是JavaScript的一种语法扩展,它和模版语法很接近,但是它充分具备JavaScript的能力。
Facebook公司给jsx的定位是:jsx是JavaScript的“扩展”,而非JavaScript的某个版本,这就直接决定了浏览器并不会像天然支持JavaScript一样地支持JSX。那jsx语法如何在JavaScript中生效?
JSX会被编译为React.createElement( ), React.createElement( ) 将会返回一个叫做ReactElement的JS对象。JSX在被编译后, 会被变为一个针对React.createElement( ) 的调用。暂时不关注React.createElement( ) 这个API做了什么,先说说JSX是如何被编译为React.createElement( ) 形式的调用的。
编译这个过程是由Babel完成。
那Babel又是什么了?
Babel是一个工具链,主要用于将ECMAScript2015+版本的代码转换为向后兼容的JavaScript语法,以便能运行在当前和旧版本的浏览器或其他环境中。
JSX其实是React.createElement( ) 这个方法调用的语法糖形式。React为什么选用JSX?既然JSX等价于React.createElement( )调用,那React官方为什么不直接引导开发者使用React.createElement( ) 来创建元素了?原因是JSX的书写大大优于React.createElement( ) ,JSX使用HTML标签来创建虚拟DOM,降低学习成本同时提高研发效率和体验。
React.createElement( ) 源码:
/**
* React的创建元素的方法
*/
export function createElement(type, config, children){
// propName 变量用于存储后面需要用到的元素属性
let propName;
// props 变量用于存储元素属性的键值对集合
const props = {};
// key,ref,self,source均为React元素的属性
let key = null;
let ref = null;
let self = null;
let source = null;
// config对象中存储的是元素的属性
if(config != null){
// 进来后的第一件事就是依次对ref,key,self和source属性赋值
if(hasValidRef(config)){
ref = config.ref;
}
// 此处将key值字符串化
if(hasValidKey(config)){
key = ''+config.key;
}
self = config.__self === undefined ? null:config.__self;
source = config.__source === undefined ? null:config.__source;
// 接着就是要把config里面的属性都一个一个挪到props这个之前声明好的对象中
for(propName in config){
// 筛选出可以提进props对象中的属性
if(hashOwnProperty.call(config,propName)) && !RESERVED_PROPS.hasOwnProperty(propsName){
props[propName] = config[propName]
}
}
}
// childrenLength指的是当前元素的子元素的个数,减去的2是type和config两个参数占用的长度
const childrenLength = arguments.length - 2;
// 如果抛去type和config,就只剩下一个参数,一般意味着文本节点或者一个字节点
if(childrenLength ===1){
// 直接将这个参数的值复制给props.children
props.children = children
}else if(childrenLength>1){ // 处理嵌套多个子元素的情况
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组中
for(let i=00;i<childrenLength.length;i++){
childArray[i] = arguments[i+2];
}
//最后将这个数组赋值给props.children
props.children = childArray
}
// 处理defaultProps
if(type&&type.defaultProps){
const defaultProps = type.defaultProps;
for(propName in defaultProps){
if(props[propName] === undefined){
props[propName] = defaultProps[propName]
}
}
}
// 最后返回一个调用ReactElement执行方法并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props
);
}
function createElement(type, config, children)
- type:表示节点类型,html标签,React组件类型或者ReactFragment类型
- config:以对象形式传入,组件所有的属性都会以检值对的形式存储在config对象中
- children:在这里是传参的第三项,可以是另一个React.createElement的调用或者字符串
React.createElement的函数体拆解:
React.createElement的处理逻辑并不复杂,基本就是格式化数据。
React.createElement就像是开发者和ReactElement调用之间的一个转换器,数据格式化层。从开发者处接受相对简单的参数,然后将这些参数按照ReactElement的预期做一层格式化,最终通过调用ReactElement来实现虚拟DOM的创建。
React.createElement函数执行后会返回一个ReactElement函数的调用
所以重点在ReactElement上。
ReactElement源码:
const ReactElement = function(type,key,ref,self,source,owner,props){
const element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 内置属性赋值
type:type,
key:key,
ref:ref,
props:props,
//记录创造该元素的组件
_owner:owner,
};
if(__DEV__){
//这里是一些针对__DEV__环境下的处理,对于理解主要逻辑意义不大,省略
}
return element;
}
ReactElement的代码很短,作用是组装,ReactElement将传入的参数按照一定的规范组装进elemnt对象中,并将它返回给React.createElement。
下图就是上面的JSX 转为的ReactElement 实例对象:
这个ReactElement实例对象本质就是一个Javascript实例对象。该JavaScript对象是对DOM的描述,其实就是虚拟DOM,它还不是真实的DOM。将虚拟DOM转为真实DOM则是用过ReactDOM.render( )方法来实现。
ReactDOM.render:
生命周期函数变更及逻辑
React16 为什么要修改生命周期函数?
从React 的基本原理出发对React15,16两版的生命周期函数进行探讨,比对和总结。
生命周期背后的设计思想:
- 虚拟DOM是核心算法的基石
- 组件化和工程化
每个组件都既是“封闭”的,也是“开放”的。
封闭针对的是渲染工作流而言,在组件自身的渲染工作流(从组件数据改变到组件实际更新发生的过程)中,每个组件都只处理它内部的渲染逻辑。在没有数据流交互的情况下,组件与组件之间可以相互隔离。
开发则是针对组件之间的通信而言,React允许开发者基于单项数据流的原则,完成组件之间的通信,而组件之间的通信又将改变通信双方/某一方内部的数据进而对渲染结果构成影响。
render函数算是生命周期函数中的核心,其中虚拟DOM的生成和组件的渲染工作流都离不开render函数,其他生命周期函数算得上组件的躯干。
React15中的生命周期函数:
constructor:
render函数在执行的过程中并不会去操作真实的DOM,它的智能是把需要渲染的内容返回出来。真实DOM的渲染工作在挂载阶段由ReactDOM.render完成。
componentDidMount方法在渲染结束后被处罚,真实的DOM已经挂在到页面上,可以在这个生命周期里执行真实DOM相关的操作,此外,异步请求数据初始化等操作可以放在这个生命周期函数中。
对于组件的componentWillReceiveProps生命周期函数,如果父组件导致组件父组件重新渲染,既是传给子组件的props没有改变,也会触发componentWillReceiveProps函数,如果只想处理更改,请确保进行当前值与变更值的比较。
componentWillReceiveProps并不是由传给改组件的props的变化触发,而是由副组件的更新触发。
shouldComponentUpdata(nextProps,nextState)
render方法伴随着对虚拟DOM的构建和对比,过程耗时长,而在React中很多时候不经意间就平频繁的调用了render函数,为了避免不必要的调用Render,可以使用shouldComponentUpdata生命周期函数。React组件会根据shouldComponentUpdata的返回值来决定是否执行该生命周期函数之后的生命周期函数,进而决定是否对组件进行re-render(重渲染),shouldComponentUpdata默认返回的值是true。
手动在shouldComponentUpdata中添加判断逻辑或者直接在项目中引入PureComponent的最佳实践来实现有条件的re-render。
组件销毁的常见操作:
- 组件在父组件中被移除
- 组件中设置了key属性,父组件在render的过程中,发现key值和上次不一致时也会销毁组件
React16的生命周期函数:
理解React16中的生命周期函数,同时对比新旧两个版本中生命周期函数的差异以及为什么改变生命周期函数。
在React16.4之后相比于React16.3在生命周期函数方面做了微调。主要就是微调在更新过程中的getDerivedStateFromProps生命周期函数,在React16.4中任何因素触发的组件更新流程都会触发getDerivedStateFromProps,而在16.3中只有父组件的更新会触发该生命周期。
Mounting阶段:
生命周期函数升级过程中的主要矛盾,工作流层面的改变。对现有方法的迭代细节和不在主要工作流中的componentDidCatch等生命周期不再进行说明。
getDerivedStateFromProps生命周期函数并不是代替之前的componentWillMount生命周期函数,componentWillMount函数的存在本身就很鸡肋,同时危险,因此它不值得被代替而是直接被废弃了。
getDerivedStateFromProps生命周期函数设计的目的就一个,使用来自父组件的props来派生/更新自己组件的state。getDerivedStateFromProps它试图替代掉React15中的componentWillReceiveProps生命周期函数。
React团队为了明确该getDerivedStateFromProps生命周期函数的用途,直接从命名层面就约束了该生命周期函数的用途。 所以开发者如果不是出于该生命周期的目的来使用它的话,严格上来说都是不符合规范的。该生命周期函数会在初始化/更新时调用,因为派生组件自己的state在组件初始化阶段和组件更新阶段都有可能需要。
React16以提供特定生命周期函数的形式对这类特定的诉求提供更直接的支持。
认识getDerivedStateFromProps函数:
调用该方法的注意点:
- 该方法是类的静态方法,静态方法中的this并不指向组件实例,因此在该方法内部访问组件实例this是不行的
-
该函数接受两个参数props和state
props:当前组件接受到的来自父组件的props
state:当前组件自身的state
- 该函数需要返回一个对象,React库需要用该返回值来更新组件的state,在确实没有需要使用父组件的props派生组件自己的state的时候,可以不用使用该生命周期函数 ,同时该返回对象对组件已有的state的更新动作并不是覆盖式的更新,而是针对某个属性的定向更新
Updating阶段:
为什么要用getDerivedStateFromProps 代替 componentWilllReceiveProps?
getDerivedStateFromProps与componentDidUpdate一起,这个getDerivedStateFromProps函数涵盖过时的componentWilllReceiveProps的所有用例。getDerivedStateFromProps 只专注一件事:props到state的映射。
- getDerivedStateFromProps是试图代替componentWilllReceiveProps出现的
- getDerivedStateFromProps不完全等于componentWilllReceiveProps
getDerivedStateFromProps作为静态方法,内部拿不到组件实例this,这就导致开发者无法在该函数中做任何this.setStae等可能产生副作用的操作。React16在强制推行只用getDerivedStateFromProps来完成props到state的映射,意在确保生命周期函数的行为更加能预测可控,从根源上帮助开发者避免不合理的开发方式,避免生命周期函数的滥用,也是在为fiber架构铺路。
认识getSnapshotBeforUpdate函数:
getSnapshotBeforUpdate函数的返回值会作为componentDidUpdate函数的第三个参数,该生命周期函数是在render函数执行之后,真实DOM更新之前。在该函数中可以获取到更新前的真实DOM和更新前后的state和props信息。
getSnapshotBeforUpdate常常与componentDidUpdate配合使用。
那为什么componentWillUpdata要被废弃?
因为它不利于fiber架构。
Fiber架构
Fiber是React16对React核心算法的一次重写。 该架构可以使得原本同步的渲染过程变为异步的。在React16之前,每触发一次组件的更新,React都会构建一个新的虚拟DOM树,通过与上一次的虚拟DOM树进行diff,实现对DOM的定向更新,这个过程是深度优先的过程,同步渲染的递归调用栈是非常深的,只有最底层的调用返回了,整个渲染过程才会开始逐层返回。 这个漫长且不可打断的更新过程将可能对用户体验造成极大影响。同步渲染开始便会一直占用主线程,直到递归彻底完成,在这个过程当中,浏览器没有办法处理任何渲染之外的任务,会进入一种无法处理用户交互的状态,因此如果渲染时间稍微长一些,页面就可能卡顿或卡死。而React16中引入的Fiber架构可以解决该问题,Fiber会将一个大的更新任务拆解为许多小任务,每当执行完一个小任务后,渲染线程都会释放主线程,看看是否有优先级更高的工作要处理,确保不阻塞的情况,进而避免同步渲染带来的卡顿。在这个过程当中,渲染线程可以被打断,实现异步渲染。
- 同步渲染和异步渲染
- 任务拆解和可打断特性
同步渲染变为异步渲染是如何对生命周期函数造成影响的?
commit阶段又分为pre-commit和commit阶段
为什么render阶段可以被打断而commit阶段总是同步执行?
render阶段的操作对用户来说其实是不可见的,所以打断再重启也是零感知的。而commit阶段的操作设计真实DOM的渲染,用户可见,所以必须以同步的方式求稳。
生命周期函数变更后面的原因:
在Fiber架构下,render阶段是允许暂停,终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程占用主线程时,该任务被重启的形式是重复执行一遍整个任务,而不是接着上次执行到的代码行继续执行。这就导致render阶段的生命周期函数都是有可能被重复执行的。
而React16中废除的componentWillMount,componentWillUpdate,componentWillReciveProps 它们都处于render阶段,都可能重复被执行,而且这些API有被滥用时,在重复执行时可能有风险。
在这些已废除的生命周期函数不合理的操作:
- setState
- fetch异步请求
- 操作真实DOM
最佳实践:
- 将上述的操作转到componentDidxxx中完成,比如在componentWillMount中发出异步请求,以为这样可以让网络请求回来得早一些,从而避免首次渲染白屏的情况,其实异步请求根本不可能快过同步代码的执行,componentWillMount结束后,render马上被触发,所以首次渲染依然会在数据返回之前执行,这样做不仅不能达到目的,还会导致服务端渲染场景下的冗余请求等问题。
- 在Fiber的异步渲染机制下,使用这些已废弃的函数可能导致非常严重的bug,假设开发者在componentWillxxx函数中发起付款请求,由于render阶段的生命周期都可能重复执行,在componentWillxxx被打断加上重启多次后,就会发出多个付款请求。 如果开发者在componentWillxxx中操作真实的DOM,那就有可能重复操作真实DOM。
- 避免在componentWillReceiveProps或者componentWillUpdate中调用setState,从而避免重复渲染死循环。
React16中改造生命周期函数的主要动机是为了配合Fiber架构带来的异步渲染机制。针对生命周期中长期被滥用的部分推行了具有强制行的最佳实践,确保了Fiber机制下数据和试图的安全性。同时也确保了生命周期方法的行为更加存粹,可控和可预测。
案例:
import React from 'react';
import ReactDOM from 'react-dom';
class LifeCircle extends React.Component{
constructor(props){
console.log('子组件的constructor执行')
super(props)
this.state = {
text:'子组件的文本'
}
}
// 初始化/更新时调用该生命周期函数
static getDerivedStateFromProps(props,state){
console.log('子组件的getDerivedStateFromProps执行')
return {
fatherText:props.text
}
}
// 初始化渲染时调用
componentDidMount(){
console.log('子组件的componentDidMount执行')
}
// 组件更新时调用
shouldComponentUpdate(nextProps,nextState){
console.log('子组件的shouldComponentUpdate执行')
return true
}
// 组件更新时调用
getSnapshotBeforUpdate(prevProps,prevState){
console.log('子组件的getSnapshotBeforUpdate执行')
return 'haha'
}
// 组件更新时调用
componentDidUpdate(nextProps,nextState,valueFromSnapshot){
console.log('子组件的componentDidUpdate执行')
console.log('子组件的componentDidUpdate执行的valueFromSnapshot',valueFromSnapshot)
}
// 组件卸载时调用
componentWillUnmount(){
console.log('子组件的componentWillUnmount执行')
}
// 点击按钮,修改子组件自生的文本状态数据的方法
changeText = ()=>{
this.setState({
text:'修改后的子组件文本'
})
}
render(){
console.log('子组件的render执行');
return (
<div className='container'>
<button onClick={this.changeText}>修改子组件的文本state</button>
<p>{this.state.text}</p>
<p>{this.props.text}</p>
</div>
)
}
}
class LifeCircleContainer extends React.Component{
state = {
text:'父组件文本',
hideChild:false
};
changeText = ()=>{
this.setState({
text:'修改后的父组件文本'
})
};
hideChild = ()=>{
this.setState({
hideChild:true
})
}
render(){
return (
<div>
<button onClick = {this.changeText}>修改父组件文本内容</button>
<button onClick = {this.hideChild}>销毁子组件</button>
{this.state.hideChild?null:<LifeCircle text={this.state.text}/>}
</div>
)
}
}
ReactDOM.render(<LifeCircleContainer/> ,document.getElementById('root'))
数据在组件间的传递
组件间通信的背后是React中环环相扣的数据流解决方案。
基于props的单项数据流
组件从概念上类似于JavaScript中的函数,他接受任意的传参(即props),并返回用于描述页面展示内容的React元素。
Hooks执行机制