什么时候需要数据提升?
1.子组件之间需要数据共享、交互的时候
2.子组件想要改变父组件中的状态
为什么更新状态的时候,要复制一份再改变,而不是在原来的基础上去改???
1)留下历史记录
2)对象变成了一个新的对象,数据就改变了
3)组件很纯净,更新方式很简单
React为什么选择jsx语法?而不是模版?
vue、angluar都选择了使用js、css、HTML模版分离的方式。这样可以降低模版与逻辑之间的耦合度,关注标记的时候就只专注于template,关注逻辑的时候只关注script。
优点: 跟HTML开发一样,很容易理解,上手很快。 基于HTML写的模版,很容易进行迁移。 修饰符非常便利。
React使用JSX,标记和行为逻辑放在一起,他认为逻辑和UI是分不开的,UI上绑定事件,事件处理后通知UI更新视觉,JSX这种表现形式再视觉上具有辅助作用。
而VUE将标记和js人工分离的方式,看似是实现了关注点分离,只是一种文件分离的方式。
react提出使用组件来实现关注点分离,每一个组件实现一个小的功能,组件逻辑的关注点只在组件本身,组合在一起实现强大的功能。react说的关注点其实是逻辑和功能上的分离。并且将会标记和逻辑放在一起会在视觉上有种辅助作用。
JSX
JSX其实是一个对象。
const element = <h1 className='aaa' ref={inputRef} key={1}>hello world!</h1>
babel编译之后:
const element = React.createElement(
'h1',
{
className:'aaa',
ref:inputRef,
key:1
},
hello world!
)
React.createElement执行之后,得到这样一个对象(虚拟DOM):
const element = {
type:'h1',
ref:inputRef,
key:1,
props:{
className:'aaa',
children:'hello world!',
}
)
这些对象其实就是react元素,也就是我们常说的虚拟DOM,ReactDOM会把这些React元素转换成真正的DOM,并且保持真正的DOM与我我们想要react元素保持一致。
知识点:JSX为什么可以预防XSS(跨站脚本攻击)?
跨站脚本攻击是指用户将代码通过输入框的形式注入,获取用户的隐私信息。
React在渲染输入内容的时候,默认会进行转义。所有输入的内容会被转换成字符串。
元素渲染
DOM元素、React元素、DOM和ReactDOM之间的关系????
React元素表示我们想要在屏幕上看到的内容,ReactDOM会将React元素(对象)渲染成DOM元素,并保持DOM元素始终与React元素状态保持一致。
知识点:为什么React元素不可变?
官网有个计时器的例子
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);
每次render时明明都会创建不同的对象,为什么只有部分元素更新,并且官方为什么说React元素不可变?
这跟React的Diff算法有关,前后两个状态生成的react元素树不一样,react会对比树节点是否发生变化,然后更新某个DOM。
如果只是元素树的引用发生变化,组件的实例不会发生变化,树节点不变,不会触发DOM更新。
所以React不会随着元素的引用改变而重新渲染,而是对比树的不同部分渲染,这也正是react高效的原因。
react元素不可变也正是我们可以使用state和生命周期的原因。
摘自react中文官网:每次组件更新时 `render` 方法都会被调用,但只要在相同的 DOM 节点中渲染
`<Clock />` ,就仅有一个 `Clock` 组件的 class 实例被创建使用。
这就使得我们可以使用如 state 或生命周期方法等很多其他特性。
组件 & props
类似于js函数,接受props,并返回react元素(对象)。
为了组件复用和将复杂的内容划分成简单清晰的逻辑块,应该保持组件抽取的好习惯。
自定义的组件应该首字母大写,首字母小写的标签是DOM原生的标签。
知识点:函数组件和普通函数有什么区别?
一般人:函数组件是大写开头,返回react元素。
大佬:函数组件大写开头,React允许在函数组件里面使用state和生命周期。
知识点:为什么props要保持只读不改?
React具有很浓的函数式编程的思想。
在函数当中,按照常理和编码规范来说,我们不会直接修改函数的入参,这里用到了纯函数的概念,我们希望一个好的函数是输入固定、输出固定的。
props其实就是纯函数中的入参,在react当中他是不可变的。原因如下:
1)如果修改组件的入参(props),组件在连续多次调用的情况下,每个组件就不会保持真正的隔离和独立。
2)水往低处流,是自然规律,如果一个子组件的状态改变会影响到其他子组件的状态,框架的更新机制会变得非常复杂。
可以把react状态的更新看成是水的流动,只有上层水(父组件)才能影响到下层水(子组件)。下层水不可能影响到上层。
大大简化react的渲染流程。
state和生命周期
如果props是函数的入参,那state就是函数的私有变量。
hook和class中的setState有什么区别?
class中的setState是合并,hook中的setState是替换。
事件处理
REACT中的事件处理和DOM中的事件处理有很多不一样的地方:
1)写法:onclick和onClick
2)事件处理函数的写法,字符串和函数
3)react将事件的回调函数中的入参合并成一个合成事件event,并且实现了各个浏览器之间的兼容,是浏览器原生事件的优化和包装。
4)使用事件委托,提高效率,使用event.target可以获取到发生事件的元素。
条件渲染
react中,null、undefined和布尔值都不会被渲染。
表单
知识点:受控组件和非受控组件区别
<input type="text" value={this.state.value} onChange={this.handleChange} />
受控组件的值受state的值的控制。
<input type="text" ref={this.ref} />
将表单数据的保存交给DOM元素来做,我们可以使用ref来获取表单的值。
使用场景
1)非受控组件
表单较简单,只希望在表单提交时或者不需要时时刻刻监听表单value的变化并做出一些反应等情况下,可以使用非受控组件。
2)需要表单校验、表单较复杂、需要时刻监听表单值的变化并快速作出反应等情况下,推荐使用受控组件。
使用受控组件的缺点:
1.必须为组件的所有用到的事件处理函数添加数据处理
2.融合非react代码时,会非常麻烦
状态提升
为什么要使用状态提升?
react中的数据应当遵守自上而下的流向,如果组件中的state,另一个组件也用到了,应该将其提升到父组件中,而不是在组件之间传递。
什么时候需要状态提升?
1.子组件之间需要共享数据或者需要进行交互时
2.子组件想要改变父组件中的数据
组合和继承??
代码分割
打包:将多个文件打包成一个bundle文件的过程,在首屏加载时就可以一次性的加载整个应用的代码。
当项目代码体积过大时,首屏加载时间会很长,我们经常采用懒加载的方式对某个包或者某个页面进行动态加载。
1)缩减首屏加载时间 2)避免加载用户不会用到的代码
代码分割的方式:
1)动态import
2)React.lazy(()=>import('./a.js'))
使用懒加载之后,为了避免页面长时间的留白,建议使用Suspense将懒加载的页面进行包裹。实现优雅加载或者降级。
模块加载失败,会出发错误,可以通过异常捕获边界技术ErrorBoundary来处理。
错误边界 Error Boundaries
只能捕获被错误边界包裹的子组件,自身错误无法捕获。
一旦子组件在渲染、生命周期和构造函数中发生错误时,就会兜底显示错误边界中的兜底文案,而不显示被Error Boundary包裹的子组件(被该Error Boundary包裹的所有子组件)。
因为react认为不显示发生错误的组件 比 显示错误的组件 更友好安全。
当在组件的最外层包裹错误边界的时候,一个组件出错,页面会展示空白,所以为了不影响其他组件的展示,最好在每一个主要组件外层套一个错误边界,比如侧边栏和主内容区。
如果没有被错误边界包裹的组件发生渲染错误时,会导致整个DOM树被卸载。
Context
使用场景:很多不同层级的组件需要访问同样的一些数据。
React.createContext()
创建一个context对象。
Context.Provider
创建Context对象,返回的Context对象会携带一个Provider组件,允许它下面的组件订阅Context的变化。
当context变化时,可以通过以下几种方式来订阅到这个变化。
1)useContext(Hook)
2)class.contextType(class)
2)Context.Consumer(当监听到的context的值用于渲染组件或React元素时用到)
关于refs
有些情况下,我们需要获取某个DOM元素或者React元素,用命令式语法改变某个元素的状态。
- 管理输入框焦点、视频播放
- 触发动画
- ...
这就需要我们能获取到元素本身,ref应运而生。
首先,讲一下使用ref的三种方式:
方法1:字符串式ref
由于效率较低,已经过时,不建议使用。
原理:可以在多个元素上添加ref属性,react会自动将这些ref对象挂载在组件实例的this.refs属性上。
方法2:回调形式的ref
// 回调形式的ref
<div ref={p => { this.node = p }}>test</div>
访问元素:
this.node
以上程序发生了什么?
render的时候,react发现元素身上有ref属性,并且是回调函数形式的ref,此时会自动帮助回调函数执行,并传入ref所在的DOM元素作为入参,成功的将指向子元素的属性,挂载在组件实例上。
方法3: createRef
// 创建容器,存储ref所在的DOM元素
this.textInput = React.createRef();
<Input ref={this.textInput} />
给DOM元素添加ref,可以通过this.textInput.current获取到该DOM元素的引用。
创建ref对象。此ref对象从组件挂载之后就不会再变,但是它的current指向可以变。
以上,我们已经了解了如何通过三种方法获取元素本身。
接下来,如果想要获取React子组件本身,或者react子组件内某个元素的引用,该怎么办呢?
<ClassInput ref={this.textInput} />
给class组件传递ref,可以通过this.textInput.current获取到该组件的实例(this.textInput)。
不能给函数组件传递ref,因为函数组件没有实例。
<FunctionInput ref={this.textInput} /> // 🙅
要想给函数组件传递ref,要用forwardRef(与useImperativeHandle一起使用),可以像class组件一样,暴露方法给父组件调用。
存在这样一种情况,我们需要获取到子组件下的某个元素的引用,包括class组件和函数组件。
refs转发就产生了。
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
通过forwardRef将ref向子组件传递,或者叫做转发ref。
这样,通过父组件的ref就可以控制子组件中的某个元素了。
但是,上面的refs转发只能用于函数组件,class组件不能通过forwardRef转发ref。
傻瓜式传递ref,通过其他props属性传递ref
父组件:
this.inputRef = React.createRef();
<Child myRef={this.inputRef} />
子组件:
<Input ref={props.myRef} />
还有一种常用的方法,将ref放在子组件元素上,然后通过实例方法暴露给父组件使用。
class Test extends React.Component {
constructor (props) {
super(props)
this.inputRef = React.createRef()
}
showValue () {
console.log(this.inputRef.current.value)
}
render () {
return <input ref={this.inputRef} />
}
}
export default Test
class Parent extends React.Component {
show(){
this.pRef.showValue();
}
render() {
return (
<Test ref={this.pRef}/>
);
}
}
高阶组件
定义:入参是组件,返回也是组件,称为高阶组件。
功能:用来解决横切关注点问题(即具有相似逻辑和行为,但是参数有差别的情况)
// HOC 容器组件
function wrap (Component) {
// 所有传给Test组件的props,都被传到了容器中,所以容器应该透传props
class MyComponent extends React.Component {
constructor (props) {
super(props)
this.inputRef = React.createRef()
}
componentDidMount () {
console.log(this.inputRef)
}
render () {
return <Component {...this.props} myref={this.inputRef} />
}
}
MyComponent.displayName = 'HOC'
return MyComponent
}
export default wrap
// HOC 被包裹组件
class Test extends React.Component {
constructor (props) {
super(props)
this.inputRef = React.createRef()
}
handleShowValue =() => {
console.log(this.inputRef.current.value)
}
render () {
console.log(333, this.props)
return (
<>
<div onClick={this.handleShowValue}>111</div>
<input ref={this.inputRef} />
</>
)
}
}
// 返回高阶组件
export default wrap(Test)
//调用Test
<Test aaa={111}>
深入JSX
性能优化
为了减少组件渲染的次数,我们可以让组件只有在props和state更新的时候,才触发重新渲染。
可以用以下几种方式:
1)React.PureComponent(浅比较)
2)shouldComponentUpdate
3)useMemo、useEffect(hook)
但是以上几种方式,判断旧对象和新对象是否一样的方法是浅比较(Object.isEqual),在对象数据层级较深时就无法判断该对象是否发生改变。
这个时候,数据不可变就发挥了优势,我们不改变数据本身,而是使用新的数据set它,当对象引用发生改变时,就表示需要重新渲染。
render prop
如果想要共享带有某种行为或者状态的组件,而组件本身会变化,行为或状态不会变,使用render prop可以改变继承这种行为的组件,而不是把这个组件和这个行为再封装成一个组件。(全局bar???)
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
在render prop中返回一个react元素,并在组件内部调用它,实现定制具有某个行为的组件
横切关注点
横跨多个模块的关注点,即多个模块具有相同的关注点。如何将这个关注点抽取出来并共享给其他模块使用,这就是横切关注点问题。
受控组件和非受控组件
非受控组件:表单数据是由DOM节点来管理的,我们获取表单数据时,通过ref即可取到。
受控组件:表单数据是由React组件去管理的,表单的值通过prop的形式传入表单,表单每次onchange都会改变传入表单的prop,使得表单的值一直受react组件的控制。
非受控组件
// 提交按钮时获取表单数据
handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
<input type="text" ref={this.input} />
使用场景:
数据存储在DOM节点中,当表单提交时可以直接获取。应用在简单的表单提交中。
受控组件:
class Form extends Component {
constructor() {
super();
this.state = {
name: "",
};
}
handleNameChange = (event) => {
this.setState({ name: event.target.value });
};
render() {
return (
<div>
<input type="text" value={this.state.name} onChange={this.handleNameChange} />
</div>
);
}
}
表单输入的数据和react存储的数据始终保持一致,我们可以随时校验表单数据。
1)表单实时校验
2)表单无数据时,disabled the button
3)校验表单格式等
总结:除了上述需要案例,简单的表单可以使用非受控组件去实现。
性能优化
渲染的时候进行性能优化是尽量减少渲染DOM的次数。
当父组件的状态发生变化时,不管子组件的props和state有没有发生改变,子组件都会被重新渲染一遍。
如何阻止子组件渲染多遍?
1)如果想要在props和state不变时,组件都不重新渲染,class组件可以直接继承React.PureComponent
2)如果想要在props不变时,组件不重新渲染,可以用React.memo()高阶组件包一层
3)可以直接用shouldComponentUpdate决定什么时候更新组件
弊端:
为了防止深层对象改变之后不会引起组件重新渲染,我们尽量不要改变原对象,每次都重新复制一份对象出来,这样就知道需要重新渲染了。
如果是深层级对象,Object.assign或者...不起作用,可以使用lodash或者immer的方式来实现deep clone。
portals
ReactDom.createPortal(this.props.children,this.el)
portals可以把任何的元素渲染到父组件之外的任何DOM内,帮助某个元素跳出原DOM结构。
portals跟其他的react子元素一样,还是存在于原来的react树结构下,所以一个在portal内部触发的事件会一直冒泡到包含该portal元素的react树的祖先。
协调
两次render返回两颗不同的虚拟DOM树,react需要比较这两个虚拟DOM树,用最小的代价更新真实DOM,将真实DOM与虚拟DOM树保持一致。
旧的算法复杂度是O(n 3 ),两棵树每个节点进行对比是O(n 2 ),更新树复杂度是O(n),所以总体复杂度是O(n 3 )。
React又提出一种新的对比方法,复杂度是O(n),只对比两棵树的同一个节点.
1.对比不同类型的元素
元素类型发生改变之后,元素以及所有元素内子元素都会销毁,并且重新生成DOM。
2.对比相同类型的元素
当对比的元素类型相同时,不会卸载DOM节点,仅会更新变化的属性。
然后继续对比其子元素。
3.当props发生变化时
render的时候,会在旧的虚拟DOM树和新的虚拟DOM树中进行比对,修改props的值。
4.对比更新子元素
对比子元素时,会同时遍历两个子元素的列表。
添加key之后,使得更新子元素更高效。
react列表中key的作用是什么?
总结:在列表中,key相当于是虚拟DOM的一个标识,在更新的时候可以提高渲染的速度。
当列表数据更新时,react会根据新数据生成新的虚拟DOM树,然后与旧的虚拟DOM树进行diff对比,来决定最后更新真实DOM的代价。
1.如果在旧数据中找到了新数据中对应的key
再去比对两条内容是否相同
1.相同。不生成新的DOM,直接复用之前的DOM
2.不同。重新生成新的真实DOM,渲染到页面上
2.如果在旧数据中没找到新数据中对应的key
生成新的真实DOM,渲染到页面
开发过程中应该如何选择key?
1.如果不会出现逆序添加或者删除的情况,可以使用index作为key
2.如果会出现以上情况,或者列表中出现输入框等,使用数据的唯一表示当作key
render props
<Move children={(mouse)=><Cat mouse={mouse} />}>
是一种多组件共享行为方式的一种解决方法,render(或其他props名称都可以)属性意在告诉组件内该渲染什么内容。
render props 会抵消React.PureComponent的优点,因为每次渲染都会生成一个新的函数,为了避免这种情况,我们可以把函数用useCallback包一层。
什么是Hook?
Hook是一些可以让你在函数组件里面钩入React state和生命周期等特性的函数.
当你想要向函数组件内加入state状态时,无需转换成class组件,通过Hook就可以将React的state特性钩入。
为什么要使用Hook?
1)Hook自定义钩子可以让我们在原生途径上实现状态和行为共享(横切关注点问题)。
2)Hook在写法上更符合我们的思维习惯,例如我们不用将绑定和解绑分开写在两个生命周期函数中,如果需要我们可以写多个useEffect,逻辑和思维上更好区分,代码更容易理解。
3)React偏向于函数式编程,而Hook让函数更加强大,可以使用state和生命周期,将函数式编程发挥到了极致,省去很多编写class的复杂代码,也不用关心this指向问题。
state Hook
可以让我们在函数组件里面使用state变量的Hook方法。
const [count, setCount] = useState(0);
初始值只有在第一次加载的时候才会用到,后面每次重新渲染,react会记住该变量最新的值,并返回给函数组件。
一般函数执行过后,变量就被销毁了,但是useState中的变量会被React记住。
React如何区分useState是那个组件的????
根据函数组件来区分的
effect Hook
可以让我们在函数组件中使用副作用的Hook。
副作用有哪些:操作DOM、请求数据、订阅事件。
是DidMount、DidUpdate和willUnMount的组合。
class中componentDidMount和Hook中的useEffect有什么区别?
componentDidMount是在组件挂载Dom之后(首次未渲染)同步执行的,如果在componentDidMount中执行setState等同步操作会占用js线程,阻塞首次渲染流程。
1)不会有页面闪烁存在
2)最好不要在componentDidMount中执行耗时的操作,首屏加载会更慢
useEffect是在组件首次渲染之后异步执行的,不会阻塞浏览器更新屏幕,响应更快,在useEffect中执行setState操作会产生内容闪烁。
结论:useEffect和componentDidMount并不相等,componentDidMount其实与useLayoutEffect相同。
为什么setState不能实时更新?
this.props和this.state都代表着已经渲染了的值,就是屏幕上已经显示的值,但是setState是异步的,不是每次setstate都会立即更新state。
全局实例讲解:
// class实例
class HelloMessage extends React.Component {
render() {
return (
<div>
// 为什么能够使用this.props?props是从父元素继承而来的吗?
Hello {this.props.name}
</div>
);
}
}
// react中好像没有new实例的过程?
<HelloMessage name="jack">
1.我们只使用class去写组件,但是好像没有new实例,为什么就能调用原型对象上的render方法?
当我们在调用组件的时候,react会自动帮我们new一个组件实例,并且在这个实例对象上调用render方法。
所以在render中使用的this指向的就是该组件实例。
2.组件实例中都有什么?
react在实例化组件的时候,执行完构造函数之后,自动给实例加了props、state和refs(旧版本的字符串ref留下的属性--用于收集rendern中元素上的ref,新版本废弃)三个主要属性。
所以,组件render方法中就可以访问this.props,而this.props就是调用组件时,给组件传输的标签属性。
3.既然能用this.props,为啥还要搞super(props)?
因为props是在构造函数之后加在实例上的,所以构造函数中我们要想使用this.props就不行!
如果我非要使用呢?
使用super(props)相当于:this.props = props;
Test.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
};
Test.defaultProps = {
age: 18,
};
当我们想要对props进行限制或者补充的时候,会使用props-type这个包。
可以对props的类型进行校验,并且设定props的默认值
加入props限制之后
class Test extends React.Component {
// 通过static设置的方法,可以作为类的静态方法,相当于在类本身调用的方法
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
};
static defaultProps = {
age: 18,
};
state = {
b: 2,
};
changeNum = () => {
this.setState({
b: 3,
});
};
render() {
return (
<>
<div>{this.props.name}</div>
<div onClick={this.changeNum}>{this.props.age}</div>
</>
);
}
}
export default Test;
refs
// react在第一次render的时候,就会自动执行传给ref的回调函数,并且会把元素本身当作入参传入回调函数。
<Input ref={(c) => (this.inputRef = c)} />
// 创建一个ref容器,存储ref指向的元素
inputRef = React.createRef();
<Input ref={this.inputRef} />
实例讲解什么是高阶函数?
changeNum(params){
return (event) => {
// 这里统一处理param和event
}
}
// 把changeNum('参数')作为change的回调函数
<Input onChange={changeNum('参数')} />
高阶函数:
1.函数入参是函数
2.函数返回函数
举例:Promise、setTimeout、reduce、map等
函数柯里化:
通过函数返回函数的形式,实现多次输入参数,最后统一处理参数的功能。
onChange的时候,react调用的函数相当于是changeNum('参数')(event),这就是函数柯里化
另一种解决方案:
/*
onChange需要我传入函数,我就传入箭头函数
onChange的时候react会帮我调用这个箭头函数,并且传入event作为参数
*/
<Input onChange={(event)=>changeNum('参数',event)} />