React渲染问题研究以及Immutable的应用

2023-12-18 20:18

本文主要是介绍React渲染问题研究以及Immutable的应用,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在前面

这里主要介绍自己在React开发中的一些总结,关于react的渲染问题的一点研究。

另外本人一直希望在React项目中尝试使用,因此在之前已经介绍过immutable的API,可以参看这里Immutable日常操作之深入API,算是对其的一个补充。

本文所有代码请参看github仓库:https://github.com/Rynxiao/immutable-react

渲染房间列表

这个例子主要是写了同时渲染1000个房间,如果我添加一个房间或者修改一个房间,在react中不同的实现方式下render函数将会表现出什么样的结果?以及针对不同结果的一些思考和优化。大致的列表例子如下:生成1000个不同的房间盒子,颜色随机。

rooms

项目整体目录结构大致是这样的:

fileTree

下面主要来看ListDetail.js中是如何写的:

  • 父组件List
  • 子组件RoomDetail,子组件的功能只是纯粹的渲染功能,自身并没有任何操作

子组件:

// 子组件
class RoomDetail extends React.Component {constructor(props) {super(props);}render() {let room = this.props.room;return (<li className="list-item" style={{ backgroundColor: room.backgroundColor }}>{ room.number }</li>);}}

父组件:

// 父组件
export default class List extends React.Component {// ...constructor(props) {super(props);this.addRoom = this.addRoom.bind(this);this.modifyRoom = this.modifyRoom.bind(this);this.state = {roomList: this.generateRooms(),newRoom: 0};}// ...render() {return (<div><h2 className="title">React的列表渲染问题</h2><div><a className="back" href="#/">返回首页</a></div><div className="btn-operator"><button onClick={ this.addRoom }>Add A Room</button><button onClick={ this.modifyRoom }>Modify A Room</button></div><ul className="list-wrapper">{this.state.roomList.map((room, index) => {return <RoomDetail key={ `roomDetail${index}` } room={ room } />})}</ul></div>);}
}

下面我们来添加一个房间试试

