react虚拟DOM
什么是DOM? 浏览器在解析HTML文档时,会将每个标签元素抽象成DOM的节点,按照标签元素层次分明的结构,将HTML文档构建成一棵DOM树
什么是虚拟DOM? 虚拟DOM(Virtual DOM)是在真实DOM树的基础上建立了一个抽象层,通过一个JavaScript对象表示真实DOM树结构,包括DOM树节点的标签名、属性、事件监听及子元素等。通过事务处理机制,将多次DOM修改的结果一次性更新到页面上,从而有效的减少页面渲染次数,减少修改DOM重绘重排的时间,提高渲染性能。React在内存中维护一个跟真实DOM一样的虚拟DOM树,再改动完组件后,会再生成一个新的虚拟DOM,React会将新的虚拟DOM和原来的虚拟DOM进行对比,找出两个DOM树的不同的地方(diff),然后在真实DOM上更新diff,提高渲染速度。
diff算法的原理 diff算法探讨的就是虚拟DOM树发生变化后,生成DOM树更新补丁的方式、它通过对比新旧两颗虚拟DOM树的变更差异,将更新补丁作用于真实DOM,以最小的成本完成试图更新。
diff算法原则一: 由于Web UI中DOM节点跨层级的移动操作特别少,DOM节点跨层级的移动操作特别少,React对DOM树进行分层比较,两棵树只会对同一层次的节点进行比较。如果发现节点不存在,那么这个节点以及它的子节点就会被完全删除,不会进行下一步的比较。这样只需要一次遍历,就可以完成整个DOM树的比较。
diff算法原则二: React基于组件构建应用,如果节点是组件,先看组件类型:(1)如果是同一类型组件,按照原策略继续比较虚拟DOM树;(2)如果不是同一类型组件,则替换该组件及其所有子节点。如图所示。当组件B改变为组件E时,一旦React判断出B和E是不同类型的组件,就不会继续比较二者的结构,而是直接删除组件B,重新创建组件E。
diff算法原则三: 当节点处于同一层级时,react diff提供三种节点层级操作:(1)插入:新的组件类型不在原集合中,需要对新节点执行插入操作。(2)移动:新的组件在原集合中,且element可更新使用(如修改位置),需要进行移动操作。(3)删除:原集合中的组件在新集合中不存在,或者存在但element发生改变,无法更新使用,则需要进行删除操作。
如图所示,显而易见只对DOM树进行删除了节点A的操作,但根据上述节点层级操作中原理,其过程为:原集合中包含两个span节点,新集合中包含一个span节点,此时新旧集合进行差异比较,发现原集合中A节点的属性发生改变,B节点被删除,因此在新的虚拟DOM树中执行操作为:删除A节点->删除B节点->创建新的A插入。
React发现由于位置发生改变导致出现多余的删除和创建操作,提供了一种简单的优化策略,即允许对同一层级的子节点添加唯一key进行区分。如图 10所示,对新旧集合进行差异比较,通过key值发现仅原集合中A节点在新集合中不存在,则只需要进行删除A节点的操作即可。
react JSX
react三大核心属性state
setState为什么默认采用异步
假如所有setState是同步的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnodediff + dom修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个setState合并成一次组件更新。
setState什么时候是同步
在setTimeout、setState 的回调函数中或者原生事件中,setState是同步的。
componentDidMount() {
// count 初始值为 0
this.setState({ count: this.state.count + 1 })
console.log('1', this.state.count) // 0
this.setState({ count: this.state.count + 1 })
console.log('2', this.state.count) // 0,两次更新被合并
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log('3', this.state.count) // 2
})
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log('4', this.state.count) // 3
})
}
this.setState({
count: this.state.count + 1
}, () => {
console.log('count by callback', this.state.count) // 回调函数中可以拿到最新的 state
})
bodyClickHandler = () => {
this.setState({
count: this.state.count + 1
})
console.log('count in body event', this.state.count)
}
componentDidMount() {
// 自己定义的 DOM 事件,setState 是同步的
document.body.addEventListener('click', this.bodyClickHandler)
}
setState为什么是异步的
在 React的 setState 函数实现中,会根据一个变量 isBatchingUpdates判断是直接更新 this.state 还是放到一个updateQueue中延时更新。而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state。但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true。而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates将isBatchingUpdates修改为true。这样由 React 控制的事件处理过程 setState 不会同步更新 this.state,而是异步的。所以说setstate本身是同步的,一旦走了react内部的合并逻辑,放入了updateQueue队列中就变成异步了。
如何避免setState合并?
// 传入函数,不会被合并。执行结果是 +3
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
为什么不要直接修改state的值?
为了不可变数据结构,不可变数据结构为了SCU(shouldComponentUpdate),SCU为了性能优化。如果直接修改值,那么由于数组或者对象的引用不会改变,react如果要判断值是否改变就需要去深层次对比,浪费性能。而如果采用SCU,那么只需要浅比较就可以,如果引用发生了改变说明值发生了变化,就进行更新。
react三大核心属性props
默认值和类型检查
this.defaultProps = {
value:'',
onChange:()=>{},
name:''
};
this.propTypes={
value: propsTypes.string.isRequired,
name: propsTypes.string,
onChange: propsTypes.func,
filter:PropTypes.shape({
area:PropTypes.string,
price:PropTypes.number
})
};
props.childredn
可以是文本、react元素、组件、函数
class Parent extends React.Component {
render() {
return (
<Child>
<div>slot1</div>
<div>slot2</div>
<div>slot3</div>
</Child>
)
}
}
class Child extends React.Component {
render() {
return (
<div>
<div>{this.props.children[2]}</div>
<div>{this.props.children[1]}</div>
<div>{this.props.children[0]}</div>
</div>
)
}
}
react三大核心属性refs
受控组件: 表单组件的状态/数据只由state 维护 修改只能通过setState()来更新,表单数据是由 React组件来管理。
class NameForm extends React.Component {
constructor (props) {
super(props);
this.state = {
username: "BOb"
}
}
onChange (e) {
console.log(e.target.value);
this.setState({
username: e.target.value
})
}
render () {
return <input name="username" value={this.state.username} onChange={(e) => this.onChange(e)} />
}
}
非受控组件: 使用ref来从 DOM 节点中获取表单数据,表单数据将交由 DOM 节点来处理。
export class NameForm extends Component {
constructor (props) {
super(props);
this.inputRef = React.createRef();
}
handleSubmit = (e) => {
console.log('我们可以获得input内的值为', this.inputRef.current.value);
e.preventDefault();
}
render () {
return (
<form onSubmit={e => this.handleSubmit(e)}>
<input defaultValue="lindaidai" ref={this.inputRef} />
<input type="submit" value="提交" />
</form>
)
}
}
创建refs的三种方式
// 字符串形式的ref
<p ref='wenben'>文本</p>
console.log(this.refs.wenben)
class RefsExample extends Component {
handleSubmit = (event) => {
let username = this.usernameRef.current.value
let passwd = this.passwordRef.value
if (!(username === 'root' && passwd === '123')) {
event.preventDefault()
alert("信息不正确!")
}
}
constructor() {
super();
this.usernameRef = React.createRef()
}
focusTextInput = (element) => {
this.passwordRef = element // 将当前元素的对象保存起来
}
render() {
return (
<form action='' onSubmit={this.handleSubmit}>
{/*createRefs()创建refs*/}
用户名:<input ref={this.usernameRef} type='text'/><br/>
{/*回调函数创建refs*/}
密 码:<input ref={this.focusTextInput} type='password'/><br/>
<button>提交</button>
</form>
);
}
}
refs的使用场景及步骤
管理焦点,文本选择或媒体播放;触发强制动画;集成第三方 DOM 库。
- 第一步、使用 useRef 创建 ref 对象(useRef 是 FC hooks, class 组件使用 React.createRef() 创建 )
- 第二步、赋值&使用【操作dom则绑定为dom的ref属性的值,用于保存值的时候传递内容给 ref.current】
- 第三步、访问ref内容【进行dom对应的api访问,进行 scroll 、focus等操作。又或者从current中读取保存的数据】最终的目的还是最后访问拿到对应的数据进行操作。下边我们分别用两个小 demo 简单先看看用法,理论和总结在后边一点【熟悉Refs使用的直接跳转: 核心要点】。
react生命周期
什么是挂载: 当组件实例被创建并插入 DOM 中
constructor(props)
React组件的构造函数在挂载之前被调用。在实现React.Component构造函数时,需要先在添加其他内容前,调用super(props),用来将父组件传来的props绑定到这个类中,使用this.props将会得到。
constructor中应当做些初始化的动作,如:初始化state,将事件处理函数绑定到类实例上,但也不要使用setState()。如果没有必要初始化state或绑定方法,则不需要构造constructor,或者把这个组件换成纯函数写法。当然也可以利用props初始化state,在之后修改state不会对props造成任何修改,但仍然建议大家提升状态到父组件中,或使用redux统一进行状态管理。
constructor(props) {
super(props);
this.state = {
isLiked: props.isLiked
};
}
componentDidMount
当组件挂载后这个函数就会被执行, 我们的一些数据请求通常都会放在这个生命周期中。componentDidMount 中去重新 setState 的时候,是一个同步的过程,也就是说会立刻进行一次 rerender ,此时js线程仍然被占用。在这个时候我们上一次的内容其实已经到了DOM中,但是渲染线程无法渲染内容到用户界面上,所以用户看不见任何东西。会阻塞渲染的生命周期,在这里面最好不要去执行一些非常耗时的逻辑,这样会让我们的首屏出现的更慢。
componentWillReceiveProps
- 组件初次渲染时不会执行componentWillReceiveProps;
- 当props发生变化时执行componentWillReceiveProps;
- 在这个函数里面,旧的属性仍可以通过this.props来获取;
- 此函数可以作为 react 在 prop 传入之后, render() 渲染之前更新 state 的机会。即可以根据属性的变化,通过调用this.setState()来更新你的组件状态,在该函数中调用 this.setState() 将不会引起第二次渲染。
- 也可在此函数内根据需要调用自己的自定义函数,来对prop的改变做出一些响应。
- 当父组件向子组件传递引用类型(或复合类型,比如对象、数组等)的属性时,要注意打印的this.props和nextProps的内容是一致的,因为引用类型在内存中只有一份,传值时是浅拷贝。
在componentWillReceiveProps这个回调函数中,我们可以获取到就是props,通过this.props来获取,然后新的props则是通过函数的参数传入,在这里我们可以比较两个props从而对本组件的state作出安全的变更然后重新渲染我们的组件或者是触发子组件内的某些方法。
componentWillReceiveProps(nextProps) {
if (this.props.sharecard_show !== nextProps.sharecard_show){
//在这里我们仍可以通过this.props来获取旧的外部状态
//通过新旧状态的对比,来决定是否进行其他方法
if (nextProps.sharecard_show){
this.handleGetCard();
}
}
}
// 子组件内
componentWillReceiveProps(nextProps) {
if (this.props.sharecard_show !== nextProps.sharecard_show){
if (nextProps.sharecard_show){
this.handleGetCard();
}
}
}
/* 子组件更新触发父组件的回调函数,导致父组件更新,进而又导致子组件的componentWillReceiveProps陷入死循环*/
handleGetCard() {
this.props.test()
}
//父组件内
test() {
this.setState({
sharecard_show: !this.state.sharecard_show
})
}
shouldComponentUpdate 当props或者state的值发生改变时进行自定义的判断,判断是否更新相应的值。
// 实现只有当props.color或者state.count的值改变才需要更新。shouldComponentUpdate仅检查了props.color或state.count是否改变。如果这些值没有改变,那么这个组件不会更新。在实现的时候既可以进行浅比较,也可以进行深比较。
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
// 如果只是进行浅比较的话其实可以通过继承React.PureComponent来实现。但是当传递的值是对象或者数组的时候这种方法就失效了,因为数组添加一个元素进行浅比较的时候仍是相等的。使用纯组件的时候需要注意在改变值的时候进行深拷贝。
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
componentDidUpdate
当组件的state或者props发生变化更新完成之后会调用此函数,一般用于判断组件的state或者props是否发生变化。
componentDidUpdate(prevProps, prevState) {
if (prevProps.location.pathname !== this.props.location.pathname) {
//业务处理逻辑
}
}
forceUpdate
forceUpdate就是重新render。有些变量不在state上,当时你又想达到这个变量更新的时候,刷新render;或者state里的某个变量层次太深,更新的时候没有自动触发render。这些时候都可以手动调用forceUpdate自动触发render。调用forceUpdate()会导致组件跳过shouldComponentUpdate(),直接调用render()。
// Sub.js
class Sub extends React.Component{
construcotr(){
super();
this.name = "yema";
}
refChangeName(name){
this.name = name;
this.forceUpdate();
}
render(){
return (<div>{this.name}</div>);
}
}
// App.js
class App extends React.Component{
handleClick(){
this.subRef.refChangeName("yemafuren");
}
render(){
return (<div>
<Sub ref={(sub)=>{this.subRef = sub;}} />
<button onClick={this.handleClick}>click</button>
</div>);
}
}
getDerivedStateFromProps
将传入的props映射到state上面。getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过this访问到class的属性,也并不推荐直接访问属性。而是应该通过参数提供的nextProps以及prevState来进行判断,根据新传入的props来映射到state。每当setState和forceUpdate时会执行,意味着即使你的props没有任何变化,而是父state发生了变化,导致子组件发生了re-render,这个生命周期函数依然会被调用。getDerivedStateFromProps方法一定要return一个值。
www.infoq.cn/article/JCp…
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 当传入的type发生变化的时候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}
react native组件
Image
- 加载项目中的静态资源
<Image
style={styles.image}
// ./表示当前文件目录 ../ 父目录
source={require('./reactlogo.png')}
/>
- 加载原生图片资源
<Image
source={{uri: 'launcher_icon'}}
style={{width: 38, height: 38}}
/>);
- 加载网络图片
<Image
source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}}
style={{width: 38, height: 38}}
/>);
ScrollView
FlatList
为什么ListView对于大数据量的情况下性能会很差呢?
深入ListView的原理你会发现,ListView对列表中的Item是全量渲染的,并且没有复用机制,这就难以避免当让ListView渲染大数据量的时候会发生以下两个问题:
1、第一次打开与切换Tab时会出现卡顿或白屏的情况:这是因为ListView对所有的Item都是全量渲染的,比如:ListView中有100条Item,只有等这100条Item都渲染完成,ListView中的内容才会展示,这就难以避免卡顿白屏的问题;
2、滑动列表时会出现卡顿与不跟手:因为ListView中展示了大量数据的时候,滑动列表你会发现没有少量数据的时候的跟手与流畅,这是因为ListView为了渲染大量数据需要大量的内存和计算,这对手机资源是一个很大的消耗,尤其是在一些低端机上甚至会出现OOM;
VirtualizedList
VirtualizedList 是FlatList(它的data只支持普通数组) 与 SectionList 的底层实现。Vritualization 通过维护一个有限的渲染窗口(其中包含可见的元素),并将渲染窗口之外的元素全部用合适的定长空白空间代替的方式,极大的改善了内存消耗以及在有大量数据情况下的使用性能。这个渲染窗口能响应滚动行为。当一个元素离可视区太远时,它就有一个较低优先级;否则就获得一个较高的优先级。渲染窗口通过这种方式逐步渲染其中的元素(在进行了任何交互之后),以尽量减少出现空白区域的可能性。
- ListHeaderComponent:头部组件,可以是 React Component, 也可以是一个 render 函数,或者渲染好的 element。
- ListFooterComponent:尾部组件。可以是 React Component, 也可以是一个 render 函数,或者渲染好的 element。
- ListEmptyComponent:列表为空时渲染该组件。可以是 React Component, 也可以是一个 render 函数,或者渲染好的 element。
- onRefresh:类型function,如果设置了此选项,则会在列表头部添加一个标准的RefreshControl控件,以便实现“下拉刷新”的功能。同时你需要正确设置refreshing属性。
- refreshing:类型boolean,在等待加载新数据时将此属性设为 true,列表就会显示出一个正在加载的符号。
- onEndReachedThreshold:类型number,决定当距离内容最底部还有多远时触发onEndReached回调。注意此参数是一个比值而非像素单位。比如,0.5 表示距离内容最底部的距离为当前列表可见长度的一半时触发。
- onEndReached:类型function,当列表被滚动到距离内容最底部不足onEndReachedThreshold 的距离时调用。
- getItemLayout:getItemLayout是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。如果你的行高是固定的,getItemLayout用起来就既高效又简单。
高级使用
- 与ListView不同的是,渲染窗口中的所有Item在任何props改变时都会重新渲染,这在通常情况下是比较好的,因为渲染窗口的Item数量是不变的,但是如果Item比较复杂的话,你因该应确保遵循React最佳性能实践,并在适当情况下使用React.PureComponent和/或shouldComponentUpdate来限制你的组件以及子组件的渲染次数,减少不必要的渲染以及递归渲染等。
- 如果你不需要渲染就知道内容的高度的话,可以通过getItemLayout 属性来改善用户体验,这使得通过例如滚动到具体Item更平滑。比如使用 scrollToIndex滚动到指定的Item。
- 本组件继承自PureComponent而非通常的Component,这意味着如果其props在浅比较中是相等的,则不会重新渲染。所以请先检查你的renderItem函数所依赖的props数据(包括data属性以及可能用到的父组件的state),如果是一个引用类型(Object或者数组都是引用类型),则需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。
- 为了优化内存占用同时保持滑动的流畅,列表内容会在屏幕外异步绘制。这意味着如果用户滑动的速度超过渲染的速度,则会先看到空白的内容。这是为了优化不得不作出的妥协。
- 默认情况下每行都需要提供一个不重复的key属性。你也可以提供一个keyExtractor函数来生成key。
- getItemLayout是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。如果你的行高是固定的,getItemLayout用起来就既高效又简单。
为什么不能使用index作为Key