React基础

一、react元素

TIP

JSX其实只是一种语法糖,最终会通过babeljsopen in new window转译成createElement语法,最新版本转化为 Object(r.jsx)("h1",{children:"hello"}

1 特点

  • React元素是构成React应用的最小单位
  • React元素用来描述你在屏幕上看到的内容
  • React元素事实上是普通的JS对象
  • ReactDOM来确保浏览器中的DOM数据和React元素保持一致

2 jsx转化react元素

简单版本

// jsx
let element = <h1>hello</h1>;

// react元素
let element = React.createElement("h1", null, "Hello");

console.log(JSON.stringify(element, null, 4));

/**
{
	"type": "h1",
	"props": {
		"children": "Hello"
	}
}
 */

复杂版本

// jsx
let element = (
  <h1 className="title" style={{color:'red'}}>
    <span>hello</span>world
  </h1>
);

// react元素
/**
 * 参数1 标签的类型 h1 span div
 * 参数2 属性的JS对象
 * 参数3往后的都是儿子们
 */
let element = React.createElement("h1", {
  className: "title",
  style: {
    color: 'red'
  }
}, React.createElement("span", null, "hello"), "world");

console.log(JSON.stringify(element, null, 4));

/**
{
	"type": "h1",
	"props": {
		"className": "title",
		"style": {
			"color": "red"
		},
   		"children": [
      		{
				"type": "span",
				"props": {
					"children": "hello"
				}
			},
			"world"
    	]
  	}
}
 */

3 实现createElement

/**
 * @param {*} type 元素的类型
 * @param {*} config 配置的属性对象
 * @param {*} children 第一个儿子
 */
function createElement(type, config, children) {
	// 删除多余对象
	if (config) {
		delete config._owner;
		delete config._store;
	}
	let props = {...config};

	// 截取儿子数据
	if (arguments.length > 3) {
		children = Array.prototype.slice.call(arguments, 2);
	}
	// children可能是数组(多于1个儿子)
	//  也可能是一个字符串或者数字,也可能是一个null,也可能是一个react元素 
	 props.children = children;
	//  返回React元素,也就是虚拟DOM
	 return {
		 type, // 元素的类型
		 props // 元素的属性
	 }
}

let React = {
	createElement
}

export default React;

二、组件基础

1 函数组件

  • 函数组件的props参数可以为任意值
  • 函数组件接收一个单一的props对象并返回了一个React元素
import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库

/**
 * 定义一个函数组件 名字首字母必须要大写
 * 函数内部的jsx语法被转化为react.createElement元素
 * @param { } props 属性对象
 */
function Welcome(props){
  return <h1>Hello, {props.name}</h1>;
}

/**
function Welcome(props) {
  return React.createElement("h1", null, "Hello, ", props.name);
} 
*/

// 调用组件,组件名称和属性会被当成参数传入到react.createElement中
let element = <Welcome name="world"/>

/**
var element = React.createElement(Welcome, {
  name: "world"
});
*/

ReactDOM.render(
	element, 
	document.getElementById('root')
);

2 类组件

import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库

/**
 * 定义一个类组件
 */
class Welcome extends React.Component{
	// this.props = {name:'world'}
  	render(){
    	return <h1>hello,{this.props.name}</h1>;
  	}
}

// 调用组件,组件名称和属性会被当成参数传入到react.createElement中
let element = <Welcome name="world"/>

ReactDOM.render(
	element, 
	document.getElementById('root')
);

3 总结

  • React元素可能是字符串(原生DOM类型),也可能一个函数(函数组件),也可能是一个类(类组件)
  • 在定义组件元素的时候,会把JSX所有的属性封装成一个props对象传递给组件
  • 组件的名称一定要首字母大写,React是通过首字母来区分原生还是自定义组件
  • 组件要先定义,再使用
  • 组件要返回并且只能返回一个React根元素,否则会报错:JSX expressions must have one parent element

三、状态

  • 组件的数据来源有两个地方,分别是属性对象和状态对象
  • 属性是父组件传递过来的(默认属性,属性校验),不能修改
  • 状态是内部产生的,可以改,状态只能用在类组件里
  • 唯一能给this.state赋值的地方就是构造函数,只能初始值
  • 其它地方要想改变状态只能调用setState()方法
  • 每当你调用setState方法的时候就会引起组件的刷新,组件会重新调用一次render方法,得到新虚拟DOM,进行DOM更新
  • 属性和状态的变化都会影响视图更新

1 使用

import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库

// 定时器,每秒更新+1
class Clock extends React.Component {
	constructor(props) {
		super(props);
		// 初始化状态
		this.state = {
			date:new Date()
		};
        setInterval(this.tick, 1000);
	}
	
	tick=()=>{
		// Do not mutate state directly. Use setState()
		// 通过setState()修改状态
		this.setState({date:new Date()});
	}
	
    render(){
        return (
            <div>
                <h1>Hello</h1>
                <h2>当前的时间:{this.state.date.toLocaleTimeString()}</h2>
            </div>
        )
    }
}

let element = <Clock/>

ReactDOM.render(
	element, 
	document.getElementById('root')
);

2 异步更新

  • 状态是封闭的,只有组件自己能访问和修改
  • 出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用,进行批量执⾏, 多key一次执⾏,相同key合并
  • 可能是异步的,在事件处理函数中或生命周期函数中批量更新是异步的,其它地方都是直接同步更新的
import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库

class Counter extends React.Component {
	state = { number: 0 }
	
	/**
	 * 在事件处理函数中,
	 *   setState 的调用会批量执行
	 *   setState 不会修改this.state,需要等实际处理函数结束之后,再进行更新
	 */
	handleClick = () => {
		// setState第一个参数为对象的情况
		this.setState({ number: this.state.number + 1 });
		console.log(this.state.number); // 0
		this.setState({ number: this.state.number + 1 });
		console.log(this.state.number); // 0
	}
	// 点击第一次   打印两个0  最后状态变为1
	// 点击第二次   打印两个1  最后状态变为2
	
    render(){
        return (
            <div>
                <p>number:{this.state.number}</p>
                <button onClick={this.handleClick}>+</button>
            </div>
        )
    }
}

ReactDOM.render(<Counter />, document.getElementById('root'));

setState第一个参数为函数的情况

state = { number: 0 }

handleClick = () => {
	// 函数进行修改,第一个函数的状态返回值为第二个函数的参数
	this.setState((state) => ({ number: state.number + 1 }));
	console.log(this.state.number); // 0
	this.setState((state) => ({ number: state.number + 1 }));
	console.log(this.state.number); // 0
}
// 点击第一次   打印两个0  最后状态变为2
// 点击第二次   打印两个2  最后状态变为4

// 原理如下:
let queue = [];
queue.push((state) => ({ number: state.number + 1 }));
queue.push((state) => ({ number: state.number + 1 }));

let state = { number: 0 };
let result = queue.reduce((newState, action) => {
	return action(newState);
}, state);
console.log(result); // 2

setState的第二个参数可以获取立刻更新的值

state = { number: 0 }

handleClick = () => {
	// setState第二个参数为回调函数,里面可以获取每次更新的最新值
	this.setState({ number: this.state.number + 1 }, () => {
		console.log('callback1', this.state.number); // 1
	});
	console.log(this.state.number); // 0
	this.setState({ number: this.state.number + 1 }, () => {
		console.log('callback2', this.state.number); // 1
	});

	// 点击第一次,因为批量执行,最后再回调函数中打印的是1
	// 点击第二次,因为批量执行,最后再回调函数中打印的是2
}
  • React能管控的地方,就是批量的、异步的,比如事件处理函数中、生命周期中
  • React不能管控的地方,就是非批量的、同步的,比如setInterval setTimeout 原生DOM事件等
  • 如果要同步获取最新状态值,三种⽅方式:
    1. 传递回调函数给setState
    2. 写在定时器中
    3. 写在JS的原生事件中
state = { number: 0 }

handleClick = () => {
	//在其它react不能管控的地方,就是同步执行的
	Promise.resolve().then(() => {
		this.setState({ number: this.state.number + 1 }, () => {
			console.log('callback3', this.state.number); // 1
		});
		console.log(this.state.number); // 1
		this.setState({ number: this.state.number + 1 }, () => {
			console.log('callback4', this.state.number); // 2
		});
		console.log(this.state.number); // 2
	});
	setTimeout(() => {
		this.setState((state) => ({ number: state.number + 1 }));
		console.log(this.state.number); // 3
		this.setState((state) => ({ number: state.number + 1 }));
		console.log(this.state.number); // 4
	});
}

原生事件中如果要组件刷新,需要调用forceUpdate

/**
 * 组件强制更新方法
 * 1.获取老的虚拟DOM =》 React元素
 * 2.根据最新的属生和状态计算新的虚拟DOM
 * 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
 */
forceUpdate(){
	console.log('updateComponent');
	let oldRenderVdom = this.oldRenderVdom;//老的虚拟DOM
	//根据老的虚拟DOM查到老的真实DOM
	let oldDOM = findDOM(oldRenderVdom);
	let newRenderVdom = this.render();//计算新的虚拟DOM
	compareTwoVdom(oldDOM.parentNode,oldRenderVdom,newRenderVdom);//比较差异,把更新同步到真实DOM上
	this.oldRenderVdom=newRenderVdom; 
}

3 批量更新器

// 更新器
class Updater {
    constructor(){
		// 初始状态
        this.state={name:'test', number:0};
        this.queue = [];
	}
	// 更新状态方法
    setState(newState){
        this.queue.push(newState);
	}
	// 批量更新
    flush(){
        for(let i=0;i<this.queue.length;i++){
            let update = this.queue[i];
            if(typeof update === 'function'){
                this.state = {...this.state,...update(this.state)};
            }else{
                this.state = {...this.state,...update};
            }
        }
    }
}
let updater = new Updater();
updater.setState({number: 1}); 
updater.setState((previousState)=>({number: previousState.number+1}));
updater.setState({number: 2}); 
updater.setState({number: 3}); 

updater.flush();
console.log(updater.state); // { name: 'test', number: 3 }

4 同步更新器

// 更新器
class Updater {
    constructor(){
		// 初始状态
        this.state={name:'test', number:0};
        this.queue = [];
	}
	// 更新状态方法
    setState(update){
        if(typeof update === 'function'){
            this.state = {...this.state,...update(this.state)};
        }else{
            this.state = {...this.state,...update};
        }
    }
}
let updater = new Updater();
updater.setState({number: 1}); 
console.log(updater.state); // { name: 'test', number: 1 }
updater.setState((previousState)=>({number: previousState.number+1}));
console.log(updater.state); // { name: 'test', number: 2 }
updater.setState({number: 3}); 
console.log(updater.state); // { name: 'test', number: 3 }
updater.setState({number: 4}); 
console.log(updater.state); // { name: 'test', number: 4 }

四、事件处理

  • React使用的是自定义的合成事件,合成事件的优点:
    1. 进行浏览器兼容,实现更好的跨平台
    2. 避免垃圾回收,16以前利用事件池(数组存储事件),17已经移除事件池
    3. 方便事件统一管理和事务机制

1 react事件基础

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
  • 你不能通过返回 false 的方式阻止默认行为。你必须显式的使用preventDefault
import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	// 事件处理函数
	handleClick(e) {
		// 阻止默认事件
        e.preventDefault();
        alert('The link was clicked.');
    }
	render() {
        return (
            <a href="http://www.baidu.com" onClick={this.handleClick}>
                Click me
          </a>
        );
    }
}

