Fragment
可以让你聚合一个子元素列表,并且不在DOM中增加额外节点Fragment
看起来像空的 JSX
标签import React from 'react';
import ReactDOM from 'react-dom';
import Table from './components/Table';
let data = [
{id:1,name:'zhufeng',age:10},
{id:2,name:'jiagou',age:10}
]
ReactDOM.render(<Table data={data} />, document.getElementById('root'));
src\components\Table.js
import React from "react";
class Columns extends React.Component {
render() {
let data = this.props.data;
//Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>
return (
<><td>{data.id}</td><td>{data.name}</td><td>{data.age}</td></>
)
}
}
export default class Table extends React.Component {
render() {
return (
<table>
<thead>
<tr>
<td>ID</td>
<td>Name</td>
<td>Age</td>
</tr>
</thead>
<tbody>
{
this.props.data.map((item, index) => (
<tr key={index}>
<Columns data={item} />
</tr>
))
}
</tbody>
</table>
);
}
}
props
或state
变更,React
会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM,当它们不相同时 React 会更新该 DOMshouldComponentUpdate
来进行优化shouldComponentUpdate
方法会在重新渲染前被触发。其默认实现是返回 true,如果组件不需要更新,可以在shouldComponentUpdate
中返回 false
来跳过整个渲染过程。其包括该组件的 render
调用以及之后的操作import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class App extends Component{
state = {counter:{number:0}}
add = ()=>{
let oldState = this.state;
let amount = parseInt(this.amount.value);
let newState = {...oldState,counter:amount==0?oldState.counter:{number:oldState.counter.number+amount}};
this.setState(newState);
}
render(){
console.log('App render');
return (
<div>
<Counter counter={this.state.counter}/>
<input ref={inst=>this.amount = inst}/>
<button onClick={this.add}>+</button>
</div>
)
}
}
class Counter extends React.Component{
render(){
console.log('Counter render');
return (
<p>{this.props.counter.number}</p>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
PureComponent
,它会在render
之前帮组件自动执行一次shallowEqual
(浅比较),来决定是否更新组件PureComponent
通过prop
和state
的浅比较来实现shouldComponentUpdate
import React, { Component } from "react";
import ReactDOM from "react-dom";
+class PureComponent extends Component {
+ shouldComponentUpdate(newProps) {
+ return !shallowEqual(this.props, newProps);
+ }
+}
+function shallowEqual(obj1, obj2) {
+ if (obj1 === obj2) {
+ return true;
+ }
+ if (typeof obj1 != "object" ||obj1 === null ||typeof obj2 != "object" ||obj2 === null) {
+ return false;
+ }
+ let keys1 = Object.keys(obj1);
+ let keys2 = Object.keys(obj2);
+ if (keys1.length != keys2.length) {
+ return false;
+ }
+ for (let key of keys1) {
+ if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
+ return false;
+ }
+ }
+ return true;
+}
class App extends Component {
state = { counter: { number: 0 } };
add = () => {
let oldState = this.state;
let amount = parseInt(this.amount.value);
let newState = {
...oldState,
counter:
amount == 0
? oldState.counter
: { number: oldState.counter.number + amount }
};
this.setState(newState);
};
render() {
console.log("App render");
return (
<div>
<Counter counter={this.state.counter} />
<input ref={inst => (this.amount = inst)} />
<button onClick={this.add}>+</button>
</div>
);
}
}
+class Counter extends PureComponent {
render() {
console.log("Counter render");
return <p>{this.props.counter.number}</p>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Immutable Data
就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象一个新的 Immutable
对象Persistent Data Structure
(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy
把所有节点都复制一遍带来的性能损耗Collection
、List
、Map
、Set
、Record
、Seq
cnpm install immutable -S
let { Map } = require("immutable");
const map1 = Map({ a: { aa: 1 }, b: 2, c: 3 });
const map2 = map1.set('b', 50);
console.log(map1 !== map2); // true
console.log(map1.get('b')); // 2
console.log(map2.get('b')); // 50
console.log(map1.get('a') === map2.get('a')); // true
import React, { Component } from "react";
import ReactDOM from "react-dom";
+ import { Map,is } from "immutable";
class PureComponent extends Component {
shouldComponentUpdate(newProps) {
return !shallowEqual(this.props, newProps);
}
}
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) {
return true;
}
if (typeof obj1 != "object" ||obj1 === null ||typeof obj2 != "object" ||obj2 === null) {
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if (keys1.length != keys2.length) {
return false;
}
for (let key of keys1) {
+ if (!obj2.hasOwnProperty(key) || !is(obj1[key],obj2[key])) {
return false;
}
}
return true;
}
class App extends Component {
+ state = { counter: Map({ number: 0 }) };
add = () => {
/**
let oldState = this.state;
let amount = parseInt(this.amount.value);
this.setState({counter:{ number: oldState.counter.number + amount }});
*/
+ this.state.counter = this.state.counter.set('number',this.state.counter.get('number') + parseInt(this.amount.value));
+ this.setState(this.state);
};
render() {
console.log("App render");
return (
<div>
<Counter counter={this.state.counter} />
<input ref={inst => (this.amount = inst)} />
<button onClick={this.add}>+</button>
</div>
);
}
}
class Counter extends PureComponent {
render() {
console.log("Counter render");
return <p>{this.props.counter.number}</p>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
React.memo()
是一个高阶函数,它与 React.PureComponent
类似,但是一个函数组件而非一个类memoization
(memorization)方案是一种将函数执行结果用变量缓存起来的方法import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Map,is } from "immutable";
class PureComponent extends Component {
isPureReactComponent = true;
shouldComponentUpdate(newProps, newState) {
return (
!shallowEqual(this.props, newProps)
);
}
}
class App extends Component {
state = { title:'计数器',counter: Map({ number: 0 }) };
add = () => {
this.state.counter = this.state.counter.set('number',this.state.counter.get('number') + parseInt(this.amount.value));
this.setState(this.state);
};
render() {
console.log("App render");
return (
<div>
+ <Title title={this.props.title}/>
<Counter counter={this.state.counter} />
<input ref={inst => (this.amount = inst)} />
<button onClick={this.add}>+</button>
</div>
);
}
}
+function memo(Func){
+ class Proxy extends PureComponent{
+ render(){
+ return <Func {...this.props}/>
+ }
+ }
+ return Proxy;
+}
+const Title = memo(props=>{
+ console.log('Title render');
+ return <p>{props.title}</p>;
+});
class Counter extends PureComponent {
render() {
console.log("Counter render");
return <p>{this.props.counter.get('number')}</p>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) {
return true;
}
if (
typeof obj1 != "object" ||
obj1 === null ||
typeof obj2 != "object" ||
obj2 === null
) {
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if (keys1.length != keys2.length) {
return false;
}
for (let key of keys1) {
if (!obj2.hasOwnProperty(key) || !is(obj1[key],obj2[key])) {
return false;
}
}
return true;
}
default export
的 React 组件import React, { Component, Suspense } from 'react'
import ReactDOM from 'react-dom';
import Loading from './components/Loading';
function lazy(loadFunction){
return class LazyComponent extends React.Component{
state = {Comp:null}
componentDidMount(){
loadFunction().then(result=>{
this.setState({Comp:result.default});
});
}
render(){
let Comp = this.state.Comp;
return Comp?<Comp {...this.props}/>:null;
}
}
}
const AppTitle = React.lazy(()=>import(/* webpackChunkName: "title" */'./components/Title'))
class App extends Component{
state = {visible:false}
show = ()=>{
this.setState({visible:true});
}
render() {
return (
<>
{this.state.visible&&(
<Suspense fallback={<Loading/>}>
<AppTitle/>
</Suspense>
)}
<button onClick={this.show}>加载</button>
</>
)
}
}
ReactDOM.render(<App />, document.querySelector('#root'));
componentDidCatch
方法来处理static getDerivedStateFromError()
渲染备用 UI ,使用 componentDidCatch()
打印错误信息。import React, { Component, Suspense } from 'react'
import ReactDOM from 'react-dom';
import Loading from './components/Loading';
+ const AppTitle = React.lazy(()=>import(/* webpackChunkName: "title" */'./components/Title'))
class App extends Component{
+ state = {visible:false,isError: false}
show = ()=>{
this.setState({visible:true});
}
+ static getDerivedStateFromError(error) {
+ return { isError: true };
+ }
+ componentDidCatch (err, info) {
+ console.log(err, info)
+ }
render() {
if (this.state.isError) {
return (<div>error</div>)
}
return (
<>
{this.state.visible&&(
<Suspense fallback={<Loading/>}>
<AppTitle/>
</Suspense>
)}
<button onClick={this.show}>加载</button>
</>
)
}
}
ReactDOM.render(<App />, document.querySelector('#root'));
cnpm i @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env @babel/preset-react babel-loader html-webpack-plugin webpack webpack-cli webpack-dev-server webpack-merge webpack-node-externals memory-fs require-from-string react-content-loader react-router-dom prerender-spa-plugin react-lazyload react-window immutable -D
npx webpack --config webpack.skeleton.js
npx webpack
src\skeleton.js
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import ContentLoader from 'react-content-loader';
export default ReactDOMServer.renderToStaticMarkup(<ContentLoader />);
src\index.js
import React from "react";
import ReactDOM from "react-dom";
let style = { width: "100%", height: "300px", backgroundColor: "orange" };
setTimeout(() => {
ReactDOM.render(<div style={style}></div>, document.getElementById("root"));
}, 2000);
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
webpack.base.js
const path = require('path');
module.exports = {
mode:'development',
devtool:"none",
context: process.cwd(),
output: {
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env","@babel/preset-react"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }]
]
}
},
include: path.join(__dirname, "src"),
exclude: /node_modules/
}
]
}
};
webpack.skeleton.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { smart } = require("webpack-merge");
const base = require("./webpack.base");
const nodeExternals = require('webpack-node-externals');
module.exports = smart(base, {
target: 'node',
mode: "development",
context: process.cwd(),
entry: "./src/skeleton.js",
output:{
filename:'skeleton.js',
libraryTarget: 'commonjs2'
},
externals: nodeExternals()
});
webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { smart } = require("webpack-merge");
const base = require("./webpack.base");
const SkeletonWebpackPlugin = require('./SkeletonWebpackPlugin');
module.exports = smart(base, {
mode: "development",
context: process.cwd(),
entry: {main:"./src/index.js"},
output:{
filename:'main.js'
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html", //指定模板文件
filename: "index.html" //产出后的文件名
}),
new SkeletonWebpackPlugin({
webpackConfig: require('./webpack.skeleton')
})
]
});
SkeletonWebpackPlugin.js
in-memory
filesystemlet requireFromString = require('require-from-string');
let result = requireFromString('module.exports = "hello"');
console.log(result);// hello
let webpack = require("webpack");
let path = require('path');
let MFS = require("memory-fs");
var requireFromString = require("require-from-string");
let mfs = new MFS();
class SkeletonPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
let { webpackConfig } = this.options;
compiler.hooks.compilation.tap("SkeletonPlugin", compilation => {
compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tapAsync(
"SkeletonPlugin",
(htmlPluginData, callback) => {
let outputPath = path.join(webpackConfig.output.path,webpackConfig.output.filename);
let childCompiler = webpack(webpackConfig);
childCompiler.outputFileSystem = mfs;
childCompiler.run((err, stats) => {
let skeleton= mfs.readFileSync(outputPath, "utf8");
let skeletonHtml = requireFromString(skeleton);
if (skeletonHtml.default) {
skeletonHtml = skeletonHtml.default;
}
htmlPluginData.html=htmlPluginData.html.replace(`<div id="root"></div>`,`<div id="root">${skeletonHtml}</div>`);
callback(null, htmlPluginData);
});
}
);
});
}
}
module.exports = SkeletonPlugin;
prerender-spa-plugin
利用了 Puppeteer
的爬取页面的功能。Puppeteer
是一个 Chrome官方出品的 headless Chrome node
库。它提供了一系列的 API, 可以在无 UI 的情况下调用 Chrome 的功能, 适用于爬虫、自动化处理等各种场景Webpack
构建阶段的最后,在本地启动一个 Puppeteer
的服务,访问配置了预渲染的路由,然后将 Puppeteer
中渲染的页面输出到 HTML 文件中,并建立路由对应的目录src\index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router,Route,Link} from 'react-router-dom';
let Home = props=><div>Home</div>
let User = props=><div>User</div>
let Profile = props=><div>Profile</div>
ReactDOM.render(
<Router>
<>
<Link to="/">home</Link>
<Link to="/user">user</Link>
<Link to="/profile">profile</Link>
<Route path="/" exact={true} component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile}/>
</>
</Router>
,document.getElementById('root'));
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const PrerenderSPAPlugin = require("./prerender-spa-plugin");
module.exports = {
mode: "development",
context: process.cwd(),
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }]
]
}
},
include: path.join(__dirname, "src"),
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html", //指定模板文件
filename: "index.html" //产出后的文件名
}),
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, "dist"),
routes: ["/","/user","/profile"]
})
]
};
prerender-spa-plugin.js
const path = require("path");
const Prerenderer = require("@prerenderer/prerenderer");
const PuppeteerRenderer = require("@prerenderer/renderer-puppeteer");
class PrerenderSPAPlugin {
constructor(options) {
this._options = options;
this._options.renderer = new PuppeteerRenderer({ headless: true });
}
apply(compiler) {
let _this = this;
const compilerFS = compiler.outputFileSystem;
const afterEmit = (compilation, done) => {
const PrerendererInstance = new Prerenderer(_this._options);
PrerendererInstance.initialize()
.then(() => {
return PrerendererInstance.renderRoutes(_this._options.routes || []);
})
.then(renderedRoutes => {
let promises = renderedRoutes.map(rendered => {
return new Promise(function(resolve) {
rendered.outputPath = path.join(
_this._options.staticDir,
rendered.route,
"index.html"
);
let dir = path.dirname(rendered.outputPath);
compilerFS.mkdirp(dir, (err, made) => {
compilerFS.writeFile(
rendered.outputPath,
rendered.html,
err => {
resolve();
}
);
});
});
});
return Promise.all(promises);
})
.then(() => {
PrerendererInstance.destroy();
done();
});
};
compiler.hooks.afterEmit.tapAsync("PrerenderSPAPlugin", afterEmit);
}
}
module.exports = PrerenderSPAPlugin;
不适合不同的用户看都会不同的页面,这种类型的页面不适用预渲染 对于一些经常发生变化的页面,如体育比赛等,会导致编译后的数据不是实时更新的
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin=require('html-webpack-plugin');
module.exports = {
mode:'development',
context: process.cwd(),
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env","@babel/preset-react"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }]
]
}
},
include: path.join(__dirname, "src"),
exclude: /node_modules/
},
{
test:/\.(jpg|png|gif)$/,
use:{loader:'url-loader',options:{limit:0}}
},
{
test:/\.css$/,
use:["style-loader",'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template:'./src/index.html',//指定模板文件
filename:'index.html',//产出后的文件名
})
]
};
src\index.js
import React from "react";
import ReactDOM from "react-dom";
import './index.css';
import LazyLoad from "./react-lazyload";
const App = (props) => {
return (
<ul className="list" style={{overflow:'auto'}}>
{
props.images.map((image,index)=>(
<LazyLoad key={index} height={200} >
<li> <img src={image} /></li>
</LazyLoad>
))
}
</ul>
);
};
let images = [
require('./images/1.jpg'),
require('./images/2.jpg'),
require('./images/3.jpg'),
require('./images/4.jpg'),
require('./images/5.jpg'),
require('./images/6.jpg'),
require('./images/7.jpg'),
require('./images/8.jpg'),
]
ReactDOM.render(<App images={images}/>, document.getElementById("root"));
src\index.css
*{
margin: 0;
padding: 0;
}
ul,li{
list-style: none;
}
li img{
width:100%;
height:100%;
}
getClientRects()
方法返回的一组矩形的集合, 即:是与该元素相关的CSS 边框集合src\react-lazyload.js
import React from "react";
import ReactDOM from "react-dom";
let listeners = [];
let lazyLoadHandler = () => {
for (var i = 0; i < listeners.length; ++i) {
var listener = listeners[i];
checkVisible(listener);
}
};
let checkVisible = component => {
let node = ReactDOM.findDOMNode(component);
let { top } = node.getBoundingClientRect();
let visible = top <= (window.innerHeight || document.documentElement.clientHeight);
if (visible) {
listeners = listeners.filter(item => item != component);
component.setState({visible});
}
};
class LazyLoad extends React.Component {
state = {visible:false}
constructor(props) {
super(props);
this.divRef = React.createRef();
}
componentDidMount() {
if (listeners.length == 0) {
window.addEventListener("scroll", lazyLoadHandler);
}
listeners.push(this);
checkVisible(this);
}
render() {
return this.state.visible ? (
this.props.children
) : (
<div
style={{ height: this.props.height }}
className="lazyload-placeholder"
ref={this.divRef}
/>
);
}
}
export default LazyLoad;
index.js
import React, { Component, lazy, Suspense } from "react";
import ReactDOM from "react-dom";
import { FixedSizeList as List } from './react-window';
import './index.css'
const Row = ({ index, style }) => {
return <div key={index} style={{...style,backgroundColor:getRandomColor(),lineHeight:'30px',textAlign:'center'}}>Row {index+1}</div>
};
const Container = () => (
<List
height={150}
itemCount={100}
itemSize={30}
width={'100%'}
>
{Row}
</List>
);
ReactDOM.render(<Container/>, document.querySelector("#root"));
function getRandomColor( ) {
var rand = Math.floor(Math.random( ) * 0xFFFFFF).toString(16).toUpperCase();
if(rand.length == 6){
return '#'+rand;
}else{
return getRandomColor();
}
}
index.css
*{
margin: 0;
padding: 0;
}
ul,li{
list-style: none;
}
react-window.js
import React, { Component} from "react";
export class FixedSizeList extends React.Component{
state = {start:0}
constructor(){
super();
this.containerRef = React.createRef();
}
componentDidMount(){
this.containerRef.current.addEventListener('scroll',()=>{
let scrollTop = this.containerRef.current.scrollTop;
let start = Math.floor(scrollTop/this.props.itemSize);//起始的索引
this.setState({start});
});
}
render(){
let {width,height,itemCount,itemSize} = this.props;
let children = [];
let size = Math.floor(height/itemSize)+1;//每页的条数
let itemStyle = {height:itemSize,width:'100%',position:'absolute',left:0,top:0};
for(let index=this.state.start;index<this.state.start+size;index++){
let style = {...itemStyle,top:(index)*itemSize};
children.push(this.props.children({index,style}));
}
let containerStyle = {height,width:width||'100%',position:'relative',overflow:'auto'};
return (
<div style={containerStyle} ref={this.containerRef}>
<div style={{width:'100%',height:itemSize*itemCount}}>
{children}
</div>
</div>
)
}
}
当节点处于同一层级时,React diff
提供了三种节点操作,分别为:INSERT(插入)、MOVE(移动)和 REMOVE(删除)
component
类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作component
类型,就需要做移动操作,可以复用以前的 DOM 节点component
不在新集合里的,也需要执行删除操作