<

1.React18 #

1.1 大纲 #

2.创建项目 #

2.1 安装依赖 #

npm install react  react-dom  @types/react @types/react-dom --save
npm install vite  @vitejs/plugin-react-refresh --save-dev

2.2 vite.config.js #

vite.config.js

import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
export default defineConfig({
  plugins: [reactRefresh()]
})

2.2 package.json #

package.json

{
  "scripts": {
    "start": "vite"
  },
}

2.3 index.html #

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vite App</title>
</head>

<body>
  <div id="root"></div>
  <script src="/src/main.jsx" type="module"></script>
</body>

</html>

2.4 main.jsx #

src\main.jsx

import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
const element = <div>app</div>
render(element, root);
createRoot(root).render(element);

3. 批量更新 #

3.1 老的批量更新 #

3.1.1 main.jsx #

src\main.jsx

import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
+import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
+const element = <OldBatchUpdatePage />
render(element, root);
//createRoot(root).render(element);

3.1.2 OldBatchUpdatePage.jsx #

src\routes\OldBatchUpdatePage.jsx

import React, { Component } from 'react'
import { unstable_batchedUpdates } from 'react-dom'
class OldBatchUpdatePage extends Component {
  state = { number: 0 }
  handleCLick = () => {
    this.setState({ number: this.state.number + 1 });
    console.log("number", this.state.number);
    this.setState({ number: this.state.number + 1 });
    console.log("number", this.state.number);
    setTimeout(() => {
      //unstable_batchedUpdates(() => {
      this.setState({ number: this.state.number + 1 });
      console.log("number", this.state.number);
      this.setState({ number: this.state.number + 1 });
      console.log("number", this.state.number);
      //})
    }, 0);
  };

  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={this.handleCLick}>+</button>
      </div>
    );
  }
}
export default OldBatchUpdatePage;

3.1.3 1.批量更新老实现.js #

let isBatchingUpdate = false;
let updateQueue = [];
let state = { number: 0 };
function setState(newState) {
  if (isBatchingUpdate) {
    updateQueue.push(newState);
  } else {
    state = newState;
  }
}
const handleCLick = () => {
  setState({ number: state.number + 1 });
  console.log("number", state.number);
  setState({ number: state.number + 1 });
  console.log("number", state.number);
  setTimeout(() => {
    setState({ number: state.number + 1 });
    console.log("number", state.number);
    setState({ number: state.number + 1 });
    console.log("number", state.number);
  }, 0);
};
function batchedUpdates(fn) {
  isBatchingUpdate = true;
  fn();
  isBatchingUpdate = false;
  updateQueue.forEach(newState => {
    state = newState
  });
}
batchedUpdates(handleCLick);

3.2 新的批量更新 #

3.2.1 main.jsx #

src\main.jsx

import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
+import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
+const element = <NewBatchUpdatePage />
render(element, root);
//createRoot(root).render(element);

3.2.2 NewBatchUpdatePage.jsx #

src\routes\NewBatchUpdatePage.jsx

import React, { Component } from 'react'
import { flushSync } from 'react-dom'
class NewBatchUpdatePage extends Component {
  state = { number: 0 }
  handleCLick = () => {
    this.setState({ number: this.state.number + 1 });
    console.log("number", this.state.number);
    this.setState({ number: this.state.number + 1 });
    console.log("number", this.state.number);
    setTimeout(() => {
      flushSync(() => {
        this.setState({ number: this.state.number + 1 });
      });
      console.log("number", this.state.number);
      flushSync(() => {
        this.setState({ number: this.state.number + 1 });
      });
      console.log("number", this.state.number);
    }, 0);
  };

  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={this.handleCLick}>+</button>
      </div>
    );
  }
}
export default NewBatchUpdatePage

3.2.3 2.批量更新新实现.js #

let updateQueue = [];
let lastPriority = -1;
let state = { number: 0 };
const InputPriority = 1;
const NormalPriority = 1;
let lastUpdatePriority;