ReactDOM.render(
	<Counter/>, 
	document.getElementById('root')
);

2 事件原理

  • 在dom节点上挂一个对象,存储各种监听函数,最后将整个事件委托到document节点上
  • 在点击事件的时候,会从事件源开始依次向上模拟冒泡,执行事件函数
import { updateQueue } from './Component';

// 调用
function updateProps(dom, oldProps, newProps) {
	for (let key in newProps) {
		if (key === 'children') { }
		if (key === 'style') {

		} else if (key.startsWith('on')) {//onClick
			//dom[key.toLocaleLowerCase()]=newProps[key];//dom.onclick=handleClick
			addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
		}
	}
}

/**
 * 实现的事件委托,把所有的事件都绑定到document上
 * @param {*} dom dom 真实DOM
 * @param {*} eventType 事件类型onclick
 * @param {*} handler 合成事件监听函数
 */
export function addEvent(dom, eventType, handler) {
	// 先给dom绑定一个store属性,然后在里面绑定各种事件回调函数handler
	let store = dom.store || (dom.store = {});
	store[eventType] = handler; // store.onclick = handler;
	//如果有很多个元素都绑定 click事件,往document持的时候只挂一次
	// 事件委托,不管你给哪个DOM元素上绑事件,最后都统一代理到document上去了,而且如果有多次绑定,也只挂一次
	if (!document[eventType]) {
		document[eventType] = dispatchEvent;
	}
	// 等价于:document.addEventListener('click',dispatchEvent);
}