// 添加房间
addRoom() {let newRoom = { number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' };let newList = this.state.roomList;newList.push(newRoom);this.setState({ roomList: newList });
}

这个操作主要是生成一个新的房间,然后从state中取出当前的房间列表,然后再当前的房间列表中添加一个新的房间,最后将整个列表从新设置到状态中。

很显然,此时由于父组件的状态发生了变化,会引起自身的render函数执行,同时列表开始重新遍历,然后将每一个房间信息重新传入到子组件中。是的,重新传入,就代表了子组件将会重新渲染。我们可以来做一个测试,在子组件的render方法中加入如下打印:

render() {let room = this.props.room;console.log(`.No${room.number}`);return (// ...);
}

不出意外的发现了所有的子组件渲染的证据:

childrenAllRender

同时利用chormePerformance检测的信息如下:

chromeTotalBefore

调用的方法堆栈如下:

chromeFunctionBefore

渲染子组件的时间达到764ms,同时在堆栈中可以看到大量的receiveComponentupdateChildren方法的执行。那么有没有什么办法只渲染改变的部分呢?在react官网性能监控这一小节中有提到一个方法,将子组件继承React.PureComponent可以局部有效防止渲染。加上之后的代码是这样的:

class RoomDetail extends React.PureComponent {// ...
}

所有的东西都没有变化,只是将Component换成了PureComponent。下面我们再来测试一下:

childrenOneRender

性能检测图如下:

chromeTotalAfter

效果出奇的好,果然只是渲染了一次,并且速度提升了10几倍之多。

其中的原理是在组件的shouldComponentUpdate方法中进行了propsstate的比较,如果认为他们相等,则会返回false,否则则会返回true

// react/lib/ReactComponentWithPureRenderMixin.js
var ReactComponentWithPureRenderMixin = {shouldComponentUpdate: function (nextProps, nextState) {return shallowCompare(this, nextProps, nextState);}
};

同时官网也说了,这只是局部有效,为什么呢?因为这些值的比较都只是浅比较,也就是只是第一层的比较。那么会出现什么问题,我们来看下面的操作:

修改其中的一个房间:

// 修改房间
modifyRoom() {let newList2 = this.state.roomList;newList2[0] = { number: 'HAHA111', backgroundColor: '#0f0' };this.setState({ roomList: newList2 });
}

很意外,当我添加了一个房间之后,发现第一个房间并没有我们想象中的发生变化。为什么?

原因是我虽然修改了第一个房间的数据,当时我并没有修改他的引用地址。类似下面这样的:

var arr = [{ a: 1 }, { b: 2 }];
var arr2 = arr1;
arr2[0] = { c: 1 };
arr === arr2;   // true

因此在子组件中比较房间的时候,就会出现比较的值相等的情况,此时将会返回false

那么有没有办法改变这个问题,我找到了两个办法:

  • 从数据源头入手
  • 从子组件是否渲染条件入手

从数据源头入手,即为改造数据,将数据进行深拷贝,使得原先的引用与新得到的对象的引用不相同即可。关于深拷贝的实现方法有很多,我这里贴一个,之后再仔细做研究。

// 这个函数可以深拷贝 对象和数组
var deepCopy = function(obj){var str, newobj = obj.constructor === Array ? [] : {};if(typeof obj !== 'object'){return;} else if(window.JSON){str = JSON.stringify(obj), //系列化对象newobj = JSON.parse(str); //还原} else {for(var i in obj){newobj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i]; }}return newobj;
};

在ES6中提供了一种解构方式,这种方式也可以实现数组的深层次拷贝。类似这样的

let arr = [1, 2, 3, 4];
let arr1 = [...arr];
arr1 === arr;   // false// caution
let arr = [{ a: 1 }, { b: 2 }];
let arr1 = [...arr];
arr1 === arr;       // false
arr1[0] = { c: 3 };
arr1[0] === arr[0];   // false
arr1[1] === arr[1];   // true

因此我把modifyRoom函数进行了如此改造:

// 修改房间
modifyRoom() {let newList2 = [...this.state.roomList];newList2[0] = { number: 'HAHA111', backgroundColor: '#0f0' };this.setState({ roomList: newList2 });
}

因此在比较第一个对象的时候,发现它们已经不相等了,则会重新渲染。

从子组件是否渲染条件入手,可以不需要使用React.PureComponent,而直接在shouldComponentUpdate方法入手。因为两次值改变之后,我清楚得可以知道,改变的值只是第一个对象中的数值改变。那么我可以这么写来判断:

class RoomDetail extends React.Component {constructor(props) {super(props);}shouldComponentUpdate(nextProps, nextState) {if (nextProps.room.number === this.props.room.number) {return false;} return true;}render() {let room = this.props.room;return (<li className="list-item" style={{ backgroundColor: room.backgroundColor }}>{ room.number }</li>);}}

同样得可以达到效果。但是如果在shouldComponentUpdate中存在着多个propsstate中值改变的话,就会使得比较变得十分复杂。

应用Immutable.js来检测React中值的变化问题

在官网上来说,immutable提供的数据具有不变性,被称作为Persistent data structure,又或者是functional data structure,非常适用于函数式编程,相同的输入总会预期到相同的输出。

2.1 immutable的性能

immutable官网以及在知乎中谈到为什么要使用immutable的时候,会看到一个关键词efficient。高效地,在知乎上看到说是性能十分好。在对象深复制、深比较上对比与Javascript的普通的深复制与比较上来说更加地节省空间、提升效率。我在这里做出一个实验(这里我并不保证实验的准确性,只是为了验证一下这个说法而已)。

实验方法:我这里会生成一个对象,对象有一个广度与深度,广度代表第一层对象中有多少个键值,深度代表每一个键值对应的值会有多少层。类似这样的:

{"width0": {"key3": {"key2": {"key1": {"key0":"val0"}}}},"width1": {"key3": {"key2": {"key1": {"key0":"val0"}}}},"width2": {"key3": {"key2": {"key1": {"key0":"val0"}}}},// ..."widthN": {"key3": {"key2": {"key1": {"key0":"val0"}}}}
}

因此实际上在javascript对象的复制和比较上,需要遍历的次数其实是width * deep

在复制的问题上,我做了三种比较。

  • deepCopy(obj)
  • JSON.parse(JSON.stringify(obj))
  • Immutable

最终得到的数据为:

 deepCopy( μs )JSON( μs )Immutable( μs )
20 * 504000900020
20 * 50080001000020
20 * 5000100001400020

在比较上,我只比较了两种方式:

  • javascript deep compare
  • Immutable.is

代码如下:

let startTime1 = new Date().getTime();
let result1 = Equals.equalsObject(gObj, deepCopyObj);
let endTime1 = new Date().getTime();
console.log(result1);
console.log(`deep equal time ${(endTime1-startTime1)*1000}μs`);let startTime2 = new Date().getTime();
let result2 = is(this.state.immutableObj, this.state.aIObj);
let endTime2 = new Date().getTime();
console.log(result2);
console.log(`immutable equal time ${(endTime2-startTime2)*1000}μs`);

最终得到的数据为:

 deepCompare( μs )Immutable.is( μs )
20 * 507000
20 * 50100027000
20 * 500600024000
20 * 5000840005000

数据的设计上可能太过单一,没有涉及到复杂的数据,比如说对象中再次嵌套数组,并且在每一个键值对应的值得广度上设计得也太过单一,只是一条直线下来。但是当数据量达到一定的程度时,其实也说明了一些问题。

总结:

  1. 对象复制上来说,基本上Immutable可以说是零消耗
  2. 对象比较上,当对象深层嵌套到一定规模,反而Immutable.is()所用的时间会更少
  3. 但是在数据方面来说,Immutable并快不了多少

当然只是测试,平时中的纵向嵌套达到三层以上都会认为是比较恐怖的了。

于是我去google翻了翻,看看有没有什么更好的demo,下面我摘录一些话。

What is the benefit of immutable.js?

Immutable.js makes sure that the "state" is not mutated outside of say redux. For smaller projects, personally i don't think it is worth it but for bigger projects with more developers, using the same set of API to create new state in reduce is quite a good idea

It was mentioned many times before that Immutable.js has some internal optimizations, such as storing lists as more complex tree structures which give better performance when searching for elements. It's also often pointed out that using Immutable.js enforces immutability, whereas using Object.assign or object spread and destructuring assignments relies to developers to take care of immutability. EDIT: I haven't yet seen a good benchmark of Immutable.js vs no-library immutability. If someone knows of one please share. Sharing is caring :)

Immutable.js adds two things: Code enforcement: by disallowing mutations, you avoid strange errors in redux and react. Code is substantially easier to reason about. Performance: Mutation operations for larger objects are substantially faster as the internals are a tree structure that does not have to copy the entirety of an object every assignment. In conclusion: it's a no brainer for decently scoped applications; but for playing around it's not necessary.

https://github.com/reactjs/redux/issues/1262

yes, obviously mutable is the fastest but it won't work with how redux expects the data, which is immutable

Performance Tweaking in React.js using Immutable.js

But wait… This is can get really ugly really fast. I can think of two general cases where your shouldComponentUpdate can get out of hand.

// Too many props and state to check!shouldComponentUpdate(nextProps, nextState) {return (this.props.message !== nextProps.message ||this.props.firstName !== nextProps.firstName ||this.props.lastName !== nextProps.lastName ||this.props.avatar !== nextProps.avatar ||this.props.address !== nextProps.address ||this.state.componentReady !== nextState.componentReady// etc...);}

是的,我并没有得出Immutable在性能上一定会很快的真实数据。但是不得不提到的是他在配合Redux使用的时候的一个天然优势——数据是不变的。并且在最后一个链接中也提到,在配合React使用中通过控制shouldComponentUpdate来达到优化项目的目的。

however,Let's write some examples about immutable used in react to make sense.

2.2 房间列表加入Immutable

在父组件中的改变:

constructor(props) {super(props);this.addRoom = this.addRoom.bind(this);this.modifyRoom = this.modifyRoom.bind(this);this.state = {// roomList: this.generateRooms()roomList: fromJS(this.generateRooms()),newRoom: 0};
}addRoom() {// let newRoom = { number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' };// let newList = this.state.roomList;// newList.push(newRoom);let newRoom = Map({ number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' });let newList = this.state.roomList.push(newRoom);this.setState({ roomList: newList });
}modifyRoom() {// let newList = [...this.state.roomList];// newList[0] = { number: 'HAHA111', backgroundColor: '#0f0' };let list = this.state.roomList;let newList = list.update(0, () => {return Map({ number: 'HAHA111', backgroundColor: '#0f0' });});this.setState({ roomList: newList });
}

子组件中:

shouldComponentUpdate(nextProps, nextState) {return !is(formJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState));
}

将数据源用Immutable初始化之后,之后再进行的数据改变都只要遵守ImmutableJS的相关API即可,就可以保证数据的纯净性,每次返回的都是新的数据。与源数据的比较上就不可能会存在改变源数据相关部分之后,由于引用相等而导致数据不相等的问题。

三、在Redux中运用immutable

我在项目底下新建了一个项目目录redux-src,同时在项目中增加了热更新。新建了webpack.config.redux.js,专门用来处理新加的redux模块。具体代码可以上github上面去看。因此新的目录结构如下:

redux-tree

webpack.config.redux.js文件如下:


'use strict';
var webpack = require("webpack");
var ExtractTextPlugin = require("extract-text-webpack-plugin");  //css单独打包module.exports = {devtool: 'eval-source-map',entry: [__dirname + '/redux-src/entry.js', //唯一入口文件"webpack-dev-server/client?http://localhost:8888","webpack/hot/dev-server"],output: {path: __dirname + '/build', //打包后的文件存放的地方filename: 'bundle.js',      //打包后输出文件的文件名publicPath: '/build/'},module: {loaders: [{ test: /\.js$/, loader: "react-hot!jsx!babel", include: /src/},{ test: /\.css$/, loader: ExtractTextPlugin.extract("style", "css!postcss")},{ test: /\.scss$/, loader: ExtractTextPlugin.extract("style", "css!postcss!sass")},{ test: /\.(png|jpg)$/, loader: 'url?limit=8192'}]},postcss: [require('autoprefixer')    //调用autoprefixer插件,css3自动补全],plugins: [new ExtractTextPlugin('main.css'),new webpack.HotModuleReplacementPlugin()]
}

在项目中运行npm run redux,在浏览器输入localhost:8888即可看到最新的模块。

这里关于如何在react中使用redux,这里就不多说了,如果不明白,可以去看 http://cn.redux.js.org/ 或者到我之前写的 redux的一个小demo中去看。

重点说说如何在reducer中使用Immutable,以及在List.js中如何通过发送Action来改变store

redux-src/redux/reducers/index.js
import { fromJS } from 'immutable';
import { combineReducers } from 'redux';import { ADD_ROOM, MODIFY_ROOM, MODIFY_NEWROOM_NUM } from '../const';
import { addRoom, modifyRoom, modifyNewRoomNum } from '../actions';// ... generateRooms()const initialState = fromJS({roomList: generateRooms(),newRoom: 0
});function rooms(state = initialState, action) {switch(action.type) {case ADD_ROOM: return state.updateIn(['roomList'], list => list.push(action.room));case MODIFY_ROOM:return state.updateIn(['roomList', 0], room => action.room);case MODIFY_NEWROOM_NUM:return state.updateIn(['newRoom'], num => ++num);default:return state;}
}export default combineReducers({rooms
});

