大家好,我是微微笑的蜗牛,🐌。
上一篇文章介绍了 virtual dom,以及简单的 diff 操作。
这篇文章将介绍 Component、State、组件生命周期的实现。
React.Component
在 React 中,Component 是所有组件的基类。
它内部定义了 render 方法用于返回节点信息,同时包含 state、props 属性用于存储数据。在更新 state 后,组件会自动根据前后数据差异来更新页面。
比如我们可自定义 MyComponent,继承自 React.Component。在 render 方法中返回节点描述。
class MyComponent extends React.Component {
render() {
const { name } = this.state;
return <span>{name}</span>;
}
}
在使用自定义组件时,像标签一样使用即可。
const element = (
<MyComponent id="container">
<a href="/bar">bar</a>
</MyComponent>
);
上述也是 jsx 的写法,那么它在经过 babel 转义后,结构会与 div 这类内置标签的有所不同吗?
通过 babel 提供的工具,以上代码转义后的结构如下:
image我们可以发现,组件和普通标签转换后还是有些差异的。
标签传入的节点类型是字符串,而组件却是类 MyComponent。所以,这可作为区分它们的标志。
Component 实现
我们可以仿造 React 定义自己的 Component 基类,包括 props 和 state。
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
}
由于组件和标签经 babel 转换后的类型不同,各自的处理也不一样。这样一来,构建 virutal dom 的实现需要做出修改,根据类型做不同处理。
// virtual dom,保存真实的 dom,element,childInstance
function instantitate(element) {
const { type, props } = element;
// 如果 type 是字符串,则表明非组件
const isDomElement = typeof type === "string";
if (isDomElement) {
// 省略
} else {
// 处理组件
}
}
组件 vdom 结构
由于组件的 render 方法返回的是节点信息,那么我们必然会主动调用到 render 方法来获取节点描述信息。因此,在 vdom 中关联组件的实例是很有必要的,这样才能方便调用到组件的 render 方法。
所以呢,在原有 vdom 的结构中就需多出一个字段来记录其关联的组件实例,我们称之为 publicInstance。
同时,由于组件的 render 方法只能返回一个节点,所以子节点的 virtual dom 无需用数组来保存,只需一个普通对象即可。
组件 vdom 结构修改如下:
- 新增 publicInstance,保存关联的组件实例。
- 子节点 vdom 由
childInstances → childInstance
,不再是数组。
组件 vdom 各字段定义如下:
{
dom,
element,
childInstance,
publicInstance,
}
组件 vdom 构建
组件 vdom 构建方式也有所改变,大体过程如下:
- 创建组件实例
- 调用组件 render 方法,返回节点描述信息
- 创建子节点 dom & vdom
- 返回 vdom 结构
const instance = {};
// 创建组件实例
const publicInstance = createPublicInstance(element, instance);
// 调用组件的 render 方法,返回节点
const childElement = publicInstance.render();
// 调用 instantiate 创建 dom & virual dom,因为 render 方法只能返回一个节点
const childInstance = instantitate(childElement);
const dom = childInstance.dom;
Object.assign(instance, {
dom,
element,
childInstance,
publicInstance,
});
return instance;
State
我们知道,Component 是以数据来驱动界面刷新的。当要刷新 UI 时,需主动调用 setState 来更新数据,直接更新 state 是无效的。
😃用头发丝想想,在 setState 方法中,一定会涉及到 dom diff 的处理。
这样一来,在 Component 内部,也就需要知道对应的 vdom 实例,以此来触发 diff 操作。
下面,我们来看看这两个关键点的实现。
1. 在创建组件实例时,关联 vdom 节点。
// 创建组件对象,内部关联 vdom 实例
function createPublicInstance(element, internalInstance) {
const { type, props } = element;
// 创建组件实例
const publicInstance = new type(props);
// 关联 vdom 节点
publicInstance.__internalInstance = internalInstance;
return publicInstance;
}
可注意看,组件实例的创建是使用 new type(props);
的方式,同时传入了 props。
从上图 babel 转义后的 type 数据,应该就能明白为什么可以这样写。
2. 调用 setState 时,触发 diff 操作。
先更新 state 数据,然后做 diff 操作更新组件。
setState(state) {
this.state = Object.assign({}, this.state, state);
// __interalInstace 为 virutal dom
updateInstance(this.__internalInstance);
}
function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}
注意,由于普通标签和组件的子节点 vdom 结构是不一样的。一个是数组,一个是单独的对象,所以在进行 diff 时也需区分处理。
普通标签处理
普通标签的 reconcile 处理:
- 更新节点属性
- 处理子节点 diff
- 更新 vdom 属性
代码处理如下:
if (typeof element.type === "string") {
console.log("reuse dom");
// 重用节点,更新属性
updateDomProperties(instance.dom, instance.element.props, element.props);
// 处理子节点 diff
instance.childInstances = reconcileChildren(instance, element);
// 更新 element
instance.element = element;
return instance;
}
组件处理
组件的 reconcile 处理:
- 组件属性的更新
- 调用组件 render 方法
- 进行 diff 操作
- 更新 vdom 各个属性
代码处理如下:
{
console.log("update component");
// component
// 更新 props
instance.publicInstance.props = element.props;
// 重新构建节点信息
const childElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
// 更新 dom 节点
const childInstance = reconcile(parentDom, oldChildInstance, childElement);
// 更新 vdom 各个属性
instance.childInstance = childInstance;
instance.dom = childInstance.dom;
instance.element = element;
return instance;
}
组件生命周期
React 中的组件有一系列的生命周期。在此,我们打算实现如下生命周期:
// 组件即将挂载
componentWillMount();
// 组件挂载完毕
componentDidMount();
// 组件即将移除
componentWillUnmount();
// 将收到新的 props
componentWillReceiveProps(nextProps);
// 组件即将更新
componentWillUpdate(nextProps, nextState);
// 组件更新完毕
componentDidUpdate(prevProps, prevState);
componentWillMount
- 含义:组件即将挂载。
- 调用时机:在组件即将添加到 dom 树上调用。
function reconcile(parentDom, instance, element) {
if (instance == null) {
const isComponent = typeof newInstance.element.type !== "string";
const newInstance = instantitate(element);
// 组件生命周期 componentWillMount
if (isComponent) {
newInstance.publicInstance.componentWillMount();
}
parentDom.appendChild(newInstance.dom);
// 组件生命周期 componentDidMount
if (isComponent) {
newInstance.publicInstance.componentDidMount();
}
return newInstance;
}
// ...
}
componentDidMount
- 含义:组件挂载完毕。
- 调用时机:在组件添加到 dom 树后调用。代码可参照 componentWillMount 一节。
componentWillUnmount
- 含义:组件即将移除。
- 调用时机:准备从 dom 树移除时调用。
function reconcile(parentDom, instance, element) {
if (element == null) {
console.log("remove dom");
// remove,若新子节点数 < 原节点数,需移除
parentDom.removeChild(instance.dom);
// 组件生命周期 componentWillUnmount
if (typeof instance.element.type !== "string") {
instance.publicInstance.componentWillUnmount();
}
return null;
}
}
componentWillReceiveProps
- 含义:组件即将更新属性。
- 调用时机:在 reconcile 的组件 diff 中处理。
function reconcile(parentDom, instance, element) {
// ....
else {
// component
// 更新 props
// 组件生命周期 componentWillReceiveProps
instance.publicInstance.componentWillReceiveProps(element.props);
instance.publicInstance.props = element.props;
// ...
return instance;
}
}
componentWillUpdate
- 含义:组件即将更新。
- 调用时机:在更新 dom 树前,需调用 shouldComponentUpdate 判断是否需要真正的更新。
shouldComponentUpdate 的默认实现是:判断当前属性和传入属性是否不等,或者当前 state 和传入 state 是否不等。任一不等,即认为组件应该进行更新。
shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state;
}
componentWillUpdate 调用时机,在更新组件前。
setState(nextState) {
if (this.__internalInstance && this.shouldComponentUpdate(this.props, nextState)) {
// 组件生命周期 componentWillUpdate
this.componentWillUpdate(this.props, nextState);
this.state = Object.assign({}, this.state, nextState);
updateInstance(this.__internalInstance);
// 组件生命周期 componentDidUpdate
this.componentDidUpdate(this.props, nextState);
} else {
this.state = Object.assign({}, this.state, nextState);
}
}
componentDidUpdate
- 含义:组件更新完毕。
- 调用时机:在调用 updateInstance 后,即可触发。代码可参照 componentWillUpdate 一节。
Demo
这样,我们就可以继承于自己实现的 SLReact.Component,来自定义组件了。
class Story extends SLReact.Component {
constructor(props) {
super(props);
this.state = {
likes: ramdomLikes(),
};
}
componentDidMount() {
super.componentDidMount();
console.log("Story componentDidMount");
}
handleClick() {
this.setState({
likes: this.state.likes + 1,
});
}
render() {
const { name, url } = this.props;
const { likes } = this.state;
return (
<li>
<button onClick={(e) => this.handleClick()}>{likes}🐶</button>
<a href={url}>{name}</a>
</li>
);
}
}
跟 React 中的写法一毛一样。
以上只是组件的定义。有兴趣的童鞋可下载完整 Demo 代码,自行运行试试。
Demo 地址:https://github.com/silan-liu/slreact/tree/master/part4。
总结
这篇文章主要介绍了 Component 和 State 的实现,组件实例和 virutal dom 实例相互关联。
至此,「听说你想写个 React」已全部完结,整体内容比较简单,感谢阅读~
该系列全部文章如下:
网友评论