function setState(newState, priority) {
  updateQueue.push(newState);
  if (lastUpdatePriority === priority) {
    return;
  }
  lastUpdatePriority = priority;
  setTimeout(() => {
    updateQueue.forEach(newState => {
      state = newState
    });
  });
}
function flushSync(fn) {
  lastUpdatePriority = null;
  fn();
}

const handleCLick = () => {
  setState({ number: state.number + 1 }, InputPriority);
  console.log("number", state.number);
  setState({ number: state.number + 1 }, InputPriority);
  console.log("number", state.number);
  setTimeout(() => {
    setState({ number: state.number + 1 }, NormalPriority);
    console.log("number", state.number);
    setState({ number: state.number + 1 }, NormalPriority);
    console.log("number", state.number);
  }, 500);
};
handleCLick();

4. Suspense #

4.1 main.jsx #

src\main.jsx

import React from 'react'
-import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
+import SuspensePage from './routes/SuspensePage';
+const element = <SuspensePage />
-render(element, root);
createRoot(root).render(element);

4.2 SuspensePage.jsx #

src\routes\SuspensePage.jsx

import React, { Component, Suspense } from 'react'
import ErrorBoundary from './components/ErrorBoundary';
import { fetchUser } from '../fakeApi';
import { wrapPromise } from '../utils';
const userPromise = fetchUser('1');
const userResource = wrapPromise(userPromise);
class SuspensePage extends Component {
  render() {
    return (
      <Suspense fallback={<h3>Loading User......</h3>}>
        <ErrorBoundary>
          <User />
        </ErrorBoundary>
      </Suspense >
    );
  }
}
function User() {
  const user = userResource.read();
  return <div>{user.id}:{user.name}</div>
}

export default SuspensePage;

4.3 ErrorBoundary.jsx #

src\routes\components\ErrorBoundary.jsx

import React from 'react'
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return {
      hasError: true,
      error
    };
  }
  render() {
    if (this.state.hasError) {
      return (
        <>
          {this.state.error.msg}
        </>
      );
    }
    return this.props.children;
  }
}
export default ErrorBoundary

4.4 fakeApi.jsx #

src\fakeApi.jsx

export function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id, name: `姓名${id}` });
      //reject({ msg: '获取数据失败' });
    }, 1000 * Number(id));
  });
}

4.5 utils.jsx #

src\utils.jsx

export function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    },
    (e) => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}

5. startTransition #

5.1 main.jsx #

src\main.jsx

import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
import SuspensePage from './routes/SuspensePage';
+import StartTransitionPage from './routes/StartTransitionPage';
+const element = <StartTransitionPage />
//render(element, root);
createRoot(root).render(element);

5.2 StartTransitionPage.jsx #

src\routes\StartTransitionPage.jsx

import React, { startTransition, useEffect, useState } from 'react';
function getSuggestions(keyword) {
  let items = new Array(10000).fill(keyword)
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(items);
    }, 1000 * keyword.length);
  });
}

function Suggestion(props) {
  const [suggestions, setSuggestions] = useState([]);
  useEffect(() => {
    if (props.keyword && props.keyword.length > 0) {
      getSuggestions(props.keyword).then(suggestions => {
        startTransition(() => {
          setSuggestions(suggestions);
        })
      })
    }
  }, [props.keyword]);
  useEffect(() => {
    console.log(props.keyword);
  })
  return (
    <ul>
      {
        suggestions.map((item, index) => (<li key={index}>{item}</li>))
      }
    </ul>
  )
}
function StartTransitionPage() {
  const [keyword, setKeyword] = useState < string > ("");
  const handleChange = (event) => {
    setKeyword(event.target.value);
  };
  return (
    <div>
      <div>关键字<input value={keyword} onChange={handleChange} /></div>
      <Suggestion keyword={keyword} />
    </div>
  );
}
export default StartTransitionPage