跟之前List.js中的state中声明的最开始状态一样。这里依旧维持一个最开始的房间列表以及一个新增房间的序号数。只不过这里的最初状态是通过Immutable.js处理过的,所以在reducer中的所有操作都必须按照其API来。

redux-src/components/List.js

其实这个文件也没有作多处修改,基本可以看引入了immutablestate管理的Detail.js。只是在操作上显得更加简单了。

addRoom() {let { newRoom, onAddRoom, onModifyRoomNum } = this.props;let room = Map({ number: `newRoom${newRoom}`, backgroundColor: '#f00' });onAddRoom(room);onModifyRoomNum();
}modifyRoom() {let { onModifyRoom } = this.props;let room = Map({ number: 'HAHA111', backgroundColor: '#0f0' });onModifyRoom(room);
}
监控图

运用Redux-DevTools工具可以清楚地看出当前redux中的数据变化,以及操作。

日志模式:

reduxDevToolsLog
监控模式:

reduxDevToolsInspector

总结

运用redux的好处就是全局数据可控。在redux中运用immutable data也是redux所提倡的,我们不再会因为值没有深拷贝而找不到值在何处何时发生了变化的情况,接而引发的就是组件莫名其妙地不会re-render,同时由于immutable.js在值复制上的高效性,因此在性能上来说,会比用传统javascript中的深拷贝上来说提升会很多。


作者: Ryn_xiao 
链接:http://www.imooc.com/article/20177
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作!

这篇关于React渲染问题研究以及Immutable的应用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/509642

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu1394(线段树点更新的应用)

题意:求一个序列经过一定的操作得到的序列的最小逆序数 这题会用到逆序数的一个性质,在0到n-1这些数字组成的乱序排列,将第一个数字A移到最后一位,得到的逆序数为res-a+(n-a-1) 知道上面的知识点后,可以用暴力来解 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#in