setState
时,React render
是如何工作的?咱们可以将"render
"分为两个步骤:
render
方法被调用时,它返回一个新的组件的虚拟 DOM 结构。当调用setState()
时,render
会被再次调用,因为默认情况下shouldComponentUpdate
总是返回true
,所以默认情况下 React 是没有优化的。Props 是 React 中属性的简写。它们是只读组件,必须保持纯,即不可变。它们总是在整个应用中从父组件传递到子组件。子组件永远不能将 prop 送回父组件。这有助于维护单向数据流,通常用于呈现动态生成的数据。
PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能。
在React中,当prop或者state发生变化时,可以通过在shouldComponentUpdate生命周期函数中执行return false来阻止页面的更新,从而减少不必要的render执行。React.PureComponent会自动执行 shouldComponentUpdate。
不过,pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。
使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。
构造函数主要用于两个目的:
所以,当在React class中需要设置state的初始值或者绑定事件时,需要加上构造函数,官方Demo:
class LikeButton extends React.Component {
constructor() {
super();
this.state = {
liked: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({liked: !this.state.liked});
}
render() {
const text = this.state.liked ? 'liked' : 'haven\'t liked';
return (
<div onClick={this.handleClick}>
You {text} this. Click to toggle. </div>
);
}
}
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
构造函数用来新建父类的this对象;子类必须在constructor方法中调用super方法;否则新建实例时会报错;因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法;子类就得不到this对象。
注意:
(1)props
props是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的props来重新渲染子组件,否则子组件的props以及展现形式不会改变。
(2)state
state的主要作用是用于组件保存、控制以及修改自己的状态,它只能在constructor中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的this.setState来修改,修改state属性会导致组件的重新渲染。
(3)区别
(1)共同点
(2)不同点
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
React 官方对 Portals 的定义:
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案
Portals 是React 16提供的官方解决方案,使得组件可以脱离父组件层级挂载在DOM树的任何位置。通俗来讲,就是我们 render 一个组件,但这个组件的 DOM 结构并不在本组件内。
Portals语法如下:
ReactDOM.createPortal(child, container);
一般情况下,组件的render函数返回的元素会被挂载在它的父级组件上:
import DemoComponent from './DemoComponent';
render() {
// DemoComponent元素会被挂载在id为parent的div的元素上
return (
<div id="parent">
<DemoComponent />
</div>
);
}
然而,有些元素需要被挂载在更高层级的位置。最典型的应用场景:当父组件具有overflow: hidden
或者z-index
的样式设置时,组件有可能被其他元素遮挡,这时就可以考虑要不要使用Portal使组件的挂载脱离父组件。例如:对话框,模态窗。
import DemoComponent from './DemoComponent';
render() {
// DemoComponent元素会被挂载在id为parent的div的元素上
return (
<div id="parent">
<DemoComponent />
</div>
);
}
JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引入更多的标准。
其实 React 本身并不强制使用 JSX。在没有 JSX 的时候,React 实现一个组件依赖于使用 React.createElement 函数。代码如下:
class Hello extends React.Component {
render() {
return React.createElement(
'div',
null,
`Hello ${this.props.toWhat}`
);
}
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
而 JSX 更像是一种语法糖,通过类似 XML 的描述方式,描写函数对象。在采用 JSX 之后,这段代码会这样写:
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}
ReactDOM.render(
<Hello toWhat="World" />,
document.getElementById('root')
);
通过对比,可以清晰地发现,代码变得更为简洁,而且代码结构层次更为清晰。
因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在手写一棵结构树。而XML 在树结构的描述上天生具有可读性强的优势。
但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运行的时候,会使用 Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。
总结: JSX 是一个 JavaScript 的语法扩展,结构类似 XML。JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。
React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。
React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿。
为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
一、更容易复用代码
二、清爽的代码风格+代码量更少
缺点
状态不同步
不好用的useEffect,
Refs 提供了一种方式,用于访问在 render 方法中创建的 React 元素或 DOM 节点。Refs 应该谨慎使用,如下场景使用 Refs 比较适合:
Refs 是使用 React.createRef()
方法创建的,他通过 ref
属性附加到 React 元素上。要在整个组件中使用 Refs,需要将 ref
在构造函数中分配给其实例属性:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
}
render() {
return <div ref={this.myRef} />
}
}
由于函数组件没有实例,因此不能在函数组件上直接使用 ref
:
function MyFunctionalComponent() {
return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// 这将不会工作!
return (
<MyFunctionalComponent ref={this.textInput} />
);
}
}
但可以通过闭合的帮助在函数组件内部进行使用 Refs:
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 回调才可以引用它
let textInput = null;
function handleClick() {
textInput.focus();
}
return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} /> <input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
注意:
ref
的返回值取决于节点的类型:ref
属性被用于一个普通的 HTML 元素时,React.createRef()
将接收底层 DOM 元素作为他的 current
属性以创建 ref
。ref
属性被用于一个自定义的类组件时,ref
对象将接收该组件已挂载的实例作为他的 current
。ref
时可使用传递 Refs 或回调 Refs。React-intl是雅虎的语言国际化开源项目FormatJS的一部分,通过其提供的组件和API可以与ReactJS绑定。
React-intl提供了两种使用方法,一种是引用React组件,另一种是直接调取API,官方更加推荐在React项目中使用前者,只有在无法使用React组件的地方,才应该调用框架提供的API。它提供了一系列的React组件,包括数字格式化、字符串格式化、日期格式化等。
在React-intl中,可以配置不同的语言包,他的工作原理就是根据需要,在语言包之间进行切换。
由ES6的继承规则得知,不管子类写不写constructor,在new实例的过程都会给补上constructor。
所以:constructor钩子函数并不是不可缺少的,子组件可以在一些情况略去。比如不自己的state,从props中获取的情况
使用了装饰模式,高阶组件的运用:
function withWindowWidth(BaseComponent) {
class DerivedClass extends React.Component {
state = {
windowWidth: window.innerWidth,
}
onResize = () => {
this.setState({
windowWidth: window.innerWidth,
})
}
componentDidMount() {
window.addEventListener('resize', this.onResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
}
render() {
return <BaseComponent {...this.props} {...this.state}/>
}
}
return DerivedClass;
}
const MyComponent = (props) => {
return <div>Window width is: {props.windowWidth}</div>
};
export default withWindowWidth(MyComponent);
装饰模式的特点是不需要改变 被装饰对象 本身,而只是在外面套一个外壳接口。JavaScript 目前已经有了原生装饰器的提案,其用法如下:
@testable
class MyTestableClass {
}
相似之处:
不同之处:
1)数据流
Vue默认支持数据双向绑定,而React一直提倡单向数据流
2)虚拟DOM
Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
3)组件化
React与Vue最大的不同是模板的编写。
具体来讲:React中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 完组件之后,还需要在 components 中再声明下。
4)监听数据变化的实现原理不同
5)高阶组件
react可以通过高阶组件(Higher Order Components-- HOC)来扩展,而vue需要通过mixins来扩展。
原因高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不采用HOC来实现。
6)构建工具
两者都有自己的构建工具
7)跨平台
在React中,当涉及组件嵌套,在父组件中使用props.children
把所有子组件显示出来。如下:
function ParentComponent(props){
return (
<div>
{props.children} </div>
)
}
如果想把父组件中的属性传给所有的子组件,需要使用React.Children
方法。
比如,把几个Radio组合起来,合成一个RadioGroup,这就要求所有的Radio具有同样的name属性值。可以这样:把Radio看做子组件,RadioGroup看做父组件,name的属性值在RadioGroup这个父组件中设置。
首先是子组件:
//子组件
function RadioOption(props) {
return (
<label>
<input type="radio" value={props.value} name={props.name} />
{props.label} </label>
)
}
然后是父组件,不仅需要把它所有的子组件显示出来,还需要为每个子组件赋上name属性和值:
//父组件用,props是指父组件的props
function renderChildren(props) {
//遍历所有子组件
return React.Children.map(props.children, child => {
if (child.type === RadioOption)
return React.cloneElement(child, {
//把父组件的props.name赋值给每个子组件
name: props.name
})
else
return child
})
}
//父组件
function RadioGroup(props) {
return (
<div>
{renderChildren(props)} </div>
)
}
function App() {
return (
<RadioGroup name="hello">
<RadioOption label="选项一" value="1" />
<RadioOption label="选项二" value="2" />
<RadioOption label="选项三" value="3" />
</RadioGroup>
)
}
export default App;
以上,React.Children.map
让我们对父组件的所有子组件又更灵活的控制。
虚拟DOM相对原生的DOM不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量DOM时,由于多了一层虚拟DOM的计算,虚拟DOM也会比innerHTML插入慢。它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。
在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。**虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。
(1)遍历数组:map && forEach
import React from 'react';
class App extends React.Component {
render() {
let arr = ['a', 'b', 'c', 'd'];
return (
<ul>
{
arr.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
)
}
}
class App extends React.Component {
render() {
let arr = ['a', 'b', 'c', 'd'];
return (
<ul>
{
arr.forEach((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
)
}
}
(2)遍历对象:map && for in
class App extends React.Component {
render() {
let obj = {
a: 1,
b: 2,
c: 3
}
return (
<ul>
{
(() => {
let domArr = [];
for(const key in obj) {
if(obj.hasOwnProperty(key)) {
const value = obj[key]
domArr.push(<li key={key}>{value}</li>)
}
}
return domArr;
})()
}
</ul>
)
}
}
// Object.entries() 把对象转换成数组
class App extends React.Component {
render() {
let obj = {
a: 1,
b: 2,
c: 3
}
return (
<ul>
{
Object.entries(obj).map(([key, value], index) => { // item是一个数组,把item解构,写法是[key, value]
return <li key={key}>{value}</li>
})
}
</ul>
)
}
}
原理如下。
(1)节点之间的比较。
节点包括两种类型:一种是 React组件,另一种是HTML的DOM。
如果节点类型不同,按以下方式比较。
如果 HTML DOM不同,直接使用新的替换旧的。如果组件类型不同,也直接使用新的替换旧的。
如果 HTML DOM类型相同,按以下方式比较。
在 React里样式并不是一个纯粹的字符串,而是一个对象,这样在样式发生改变时,只需要改变替换变化以后的样式。修改完当前节点之后,递归处理该节点的子节点。
如果组件类型相同,按以下方式比较。
如果组件类型相同,使用 React机制处理。一般使用新的 props替换旧的 props,并在之后调用组件的 componentWillReceiveProps方法,之前组件的 render方法会被调用。
节点的比较机制开始递归作用于它的子节点。
(2)两个列表之间的比较。
一个节点列表中的一个节点发生改变, React无法很妤地处理这个问题。循环新旧两个列表,并找出不同,这是 React唯一的处理方法。
但是,有一个办法可以把这个算法的复杂度降低。那就是在生成一个节点列表时给每个节点上添加一个key。这个key只需要在这一个节点列表中唯一,不需要全局唯一。
(3)取舍
需要注意的是,上面的启发式算法基于两点假设。
类型相近的节点总是生成同样的树,而类型不同的节点也总是生成不同的树
可以为多次 render都表现稳定的节点设置key。
上面的节点之间的比较算法基本上就是基于这两个假设而实现的。要提高 React应用的效率,需要按照这两点假设来开发。
若虚拟DOM中的内容没有发生改变,直接使用旧的虚拟DOM
若虚拟DOM中的内容发生改变了,则生成新真实的DOM,随后替换页面中之前的真实DOM
根据数据创建真实DOM,随后渲染到页面
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。