从零开始写一个React - setState和生命周期
在前面一篇中我们用instantiateReactComponent
方法来根据node
的不同来返回不同的组件实例,之前的分类可能有些问题,就是当该组件中JSX
部分有返回null
的情况,instantiateReactComponent
就不能返回正确的组件,所以在这里加了一种新的组件类型:ReactEmptyComponent
,作用就是返回一段空的注释,标记这是一个空组件:
export function instantiateReactComponent(node) {
if (lodash.isNull(node) || lodash.isUndefined(node)) {
return new ReactEmptyComponent(null);
}
// ...
}
// 空组件
export class ReactEmptyComponent {
constructor(node) {
this.type = "ReactEmptyComponent";
this._currentElement = null;
this._rootNodeID = null;
}
/**
* 空组件挂载直接返回一段空注释回去
*/
mountComponent(rootID) {
this._rootNodeID = rootID;
return `<!-- empty component data-reactid="${this._rootNodeID}" -->`;
}
}
我们之前简单实现了一个初始化渲染的过程,现在我们一起实现一个setState
方法以及组件后面的更新逻辑。setState
是在组件中被调用的,所以我们需要在之前的Component
类中加入一个setState
方法:
export class Component {
// ...
setState(newState, callback) {
const stacks = StackTrace.getSync();
for(let {functionName, source} of stacks) {
if(RENDER_REG.test(functionName) && RENDER_REG.test(source)) {
throw new Error("callStack Error: you can't call setState in render method!");
}
}
this._reactInternalInstance.receiveComponent(null, newState);
if (lodash.isFunction(callback)) {
callback();
}
}
// ...
}
之前一篇我们说到一共可分成文本组件,浏览器标签组件,自定义标签组件,所以我们需要在这三个组件中各实现一个receiveComponent
来接收新组件并且实现相应更新:
对于普通的文本节点,要做的相对简单,就是在receiveComponent
中去更新相关DOM
的textContent
:
export class ReactDOMTextComponent {
// ...
/**
* 接收到新组件
* @param {String} text [接收到的新组件]
*/
receiveComponent(text) {
const nextStringText = ("" + text);
if (nextStringText !== this._currentElement) {
this._currentElement = nextStringText;
// 更新相关DOM的textContent
$(`[data-reactid='${this._rootNodeID}']`).textContent = nextStringText;
}
}
}
在自定标签组件中,我们需要做的事情大概如下
- 如果调用时传入了新的
Vnode
就把当前的_currentElement
改成新传入的Vnode
- 合并新老
state
- 调用组件实例下的
shouldComponentUpdate
根据返回的布尔值去判断是否需要更新组件 - 调用组件实例下的
componentWillUpdate
- 调用组件的
render
去拿到新的Vnode
,和之前的做对比,如果之前的组件Vnode
不存在,就直接调用instantiateReactComponent
返回新的组件实例 - 调用组件生命周期下的
componentDidUpdate
方法
export class ReactCompositeComponent {
// ...
/**
* 接收到新组件, 更新实例下的state, 组件生命周期方法调用
* @param {ReactElement} nextElement [新的Vnode]
* @param {Object} newState [this.setState(state)中的state]
*/
receiveComponent(nextElement, newState) {
// 如果接受了新的, 就使用最新的element
this._currentElement = nextElement || this._currentElement;
const { _rootNodeID } = this;
let inst = this._instance,
// nextState和nextProps的处理
nextState = Object.assign(inst.state || {}, newState),
nextProps = lodash.clone(this._currentElement.props),
finalProps,
prevComponentInstance,
prevRenderedElement,
nextRenderedElement,
nextMarkup,
child;
// 修改组件的state和props
this._instance.state = nextState;
this._instance.props = nextProps;
inst.state = nextState;
inst.props = nextProps;
// 声明周期shouldComponentUpdate
if (!inst.shouldComponentUpdate(nextProps, nextState)) {
return;
}
// 声明周期componentWillUpdate
inst.componentWillUpdate(nextProps, nextState);
// 之前的组件组件实例
prevComponentInstance = this._renderedComponent;
// 之前的组件元素
prevRenderedElement = prevComponentInstance._currentElement;
// 即将被渲染的新组件元素
nextRenderedElement = inst.render();
// 判断是需要更新还是直接就重新渲染
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
prevComponentInstance.receiveComponent(nextRenderedElement);
} else {
// 重新new一个对应的component
this._renderedComponent = instantiateReactComponent(nextRenderedElement);
// 重新生成对应的元素内容
nextMarkup = this._renderedComponent.mountComponent(_rootNodeID);
// 替换整个节点
$(`[data-reactid="${_rootNodeID}"]`).innerHTML = nextMarkup;
}
inst.componentDidUpdate();
}
}
在组件的render
被重新调用之后,最后还是需要要更新DOM
的,所以在ReactDOMComponent
下的receiveComponent
里我们需要对组件里的DOM
下的属性和结构进行更新。
在React
中,有一套diff
算法来比较新老组件间的差异,返回需要更新的队列,然后统一对DOM
结构进行更新,在ReactDOMComponent
下的receiveComponent
中,我们需要完成下面的几件事情
- 拿到老的
props
和新的props
做,在_updateDOMProperties
中对DOM
下的属性进行更新 - 调用
_updateDOMChildren
,传入新的组件子节点,去拼凑差异队列,然后更新DOM
- 修改
currentElement
变成本次渲染的,供下次使用
export class ReactDOMComponent {
/**
* 接收到新组件
* @param {Object} nextElement [新组件]
*/
receiveComponent(nextElement) {
const lastProps = lodash.clone(this._currentElement.props),
nextProps = nextElement.props;
// 需要单独的更新属性
this._updateDOMProperties(lastProps, nextProps);
// 再更新子节点
this._updateDOMChildren(nextElement.props.children);
// 修改currentElement变成本次渲染的
this._currentElement = nextElement;
}
/**
* 更新组件中相关DOM的属性
* @param {Object} lastProps [旧属性]
* @param {Object} nextProps [新属性]
* @private
*/
_updateDOMProperties(lastProps, nextProps) {
const { _rootNodeID } = this,
element = $(`[data-reactid="${_rootNodeID}"]`);
let propKey, propValue, eventType, removed;
for (propKey in lastProps) {
// 只删除老属性中有但是新属性中没有的
if (hasOwnProperty(lastProps, propKey) && !hasOwnProperty(nextProps, propKey)) {
propValue = lastProps[propKey];
// 之前的事件代理需要解除
if (EVENT_REG.test(propKey)) {
eventType = propKey.replace("on", "");
Event.undelegate({
element: doc,
type: eventType,
selector: `[data-reactid="${_rootNodeID}"]`
});
continue;
} else if (propKey === "className") {
removed = "class";
} else {
removed = propKey;
}
// 删除DOM上的相关属性
element.removeAttribute(removed);
}
}
// 开始遍历新属性集合
for (propKey in nextProps) {
if (hasOwnProperty(nextProps, propKey) && propKey !== "children") {
propValue = lastProps[propKey];
// 重新代理事件
if (EVENT_REG.test(propKey)) {
eventType = propKey.replace("on", "");
Event.undelegate({
element: doc,
type: eventType,
selector: `[data-reactid="${_rootNodeID}"]`
});
Event.delegate({
element: doc,
type: eventType,
selector: `[data-reactid="${_rootNodeID}"]`,
handler: propValue,
context: null
});
continue;
} else if (propKey === "className") {
element.setAttribute("class", propValue);
} else if (propKey === "style") {
if (lodash.isObject(propValue)) {
propValue = toStyle.string(propValue);
}
element.setAttribute(propKey, propValue);
} else {
element.setAttribute(propKey, propValue);
}
}
}
}
/**
* 更新子元素
* @param {Array} nextChildrenElements [被更新的组件队列]
*/
_updateDOMChildren(nextChildrenElements) {
if (nextChildrenElements && nextChildrenElements.length) {
update.updateDepth++;
// 递归找出差别, 组装差异对象
update.diff(update.diffQueue, nextChildrenElements, this);
update.updateDepth--;
// 应用更新
if (update.updateDepth === 0) {
update.patch(update.diffQueue);
}
}
}
}
在_updateDOMChildren
中我们调用了update.diff
和update.patch
方法,一个对比一个应用,这里我是把diff
和patch
放到一个对象下作为一个模块暴露出去的,下面就是具体的实现代码:
// 定义更新类型(移动已经存在的,删除节点,插入的新标签)
const UPDATE_TYPES = {
MOVE_EXISTING: 1,
REMOVE_NODE: 2,
INSERT_MARKUP: 3
};
export const update = {
// 更新深度标识
updateDepth: 0,
// 更新队列
diffQueue: [],
/**
* 递归找出差别, 组装差异对象, 添加到更新队列diffQueue
* @param {Array} diffQueue [更新队列]
* @param {Array} nextChildrenElements [新的子组件集合]
* @param {Object} component [被diff的组件]
* @return {Array} [需要更新的内容]
*/
diff(diffQueue, nextChildrenElements, component) {
// 获取到当前组件下已经渲染的组件集合,把component._renderedChildren扁平成一个对象,如果child有key,就拿key作为对应的属性名,否则用下标做属性名,具体实现可以看下面
const prevChildren = flattenChildren(component._renderedChildren),
// 生成新的子节点的component对象集合(如果是组件有更新, 就复用原来的, 如果是新增就是新的组件实例)
nextChildren = generateComponentChildren(prevChildren, nextChildrenElements);
let lastIndex = 0,
nextIndex = 0,
prevChild = null,
nextChild = null,
name, props, propKey, eventType;
// 枚举nextChildren
for (name in nextChildren) {
if (!hasOwnProperty(nextChildren, name)) {
continue;
}
prevChild = prevChildren && prevChildren[name];
nextChild = nextChildren[name];
// 两个相同说明是使用的同一个component,所以我们需要做移动的操作
if (lodash.isEqual(prevChild, nextChild)) {
if (prevChild._mountIndex < lastIndex) {
this.diffQueue.push({
parentId: component._rootNodeID,
parentNode: $(`[data-reactid="${component._rootNodeID}"]`),
type: UPDATE_TYPES.MOVE_EXISTING,
// 从组件原来的mountIndex
fromIndex: prevChild._mountIndex,
// 到nextIndex
toIndex: nextIndex
});
}
// 缓存上次遍历时最大的index
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
} else {
// 之前存在子节点, 需要先将子节点移除
if (prevChild) {
this.diffQueue.push({
parentId: component._rootNodeID,
parentNode: $(`[data-reactid="${component._rootNodeID}"]`),
type: UPDATE_TYPES.REMOVE_NODE,
fromIndex: prevChild._mountIndex,
toIndex: null
});
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
props = (prevChild._currentElement && prevChild._currentElement.props) ? prevChild._currentElement.props : {};
// 对移除的子节点需要进行事件代理的接触,防止重复
for (propKey in props) {
if (hasOwnProperty(props, propKey) && EVENT_REG.test(propKey)) {
eventType = propKey.replace("on", "");
Event.undelegate({
element: doc,
type: eventType,
selector: `[data-reactid="${prevChild._rootNodeID}"]`
});
}
}
}
// 新增的节点, 需要push到diffQueue
if (nextChild) {
this.diffQueue.push({
parentId: component._rootNodeID,
parentNode: $(`[data-reactid="${component._rootNodeID}"]`),
type: UPDATE_TYPES.INSERT_MARKUP,
fromIndex: null,
toIndex: nextIndex,
markup: nextChild.mountComponent(`${component._rootNodeID}.${name}`)
});
}
}
// 更新_mountIndex和nextIndex
nextChild._mountIndex = nextIndex;
nextIndex++;
// 把nextChildren克隆一份给_renderedChildren
component._renderedChildren = makeArray(nextChildren);
}
},
/**
* 应用更新, 执行DOM操作
* @param {Array} updates [差异对象集合]
*/
patch(updates) {
let initialChildren = {},
deleteChildren = [],
updatedIndex, updatedChild, parentID;
for (let update of updates) {
updatedIndex = update.fromIndex;
updatedChild = update.parentNode.children[updatedIndex];
parentID = update.parentID;
// 把所有需要更新的节点都保存下来
initialChildren[parentID] = initialChildren[parentID] || [];
// 使用parentID作为简易命名空间
initialChildren[parentID][updatedIndex] = updatedChild;
// 所有需要修改的节点先删除,对于move的,后面再重新插入到正确的位置即可
if (!lodash.isNull(updatedChild) && !lodash.isUndefined(updatedChild)) {
deleteChildren.push(updatedChild);
}
}
// 删除需要删除的节点
for (let child of deleteChildren) {
child.parentNode.removeChild(child);
}
for (let updateItem of updates) {
switch (updateItem.type) {
// 插入新元素
case UPDATE_TYPES.INSERT_MARKUP:
insertChildAt(updateItem.parentNode, updateItem.markup, updateItem.toIndex);
break;
// 元素位置发生改变
case UPDATE_TYPES.MOVE_EXISTING:
insertChildAt(updateItem.parentNode, initialChildren[updateItem.parentID][updateItem.fromIndex], updateItem.toIndex);
break;
// 上面已经删除, 所以不需要处理
case UPDATE_TYPES.REMOVE_NODE:
break;
default:
break;
}
}
// 重置相关变量
this.reset();
},
/**
* 重置相关变量
*/
reset() {
this.updateDepth = 0;
this.diffQueue = [];
}
};
在diff
中,我们看到了两个新方法,分别是flattenChildren
和generateComponentChildren
,我们先看下flattenChildren
的实现:
/**
* 把原来是数组的子组件集合转换成Map返回
* @param {Array} componentChildren [子组件集合]
* @return {Object} [输出的Map, 每个子组件的key或者一个随机数做key]
*/
function flattenChildren(componentChildren) {
let childrenMap = {},
child, name, i, len;
for (i = 0, len = componentChildren.length; i < len; i++) {
child = componentChildren[i];
name = child && child._currentelement && child._currentelement.key ? child._currentelement.key : i.toString(36);
childrenMap[name] = child;
}
return childrenMap;
}
在generateComponentChildren
我们大概需要完成下面几件事情:
- 遍历拿到即将渲染的新组件
children
(做参数nextChildrenElements
传入)和老节点进行对比 - 如果老节点存在且和新节点有差异,即调用老节点下的
receiveComponent
去更新 - 否则如果老节点不存在,则重新调用
instantiateReactComponent
返回一个组件实例
/**
* 生成子节点elements的component集合
* @param {Object} prevChildren [flattenChildren返回的Map]
* @param {Array} nextChildrenElements [即将要渲染的节点]
* @return {Object} [子节点elements的component集合]
*/
function generateComponentChildren(prevChildren, nextChildrenElements) {
let nextChildren = {},
index, len, name, prevChild, prevElement, nextElement, nextChildInstance, element;
nextChildrenElements = nextChildrenElements || [];
for (index = 0, len = nextChildrenElements.length; index < len; index++) {
element = nextChildrenElements[index];
name = (element && element.key) ? element.key : index;
prevChild = prevChildren && prevChildren[name];
prevElement = prevChild && prevChild._currentElement;
nextElement = element;
// 组件有更新, 调用当前组件下的reciveComponent去更新组件
if (shouldUpdateReactComponent(prevElement, nextElement)) {
prevChild.receiveComponent(nextElement);
nextChildren[name] = prevChild;
} else {
// 新节点, 实例化新组件
nextChildInstance = instantiateReactComponent(nextElement);
nextChildren[name] = nextChildInstance;
}
}
return nextChildren;
}
在之前好几个地方我们都看到了shouldUpdateReactComponent
这个方法,它完成的功能主要是判断两个Vnode
之前是否有差异,返回布尔值,主要完成下面几件事情:
- 如果新老
Vnode
有一个或者都为空,直接返回false
- 文本节点之间的对比
- 当新的
Vnode
的type
是object
,比较老节点的type
和key
,并且拿到两者的children
数组,做一个简单的长度对比
/**
* 判断组件是否需要更新
* @param {Object} prevElement [老的vnode]
* @param {Object} nextElement [新的vnode]
* @return {Boolean} [标识组件是否需要更新]
*/
export function shouldUpdateReactComponent(prevElement, nextElement) {
// 排除为空的情况
if (!lodash.isNull(prevElement) && !lodash.isNull(nextElement) && !lodash.isUndefined(prevElement) && !lodash.isUndefined(nextElement)) {
const prevType = typeof prevElement,
nextType = typeof nextElement;
// 纯文本组件
if (prevType === "number" || prevType === "string") {
return (nextType === "number" || nextType === "number");
} else {
let prevChildren = [],
nextChildren = [],
childEqual = true;
if(prevElement && prevElement.props) {
prevChildren = prevElement.props.children || [];
}
if (nextElement && nextElement.props) {
nextChildren = nextElement.props.children || [];
}
childEqual = prevChildren.length === nextChildren.length;
return (nextType === "object" &&
(prevElement.type === nextElement.type) &&
(prevElement.key === nextElement.key) &&
childEqual);
}
}
return false;
}
到这里我们就实现一个setState
和生命周期,在例子中实现了一个Todo
,一起看下效果
至此我们就实现了一个简单的React
,但是仅仅实现了虚拟节点
,差异算法
,props单向数据流
,还有很多更优秀的没实现,比如批量更新,事件优化,组件中的refs
,服务端渲染等等,只是一个玩具,对于想深入了解React
原理的可能会有些帮助。