本文为系列文章:
手写虚拟DOM(一)—— VirtualDOM介绍
手写虚拟DOM(二)—— VirtualDOM Diff
手写虚拟DOM(三)—— Diff算法优化
手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key
手写虚拟DOM(五)—— 自定义组件
手写虚拟DOM(六)—— 事件处理
手写虚拟DOM(七)—— 异步更新
一、前言
今天,我们继续在之前项目的基础上扩展功能。
现在流行的前端框架都支持自定义组件,组件化开发已经成为提高前端开发效率的银弹。
下面我们就将自定义组件功能加到项目中去,目标是正确的渲染和更新自定义组件。
二、JSX语法
要想正确的渲染组件,第一步就是要告诉JSX某个标签是自定义组件:只要标签名的首字母大写
就可以了。
下面的例子里,DemoComp就是一个自定义组件:
<div>
<div>Header</div>
<DemoComp/>
</div>
经过Babel jsx语法转换之后如下:
v(
"div",
null,
v(
"div",
null,
"Header"
),
v(
DemoComp,
null
)
)
当首字母大写当时候,JSX会将标签名当作变量处理,而不是像普通标签一样当字符串处理。
解决了识别自定义标签的问题,下一步就是定义标签了。
三、定义基类Component
在React中,所有自定义组件都要继承Component基类,它为我们提供了一系列生命周期方法和修改组件的方法。
我们也对应的定义一个自己的Component类:
/*****************************************************************************************************************
* Define a basic Component
*****************************************************************************************************************/
class Component {
constructor(props) {
this.props = props;
this.state = {};
}
setState(newState) {
this.state = {...this.state, ...newState};
diff(this._dom, this.render());
}
render() {
throw new Error('Component should define its own render');
}
}
如果用一句话描述Component,那就是属性和状态的UI表达
。
我们先不考虑生命周期函数,先定义一个最精简版的Component。
首先在初始化的时候,需要传入props属性,然后提供一个setState方法来改变组件的状态,最后就是子类必须要实现的render函数。
如果子类没有实现,就会沿着原型链查找到Component类,然后会抛出一个错误。
四、继承基类,实现自定义组件
class DemoComp extends Component {
constructor(props) {
super(props);
this.state = {
name: 'chris'
};
this.interval();
}
interval = () => {
setInterval(() => {
this.setState({name: 'chris-' + Math.floor(Math.random() * 100)});
}, 2000);
};
render() {
return (
<div>
<div>This is DemoComp....props = {this.props.value}</div>
<div>DemoComp.state.name = {this.state.name}</div>
</div>
);
}
}
其中,方法 interval 是 class 的属性(ES6),在ES2015中需要转成 function,
所以,还需要安装babel插件:@babel/plugin-proposal-class-properties
当然,也可以直接写成 function,这样就不用安装插件。
五、组件渲染逻辑
一切渲染,都是通过 diff 来完成,不论是首次,还是之后的更新、删除、替换等。
因此,我们要对tag为函数类型(自定义组件)的节点做特殊处理,同时对新建的节点,也要加入一些额外的逻辑:
function diff(srcDOM, destDOM, parent, _component) {
if (typeof destDOM === 'object' && typeof destDOM.tag === 'function') {
buildComponent(srcDOM, destDOM, parent);
return false;
}
// 原dom没有,新vdom有,则表明是新增节点
if (srcDOM === undefined) {
srcDOM = createElement(destDOM);
if (_component) {
srcDOM._component = _component;
srcDOM._componentConstructor = _component.constructor;
_component._dom = srcDOM;
}
parent.appendChild(srcDOM);
return false;
}
......
}
function buildComponent(dom, component, parent) {
const {tag, props, children} = component;
props.children = children;
let _component = dom && dom._component;
if (_component === undefined) {
_component = new tag(props)
} else {
_component.props = props;
}
diff(dom, _component.render(), parent, _component);
}
如果是自定义组件,会调用buildComponent
方法。先获取vdom
最新的属性,包括children
。
如果dom对象有_component属性,说明是组件更新的过程,否则为组件创建的过程。
- 如果是创建过程则直接实例化一个对象;
- 如果是更新过程,则传入最新的
props
; - 最后通过组件的
render
方法得到最新的vdom
后,再进行diff
操作;
diff
多了一个_component的
参数,在新建dom
节点的时候,如果有这个参数,说明是自定义组件创建的节点,需要用_component
和_componentConstructor
做一下标识。
其中_component
上面就用到了,用来判断是组件更新过程还是组件创建过程;_componentConstructor
用在组件更新过程中判断组件的类型是否相同。
function isEqual(vdom, element) {
......
// 自定义组件 tag 判断
if (typeof vdom.tag === 'function') {
return element._componentConstructor === vdom.tag;
}
......
}
到此为止,自定义组件的被动更新过程已经完成了。
六、setState
class Component {
......
setState(newState) {
this.state = {...this.state, ...newState};
diff(this._dom, this.render());
}
......
}
该方法定义在基类中,合并当前状态和最新状态(如果有相同key,则后者覆盖前者)。
然后,diff 当前真实 dom 和 子组件的 virtual dom。
七、示例
刚开始如下图:
2秒之后:
可以看到
props
和state
都得到了正确都渲染。
八、总结
本文基于上一个版本的代码,加入了对自定义组件的支持,大大提高代码的复用性。
项目源码:
https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-05