function dispatchEvent(event) {
	// target事件源=button,那个DOM元素 类型type=click
	let { target, type } = event;
	let eventType = `on${type}`; // onclick

	// 把队列设置为批量更新模式
	updateQueue.isBatchingUpdate = true;

	let syntheticEvent = createSyntheticEvent(event);
	//模拟事件冒泡的过程
	while (target) {
		let { store } = target;
		let handler = store && store[eventType];
		handler && handler.call(target, syntheticEvent);
		target = target.parentNode;
	}

	// 事件处理完毕之后,改为false,然后批量更新状态
	updateQueue.isBatchingUpdate = false;
	updateQueue.batchUpdate();
}

//在源码里此处做了一些浏览器兼容性的适配
function createSyntheticEvent(event) {
	let syntheticEvent = {};
	for (let key in event) {
		syntheticEvent[key] = event[key];
	}
	return syntheticEvent;
}

3 this指向

  • 事件函数写成箭头函数
  • 事件函数为普通函数,调用用匿名函数
  • bind改变this,在构造函数、render中、调用方法中都可以bind
  • 如果要传参数,只能使用匿名函数或者箭头函数,本质都一样的
import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	constructor(props){
        super(props);
		
       // this.handleClick = this.handleClick.bind(this);
    }
    handleClick1 = ()=>{
        console.log('this is:', this);
	}
	handleClick2() {
		console.log('this is:', this);
	}
	handleClick3 = (amount)=>{
        console.log('this is:', this, amount);
    }
	render() {
		 // this.handleClick = this.handleClick.bind(this);
        return (
			<div>
				{/* 1 事件函数写成箭头函数(公共属性),this用于指向类的实例 */}
				<button onClick={this.handleClick1}>+</button>
				{/* 2 事件函数为普通函数,调用用匿名函数 */}
				<button onClick={() => this.handleClick2()}>-</button>
				{/* 3 bind改变this指向 */}
				<button onClick={this.handleClick2.bind(this)}>bind-</button>

				{/* 第一种:通过 bind 绑定 this 传参 */}
				<button onClick={this.handleClick3.bind(this,3)}>bind传参</button>
				{/* 第二种:通过箭头函数绑定 this 传参 */}
				<button onClick={() => this.handleClick3(3)}>箭头传参</button>
			</div>
        );
    }
}

ReactDOM.render(<Counter/>, document.getElementById('root'));

4 区别

原生事件React 合成事件
命名方式名称全部小写 onclick、onblur名称采用小驼峰 onClick、onBlur
函数语法字符串函数
阻止默认行为方式事件返回 false使用 e.preventDefault() 方法
/* 1 事件名称命名方式不同 */
// 原生事件 - 全小写
<button onclick="handleClick()">原生事件</button>
// React 合成事件 - 小驼峰
const button = <button onClick={handleClick}>react合成事件</button>


/* 2 事件处理函数写法不同 */
// 原生事件 - 字符串
<button onclick="handleClick()">原生事件</button>
// React 合成事件 - 函数
const button = <button onClick={handleClick}>react合成事件</button>


/* 2 阻止默认行为方式不同 */
// 原生事件 - return false
<a href="" onclick="console.log('阻止原生事件'); return false">原生事件</a>
// React 合成事件 - e.preventDefault();
const handleClick = e => {
  e.preventDefault();
  console.log('阻止原生事件');
}
const clickElement = <a href="" onClick={handleClick}>react合成事件</a>

5 执行顺序

  • React把事件委托到document对象上
  • 当真实DOM元素触发事件,先处理原生事件,然后会冒泡到 document 对象后,再处理 React 事件
  • React事件绑定的时刻是在reconciliation调和阶段,会在原生事件的绑定前执行
  • 目的和优势
    1. 进行浏览器兼容,React 采用的是顶层事件代理机制,能够保证冒泡一致性
    2. 事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象(React17中被废弃)

React 17以前,16版本的事件顺序

import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	parentRef = React.createRef();
	childRef = React.createRef();

	componentDidMount() {
		this.parentRef.current.addEventListener("click", () => {
			console.log("父元素原生捕获");
		}, true);
		this.parentRef.current.addEventListener("click", () => {
			console.log("父元素原生冒泡");
		});

		this.childRef.current.addEventListener("click", () => {
			console.log("子元素原生捕获");
		}, true);
		this.childRef.current.addEventListener("click", () => {
			console.log("子元素原生冒泡");
		});

		document.addEventListener('click', () => {
			console.log("document原生捕获");
		}, true);
		document.addEventListener('click', () => {
			console.log("document原生冒泡");
		});
	}

	parentCapture = () => {
		console.log("父元素React事件捕获");
	};
	parentBubble = () => {
		console.log("父元素React事件冒泡");
	};

	childCapture = () => {
		console.log("子元素React事件捕获");
	};
	childBubble = () => {
		console.log("子元素React事件冒泡");
	};

	render() {
		return (
			<div
				ref={this.parentRef}
				onClick={this.parentBubble}
				onClickCapture={this.parentCapture}>
				<p
					ref={this.childRef}
					onClick={this.childBubble}
					onClickCapture={this.childCapture}>
					事件执行顺序
				</p>
			</div>
		);
	}
}