5.3 UpdatePriorityPage.jsx #

UpdatePriorityPage.js

import React, { startTransition, useEffect } from 'react'
import { flushSync } from 'react-dom';
function UpdatePriorityPage() {
  let [state, setState] = React.useState('');
  useEffect(() => {
    console.log(state);
  });
  const update = () => {
    flushSync(() => startTransition(() => setState(state => state + 'A')));
    flushSync(setState(state => state + 'B'));
    flushSync(() => startTransition(() => setState(state => state + 'C')));
    flushSync(setState(state => state + 'D'));
  }
  return (
    <>
      <div>{state}</div>
      <button onClick={update}>更新</button>
    </>
  )
}
export default UpdatePriorityPage;

6. useDeferredValue #

6.1 main.jsx #

src\main.jsx

import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
import SuspensePage from './routes/SuspensePage';
import StartTransitionPage from './routes/StartTransitionPage';
+import UseDeferredValuePage from './routes/UseDeferredValuePage';
+const element = <UseDeferredValuePage />
createRoot(root).render(element);

6.2 src\routes\UseDeferredValuePage.jsx #

src\routes\UseDeferredValuePage.jsx

import React, { useDeferredValue, useEffect, useState } from 'react';
function getSuggestions(keyword) {
  let items = new Array(10000).fill(0).map((item, index) => keyword + index);
  return Promise.resolve(items);
}

function Suggestion(props) {
  const [suggestions, setSuggestions] = useState < Array < string >> ([]);
  useEffect(() => {
    getSuggestions(props.keyword).then(suggestions => {
-      startTransition(() => {
         setSuggestions(suggestions);
-      })
    })
  }, [props.keyword]);

  return (
    <ul>
      {
        suggestions.map((item) => (<li key={item}>{item}</li>))
      }
    </ul>
  )
}
function StartTransitionPage() {
  const [keyword, setKeyword] = useState < string > ("");
+ const deferredText = useDeferredValue(keyword);
  const handleChange = (event) => {
    setKeyword(event.target.value);
  };
  return (
    <div>
      <div>关键字<input value={keyword} onChange={handleChange} /></div>
+     <Suggestion keyword={deferredText} />
    </div>
  );
}
export default StartTransitionPage

7. useTransition #

7.1 main.jsx #

src\main.jsx

import React from 'react'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
import SuspensePage from './routes/SuspensePage';
import StartTransitionPage from './routes/StartTransitionPage';
import UseDeferredValuePage from './routes/UseDeferredValuePage';
+import UseTransitionPage from './routes/UseTransitionPage';
+const element = <UseTransitionPage />
createRoot(root).render(element);

7.2 UseTransitionPage.jsx #

src\routes\UseTransitionPage.jsx

import React, { Suspense, useState, useTransition } from 'react'
import ErrorBoundary from './components/ErrorBoundary';
import { fetchUser } from '../fakeApi';
import { wrapPromise } from '../utils';
const user1Resource = wrapPromise(fetchUser('1'));
const user5Resource = wrapPromise(fetchUser('5'));
function UseTransitionPage() {
  const [resource, setResource] = useState(user1Resource);
  const [isPending, startTransition] = useTransition();
  return (
    <>
      <Suspense fallback={<h3>Loading User......</h3>}>
        <ErrorBoundary>
          <User resource={resource} />
        </ErrorBoundary>
      </Suspense>
      <button onClick={() => {
        startTransition(() => {
          setResource(user5Resource)
        });
      }}>user5</button>
      <h3>{isPending && <p>isPending...</p>}</h3>
    </>
  );
}
function User({ resource }) {
  const user = resource.read();
  return <div>{user.id}:{user.name}</div>
}
export default UseTransitionPage;

9.基础知识 #

9.1. 并发更新 #

9.2 更新优先级 #

jia_sai_1625651545473

9.3 双缓冲 #

shuang_huan_cun_hui_tu

9.4 水合 #




10. React18升级指南 #