1. 使用Fragment #

1.1 index.js #

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'));

1.2 Table.js #

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>
    );
  }
}

2. PureComponent #

2.1 重复渲染 #

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')
)

2.2 PureComponent #

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"));

2.3 PureComponent+Immutable.js #

immutablejs

2.4.1 immutable #

2.4.1.1 安装 #
cnpm install immutable -S
2.4.1.2 使用 #
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
2.4.1.3 重构 #
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"));

3. memo #

3.1 memoization(memorization)方案 #

3.2 优化 #

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;
}

4. Lazy+Error Boundaries #

4.1 React.Lazy #

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'));

4.2 错误边界(Error Boundaries) #

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'));

5. 骨架屏 #

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

webpackflowes

5.1 skeleton.js #

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 />);

5.2 index.js #

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);

5.3 index.html #

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>

5.4 webpack.base.js #

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/
      }
    ]
  }
};

5.5 webpack.skeleton.js #

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()
});

5.6 webpack.config.js #

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')
    })
  ]
});

5.7 SkeletonWebpackPlugin.js #

SkeletonWebpackPlugin.js

let 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;

6. 预渲染 #

6.1 src\index.js #

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'));

6.2 src\index.html #

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>

6.3 webpack.config.js #

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"]
    }) 
  ]
};

6.4 prerender-spa-plugin.js #

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;

不适合不同的用户看都会不同的页面,这种类型的页面不适用预渲染 对于一些经常发生变化的页面,如体育比赛等,会导致编译后的数据不是实时更新的

7. 图片懒加载 #

7.1 webpack.config.js #

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',//产出后的文件名
        })
  ]
};

7.2 index.js #

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"));

7.3 index.css #

src\index.css

*{
    margin: 0;
    padding: 0;
}
ul,li{
    list-style: none;
}
li img{
    width:100%;
    height:100%;
}

7.4 react-lazyload.js #

getBoundingClientRect

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;

8. 长列表优化 #

8.1 index.js #

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();
    }
}

8.2 index.css #

index.css

*{
    margin: 0;
    padding: 0;
}
ul,li{
    list-style: none;
}

8.3 react-window.js #

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>
        )
    }
}

9. key的优化 #

9.1 diff策略 #

9.2 tree diff #

9.3 组件diff #

9.4 element diff #

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT(插入)、MOVE(移动)和 REMOVE(删除)

domdiff

10. React 性能分析器 #

10.1 分析解析 #

10.2 react-flame-graph #