ReactDOM.render(<Counter />, document.getElementById('root'));

/**
document原生捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
父元素React事件捕获
子元素React事件捕获
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
 */

react-event

实现原理

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

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>

<body>
	<div id="parent">
		<p id="child">事件执行</p>
	</div>

	<script>
		function dispatchEvent(event) {
			let paths = [];
			let current = event.target;
			while (current) { // 从事件源,一直查找到最顶层的事件
				paths.push(current);
				current = current.parentNode;
			}
			console.log(paths); //  [p#child, div#parent, body, html, document]

			//模拟捕获和冒泡,其实在这个时候,原生的捕获阶段已经结束 了
			// 先倒序执行,模拟捕获
			for (let i = paths.length - 1; i >= 0; i--) {
				let handler = paths[i].onClickCapture;
				handler && handler();
			}
			// 然后正序执行,模拟冒泡
			for (let i = 0; i < paths.length; i++) {
				let handler = paths[i].onClick;
				handler && handler();
			}
		}

		//注册React事件的事件委托 
		document.addEventListener('click', dispatchEvent);

		let parent = document.getElementById('parent');
		let child = document.getElementById('child');
		parent.addEventListener("click", () => {
			console.log("父元素原生捕获");
		}, true);
		parent.addEventListener("click", () => {
			console.log("父元素原生冒泡");
		});
		child.addEventListener("click", () => {
			console.log("子元素原生捕获");
		}, true);
		child.addEventListener("click", () => {
			console.log("子元素原生冒泡");
		});

		document.addEventListener('click', () => {
			console.log("document原生捕获");
		}, true);
		//React会执行一个 document.addEventListener('click',dispatchEvent);
		//这个注册是在React注册这后注册的,所以后执行
		document.addEventListener('click', () => {
			console.log("document原生冒泡");
		});

		parent.onClickCapture = function () { console.log('父元素React事件捕获') }
		parent.onClick = function () { console.log('父元素React事件冒泡') }
		
		child.onClickCapture = function () { console.log('子元素React事件捕获') }
		child.onClick = function () { console.log('子元素React事件冒泡') }

		/**
		document原生捕获
		父元素原生捕获
		子元素原生捕获
		子元素原生冒泡
		父元素原生冒泡
		父元素React事件捕获
		子元素React事件捕获
		子元素React事件冒泡
		父元素React事件冒泡
		document原生冒泡
		*/
	</script>
</body>

</html>

React 17以后的事件顺序,恢复为跟浏览器正常顺序一致了,因为事件代理到了挂载容器上,目的是为了一个页面可以有多个react版本

import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	parentRef = React.createRef();
	childRef = React.createRef();

	componentDidMount() {
		this.parentRef.current.addEventListener("click", () => {
			console.log("父元素原生捕获");
		}, true);
		this.parentRef.current.addEventListener("click", () => {
			console.log("父元素原生冒泡");
		});

		this.childRef.current.addEventListener("click", () => {
			console.log("子元素原生捕获");
		}, true);
		this.childRef.current.addEventListener("click", () => {
			console.log("子元素原生冒泡");
		});

		document.addEventListener('click', () => {
			console.log("document原生捕获");
		}, true);
		document.addEventListener('click', () => {
			console.log("document原生冒泡");
		});
	}

	parentCapture = () => {
		console.log("父元素React事件捕获");
	};
	parentBubble = () => {
		console.log("父元素React事件冒泡");
	};

	childCapture = () => {
		console.log("子元素React事件捕获");
	};
	childBubble = () => {
		console.log("子元素React事件冒泡");
	};

	render() {
		return (
			<div
				ref={this.parentRef}
				onClick={this.parentBubble}
				onClickCapture={this.parentCapture}>
				<p
					ref={this.childRef}
					onClick={this.childBubble}
					onClickCapture={this.childCapture}>
					事件执行顺序
				</p>
			</div>
		);
	}
}

ReactDOM.render(<Counter />, document.getElementById('root'));

/**
document原生捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
 */

实现原理

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

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>

<body>
	<div id="root">
		<div id="parent">
			<p id="child">事件执行</p>
		</div>
	</div>

	<script>
		function dispatchEvent(event, useCapture) {
			let paths = [];
			let current = event.target;
			while (current) {
				paths.push(current);
				current = current.parentNode;
			}
			console.log(paths); //  [p#child, div#parent, body, html, document]

			//模拟捕获和冒泡,其实在这个时候,原生的捕获阶段已经结束 了
			if (useCapture) {
				// 倒序执行,模拟捕获
				for (let i = paths.length - 1; i >= 0; i--) {
					let handler = paths[i].onClickCapture;
					handler && handler();
				}
			} else {
				// 正序执行,模拟冒泡
				for (let i = 0; i < paths.length; i++) {
					let handler = paths[i].onClick;
					handler && handler();
				}
			}
		}

		//注册React事件的事件委托,它是先注册的,它要先执行
		let root = document.getElementById('root');
		root.addEventListener('click', (event) => dispatchEvent(event, true), true); // 捕获
		root.addEventListener('click', (event) => dispatchEvent(event, false));//冒泡
		
		let parent = document.getElementById('parent');
		let child = document.getElementById('child');
		parent.addEventListener("click", () => {
			console.log("父元素原生捕获");
		}, true);
		parent.addEventListener("click", () => {
			console.log("父元素原生冒泡");
		});
		child.addEventListener("click", () => {
			console.log("子元素原生捕获");
		}, true);
		child.addEventListener("click", () => {
			console.log("子元素原生冒泡");
		});

		document.addEventListener('click', () => {
			console.log("document捕获");
		}, true);
		document.addEventListener('click', () => {
			console.log("document冒泡");
		});

		parent.onClickCapture = function () { console.log('父元素React事件捕获') }
		parent.onClick = function () { console.log('父元素React事件冒泡') }
		
		child.onClickCapture = function () { console.log('子元素React事件捕获') }
		child.onClick = function () { console.log('子元素React事件冒泡') }

		/**
		document原生捕获
		父元素React事件捕获
		子元素React事件捕获
		父元素原生捕获
		子元素原生捕获
		子元素原生冒泡
		父元素原生冒泡
		子元素React事件冒泡
		父元素React事件冒泡
		document原生冒泡
		*/
	</script>
