类组件的状态更新依赖setState方法。setState方法接收两种形式的参数:对象--同步更新;函数--异步更新。本文先做同步更新的处理,最后处理异步更新。
一、准备知识
1. 组件状态
- 组件的数据来源:属性对象、状态对象
- 属性是父组件传递过来的
- 状态是自己内部的,改变状态唯一的方式时
setState
- 属性和状态的变化都会影响视图更新
- 不要直接修改state,
构造函数
是唯一可以给this.state
赋值的地方
2. 绑定事件
React中绑定事件和原生不一样:
- 属性为驼峰命名而不是小写
- 值是函数的引用地址而不是字符串
二、实现思路
1. 实现setState()
setState()方法的调用,实际上是实例去父类Component上拿到setState方法,因此:
在component.js
中,给Component
类加上setState
方法,入参为partialState(部分状态)
,来记录你需要更改的状态。内部调用更新器的addState
方法,委托给更新器去进行状态的更新。
2. 实现更新器-状态更新
Updater
本质是一个类,继承Component组件实例。内部存放了组件实例、将要更新的状态队列pendingStates
(因为setState不是立即修改,所以需要集合来保存所有需要更新的状态)。
- addState:入参状态对象、把状态存进等待更新的数组,调用触发更新方法。
创建
shoudlUpdate
方法用于更新状态。状态的更新依赖于组件实例的state和最新的状态state,为方便复用我们抽出getState
方法专门用于计算将要更新的新状态。状态的更新引起组件的更新,我们调用实例的forceUpdate
强制更新方法。
- emitUpdate:触发更新。调用更新组件方法。
- shoudlUpdate:更新状态。解构出类的实例
classInstance
和等待生效的状态数组pendingStates
,如果状态数组有值,调用shouldUpdate
更新状态的方法,传入类的实例和最新状态。 - getState:基于老状态的值获取新状态。解构出类的实例
classInstance
和等待更新的状态数组pendingStates
,遍历pendingStates
获取每一个要更新的值,从实例上解构出老状态state
,进行状态合并,合并后清空pendingStates
,返回合并后的state
。 - shouldUpdate:入参类的实例、新状态。把新状态赋值给类的老状态state。调用Component实例的强制更新方法forceUpdate。
3. 实现forceUpdate--组件更新
怎么触发页面更新?每个页面对应的是一个div/span等(只能返回唯一的React元素)。因此:需要基于新的状态重新调用render方法,返回新的虚拟DOM ===> 把新的虚拟DOM转换为新的真实DOM ===> 把新的真实DOM替换掉老的页面结构
- forceUpdate: 调用
render
方法获取基于新的状态要渲染新的虚拟DOM,把虚拟DOM转为真实DOM的方法createDOM,定义在react-dom.js,因此这一步骤在react-dom.js中实现。引入compareToVdom
,传入需要的三个参数。
- 父真实DOM:
oldDOM.parentNode
- 老的虚拟DOM:在第一次挂载
mountClassCompoent
的时候,要在类的实例上添加oldRenderVdom
属性,记录老的虚拟DOM。之后就可以通过oldRenderVdom
属性获取组件对应的老的虚拟DOM - 新的虚拟DOM:调用组件的
render
方法获取 重新渲染之后,要把这次新的虚拟DOM赋值给oldRenderVdom
属性作为下一次需要更新时老的虚拟DOM的值。
- compareToVdom:新老虚拟DOM进行比较,返回新的虚拟DOM。入参为
parentDOM
父真实DOM、oldVdom
老虚拟DOM、newVdom
新的虚拟DOM。调用原生方法replaceChild
把上一次虚拟DOM对应的真实DOM替换为新的DOM
4. 实现事件绑定
要想实现交互-->状态更新-->页面更新,首先需要绑定交互事件。事件是作为props传递给组件的,因此我们在
updateProps
中进行判断,进行事件的绑定
如果属性是以'on'开头说明属性是一个事件处理函数,拿本案例举例:点击事件就需要变成dom.onclick = 处理函数的形式
。
5. 实现异步更新
React源码中,setState支持两种传参形式:1.对象--同步;对象可以直接展开属性进行合并;2.函数--异步。函数要基于上个状态计算下个状态,最后进行合并。因此要把上面的代码做一点改进,也要支持函数参数。
- getState: 在遍历每个属性时,先判断属性类型是否为
'function'
,如果是,把状态传入函数调用,返回的新状态再重新赋值给nextState
。接着进行状态的合并、清空将要更新的状态数组等。
三、实现类组件状态更新
1. src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
/**
* 1.组件的数据来源的有两个,一个是来自于父组件的属性,组件内可以通过this.props获取 属性是父组件的不能修改
* 2.组件的状态 state 状态对象是自己内部初始化的,可以修改,唯一能修改状态的方法就是setState
* React中绑定事件和原生不一样1,属性不是小写,而是驼峰命名 2.值不是字符串而是函数的引用地址
* 调用setState和直接 修改state有区别
* 不管改属性或者setState改状态都会引起组件的重新刷新,让视图更新
*/
class Counter extends React.Component {
constructor(props) {
super(props);
//唯一能给state赋值的地方只有构造函数
this.state = { number: 0, title: "计数器" };
}
handleClick = (event) => {
//更改状态的时候 ,只需要传递更新的变量即可。
//调用了setState之后,状态this.state并没有立刻修改,面是等handleClick执行完了之后才去更新
debugger;
this.setState({ number: this.state.number + 1 });
console.log(this.state);
};
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
ReactDOM.render(<Counter />, document.getElementById("root"));
2. src/component.js
import { compareToVdom } from "./react-dom";
/** 更新器 */
class Updater {
constructor(classInstance) {
// 保存实例
this.classInstance = classInstance;
// 等待更新的状态数组
this.pendingStates = [];
}
addState(partialState) {
this.pendingStates.push(partialState);
// 触发更新
this.emitUpdate();
}
emitUpdate() {
this.updateComponent();
}
updateComponent() {
// 解构出实例、等待更新的状态
let { classInstance, pendingStates } = this;
if (pendingStates.length > 0) {
shouldUpdate(classInstance, this.getState());
}
}
/** 基于老状态和pendingStates获取新状态 */
getState() {
let { classInstance, pendingStates } = this;
let { state } = classInstance; //老状态
// 每个分状态 ==> 合并属性
pendingStates.forEach((nextState) => {
state = { ...state, ...nextState };
});
// 清空等待更新的分状态数组
pendingStates.length = 0;
return state;
}
}
function shouldUpdate(classInstance, nextState) {
classInstance.state = nextState; // 先把新状态赋值给实例的state
classInstance.forceUpdate(); // 让类的实例强行更新
}
class Component {
// 当子类继承父类的时候,父类的静态属性也是可以继承的
// 函数组件和类组件编译后,都会变成函数,因此加上isReactComponent属性来区分是函数组件还是类组件
static isReactComponent = true;
constructor(props) {
this.props = props;
this.state = {}; // 初始值
this.updater = new Updater(this);
}
/** 更新分状态 */
setState(partialState) {
this.updater.addState(partialState);
}
/** 根据新的属性状态,计算新的要渲染的虚拟DOM */
forceUpdate() {
//获取老的虚拟DOM
let oldRenderVdom = this.oldRenderVdom;
//获取老的真实DOM
let oldDOM = oldRenderVdom.dom;
// 基于新的属性和状态,计算新的真实DOM
let newRenderVdom = this.render();
compareToVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
// 使下次更新时以上次的新DOM为比较
this.oldRenderVdom = newRenderVdom;
}
}
export { Component };
3. src/react.js
import { wrapToVdom } from "./utils";
import { Component } from "./component";
function createElement(type, config, children) {
//children永远都是数组
let ref, key;
if (config) {
delete config.__source; // source:bable编译时产生的属性
delete config.__self;
ref = config.ref; // ref可以用来引用这个真实DOM元素
key = config.key; // 用来进行DOM-DIFF优化的,是用来唯一标识某个子元素的
delete config.ref;
delete config.key;
}
let props = { ...config };
if (arguments.length > 3) {
// 如果入参多余3个,说明有多个子元素,截取后,以数组形式保存
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else if (arguments.length === 3) {
props.children = wrapToVdom(children); // 可能是React元素对象,也可能是string/number/null/und
}
return {
type,
ref,
key,
props,
};
}
const React = {
createElement,
Component,
};
export default React;
4. react-dom.js
import { REACT_TEXT } from "./constants";
/**
*把虚拟DOM变成真实DOM插入容器
* @param {*} vdom 虚拟DOM/React元素
* @param {*} container 真实DOM容器
*/
function render(vdom, container) {
mount(vdom, container);
}
/** 页面挂载真实DOM */
function mount(vdom, parentDOM) {
//把虚拟DOM变成真实DOM
let newDOM = createDOM(vdom);
//把真实DOM追加到容器上
parentDOM.appendChild(newDOM);
}
/**
* 把虚拟DOM变成真实DOM
* @param {*} vdom 虚拟DOM
* @return 真实DOM
*/
function createDOM(vdom) {
if (!vdom) return null; // null/und也是合法的dom
let { type, props } = vdom;
let dom; //真实DOM
if (type === REACT_TEXT) {
// 如果元素为文本,创建文本节点
dom = document.createTextNode(props.content);
} else if (typeof type === "function") {
> > > if (type.isReactComponent) {
> > > // 说明这是一个类组件
> > > return mountClassComponent(vdom);
> > > } else {
// 函数组件
return mountFunctionComponent(vdom);
}
} else if (typeof type === "string") {
//创建DOM节点 span div p
dom = document.createElement(type);
}
// 处理属性
if (props) {
//更新DOM的属性 后面我们会实现组件和页面的更新。
updateProps(dom, {}, props);
let children = props.children;
//如果说children是一个React元素,也就是说也是个虚拟DOM
if (typeof children === "object" && children.type) {
//把这个儿子这个虚拟DOM挂载到父节点DOM上
mount(children, dom);
} else if (Array.isArray(children)) {
reconcileChildren(children, dom);
}
}
vdom.dom = dom; // 给虚拟dom添加dom属性指向这个虚拟DOM对应的真实DOM
return dom;
}
/** 挂载类组件 */
function mountClassComponent(vdom) {
let { type: ClassComponent, props } = vdom;
// 把类组件的属性传递给类组件的构造函数,
// 创建类组件的实例,返回组件实例对象
let classInstance = new ClassComponent(props);
//可能是原生组件的虚拟DOM,也可能是类组件的的虚拟DOM,也可能是函数组件的虚拟DOM
let renderVdom = classInstance.render();
//在第一次挂载类组件的时候让类实例上添加一个oldRenderVdom=renderVdom
classInstance.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}
/** 挂载函数组件 */
function mountFunctionComponent(vdom) {
let { type: functionComponent, props } = vdom;
//获取组件将要渲染的虚拟DOM
let renderVdom = functionComponent(props);
return createDOM(renderVdom);
}
/** 如果子元素为数组,遍历挂载到容器 */
function reconcileChildren(children, parentDOM) {
children.forEach((childVdom) => mount(childVdom, parentDOM));
}
/**
* 把新的属性更新到真实DOM上
* @param {*} dom 真实DOM
* @param {*} oldProps 旧的属性对象
* @param {*} newProps 新的属性对象
*/
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
if (key === "children") {
// 子节点另外处理
continue;
} else if (key === "style") {
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
> > > } else if (/^on[A-Z].*/.test(key)) {
> > > // 绑定事件 ==> dom.onclick = 事件函数
> > > dom[key.toLowerCase()] = newProps[key];
> > > } else {
dom[key] = newProps[key];
}
}
for (let key in oldProps) {
//如果说一个属性老的属性对象里有,新的属性没有,就需要删除
if (!newProps.hasOwnProperty(key)) {
dom[key] = null;
}
}
}
/**
* @param {*} parentDOM 父真实DOM
* @param {*} oldVdom 老的虚拟DOM
* @param {*} newVdom 新的虚拟DOM
*/
export function compareToVdom(parentDOM, oldVdom, newVdom) {
// 获取oldRenderVdom对应的真实DOM
let oldDOM = oldVdom.dom;
// 根据新的虚拟DOM得到新的真实DOM
let newDOM = createDOM(newVdom);
// 把老的真实DOM替换为新的真实DOM
parentDOM.replaceChild(newDOM, oldDOM);
}
const ReactDOM = {
render,
};
export default ReactDOM;
四、总结
就本文计数器案例来讲,当点击加号触发事件回调:
updateProps绑定事件 --> setState更改状态 --> updater.addState传入需要更新的状态数组 --> pendingStates.push把分状态添加到队列 --> emitUpdate触发更新 --> updateComponent更新组件 --> shouldUpdate更新状态 --> getState合并状态 --> shouldUpdate赋值新状态 --> forceUpdae强制更新 --> compareToVdom替换为新的组件