React 深入
html直接写React
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
<div id="app"></div>
<script type="text/babel">
class Foo extends React.Component {
render() {
return (
<div>
<MouseTracker />
</div>
);
}
}
ReactDOM.render(<Foo/>, document.getElementById('app'))
</script>
react渲染
从拿到最新的数据,到将数据在页面中渲染出来,可以分为两个阶段。
- 调度阶段。这个阶段React用新数据生成新的Virtual DOM,遍历Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去。
- 渲染阶段。这个阶段 React 根据所在的渲染环境,遍历更新队列,将对应元素更新。在浏览器中,就是跟新对应的DOM元素。除浏览器外,渲染环境还可以是 Native,硬件,VR 等
react - fiber
Fiber是什么
- 以前react更新是
stack reconciler (调节器)
是递归更新子组件,在更新一颗很大的dom树,任何交互和渲染都会被阻塞。 - 新的调度策略–
Fiber reconciler
,React fiber就是把大的任务,分解为小任务,然后执行完小任务之后,会去看看有没有新任务需要执行。 - Fiber 是一种轻量的执行线程,同线程一样共享定址空间,线程靠系统调度,并且是抢占式多任务处理,Fiber 则是自调用,协作式多任务处理
Fiber原理
- requestIdleCallback 是浏览器提供的一个 api,可以让浏览器在空闲的时候执行回调,在回调参数中可以获取到当前帧剩余的时间,fiber 利用了这个参数,判断当前剩下的时间是否足够继续执行任务,如果足够则继续执行,否则暂停任务,并调用 requestIdleCallback 通知浏览器空闲的时候继续执行当前的任务
- Fiber为不同的任务设置不同的优先级,同步任务,优先级最高,fiber 架构中一种数据结构就叫做fiber,fiber是一个对象,stateNode就是节点实例的对象, fiber 基于链表结构,拥有一个个指针,指向它的父节点子节点和兄弟节点,在 diff 的过程中,依照节点连接的关系进行遍历
Fiber更新阶段
- Reconcile阶段。此阶段中,依序遍历组件,通过diff 算法,判断组件是否需要更新,给需要更新的组件加上tag。遍历完之后,将所有带有tag的组件加到一个数组中。这个阶段的任务可以被打断。
- Commit阶段。根据在Reconcile阶段生成的数组,遍历更新DOM,这个阶段需要一次性执行完。如果是在其他的渲染环境–Native,硬件,就会更新对应的元素。
Fiber的问题
- 由于 reconciliation 的阶段会被打断,可能会导致 commit 前的这些生命周期函数多次执行
- 还有一个问题是饥饿问题,意思是如果高优先级的任务一直插入,导致低优先级的任务无法得到机会执行,这被称为饥饿问题
setState流程
- setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
- setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 .setState(partialState, callback) 中的callback拿到更新后的结果。
- setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
具体的代码流程如下
- this.updater.enqueueState(this, partialState, callback, 'setState)
- 通过this,获取到fiber
- 计算过期时间
- 创建一个带有优先级的update update.payload = partialState
- enqueueUpdate(fiber, update)
- 如果没有更新队列,就创建一个,是一个链表
- appendUpdateToQueue
- scheduleWork === scheduleUpdateOnFiber(fiber,过期时间)
- 如果setState是在合成时间里,要执行event之类的一些操作,如 batchedEventUpdates
- 如果setState是在生命周期中,要执行生命周期之类的一些操作
- flushSyncCallbackQueue 完成更新 performSyncWorkOnRoot
- commitRoot 完成更新
高阶组件
类型1,函数式高阶组件,参数是组件,本质上还是函数的封装
// 抽取公用逻辑,操作props
function Foo(props) {
return (
<div id='foo'>
<input type="text" {...props}/>
</div>
)
}
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
constructor(props) {
super(props)
this.state = {
name: ''
}
this.onNameChange = this.onNameChange.bind(this)
}
onNameChange(event) {
this.setState({
name: event.target.value
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange
}
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
// 或者可以使用装饰器方式
const HocFoo = ppHOC(Foo);
类型2,基于继承的高阶组件 ,反向继承高阶组件,可以实现
渲染劫持(Render Highjacking)操作 state
class Foo extends React.Component {
constructor(props) {
super(props)
this.state = {
value: '1'
}
}
componentDidMount() {
console.log('componentDidMount Foo')
}
render() {
return (
<div id='foo'>
{
this.state.value
}
</div>
)
}
}
class Hoc extends Foo {
constructor(props){
super(props);
this.state = {
...this.state,
name: 'foo'
}
}
componentDidMount() {
super.componentDidMount();
}
render() {
console.log(this.state)
return (
<div id='hoc'>
{super.render()}
{super.render()}
</div>
)
}
}
render props
就是渲染props,抽离组件,或者直接变成子组件
class Mouse extends React.Component {
constructor(props) {
super(props);
this.state = {x: 0, y: 0};
}
handleMouseMove = event => {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{height: '100vh'}} onMouseMove={this.handleMouseMove}>
<h1>移动鼠标!</h1>
{this.props.render(this.state)}
</div>
);
}
}
function Pos(props) {
return (
<p>当前的鼠标1 位置是 ({props.mouse.x}, {props.mouse.y})</p>
)
}
class Foo extends React.Component {
render() {
return (
<div>
<Mouse render={mouse => <Pos mouse={mouse}/>}/>
</div>
);
}
}
// 或者可以通过children写成这样
class Mouse extends React.Component {
constructor(props) {
super(props);
this.state = {x: 0, y: 0};
}
handleMouseMove = event => {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{height: '100vh'}} onMouseMove={this.handleMouseMove}>
<h1>移动鼠标!</h1>
{this.props.children(this.state)}
</div>
);
}
}
class Foo extends React.Component {
render() {
return (
<div>
<Mouse>
{mouse => <p>当前的鼠标1 位置是 ({mouse.x}, {mouse.y})</p>}
</Mouse>
</div>
);
}
}
// 或者可以直接这样
class Mouse extends React.Component {
constructor(props) {
super(props);
this.state = {x: 0, y: 0};
}
handleMouseMove = event => {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{height: '100vh'}} onMouseMove={this.handleMouseMove}>
<h1>移动鼠标!</h1>
{React.cloneElement(this.props.children, this.state)}
</div>
);
}
}
function Pos1({x, y}) {
return (
<p>当前的鼠标1 位置是 ({x}, {y})</p>
)
}
class Foo extends React.Component {
render() {
return (
<div>
<Mouse>
<Pos1 />
</Mouse>
</div>
);
}
}
react给子元素props加一些属性
const newChildren = React.Children.map(this.props.children, children =>
React.cloneElement(children, { setFocus: this.setFocus }),
);
Ref
Ref 3种使用方式,函数组件,没有实例this,就不能用这种ref
class Foo extends React.Component {
constructor(props) {
super(props);
this.ref3 = React.createRef();
}
componentDidMount() {
// string ref 要被废弃,不建议使用
console.log(this.refs.ref1)
// function ref
console.log(this.ref2)
// object ref
console.log(this.ref3)
}
render() {
return (
<div>
<p ref='ref1'>ref1</p>
<p ref={r => {this.ref2 = r;}}>ref2</p>
<p ref={this.ref3}>ref3</p>
</div>
)
}
}
useRef
useRef和createRef一样都可以拿到dom,但是useRef返回的永远都是一样的,
就是 useRef第一次拿不到dom,以后哪怕组件刷新也会返回一个相同的对象。
useRef会在每次渲染时返回同一个 ref 对象,在整个组件的生命周期内是唯一的。
createRef每次都创建新的ref。
const Code = props => {
let ref = useRef(null);
return (<div ref={ref}><div>)
}
// ref的变化,从null到真实dom不会再次渲染,要想拿到,需要使用 useCallback
const ref = useCallback(node => {
if (node !== null) {
console.log(node);
}
}, []);
forwardRef
解决父组件无法获取子组件的ref的问题
const Demo1 = React.forwardRef((props, ref) => <input ref={ref}/>);
class Foo extends React.Component {
constructor(props) {
super(props);
this.ref4 = React.createRef();
}
componentDidMount() {
console.log(this.ref4.current.value = 'jaja')
}
render() {
return (
<div>
<Demo1 ref={this.ref4}/>
</div>
)
}
}
context
const {Provider, Consumer} = React.createContext('default');
class App extends React.Component {
state = {
name: 'default'
}
componentDidMount() {
setTimeout(() => {
this.setState({
name: '100s'
})
}, 1000)
}
render() {
return (
<div id='app'>
<Provider value={this.state.name}>
<Foo/>
</Provider>
</div>
)
}
}
class Foo extends React.Component {
render() {
return (
<Consumer>
{v => <h2>{v}</h2>}
</Consumer>
)
}
}
concurrent-mode
concurrent-mode包裹的组件,会有一个比较低的优先级
suspense lazy
suspense包裹的组件,只有到所有子组件都加载好,才不会再显示fallback
const Data1 = React.lazy(() => import('./data1'));
const Data2 = React.lazy(() => import('./data2'));
const Demo = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<Data1/>
<Data2/>
</Suspense>
)
}
useState
useState可以使用对象,但是改变后的值必须是一个全新的对象
useEffect
- useEffect 在每次渲染之后执行,
- useEffect 如果多个,按照顺序执行,
- useEffect 依赖不写,每次都会执行,
- useEffect 依赖空数组,类似didMount
- useEffect 可以返回一个函数,类似didUnMount,第一次不会执行,以后更新的时候,会先执行这个,再执行effect中的其他逻辑
function Counter() {
const [count, setCount] = useState(0);
console.log(1111);
useEffect(() => {
console.log('no dep');
console.log(document.getElementById('btn'));
});
useEffect(() => {
console.log('count dep');
}, ['count']);
useEffect(() => {
console.log('no dep didmount');
}, []);
useEffect(() => {
console.log('effect with cb');
const timer = setTimeout(() => {
console.log('this is a timer');
}, 2000);
return () => {
clearTimeout(timer);
};
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)} id="btn">
Click me
</button>
</div>
);
}
useLayoutEffect
和 useEffect一样,但是它在dom完成之后同步执行,用它来读取dom并同步重新渲染
useLayoutEffect里调度的更新会被同步的flush,在浏览器有一个改变要重新渲染之前。
和componentDidMount and componentDidUpdate是一样的。
见过测试,使用useEffect动画正常,它卡顿。
useContext
就是在子组件中使用context,而不需要使用consumer
const MyContext = React.createContext();
function Foo() {
let [data, setData] = useState(0);
return (
<div id="foo">
<button onClick={() => setData(++data)}>add </button>
<MyContext.Provider value={data}>
<Foo1 />
</MyContext.Provider>
</div>
);
}
function Foo1() {
let v = useContext(MyContext);
return <h2 id="foo1">{v}</h2>;
}
React.memo
包裹一个函数组件,props浅比较,防止无用的更新,类似pureComponent的功能
const Foo2 = React.memo(props => {
console.log('foo2');
return (
<div>
<div>data from props = {props.c}</div>
</div>
);
});
function Foo() {
console.log('foo1');
let [data, setData] = useState(0);
const click = () => {
data += Math.round(Math.random());
setData(data);
};
return (
<div id="foo">
<button onClick={click}>add data</button>
<Foo2 c={data} />
</div>
);
}
useReducer
useReducer可以使用action把一个state变成另外一个state 。
以下几种情况使用useReducer更好,逻辑有多个子值,集中处理。
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
}
children
// 1 map
function Foo1(props) {
return (
<div>
{
// props.children.map(c => [c, c])
React.Children.map(props.children, c => [c, c])
}
</div>
)
}
class App extends React.Component {
state = {
name: 'default'
}
render() {
return (
<div id='app'>
<Foo1>
<span>1</span>
<h2>a</h2>
</Foo1>
</div>
)
}
}
useCallBack
返回一个memozized回调函数
import React, { useState, useCallback, useEffect } from 'react';
function Child({ event }) {
console.log('child-render');
// 第五版
useEffect(() => {
console.log('child-useEffect');
event();
}, [event]);
return (
<div>
<p>child</p>
{/* <p>props-data: {data.data && data.data.openCode}</p> */}
<button onClick={event}>调用父级event</button>
</div>
);
}
const url = 'http://localhost:3322/data';
export default function Demo8() {
const [count, setCount] = useState(0);
const [data, setData] = useState(0);
// demo1 每次render的handle都是新的函数,每次都可以拿到最新的data
// const handle = async () => {
// const response = await fetch(url);
// const res = await response.json();
// console.log('handle', res);
// setData(res);
// // setData不是同步的,所以setData之后,拿到的data是上一次的
// // 但是在组件函数中,就可以拿到最新的
// console.log('fn', data);
// };
// demo2 和 demo1的效果一样的
// const handle = useCallback(async () => {
// const response = await fetch(url);
// const res = await response.json();
// console.log('接口的新数据', res);
// setData(res);
// // 这里的data还是上一次的data
// console.log('data in cb', data);
// console.log('\n');
// });
// 接口的新数据 0.3010873119062878
// parent-render====> 0.3010873119062878
// child-render
// data in cb 0
// 接口的新数据 0.07467124797072677
// parent-render====> 0.07467124797072677
// child-render
// data in cb 0.3010873119062878
// demo3 useCallback 如果有第二个参数depth,函数会被记忆化,所以每次data都是第一次的值(闭包)
// const handle = useCallback(async () => {
// const response = await fetch(url);
// const res = await response.json();
// console.log('api', res);
// setData(res);
// // 这里拿不到上一个数据了,data永远是0
// console.log('useCallback', data);
// }, []);
// demo4 handle依赖于count
// 每当count改变,都会生产一个全新的handle,会触发子组件的effect
const handle = useCallback(async () => {
const response = await fetch(url);
const res = await response.json();
console.log('res = ', res);
setData(res);
console.log('parent-useCallback', data);
}, [count]);
console.log('parent-render', data);
return (
<div>
<button
onClick={() => {
setCount(count + 1);
}}
>
count++
</button>
<p>count:{count}</p>
<p>data: {data}</p>
<p>-------------------------------</p>
<Child event={handle} />
</div>
);
}
useMemo
- 返回一个 memoized 值,和useCallback一样,当依赖项发生变化,才会重新计算 memoized 的值。
- useMemo和useCallback不同之处是:它允许应用于任何值类型(不仅仅是函数)。
- useMemo在render之前执行。
- 主要区别是 React.useMemo 将调用 fn 函数并返回其结果,而 React.useCallback 将返回 fn 函数而不调用它
import React, { useState, useMemo } from 'react';
function Demo9() {
let [count, setCount] = useState(0);
// 这是一个方法
// let handle = () => {
// console.log('handle', count);
// return count;
// };
// 依赖为空 Child组件拿到的handle永远为0
// let handle = useMemo(() => {
// console.log('handle1', count);
// return count;
// }, []);
const handle = useMemo(() => {
console.log('handle2', count);
// 大计算量的方法
return count;
}, [count]);
console.log('render-parent');
function click() {
setCount(count++);
}
return (
<div>
<p>
demo9: {count}
<button onClick={click}>++count</button>
</p>
<p>-------------------</p>
<Child handle={handle} />
</div>
);
}
function Child({ handle }) {
console.log('\n render-child');
console.log('handle', handle);
return (
<div>
<p>child</p>
<p>handle{handle}</p>
</div>
);
}
export default Demo9;
useImperativeHandle
自定义ref的返回值,使用它可以把子组件的方法,暴露给ref,父组件就可以调用
const ChildCom = React.forwardRef(
(props,ref) => {
useImperativeHandle(ref, () => ({
getData
}))
const getData = () => {
// to do something
}
return <div>ChildCom</div>
}
);
react-dnd
- React DnD 内置用了redux,
- 基于h5的拖拽api,但是这个api在触摸屏不管用,这就是为什么用了一个可插拔的方式来实现的react-dnd,
需要 html5-backend这个库, - backend handles the DOM events, 这种backend类似react的事件处理,抽象解决不同浏览器的差异,专注于处理原生dom,backends做的是把浏览器事转化为react-dnd可以处理的redux的action,
- react-dnd像redux一样,处理的是数据,不是view,所以一个元素拖动的时候,叫做某种类型的item被拖动了,
- item是用来描述被拖动的元素,例如在看板中的,一个卡片,描述为
{ cardId: 42 }
, 把拖拽数据描述为简单对象可以解耦组件,互不影响彼此 - type 是用来描述一类item的字符串 types 用来指定drag source和drop target是兼容的, 就像redux的action的type
- Monitors drag和drop都是有状态的,monitor是dnd暴露状态(通过包裹内部存储的状态)给你的组件,通过monitor可以更新props来实现拖拽效果
- 对于要更新monitors的组件,可以声明一个monitors作为参数的collecting方法,dnd会实时更新collecting方法来merge state到monitors来更新组件
- The connectors let you assign one of the predefined roles (a drag source, a drag preview, or a drop target) to the DOM nodes in your render function,
connectors 方法让你在render方法中指定链接到某一种角色(drag source等)到dom node - connectors是collect方法的第一个参数
function collect(connect, monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
// 怎么链接dropTarget
connectDropTarget: connect.dropTarget(),
}
}
// 然后在render方法中,就可以获取monitors的属性了
render() {
const { highlighted, hovered, connectDropTarget } = this.props;
// connectDropTarget包裹的元素,是一个合法的drop target 并且它的hover事件等
// 要交给backend来做
return connectDropTarget(
<div className={classSet({
'Cell': true,
'Cell--highlighted': highlighted,
'Cell--hovered': hovered
})}>
{this.props.children}
</div>
);
}
- Drag Sources and Drop Targets 都是高阶组件
自定义hook1-useCount
function useCount(initValue, step) {
const [count, setCount] = useState(initValue);
const add = () => {
let v = count + step;
setCount(v);
};
const minus = () => {
let v = count - step;
setCount(v);
};
const reset = () => {
setCount(initValue);
};
return [count, add, minus, reset];
}
export default function RootComponent() {
const [count, add, minus, reset] = useCount(100, 4);
return (
<div id="app">
<div>
<button>{count}</button>
<button onClick={add}>add</button>
<button onClick={minus}>minus</button>
<button onClick={reset}>reset</button>
</div>
</div>
);
}
自定义hook2
const useInput = (init = '') => {
const [value, setValue] = useState(init);
const bind = { value, onChange: e => setValue(e.target.value) };
const reset = () => setValue(init);
return [value, bind, reset];
};
const Demo = () => {
const [value1, bind1, reset1] = useInput('abc');
return (
<div>
<div>value1: {value1}</div>
<input type="text" {...bind1} />
<button onClick={reset1}>reset1</button>
</div>
);
};
自定义hook3
const useSize = () => {
const getSize = () => ({
width: window.innerWidth,
height: window.innerHeight
});
const [size, setSize] = useState(getSize());
const handleResize = () => {
setSize(getSize());
};
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
};
const Demo2 = () => {
const size = useSize();
return (
<div>
size:{size.width},{size.height}
</div>
);
};
自定义hook-usePrevious
function App() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<h1>
Now: {count}, before: {prevCount}
</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
function usePrevious(value) {
const ref = useRef();
// 只有在value变,才会重新执行
useEffect(() => {
ref.current = value;
console.log(ref);
}, [value]);
// 返回上一个值,发生在上边的Effect执行前的更新
return ref.current;
}
//和下边的效果一样的
let data = {
a: null
};
function usePrevious(value) {
// 只有在value变,才会重新执行
useEffect(() => {
data.a = value;
}, [value]);
// 返回上一个值,发生在上边的Effect执行前的更新
return data.a;
}