</body>

</html>

6 React17 事件系统变更

  • 更改事件委托: 首先第一个修改点就是更改了事件委托绑定节点,在16版本中,React都会把事件绑定到页面的document元素上,这在多个React版本共存的情况下就会导致另外一个React版本上绑定的事件没有被阻止触发,所以在17版本中会把事件绑定到render函数的节点上
  • 去除事件池: 17版本中移除了事件对象池,这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用event.persist() 才能正确的使用该事件,或者正确读取需要的属性

7 冒泡函数区别

 document.addEventListener('click',(event)=>{
	//event.stopPropagation(); // 不再向上冒泡了,但是本元素剩下的监听函数还是会执行
	event.stopImmediatePropagation(); // 不但不向上冒泡了,本级剩下的监听函数也不执行
	console.log(1);
});
document.addEventListener('click',(event)=>{
	console.log(2);
});

16合成事件池open in new window

五、Ref

  • Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

1 原生使用Ref

  • 可以使用 ref 去存储 DOM 节点的引用
  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
  • ref的本质就是一个对象,默认key给current,值为null
import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	constructor(props) {
		super(props);
		// 创建Ref
		this.numberA = React.createRef(); // {current:null}
		this.numberB = React.createRef();
		this.result = React.createRef();
	}
	handleClick = (event) => {
		let numberA = this.numberA.current.value;
		let numberB = this.numberB.current.value;
		this.result.current.value = parseFloat(numberA) + parseFloat(numberB);
	}

	render() {
		return (
			<>
			{/* 原生DOM使用Ref */}
				<input ref={this.numberA} />
				<input ref={this.numberB} />
				<button onClick={this.handleClick}>+</button>
				<input ref={this.result} />
			</>
		);
	}
}

ReactDOM.render( <Counter />, document.getElementById('root'));

2 类组件使用Ref

  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
import React from 'react';
import ReactDOM from 'react-dom';

class TextInput extends React.Component {
	constructor(props) {
		super(props);
		this.inputRef = React.createRef();
	}
	getTextInputFocus = () => {
		this.inputRef.current.focus();
	}
	render() {
		return <input ref={this.inputRef} />
	}
}
class Counter extends React.Component {
	constructor(props) {
		super(props);
		this.textInputRef = React.createRef();
	}
	getFormFocus = () => {
		// this.textInputRef.current就会指向TextInput类组件的实例
		this.textInputRef.current.getTextInputFocus();
	}

	render() {
		return (
			<>
				{/* 类组件使用Ref */}
				<TextInput ref={this.textInputRef} />
				<button onClick={this.getFormFocus}>获得焦点</button>
			</>
		);
	}
}

ReactDOM.render(<Counter />, document.getElementById('root'));

实现原理

function createRef(){
    return {current:null};
}

function createElement(type,config,children){
    let ref;//是用来获取虚拟DOM实例的
    if(config){
        ref = config.ref;
        delete config.ref;
    }
    return {
        type,
        props,
        ref,
        key
    }
}

function createDOM(vdom) {
	let { type, props, ref } = vdom;
	let dom;//获取 真实DOM元素
	// TODO 。。。其他逻辑
	//让虚拟DOM的dom属生指向它的真实DOM 
	vdom.dom = dom;
	if (ref) ref.current = dom;//让ref.current属性指向真实DOM的实例
	return dom;
}

3 函数组件使用Ref

  • 不能在函数组件上使用 ref 属性,因为他们没有实例
  • 但是可以通过Ref转发,将函数组件进行包裹,将其Ref传递下去
import React from 'react';
import ReactDOM from 'react-dom';

function TextInput(props, ref) {
	return <input ref={ref} />
}
// forwardRef 包裹函数组件
const ForwardedTextInput = React.forwardRef(TextInput);

class Counter extends React.Component {
	constructor(props) {
		super(props);
		this.textInputRef = React.createRef();
	}
	getFormFocus = () => {
		// this.textInputRef.current就会指向TextInput类组件的实例
		this.textInputRef.current.focus();
	}

	render() {
		return (
			<>
				{/* 函数组件使用Ref */}
				<ForwardedTextInput ref={this.textInputRef} />
				<button onClick={this.getFormFocus}>获得焦点</button>
			</>
		);
	}
}

ReactDOM.render(<Counter />, document.getElementById('root'));

实现原理: 给forwardRef返回的对象上加上ref标识,然后在虚拟DOM转为真实DOM的时候,解析并判断类型,最后执行函数组件的render方法,传入ref,最后挂载到父组件的ref.current属性上

const REACT_FORWARD_REF_TYPE = Symbol('react.forward_ref');
// 1 加上标识,返回一个对象
function forwardRef(render) {
	return {
		$$typeof: REACT_FORWARD_REF_TYPE,
		render // 原来那个函数件
	}
}

// 2 在虚拟DOM转成真实DOM的时候,判断类型,然后进行特殊处理
function createDOM(vdom) {
	let { type, props, ref } = vdom;
	let dom;//获取 真实DOM元素
	//如果type.$$typeof属性是REACT_FORWARD_REF_TYPE值
	if (type && type.$$typeof === REACT_FORWARD_REF_TYPE) {
		return mountForwardComponent(vdom)
	}
	// TODO 。。。其他逻辑
	//让虚拟DOM的dom属生指向它的真实DOM 
	vdom.dom = dom;
	if (ref) ref.current = dom;//让ref.current属性指向真实DOM的实例
	return dom;
}

function mountForwardComponent(vdom) {
	let { type, props, ref } = vdom;
	// 执行函数组件的render方法
	let renderVdom = type.render(props, ref);
	vdom.oldRenderVdom = renderVdom;
	return createDOM(renderVdom);
}

