react优秀项目案例(React怎么实现diff算法)

本文目录
- React怎么实现diff算法
- React高阶组件应如何使用
- 如何使用react创建单例组件
- 如何将react组件内数据进行共享
- react组件的性能优化有哪些方面
- react-router4.0下服务器如何配合BrowserRouter实例教程
- 怎样使用reactrouter4+redux实现路由权限控制
React怎么实现diff算法
这次给大家带来React怎么实现diff算法,React实现diff算法的注意事项有哪些,下面就是实战案例,一起来看一下。
前言
在上一篇文章,我们已经实现了React的组件功能,从功能的角度来说已经实现了React的核心功能了。
但是我们的实现方式有很大的问题:每次更新都重新渲染整个应用或者整个组件,DOM操作十分昂贵,这样性能损耗非常大。
为了减少DOM更新,我们需要找渲染前后真正变化的部分,只更新这一部分DOM。而对比变化,找出需要更新部分的算法我们称之为diff算法。
对比策略
在前面两篇文章后,我们实现了一个render方法,它能将虚拟DOM渲染成真正的DOM,我们现在就需要改进它,让它不要再傻乎乎地重新渲染整个DOM树,而是找出真正变化的部分。
这部分很多类React框架实现方式都不太一样,有的框架会选择保存上次渲染的虚拟DOM,然后对比虚拟DOM前后的变化,得到一系列更新的数据,然后再将这些更新应用到真正的DOM上。
但也有一些框架会选择直接对比虚拟DOM和真实DOM,这样就不需要额外保存上一次渲染的虚拟DOM,并且能够一边对比一边更新,这也是我们选择的方式。
不管是DOM还是虚拟DOM,它们的结构都是一棵树,完全对比两棵树变化的算法时间复杂度是O(n^3),但是考虑到我们很少会跨层级移动DOM,所以我们只需要对比同一层级的变化。
只需要对比同一颜色框内的节点
总而言之,我们的diff算法有两个原则:
对比当前真实的DOM和虚拟DOM,在对比过程中直接更新真实DOM
只对比同一层级的变化实现
我们需要实现一个diff方法,它的作用是对比真实DOM和虚拟DOM,最后返回更新后的DOM
/**
* @param {HTMLElement} dom 真实DOM
* @param {vnode} vnode 虚拟DOM
* @returns {HTMLElement} 更新后的DOM
*/
function diff( dom, vnode ) {
// ...
}接下来就要实现这个方法。
在这之前先来回忆一下我们虚拟DOM的结构:
虚拟DOM的结构可以分为三种,分别表示文本、原生DOM节点以及组件。
// 原生DOM节点的vnode
{
tag: ’p’,
attrs: {
className: ’container’
},
children:
}
// 文本节点的vnode
"hello,world"
// 组件的vnode
{
tag: ComponentConstrucotr,
attrs: {
className: ’container’
},
children:
}对比文本节点
首先考虑最简单的文本节点,如果当前的DOM就是文本节点,则直接更新内容,否则就新建一个文本节点,并移除掉原来的DOM。
// diff text node
if ( typeof vnode === ’string’ ) {
// 如果当前的DOM就是文本节点,则直接更新内容
***隐藏网址***
if ( dom.textContent !== vnode ) {
dom.textContent = vnode;
}
// 如果DOM不是文本节点,则新建一个文本节点DOM,并移除掉原来的
} else {
out = document.createTextNode( vnode );
if ( dom && dom.parentNode ) {
dom.parentNode.replaceChild( out, dom );
}
}
return out;
}文本节点十分简单,它没有属性,也没有子元素,所以这一步结束后就可以直接返回结果了。
对比非文本DOM节点
如果vnode表示的是一个非文本的DOM节点,那就要分几种情况了:
如果真实DOM和虚拟DOM的类型不同,例如当前真实DOM是一个p,而vnode的tag的值是’button’,那么原来的p就没有利用价值了,直接新建一个button元素,并将p的所有子节点移到button下,然后用replaceChild方法将p替换成button。
if ( !dom || dom.nodeName.toLowerCase() !== vnode.tag.toLowerCase() ) {
out = document.createElement( vnode.tag );
if ( dom ) {
.map( out.appendChild ); // 将原来的子节点移到新节点下
if ( dom.parentNode ) {
dom.parentNode.replaceChild( out, dom ); // 移除掉原来的DOM对象
}
}
}如果真实DOM和虚拟DOM是同一类型的,那我们暂时不需要做别的,只需要等待后面对比属性和对比子节点。
对比属性
实际上diff算法不仅仅是找出节点类型的变化,它还要找出来节点的属性以及事件监听的变化。我们将对比属性单独拿出来作为一个方法:
function diffAttributes( dom, vnode ) {
const old = dom.attributes; // 当前DOM的属性
const attrs = vnode.attrs; // 虚拟DOM的属性
// 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined)
for ( let name in old ) {
if ( !( name in attrs ) ) {
setAttribute( dom, name, undefined );
}
}
// 更新新的属性值
for ( let name in attrs ) {
if ( old ) {
setAttribute( dom, name, attrs );
}
}
}setAttribute方法的实现参见第一篇文章
对比子节点
节点本身对比完成了,接下来就是对比它的子节点。
这里会面临一个问题,前面我们实现的不同diff方法,都是明确知道哪一个真实DOM和虚拟DOM对比,但是子节点是一个数组,它们可能改变了顺序,或者数量有所变化,我们很难确定要和虚拟DOM对比的是哪一个。
为了简化逻辑,我们可以让用户提供一些线索:给节点设一个key值,重新渲染时对比key值相同的节点。
// diff方法
if ( vnode.children && vnode.children.length 》 0 || ( out.childNodes && out.childNodes.length 》 0 ) ) {
diffChildren( out, vnode.children );
}function diffChildren( dom, vchildren ) {
const domChildren = dom.childNodes;
const children = ;
const keyed = {};
// 将有key的节点和没有key的节点分开
if ( domChildren.length 》 0 ) {
for ( let i = 0; i 《 domChildren.length; i++ ) {
const child = domChildren;
const key = child.key;
if ( key ) {
keyedLen++;
keyed = child;
} else {
children.push( child );
}
}
}
if ( vchildren && vchildren.length 》 0 ) {
let min = 0;
let childrenLen = children.length;
for ( let i = 0; i 《 vchildren.length; i++ ) {
const vchild = vchildren;
const key = vchild.key;
let child;
// 如果有key,找到对应key值的节点
if ( key ) {
if ( keyed ) {
child = keyed;
keyed = undefined;
}
// 如果没有key,则优先找类型相同的节点
} else if ( min 《 childrenLen ) {
for ( let j = min; j 《 childrenLen; j++ ) {
let c = children;
if ( c && isSameNodeType( c, vchild ) ) {
child = c;
children = undefined;
if ( j === childrenLen - 1 ) childrenLen--;
if ( j === min ) min++;
break;
}
}
}
// 对比
child = diff( child, vchild );
// 更新DOM
const f = domChildren;
if ( child && child !== dom && child !== f ) {
if ( !f ) {
dom.appendChild(child);
} else if ( child === f.nextSibling ) {
removeNode( f );
} else {
dom.insertBefore( child, f );
}
}
}
}
}对比组件
如果vnode是一个组件,我们也单独拿出来作为一个方法:
function diffComponent( dom, vnode ) {
let c = dom && dom._component;
let oldDom = dom;
// 如果组件类型没有变化,则重新set props
if ( c && c.constructor === vnode.tag ) {
setComponentProps( c, vnode.attrs );
dom = c.base;
// 如果组件类型变化,则移除掉原来组件,并渲染新的组件
} else {
if ( c ) {
unmountComponent( c );
oldDom = null;
}
c = createComponent( vnode.tag, vnode.attrs );
setComponentProps( c, vnode.attrs );
dom = c.base;
if ( oldDom && dom !== oldDom ) {
oldDom._component = null;
removeNode( oldDom );
}
}
return dom;
}下面是相关的工具方法的实现,和上一篇文章的实现相比,只需要修改renderComponent方法其中的一行。
function renderComponent( component ) {
// ...
// base = base = _render( renderer ); // 将_render改成diff
base = diff( component.base, renderer );
// ...
}完整diff实现看这个文件
渲染
现在我们实现了diff方法,我们尝试渲染上一篇文章中定义的Counter组件,来感受一下有无diff方法的不同。
class Counter extends React.Component {
constructor( props ) {
super( props );
this.state = {
num: 1
}
}
onClick() {
this.setState( { num: this.state.num + 1 } );
}
render() {
return (
《p》
《h1》count: { this.state.num }《/h1》
《button onClick={ () =》 this.onClick()}》add《/button》
《/p》
);
}
}不使用diff
使用上一篇文章的实现,从chrome的调试工具中可以看到,闪烁的部分是每次更新的部分,每次点击按钮,都会重新渲染整个组件。
使用diff
而实现了diff方法后,每次点击按钮,都只会重新渲染变化的部分。
后话
在这篇文章中我们实现了diff算法,通过它做到了每次只更新需要更新的部分,极大地减少了DOM操作。React实现远比这个要复杂,特别是在React 16之后还引入了Fiber架构,但是主要的思想是一致的。
实现diff算法可以说性能有了很大的提升,但是在别的地方仍然后很多改进的空间:每次调用setState后会立即调用renderComponent重新渲染组件,但现实情况是,我们可能会在极短的时间内多次调用setState。
假设我们在上文的Counter组件中写出了这种代码
onClick() {
for ( let i = 0; i 《 100; i++ ) {
this.setState( { num: this.state.num + 1 } );
}
}相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!
推荐阅读:
实现vue多页面开发与打包步骤详解
Vue实现proxy代理步骤详解
React高阶组件应如何使用
这次给大家带来React高阶组件应如何使用,React高阶组件使用的注意事项有哪些,下面就是实战案例,一起来看一下。
前段时间在工作中写Hybrid页面时遇到了这样的一个场景,公司需要一系列的活动组件,在每个组件注册的时候都需要调用App端提供的一个接口。一开始也考虑了几种方式,包括mixin、组件继承以及react高阶组件。但经过了种种衡量,最后选择使用了高阶组件的做法。
那什么是高级组件?首先你得先了解请求ES6中的class只是语法糖,本质还是原型继承。能够更好的进行说明,我们将不会修改组件的代码。而是通过提供一些能够包裹组件的组件, 并通过一些额外的功能来增强组件。这样的组件我们称之为高阶组件(Higher-Order Component)。
1、Mixins的缺点
React官方已不推荐使用Mixins的技术来实现代码的重用,Mixins技术有一系列的缺点,首先Mixins会造成命名冲突,我们通过以下的方式来注入Mixins:
var myMixins = require(’myMixins’);
var Button = React.createClass({
mixins: ,
// ...
})如果你需要注入多个mixins,其中一个是自己的,另外的可能是第三方的。那有可能在两个mixins里使用了相同名称的方法,这会使得其中的一个不起作用,而你能做的只有修改其中一个方法的名称。另一方面,一个mixins一开始可能是非常简单的,仅仅需要实现某一个功能,但当业务越加的复杂,需要往其中加入更多的方法的时候,就会变得非常复杂。要深入了解mixins的缺点,可以查看官方博客。
2、组件继承
对于我自己来说这种方法以前使用的比较多,先创建一个BaseComponent,在其中实现一系列公共的方法,其后的每个组件都继承于这个组件,但缺点是不够灵活,在基础组件中只能实现一些比较固定的方法,而对于每个组件的定制化会有很大的限制。
3、React高阶组件
由于mixins的一系列缺点,React官方也意识到使用mixins所带来的痛点远远高于技术本身产生的优点,而高阶组件便可以代替mixins,而且当深入之后它还有着更加丰富的用法。
高阶组件(HOC)是React中对组件逻辑进行重用的高级技术。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。
高阶函数
说到高阶组件,就先得说到高阶函数了,高阶函数是至少满足下列条件的函数:
1、接受一个或多个函数作为输入
2、输出一个函数
在javascript这门函数为一等公民的语言中,高阶函数的使用还是非常之多的,像我们平时的回调函数等等,都用到了高阶函数的知识。我们先来看一个简单的高阶函数
var fun = function(x, y) {
return x + y;
}fun是一个函数,下面我们将整个函数作为参数传递给另一个函数
var comp = function(x, y, f) {
return f(x,y);
}验证一下
comp(1,2,fun) // 3高阶组件定义
类比高阶函数的定义,高阶组件就是接受一个组件作为参数,在函数中对组件做一系列的处理,随后返回一个新的组件作为返回值。
我们先定义一个高阶组件BaseActivity
const BaseActivity = (WrappedComponent) =》 {
return class extends Component {
render() {
return (
《section》
《p》我的包裹组件《/p》
《WrappedComponent /》
《/section》
)
}
}
}组件接受一个被包裹的组件作为参数,返回了一个经过处理的匿名组件。
在其他组件中使用这个高阶组件
class Example extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
width: ’100%’,
height: ’100%’
}
}
componentWillMount() {
if ((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) {
return;
} else {
this.setState({
width: ’375px’,
height: ’640px’
})
}
}
render() {
let { width, height } = this.state;
return (
《p className="activity"》
《p className="activity-content" style={{ width, height }}》
《button className="btn"》参加活动《/button》
《/p》
《/p》
)
}
}
export default BaseActivity(Example);具体用法就是在export 组件的时候,使用BaseActivity函数来包裹这个组件,看下输出的react dom内容
在Example组件外面包裹了一个匿名组件。
参数
既然高阶组件是一个函数,我们就可以向里面传递我们需要的参数
const BaseActivity = (WrappedComponent, title) =》 {
return class extends Component {
render() {
return (
《section》
《p》{title}《/p》
《WrappedComponent /》
《/section》
)
}
}
}在Example中这样export
export default BaseActivity(Example, ’这是高阶组件的参数’);我们看下输出的react dom
可以看到参数已经传递进去了。
当然还可以这样用(柯里化)
const BaseActivity (title) =》 (WrappedComponent) =》 {
return class extends Component {
render() {
return (
《section》
《p》{title}《/p》
《WrappedComponent /》
《/section》
)
}
}
}在Example中这样export
export default BaseActivity(’这是高阶组件的参数’)(Example);这种用法在ant-design的表单以及redux的connect中我们都可以看到
// ant
const WrappedDemo = Form.create()(Demo)
// redux
export default connect(mapStateToProps, mapDispatchToProps)(Counter)高阶组件还可以扩展原组件的props属性,如下所示:
const BaseActivity (title) =》 (WrappedComponent) =》 {
return class extends Component {
render() {
const newProps = {
id: Math.random().toString(8)
}
return (
《section》
《p》{title}《/p》
《WrappedComponent {...this.props} {...newProps}/》
《/section》
)
}
}
}看下输出的react dom
高阶组件的缺点
高阶组件也有一系列的缺点,首先是被包裹组件的静态方法会消失,这其实也是很好理解的,我们将组件当做参数传入函数中,返回的已经不是原来的组件,而是一个新的组件,原来的静态方法自然就不存在了。如果需要保留,我们可以手动将原组件的方法拷贝给新的组件,或者使用hoist-non-react-statics之类的库来进行拷贝。
结语
高阶函数对于初学者来说可能不太好理解,但当你深入其中,了解其中的原理之后,我们可以使用高阶函数来完成很多的工作。
相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!
推荐阅读:
通过jQuery编码转换、事件响应、页面跳转实现浏览器跳转并传递参数
js获取ModelAndView后如何使用
如何使用react创建单例组件
这次给大家带来如何使用react创建单例组件,使用react创建单例组件的注意事项有哪些,下面就是实战案例,一起来看一下。
需求背景
最近有个需求,需要在项目中添加一个消息通知弹窗,告知用户一些信息。
用户看过消息后,就不再弹窗了。
问题
很明显,这个需要后端的介入,提供相应的接口(这样可扩展性更好)。
在开发过程中,遇到个问题:由于我们的系统是多页面的,所以每次切换页面,都会去请求后端的消息接口。有一定的性能损耗。
因为是多页面系统,使用单例组件貌似也没啥意义(不过是个机会学习学习单例组件是怎么写的)。
于是,想到使用浏览器缓存来记录是否弹过窗了(当然,得设定过期时间)。
如何写单例组件
1、工具函数:
import ReactDOM from ’react-dom’;
/**
* ReactDOM 不推荐直接向 document.body mount 元素
* 当 node 不存在时,创建一个 p
*/
function domRender(reactElem, node) {
let p;
if (node) {
p = typeof node === ’string’
? window.document.getElementById(node)
: node;
} else {
p = window.document.createElement(’p’);
window.document.body.appendChild(p);
}
return ReactDOM.render(reactElem, p);
}2、组件:
export class SingletonLoading extends Component {
globalLoadingCount = 0;
pageLoadingCount = 0;
state = {
show: false,
className: ’’,
isGlobal: undefined
}
delayTimer = null;
start = (options = {}) =》 {
// ...
}
stop = (options = {}) =》 {
// ...
}
stopAll() {
if (!this.state.show) return;
this.globalLoadingCount = 0;
this.pageLoadingCount = 0;
this.setState({show: false});
}
get isGlobalLoading() {
return this.state.isGlobal && this.state.show;
}
get noWaiting() {
return this.noGlobalWaiting && this.pageLoadingCount 《 1;
}
get toPageLoading() {
return this.noGlobalWaiting && this.isGlobalLoading;
}
get noGlobalWaiting() {
return this.globalLoadingCount 《 1;
}
render() {
return 《BreakLoading {...this.state} /》;
}
}
// 使用上面的工具函数
export const loading = domRender(《SingletonLoading /》);3、使用组件:
import loading from ’xxx’;
// ...
loading.start();
loading.stop();相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!
推荐阅读:
如何使用vue中filter
怎样使用vue判断dom的class
如何将react组件内数据进行共享
这次给大家带来如何将react组件内数据进行共享,将react组件内数据进行共享的注意事项有哪些,下面就是实战案例,一起来看一下。
利用react-redux实现react组件数据之间数据共享
1.安装react-redux
$ npm i --save react-redux2.从react-redux导入Proper组件将store赋予Provider的store属性,
将根组件用Provider包裹起来。
import {Provider,connect} from ’react-redux’
ReactDOM.render(
《Provider store={store}》
《Wrap/》
《/Provider》,document.getElementById(’example’))这样根组件中所有的子组件都可以获得store中的值
3.connect二次封装根组件
export default connect(mapStateToProps,mapDispatchToProps)(Wrap)connect接收两个函数作为参数,一个mapStateToProps定义哪些store属性会被映射到根组件上的属性(把store传入react组件),一个mapDispatchToProps定义哪些行为action可以作为根组件属性(把数据从react组件传入store)
3.定义这两个映射函数
function mapStateToProps(state){
return {
name:state.name,
pass:state.pass
}
}
function mapDispatchToProps(dispatch){
return {actions:bindActionCreators(actions,dispatch)
}
}把store中的name,pass映射到根组件的name,pass属性。
actions是一个包含了action构建函数的对象,用bindActionCreators把对象actions绑定到根组件actions属性上。
4.在根组件引用子组件的位置用 《Show name={this.props.name} pass={this.props.pass}》《/Show》将store数据传入子组件.
5.在子组件中调用actions中的方法来更新store中的数据
《Input actions={this.props.actions} 》《/Input》先将actions作为属性传入子组件
子组件调用actions中的方法创建action
//Input组件
export default class Input extends React.Component{
sure(){
this.props.actions.add({name:this.refs.name.value,pass:this.refs.pass.value})
}
render(){
return (
《p》
姓名:《input ref="name" type="text"/》
密码:《input ref="pass" type="text"/》
《button onClick={this.sure.bind(this)}》登录《/button》
《/p》
)
}
}因为我们采用了bindActionCreators函数,创建action后会立即自动调用store.dispatch(action)将数据更新到store.
这样我们就利用react-redux模块完成了react各个组件之间数据共享。
相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!
推荐阅读:
使用JS操作图片只留黑白色
实战项目中如何使用vue组件
react组件的性能优化有哪些方面
这次给大家带来react组件的性能优化有哪些方面,react组件性能优化的注意事项有哪些,下面就是实战案例,一起来看一下。
高德纳: "我们应该忘记忽略很小的性能优化,可以说97%的情况下,过早的优化是万恶之源,而我们应该关心对性能影响最关键的另外3%的代码。"
不要将性能优化的精力浪费在对整体性能提高不大的代码上,而对性能有关键影响的部分,优化并不嫌早。因为,对性能影响最关键的部分,往往涉及解决方案核心,决定整体的架构,将来要改变的时候牵扯更大。
1. 单个React组件的性能优化
React利用Virtual DOM来提升渲染性能,虽然每一次页面更新都是最组件的从新渲染,但是并不是将之前的渲染内容全部抛弃重来,借助Virtual DOM,React能够计算出对DOM树的最少修改,这就是React默认情况下渲染都很迅速的秘诀;
不过,虽然Virtual DOM能够将每次DOM操作量减少到最小,但,计算和比较Virtual DOM依然是一个复杂的过程;
当然,如果能够在开始计算Virtual DOM之前就判断渲染的结果不会有变化,那么就可以不进行Virtual DOM计算和比较,速度就会更快。
2.shouldComponentUpdate的默认实现方式
既然可以对组件在开始计算Virtual DOM之前判断渲染结果不会有变化时,阻止渲染的进行,从而提升性能,那么我们自然想到使用shouldComponentUpdate(nextProp,nextState)
shouldComponentUpdate函数在render函数之前调用,决定“什么时候不需要从新渲染”;
即返回一个布尔值,决定更新是否进行下去,默认返回true,若返回false则中断更新;
shouldComponentUpdate(nextProp,nextState){
return (nextProp.completed !== this.props.completed) ||
(nextProp.text !== this.props.text)
}
其中nextProps为此次更新传入的props,对于这个组件,影响渲染内容的prop只有completed和text,只要确保这两个prop没有变化,shouldComponentUpdate就可以返回false阻止没必要的更新
但是,上述的比较只是‘浅层比较’,如果类型是基本类型,只要值相同,那么“浅层比较”
也会认为二者相同:
那,如果prop的类型是复杂的对象怎么办?
对于复杂对象,‘浅层比较’的方式只看这两个prop是不是同一个对象的引用,如果不是,哪怕对象中的内容完全一样也会认为是不同的两个prop。那么使用“深层比较”:但对对象的结构是无法预知的,如果递归对每个字段都进行“深层比较”,不光会让代码更加复杂,也可能会造成性能问题。
所以,要想判断前后的对象类型的prop是相同的,就必须要保证prop是指向同一个JavaScript对象:
《Foo styleProp = {{color: "red"}}》
要避免使用上面的传入方式,应为每次渲染都会重新创建{color: "red"}对象,引用地址每次都不同,将导致每次的styleProp都不同。
const footStyle = {color: "red"};//确保这个初始化只执行一次,不要放在render函数中
《Foo styleProp = {footStyle}》
使用‘单例模式’确保传入的styleProp指向同一个对象
如果是函数呢?
《Foo onToggle={() =》 onToggleTodo(item.id)}/》
应该避免使用上面的函数传递模式,因为这里赋值的是一个匿名函数,而且是在赋值的时候产生的,也就是说每次渲染都会产生一个新的函数,这就是问题所在。
如果要传递的prop很多呢?
恩~~用React-Redux的话,有对shouldComponentUpdate的默认实现。
3. 对多个React组件的性能优化
当一个React组件被装载、更新和卸载时,组件的一序列生命周期函数会被调用。不过,这些生命周期函数是针对一个特定的React组件函数,在一个应用中,从上而下有很多React组件组合起来,它们之间的渲染过程要更加复杂。
同样一个组件的渲染过程也要考虑三个过程:装载阶段、更新阶段、卸载阶段
对于装载阶段,组件无论如何都要彻底渲染一次,从这个React组件往下的所有子组件,都要经历一遍React组件的装载生命
周期,所以并没有多少优化的事情可做。
对于卸载阶段,只有一个生命周期函数componentWillUnmount,这个函数只是清理componentDidMount添加的事件处理监听等收尾工作,所以,也没有什么可优化的空间;
4. React更新阶段的调和(Reconciliation)过程
在组件更新过程,会构建更新Virtual DOM,并将其与之前的Virtual DOM进行比较,从而找出不同之处,使用最少的DOM操作进行更新
调和过程:即React更新中对Virtual DOM找不同的过程,通常对比两个N个节点的树形结构的算法,时间复杂度是O(n*3),如果直接
使用默认对比,节点过多的话,需要操作的数量太多,而React不可能采用这种算法;
React实际采用的算法时间复杂度是O(N)(时间复杂度只是对一个算法最好和最差情况下需要的指令操作数量级的估量)
React的Reconciliation算法并不复杂,首先检查两个树形的根节点的类型是否相同,根据相同或者不同有不同的处理方式:
节点类型不同的情况
如果树形节点的类型不相同,那就意味着改动很大,直接认为原来的那个树形结构已经没用,可以扔掉,需要从新构建DOM树,原有的树形上的React组件便会经历“卸载”的生命周期;
也就是说,对于Virtual DOM树这是一个“更新”过程,但是却可能引发这个树结构上某些组件的“装载”和“卸载”过程
如:
更新前
《p》
《Todos /》
《/p》
我们想要更新成这样:
《span》
《Todos /》
《/span》
》1. 那么在作比较的时候,一看根节点原来是p,新的是span,类型就不一样了,那么这个算法就废弃之前的p包括里面的所有子节点,从新构建一个span节点和子节点;
》2. 很明显因为根节点不同就将所有的子节点从新构建,这很浪费,但是为了避免O(N*3)的时间复杂度,React这能选择这种比较简单、快捷的方法;
》3. 所以,作为开发者,我们一定要避免上面的浪费的情景出现
节点类型相同的情况
如果两个节点类型相同时,对于DOM元素,React会保留节点对应的DOM元素,只对其节点的属性和内容做对比,然后只修改更新的部分;
节点类型相同时,对于React组件类型,React做得是根据新节点的props去更新节点的组件实例,引发组件的更新过程;
在处理完根节点对比后,React的算法会对根节点的每一个子节点重复一样的操作
多个相同子组件的情况
如果最初组件状态为:
《ul》
《TodoItem text = "First" /》
《TodoItem text = "Second" /》
《/ul》
更新后为:
《ul》
《TodoItem text = "First" /》
《TodoItem text = "Second" /》
《TodoItem text = "Third" /》
《/ul》
那么React会创建一个新的TodoItem组件实例,而前两个则进行正常的更新过程但是,如果更新后为:
《ul》
《TodoItem text = "Zero" /》
《TodoItem text = "First" /》
《TodoItem text = "Second" /》
《/ul》
(这将暴露一个问题)理想处理方式是,创建一个新的TodoItem组件实例放在第一位,后两个进入自然更新过程
但是要让react按照这种方式,就必须找两个子组件的不同之处,而现有计算两个序列差异的算法时间是O(N*2),显然则
不适合对性能要求很高的场景,所以React选择了一个看起来很傻的办法,即挨个比较每个子组件;
React首先认为把text为First的组件的text改为Zero,Second的改为First,最后创建一个text为Second的组件,这样便会破原有的两个组件完成一个更新过程,并创建一个text为Second的新组件
这显然是一个浪费,React也意到,并提供了方克服,不过需要开发人员提供一点帮助,这就是key
Key的使用
key属性可以明确的告诉React每个组件的唯一标识
如果最初组件状态为:
《ul》
《TodoItem key={1} text = "First" /》
《TodoItem key={2} text = "Second" /》
《/ul》
更新后为:
《ul》
《TodoItem key={0} text = "Zero" /》
《TodoItem key={1} text = "First" /》
《TodoItem key={2} text = "Second" /》
《/ul》
因为有唯一标识key,React可以根据key值,知道现在的第二和第三个组件就是之前的第一和第二个,便用原来的props启动更新过程,这样shouldComponentUpdate就会发生作用,避免无谓的更新;
注意:因为作为组件的唯一标识,所以key必须唯一,且不可变
下面的代码是错误的例子:
《ul》
todos.map((item,index) =》 {
《TodoItem
key={index}
text={item.text}
/》
})
《/ul》
使用数组下标作为key值,看起来唯一,但不稳定,因为随着todos数组值的不同,同样一个组件实例在不同的更新过程中数组的下标完全可能不同,把下标当做可以就会让React乱套,记住key不仅要唯一还要确保稳定不可变
需要注意:虽然key是一个prop,但是接受key的组件不能读取key的值,因为key和ref是React保留的两个特殊prop,并没有预期让组将直接访问。
相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!
推荐阅读:
Vue.js的ul-li标签如何仿制select标签
在element-ui中table表格怎样滚动加载
VUE中更改计算属性后select选中值不变的处理方法
react-router4.0下服务器如何配合BrowserRouter实例教程
react-router作为react框架路由解决方案在react项目中举足轻重。本文主要介绍了详解react-router 4.0 下服务器如何配合BrowserRouter,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望能帮助到大家。
在react-router 4.0版本中,API与先前版本相比有了很大的修改,在2.0、3.0中常用的《Router》组件作为路由底层配置组件不再常用,取而代之的是四个各有不同的路由组件:
《BrowserRouter》, 《HashRouter》, 《MemoryRouter》, 《StaticRouter》
其中《MemoryRouter》组件在内存中保存“URL”信息,不会修改浏览器的地址栏,往往用于React Native或测试环境等非浏览器环境。
而《StaticRouter》组件从名字能看出它从不修改路由,这在服务器端渲染时很有用。
《HashRouter》组件我们最为熟悉的路由组件不用再多赘述,这里来说说我在使用react-router推荐的《BrowserRouter》时遇到的坑。
《BrowserRouter》
《BrowserRouter》和《HashRouter》都可以实现前端路由的功能,区别是前者基于rul的pathname段,后者基于hash段。
***隐藏网址***
***隐藏网址***
这样的区别带来的直接问题就是当处于二级或多级路由状态时,刷新页面,《BrowserRouter》会将当前路由发送到服务器(因为是pathname),而《HashRouter》不会(因为是hash段)。
我们当然不希望前端路由被发送到后台。
在react-router 4.0 的文档中有这样一段话:
注意: 使用 hash 的方式记录导航历史不支持 location.key 和 location.state。 在以前的版本中,我们为这种行为提供了 shim,但是仍有一些问题我们无法解决。 任何依赖此行为的代码或插件都将无法正常使用。 由于该技术仅用于支持传统的浏览器,因此在用于浏览器时可以使用 《BrowserHistory》 代替。
这就要求服务器要配合前端做一些简单的修改。
修改的思想就是当收到请求的url不是功能性的,而是前端路由时,重新加载入口html文件(我的后台是nodejs)。
此处踩坑无数,在网上搜索方法后换用nginx,使用try_files字段定向到入口html,但是重定向后,webpack打包的js文件没有执行。
在查看firebug时发现此次刷新的响应头中设置了"Connection":"keep-alive";
觉得问题应该出在这里,换用nodejs用200状态配合keep-alive果然解决了问题。
在react-router 4.0 多级路由下刷新页面不会再404,而是保存了前端状态。
怎样使用reactrouter4+redux实现路由权限控制
这次给大家带来怎样使用react router4+redux实现路由权限控制,使用react router4+redux实现路由权限控制的注意事项有哪些,下面就是实战案例,一起来看一下。
总体概述
一个完善的路由系统应该是这样子的,当链接到的组件是需要登录后才能查看,要能够跳转到登录页,然后登录成功后又跳回来之前想访问的页面。这里主要是用一个权限控制类来定义路由路由信息,同时用redux把登录成功后要访问的路由地址给保存起来,登录成功时看redux里面有没有存地址,如果没有存地址就跳转到默认路由地址。
路由权限控制类
在这个方法里面,通过sessionStorage判断是否登录了,如果没有登录,就保存一下当前想要跳转的路由到redux里面。然后跳转到我们登录页。
import React from ’react’
import { Route, Redirect } from ’react-router-dom’
import { setLoginRedirectUrl } from ’../actions/loginAction’
class AuthorizedRoute extends React.Component {
render() {
const { component: Component, ...rest } = this.props
const isLogged = sessionStorage.getItem("userName") != null ? true : false;
if(!isLogged) {
setLoginRedirectUrl(this.props.location.pathname);
}
return (
《Route {...rest} render={props =》 {
return isLogged
? 《Component {...props} /》
: 《Redirect to="/login" /》
}} /》
)
}
}
export default AuthorizedRoute路由定义信息
路由信息也很简单。只是对需要登录后才能查看的路由用AuthorizedRoute定义。
import React from ’react’
import { BrowserRouter, Switch, Route, Redirect } from ’react-router-dom’
import Layout from ’../pages/layout/Layout’
import Login from ’../pages/login/Login’
import AuthorizedRoute from ’./AuthorizedRoute’
import NoFound from ’../pages/noFound/NoFound’
import Home from ’../pages/home/Home’
import Order from ’../pages/Order/Order’
import WorkOrder from ’../pages/Order/WorkOrder’
export const Router = () =》 (
《BrowserRouter》
《p》
《Switch》
《Route path="/login" component={Login} /》
《Redirect from="/" exact to="/login"/》{/*注意redirect转向的地址要先定义好路由*/}
《AuthorizedRoute path="/layout" component={Layout} /》
《Route component={NoFound}/》
《/Switch》
《/p》
《/BrowserRouter》
)登录页
就是把存在redux里面的地址给取出来,登录成功后就跳转过去,如果没有就跳转到默认页面,我这里是默认跳到主页。因为用了antd的表单,代码有点长,只需要看连接redux那两句和handleSubmit里面的内容。
import React from ’react’
import ’./Login.css’
import { login } from ’../../mock/mock’
import { Form, Icon, Input, Button, Checkbox } from ’antd’;
import { withRouter } from ’react-router-dom’;
import { connect } from ’react-redux’
const FormItem = Form.Item;
class NormalLoginForm extends React.Component {
constructor(props) {
super(props);
this.isLogging = false;
}
handleSubmit = (e) =》 {
e.preventDefault();
this.props.form.validateFields((err, values) =》 {
if (!err) {
this.isLogging = true;
login(values).then(() =》 {
this.isLogging = false;
let toPath = this.props.toPath === ’’ ? ’/layout/home’ : this.props.toPath
this.props.history.push(toPath);
})
}
});
}
render() {
const { getFieldDecorator } = this.props.form;
return (
《Form onSubmit={this.handleSubmit.bind(this)} className="login-form"》
《FormItem》
{getFieldDecorator(’userName’, {
rules: ,
})(
《Input prefix={《Icon type="user" style={{ color: ’rgba(0,0,0,.25)’ }} /》} placeholder="Username" /》
)}
《/FormItem》
《FormItem》
{getFieldDecorator(’password’, {
rules: ,
})(
《Input prefix={《Icon type="lock" style={{ color: ’rgba(0,0,0,.25)’ }} /》} type="password" placeholder="Password" /》
)}
《/FormItem》
《FormItem》
{getFieldDecorator(’remember’, {
valuePropName: ’checked’,
initialValue: true,
})(
《Checkbox》Remember me《/Checkbox》
)}
《a className="login-form-forgot" href=""》Forgot password《/a》
《Button type="primary" htmlType="submit" className="login-form-button"
loading={this.isLogging ? true : false}》
{this.isLogging ? ’Loging’ : ’Login’}
《/Button》
Or 《a href=""》register now!《/a》
《/FormItem》
《/Form》
);
}
}
const WrappedNormalLoginForm = Form.create()(NormalLoginForm);
const loginState = ({ loginState }) =》 ({
toPath: loginState.toPath
})
export default withRouter(connect(
loginState
)(WrappedNormalLoginForm))顺便说一下这里redux的使用吧。我暂时只会基本使用方法:定义reducer,定义actions,创建store,然后在需要使用redux的变量时候去connect一下redux,需要dispatch改变变量时,就直接把actions里面的方法引入,直接调用就可以啦。为了让actions和reducer里面的事件名称对的上,怕打错字和便于后面修改吧,我建了个actionsEvent.js来存放事件名称。
reducer:
import * as ActionEvent from ’../constants/actionsEvent’
const initialState = {
toPath: ’’
}
const loginRedirectPath = (state = initialState, action) =》 {
if(action.type === ActionEvent.Login_Redirect_Event) {
return Object.assign({}, state, {
toPath: action.toPath
})
}
return state;
}
export default loginRedirectPathactions:
import store from ’../store’
import * as ActionEvent from ’../constants/actionsEvent’
export const setLoginRedirectUrl = (toPath) =》 {
return store.dispatch({
type: ActionEvent.Login_Redirect_Event,
toPath: toPath
})
}创建store
import { createStore, combineReducers } from ’redux’
import loginReducer from ’./reducer/loginReducer’
const reducers = combineReducers({
loginState: loginReducer //这里的属性名loginState对应于connect取出来的属性名
})
const store = createStore(reducers)
export default store相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!
推荐阅读:
使用JS判断字符串中包含内容方法总结
Angular+RouterLink做出不同的花式跳转

更多文章:
kali上传文件到apache服务器(如何在macOS系统和KaliLinux系统之间共享文件)
2025年10月25日 23:30
c语言一维数组有几个数(c语言的一维数组中至少要有几个元素)
2025年11月27日 13:30
织梦岛隐藏要素(塞尔达传说织梦岛脸孔神殿迷宫怎么通关 脸孔神殿迷宫)
2025年9月18日 18:45
最新版android studio安装(如何安装android studio)
2025年9月24日 10:15
97的十六进制(97 98 换成八进制 如何换 如何将十进制转换成八进制或者十六进制 哪位大仙帮帮忙)
2026年4月5日 15:00
redis命令登录(软件Xshell连接Redis操作教程)
2026年6月6日 17:30
category nouns(the different lexical category in translation)
2026年1月9日 10:45
哈夫曼树编码不唯一那解码结果(如何解决哈夫曼树不唯一的问题)
2025年6月24日 02:45
关系数据库管理系统有哪些?(下面哪些属于关系型数据库管理系统())
2026年5月2日 12:15
insertrow(在Power Query中如何插入自定义行)
2025年10月10日 12:30
