六、生命周期

1 旧版生命周期

初始化 》 挂载 》 更新 》 卸载

react

  • 状态更新生命周期变化
import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	static defaultProps = {
		name: '计算器'
	}

	constructor(props) {
		super(props);
		this.state = {number: 0};
		console.log("Counter 1.constructor 初始化属性和状态");
	}

	handleClick = () => {
		this.setState({number: this.state.number + 1});
	}

	componentWillMount() {
		console.log("Counter 2.componentWillMount 组件将要挂载");
	}

	componentDidMount() {
		console.log("Counter 4.componentWillMount 组件挂载完成");
	}

	// setState会引起状态的变化,父组件更新的时候,会让子组件的属性发生变化
	// 当属性或者状态发生改变的话,会走此方法来决定 是否要渲染更新
	shouldComponentUpdate(nextProps, nextState) {
		console.log('Counter 5.shouldComponentUpdate 决定组件是否需要更新?');
		// 偶数为true 需要更新,奇数为false 不需要更新
		return nextState.number % 2 === 0;
	}

	componentWillUpdate() {
		console.log('Counter 6.componentWillUpdate 组件将要更新');
	}

	componentDidUpdate() {
		console.log('Counter 7.componentDidUpdate 组件更新完成');
	}

	render() {
		console.log("Counter 3.render 挂载");
		return (
			<div>
				<p>{this.state.number}</p>
				<button onClick={this.handleClick}>+</button>
			</div>
		)
	}
}

ReactDOM.render(<Counter />, document.getElementById('root'))

// 初始化挂载
Counter 1.constructor 初始化属性和状态
Counter 2.componentWillMount 组件将要挂载
Counter 3.render 挂载
Counter 4.componentWillMount 组件挂载完成

// 点击加号,返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,返回true
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
Counter 7.componentDidUpdate 组件更新完成
  • 状态和属性一起变化的生命周期
import React from 'react';
import ReactDOM from 'react-dom';

class ChildCounter extends React.Component {
	static defaultProps = {// 1.设置默认属性
		name: 'ChildCounter'
	}

	componentWillMount() {
		console.log('ChildCounter 1.componentWillMount 组件将要挂载');
	}

	render() {
		console.log('ChildCounter 2.render');
		return (<div>{this.props.count}</div>)
	}

	componentDidMount() {
		console.log('ChildCounter 3.componentDidMount 组件挂载完成');
	}

	componentWillReceiveProps(newProps) {
		console.log('ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性');
	}

	shouldComponentUpdate(nextProps, nextState) {
		console.log('ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?');
		// 3的倍数就更新,否则就不更新
		return nextProps.count % 3 === 0;
	}

	componentWillUpdate() {
		console.log('ChildCounter 7.componentWillUpdate  组件将要更新');
	}

	componentDidUpdate() {
		console.log('ChildCounter 8.componentDidUpdate 组件更新完成');
	}

	componentWillUnmount() {
		console.log('ChildCounter 9.componentWillUnmount 组件将要卸载');
	}
}

class Counter extends React.Component {
	static defaultProps = {
		name: 'Counter' // 设值默认属性
	}

	constructor(props) {
		super(props);
		this.state = { number: 0 }; // 设值默认状态
		console.log("Counter 1.constructor 初始化属性和状态");
	}

	handleClick = () => {
		this.setState({ number: this.state.number + 1 });
	}

	componentWillMount() {
		console.log("Counter 2.componentWillMount 组件将要挂载");
	}

	componentDidMount() {
		console.log("Counter 4.componentWillMount 组件挂载完成");
	}

	shouldComponentUpdate(nextProps, nextState) {
		console.log('Counter 5.shouldComponentUpdate 决定组件是否需要更新?');
		// 偶数为true 需要更新,奇数为false 不需要更新
		return nextState.number % 2 === 0;
	}

	componentWillUpdate() {
		console.log('Counter 6.componentWillUpdate 组件将要更新');
	}

	componentDidUpdate() {
		console.log('Counter 7.componentDidUpdate 组件更新完成');
	}

	render() {
		console.log("Counter 3.render 挂载");
		return (
			<div>
				<p>{this.state.number}</p>
				{this.state.number === 4 ? null : <ChildCounter count={this.state.number} />}
				<button onClick={this.handleClick}>+</button>
			</div>
		)
	}
}

ReactDOM.render(<Counter />, document.getElementById('root'))
// 初始化挂载
Counter 1.constructor 初始化属性和状态
Counter 2.componentWillMount 组件将要挂载
Counter 3.render 挂载
	ChildCounter 1.componentWillMount 组件将要挂载
	ChildCounter 2.render
	ChildCounter 3.componentDidMount 组件挂载完成
Counter 4.componentWillMount 组件挂载完成

// 点击加号,state = 1,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,state = 2,偶数返回true
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性
	ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 7.componentDidUpdate 组件更新完成

// 点击加号,state = 3,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,state = 4,偶数返回true,子组件卸载
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 9.componentWillUnmount 组件将要卸载
Counter 7.componentDidUpdate 组件更新完成

// 点击加号,state = 5,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,state = 6,偶数返回true,子组件重新渲染
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 1.componentWillMount 组件将要挂载
	ChildCounter 2.render
	ChildCounter 3.componentDidMount 组件挂载完成
Counter 7.componentDidUpdate 组件更新完成

// ......  一直点击加号到12
// 点击加号,state = 12,偶数返回true,子组件更新
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性
	ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?
	ChildCounter 7.componentWillUpdate  组件将要更新
	ChildCounter 2.render
	ChildCounter 8.componentDidUpdate 组件更新完成
Counter 7.componentDidUpdate 组件更新完成

2 新版生命周期

创建时 》 更新时 》 卸载时

react

  • V17可能会废弃的三个⽣命周期函数⽤getDerivedStateFromProps替代,⽬前使⽤的话加上UNSAFE_:
    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate
  • 引⼊两个新的⽣命周期函数:
    • static getDerivedStateFromProps
    • getSnapshotBeforeUpdate
  • 变更缘由: 原来(React v16.0前)的⽣命周期在React v16推出的Fiber之后就不合适了,因为如果要开启async rendering,在render函数之前的所有函数,都有可能被执⾏多次
import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	static defaultProps = {// 1.设置默认属性
		name: 'ChildCounter'
	}

	constructor(props) {
		super(props);
		this.state = { number: 0 }; // 设值默认状态
		console.log("Counter 1.constructor 初始化属性和状态");
		this.tmpRef = React.createRef();
	}

	/**
	 * 替换旧的componentWillReceiveProps
	 * 从组件的新属性中映射出一个状态
	 * 设计为静态(static)方法的原因:
	 *   1 WillReceiveProps里面调用setState 可能会让父组件刷新,父组件一刷新,子组件也会刷新,会出现死循环
	 *   2 因为static中没有this,就不能调用this.setState方法,就可以避免死循环
	 */
	static getDerivedStateFromProps(nextProps, prevState) {
		console.log("Counter 2.getDerivedStateFromProps", prevState);
		const { number } = prevState;
		if (number % 2 === 0) {
			return { number: number * 2 };
		} else if (number % 3 === 0) {
			return { number: number * 3 }; // 如果返回的分状态,会跟自己的state进行合并
		}
		return null; // 如果返回null,表示不修改状态
	}

	shouldComponentUpdate(nextProps, nextState) {
		console.log('Counter 4.shouldComponentUpdate 决定组件是否需要更新?', nextState);
		// 3的倍数就更新,否则就不更新
		return nextState.number % 4 === 0;
	}

	handleClick = () => {
		this.setState({ number: this.state.number + 1 });
	}

	render() {
		console.log("Counter 3.render");
		return (
			<div ref={this.tmpRef}>
				<p>{this.state.number}</p>
				<button onClick={this.handleClick}>+</button>
			</div>
		)
	}

	componentDidMount() {
		console.log("Counter 4.componentWillMount 挂载完成");
	}

	// 在DOM更新前执行,可以用来获取更新前的一些DOM信息
	getSnapshotBeforeUpdate() {
		console.log('Counter 6.getSnapshotBeforeUpdate 获取DOM更新前的信息');
		return this.tmpRef.current.scrollHeight;
	}
	componentDidUpdate(pervProps, pervState, scrollHeight) {
		//当前向上卷去的高度加上增加的内容高度
		console.log('Counter 7.componentDidUpdate 更新完成');
		console.log('打印高度:', this.tmpRef.current.scrollHeight, scrollHeight);
	}

	componentWillUnmount() {//清除定时器
		console.log('Counter 8.componentWillUnmount 卸载');
	}
}
ReactDOM.render(<Counter />, document.getElementById('root'))

// 初始化
Counter 1.constructor 初始化属性和状态
Counter 2.getDerivedStateFromProps {number: 0}
Counter 3.render
Counter 4.componentWillMount 挂载完成

// 点击加号,state = 1
Counter 2.getDerivedStateFromProps {number: 1}
Counter 4.shouldComponentUpdate 决定组件是否需要更新? {number: 1}

// 点击加号,state = 2
Counter 2.getDerivedStateFromProps {number: 2}
Counter 4.shouldComponentUpdate 决定组件是否需要更新? {number: 4}
Counter 3.render
Counter 6.getSnapshotBeforeUpdate 获取DOM更新前的信息
Counter 7.componentDidUpdate 更新完成
打印高度:60 60

七、组件高级

1 组件通信

  • Props属性传递可⽤于⽗⼦组件相互通信
  • Context跨层级组件之间通信
  • Redux⽆明显关系的组件间通信

2 Context

  • React中使⽤Context实现祖代组件向后代组件跨层级传值。Vue中的provide & inject来源于Context,在Context模式下有两个⻆⾊:
    • Provider:外层提供数据的组件
    • Consumer:内层获取数据的组件
  • 使用Context: 创建Context => 获取Provider和Consumer => Provider提供值 => Consumer消费值
  • React Contextopen in new window

3 高阶组件HOC

  • 高阶组件就是一个工厂函数,传给它一个组件,它返回一个新的组件
  • 高阶组件的作用其实就是为了组件之间的代码复用,主要用途为 属性代理 和反向继承
  • 高级组件来自于高阶函数

属性代理:对原有属性进行扩展

import React from 'react';
import ReactDOM from 'react-dom';

const withLoading = (OldComponent) => {
	return class extends React.Component {
		show = () => {
			let loading = document.createElement('div');
			loading.innerHTML = `<p id="loading" style="position:absolute;top:100px;left:50%;z-index:10;">loading</p>`;
			document.body.appendChild(loading);
		}
		hide = () => {
			document.getElementById('loading').remove();
		}
		render() {
			// 属性代理:对原有属性进行扩展
			return <OldComponent {...this.props} show={this.show} hide={this.hide} />
		}
	}
}

class Panel extends React.Component {
	render() {
		return (
			<div>
				{this.props.title}
				<button onClick={this.props.show}>显示</button>
				<button onClick={this.props.hide}>隐藏</button>
			</div>
		)
	}
}
// 写法一:直接包装使用
let LoadingPanel = withLoading(Panel);
ReactDOM.render(<LoadingPanel title="这是标题" />, document.getElementById('root'));
  • 装饰器写法
// 1. 安装
npm i react-app-rewired customize-cra @babel/plugin-proposal-decorators -D

// 2. 修改package.json
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
}

// 3. 编写config-overrides.js
const { override, addBabelPlugin } = require('customize-cra');

module.exports = override(
	addBabelPlugin(
		[
			"@babel/plugin-proposal-decorators", { "legacy": true }
		]
	)
);

// 4. 编写jsconfig.json
{
    "compilerOptions": {
         "experimentalDecorators": true
    }
}

// 5. 修改高阶组件代码,使用@进行扩展
const withLoading = (OldComponent) => {}

@withLoading
class Panel extends React.Component {
	....
}
ReactDOM.render(<Panel title="这是标题" />, document.getElementById('root'));

反向继承:拦截生命周期、state、渲染过程

// 一个场景:对第三方组件进行扩展

import React from 'react';
import ReactDOM from 'react-dom';

// 第三方组件,不能修改里面的代码,只能进行扩展
class AntDesignButton extends React.Component {
	state = { name: '张三' }
	componentWillMount() {
		console.log('AntDesignButton componentWillMount');
	}
	componentDidMount() {
		console.log('AntDesignButton componentDidMount');
	}
	render() {
		console.log('AntDesignButton render');
		return <button name={this.state.name} title={this.props.title} />
	}
}

// 高阶组件:反向继承
const wrapper = OldComponent => {
	return class extends OldComponent {
		state = { number: 0 }
		componentWillMount() {
			console.log('wrapper componentWillMount');
			super.componentWillMount();
		}
		componentDidMount() {
			console.log('wrapper componentDidMount');
			super.componentDidMount();
		}
		handleClick = () => {
			this.setState({ number: this.state.number + 1 });
		}
		render() {
			console.log('wrapper render');
			// 获取父组件的虚拟dom
			let renderElement = super.render();
			// 扩展新属性
			let newProps = {
				...renderElement.props,
				onClick: this.handleClick
			}
			// 老元素     新属性    新儿子
			return React.cloneElement(renderElement, newProps, this.state.number);
		}
	}
}

let WrappedButton = wrapper(AntDesignButton);
ReactDOM.render(<WrappedButton title="这是标题" />, document.getElementById('root'));

// 初始状态,先子后父
	wrapper componentWillMount
AntDesignButton componentWillMount
	wrapper render
AntDesignButton render
	wrapper componentDidMount
AntDesignButton componentDidMount

// 点击按钮
	wrapper render
AntDesignButton render

React.cloneElement原理

/**
 * 根据一个老的元素,克隆出一个新的元素
 * @param {*} oldElement 老元素
 * @param {*} newProps 新属性
 * @param {*} children 新的儿子们
 */
function cloneElement(oldElement, newProps, children) {
	if (arguments.length > 3) {
		children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
	} else {
		children = wrapToVdom(children);
	}
	// 新属性覆盖老属性  新儿子覆盖老儿子
	let props = { ...oldElement.props, ...newProps, children };
	return { ...oldElement, props };
}

// 不管原来是什么样的元素,都转成对象的形式,方便后续的DOM-DIFF
export function wrapToVdom(element) {
	if (typeof element === 'string' || typeof element === 'number') {
		//返回的也是React元素,也是虚拟DOM
		return { type: REACT_TEXT, props: { content: element } };//虚拟DOM.props.content就是此文件的内容
	} else {
		return element;
	}
}

4 Render Props

  • render-propsopen in new window
  • render prop 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
  • 具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑
  • render prop 是一个用于告知组件需要渲染什么内容的函数 prop,这也是逻辑复用的一种方式
// 移动鼠标获取坐标的案例

import React from 'react';
import ReactDOM from 'react-dom';

// 组件
class MouseTracker extends React.Component {
	state = { x: 0, y: 0 }

	handleMouseMove = event => {
		this.setState({
			x: event.clientX,
			y: event.clientY
		});
	}
	render() {
		return (
			<div onMouseMove={this.handleMouseMove}>
				{/* 
					<h1>移动鼠标</h1>
					<p>当前的鼠标位置是x={props.x}, y={props.y}</p>
				 */}
				{this.props.render(this.state)}
			</div>
		)
	}
}

ReactDOM.render(
	// render属性返回一个函数,props传入到子组件,子组件调用props.render函数传入参数
	<MouseTracker render={
		props => (
			<div>
				<h1>移动鼠标</h1>
				<p>当前的鼠标位置是x={props.x}, y={props.y}</p>
			</div>
		)
	} />
, document.getElementById('root'));

5 React.PureComponent

  • PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能
  • PureComponent会重写并自动执行 shouldComponentUpdate方法,对属性和状态都做浅比较
  • PureComponent的优点:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的
// 实现原理

export class PureComponent extends React.Component {
	// 重写shouldComponentUpdate方法
	shouldComponentUpdate(nextProps, nextState) {
		return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)
	}
}

// 浅比较两个对象
export function shallowEqual(obj1 = {}, obj2 = {}) {
	if (obj1 === obj2) { // 两个对象一样
		return true;
	}
	// 不是对象为false
	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;
	}
	// 遍历,逐个判断key和val
	for (let key of keys1) {
		if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
			return false;
		}
	}
	return true;
}

6 React.memo

  • React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo只能用于函数组
  • React.memo 默认是浅比较,可以传入第二个参数进行自定义深比较
// 使用案例
import React from 'react';
import ReactDOM from 'react-dom';

function SubCounter(props) {
	return <div>{props.count}</div>
}
// 自定义深比较
// let MemoSubCounter = React.memo(SubCounter, (prevProps, nextProps) => {
// 	return JSON.stringify(prevProps) === JSON.stringify(nextProps);
// });

// 默认浅比较
let MemoSubCounter = React.memo(SubCounter);

class Counter extends React.Component {
	state = { number: 0 }
	handleClick = (event) => {
		this.setState({ number: this.state.number + 1 });
	}
	render() {
		console.log('Counter render');
		return (
			<div>
				<p>{this.state.number}</p>
				<button onClick={this.handleClick}>+</button>
				{/* 使用memo组件 */}
				<MemoSubCounter count={this.state.number} />
			</div>
		)
	}
}
ReactDOM.render(<Counter />, document.getElementById('root'));

实现原理:React.memo返回的是一个对象

// memo封装了PureComponent
function memo(OldComponent) {
	return class extends React.PureComponent {
		render() {
			return <OldComponent {...this.props} />
		}
	}
}

// react中定义具体的虚拟dom对象,在react-dom中domdiff的时候判断类型比做出具体渲染
export const REACT_MEMO = Symbol('react.memo');

function memo(type, compare = shallowEqual) {
	return {
		$$typeof: REACT_MEMO,
		type,//原来那个真正的函数组件
		compare
	}
}