前面React已经实现了Function Component,这节主要实现Class Component。
Class Component比Function Component要复杂的多。Function Component仅仅需要实现render功能,而class Component功能要多得多。主要功能如下:
- 支持生命周期,包括(componentWillMount,componentDidMount,componentWillReceiveProps, componentWillUpdate,componentDidUpdate,componentWillUnmount)。
- 支持状态更新setState
- 支持reconciler(Virtual Dom diff)
为支持Class Component需要扩充我们的ReactElement的类型定义,扩充定义如下
type ReactElement = ReactComponentElement | ReactDOMElement;
type ReactComponentElement<TProps> = {
type : ReactClassComponent<TProps>|ReactFuncComponent<TProps>,
props : TProps
};
type ReactFuncComponent<TProps> = TProps => ReactElement
type ReactClassComponent<TProps> = (TProps) => ReactComponent<TProps>;
type ReactComponent<TProps> = {
props : TProps,
render : () => ReactElement
};
为此,mountComposite也需要对Class Component支持,因此需要我们区分Function Component和class Component。
Function or Class
如何判断一个对象是Function还是Class并非轻而易举。typeof (function(){}) 和typeof (class{})均返回'function',Function和Class一个区别是Class必须使用new 调用,否则会抛异常。因此可以使用如下方法判断。
isClass(obj){
function isClass(obj){
if(typeof obj !== 'function') return false;
try {
obj()
return false;
}catch(err){
return true;
}
}
console.log(isClass(function(){}));// false
console.log(isClass(class{})); // true
上面的方法存在几种缺陷
- obj本身执行就会抛出异常,这是需要我们精确判断抛出异常是因为class的无new调用导致的,很不幸规范没规定了抛出的具体异常信息,因此难以判断。
- 更严重的是,这里执行了obj,加入obj本身带有副作用,这将影响程序的执行逻辑。
另一种方法是Class的定义含有class关键字,可以通过Function.prototype.toString进行判断。
function isClass(v) {
return typeof v === 'function' && /^\s*class\s+/.test(v.toString());
}
这里存在的问题是babel等转译工具会将class转译为函数,并不能保证转译后的函数能通过该判断。
幸运的是我们并不需要判断一个对象是不是Class,因为React的组件都继承自Component 这个Class,因此只需要判断对象是否为Component的子类即可。通过如下判断即可
class Component {
}
Component.prototype.isReactComponent = true;
function isClass(type){
return (
Boolean(type.prototype) &&
Boolean(type.prototype.isReactComponent)
);
}
接下来就可以实现mountComposite了
function hooks(obj,name,...args){
obj[name] && obj[name].apply(obj,args);
}
function mountComposite(element){
const { type, props } = element;
const children = props.children;
if(isClass(type)){
var instance = new type(props);
instance.props = props;
hooks(instance, 'componentWillMount')
element = instance.render(element);
}else {
element = element.type(element.props);
}
return mount(element); // delegate to mount
}
class Link extends Component {
render(){
const { children } = this.props
return (
<a href="http://www.baidu.com">{children}</a>
)
}
}
var element = (
<div class="container">
<Button text="this is a button"/>
<Button text="this is another button"/>
<Link>baidu</Link>
</div>
)
var node = mount(element);
document.body.appendChild(node);
至此已经可以实现Class Component的渲染功能了。
Element && Component && Instance区别
至此我们实现的组件均没有状态,只能接受props进行渲染,这样每次更新都会造成所有的组件重新渲染,重新生成新的Dom节点和element对象,效率低下。因此我们希望尽可能复用原有的DOM节点,只对其属性进行更新,这样尽可能的减小Dom操作。因此需要存储上次生成的Dom节点和element对象,为此引入了内部实例(internal Instance)来存储这些状态。
至此我们引入了三个概念,Element、Component、和Internal Instance/Component Instance。其区别如下:
- Element:element是用来描述Dom节点或Instance的对象,element分为两种描述Dom节点的如{type: 'button', props: { id: 'btn'}}和描述Instance的如{type: Button, props: { text: 'this is a button'}},其仅仅为一个基本对象,不含有任何额外的方法。
- Component:Component是对Element组合的抽象,其用来描述组件树,其输入是props,输出是组件树,输出的组件树既可以包含描述Dom的Element,也可以包含描述instance的Element,通过Component可以将不同的Element组合为Element tree,Component也分为两种Function Component和 Class Component
- Component Instance/Internal Instance:Component Instance是Component的实例,且只有Class Component才有实例,instance用于存储局部状态(this.state等)和响应生命周期,而Internal Instance 则是React实现的细节,用于存储Component Instance以用于更新和卸载操作。
函数多态 or 对象多态
之前的实现时mount根据传入的element类型决定调用不同的mount操作。这实际上是利用了函数的多态,对于无状态的操作这很方便,但当引入状态后,函数多态变得难以处理,为此使用面向对象的方式重构代码。
mount(element){
if(typeof element === 'string') return mountText();
if(typeof element.type === 'string') return mountDom();
if(typeof element.type === 'function') return mountComposite();
}
可重构为如下形式的代码
class Mountable {
mount(){
throw new Error('must implement mount method');
}
}
class TextComponent extends Mountable{
mount(){
}
}
class DomComponent extends Mountable {
mount(){
}
}
class CompositeComponent extends Mountable {
mount(){
}
}
function instantiateComponent(element) {
if(typeof element === 'string') return new TextComponent();
if(typeof element.type === 'string') return new DomComponent();
if(typeof element.type === 'function') return CompositeComponent();
}
function mount(element){
const rootComponent = instantiateComponent(element);
return rootComponent.mount();
}
重构后的就可以在各类型的instance上存储数据了。
重构后的代码如下
class Component {
render(){
throw new Error('must implement render method');
}
}
Component.prototype.isReactComponent = true;
function isClass(type) {
return (
Boolean(type.prototype) &&
Boolean(type.prototype.isReactComponent)
);
}
function hooks(obj,name,...args){
obj[name] && obj[name].apply(obj,args);
}
class DomComponent {
constructor(element){
this.currentElement = element;
this.renderedChildren = [];
this.node = null;
}
getPublicInstance(){
return this.node;
}
mount(){
const { type, props } = this.currentElement;
let children = props.children;
children = children.filter(Boolean);
const node = document.createElement(type);
Object.keys(props).forEach(propName => {
if(propName !== 'children'){
node.setAttribute(propName, props[propName])
}
})
const renderedChildren = children.map(instantiateComponent);
this.renderedChildren = renderedChildren;
const childNodes = renderedChildren.map(child => child.mount());
for(let child of childNodes){
node.appendChild(child);
}
this.node = node;
return node;
}
}
class TextComponent {
constructor(element){
this.currentElement = element;
this.node = null;
}
getPublicInstance(){
return this.node;
}
mount(){
const node = document.createTextNode(this.currentElement);
this.node = node;
return node;
}
}
class CompositeComponent {
constructor(element){
this.currentElement = element;
this.publicInstance = null; // public instance
this.renderedComponent = null;
}
getPublicInstance(){
return this.publicInstance;
}
mount(){
const { type, props } = this.currentElement;
const children = props.children;
let instance, renderedElement;
// delegate to mount
if(isClass(type)){
instance = new type(props);
instance.props = props;
hooks(instance, 'componentWillMount');
renderedElement = instance.render();
this.publicInstance = instance;
}else {
renderedElement = type(props);
}
const renderedComponent = instantiateComponent(renderedElement);
this.renderedComponent = renderedComponent;
return renderedComponent.mount();
}
}
function instantiateComponent(element) {
if(typeof element === 'string') return new TextComponent(element); // internal instance
if(typeof element.type === 'string') return new DomComponent(element);
if(typeof element.type === 'function') return new CompositeComponent(element);
}
function mount(element){
const rootComponent = instantiateComponent(element);
return rootComponent.mount();
}
function Button(props){
return (
<button class="btn">{props.text}</button>
)
}
function createElement(type,props,...args){
props = Object.assign({},props);
let children = [].concat(...args);
props.children = children;
return { type, props };
}
class Link extends Component {
componentWillMount(){
console.log('Link will Mount');
}
render(){
const { children } = this.props
return (
<a href="http://www.baidu.com">{children}</a>
)
}
}
const element = (
<div class="container">
<Button text="this is a button"/>
<Button text="this is another button"/>
<Link>baidu</Link>
</div>
)
const node = mount(element);
document.body.appendChild(node);
此时textComponent,DomComponent和CompositeComponent的mount逻辑全部从mount函数转移到内部的mount方法内了。
这里的CompositeComponent和DOMComponent区别于Link这种Component,CompositeComponent和DOMComponent是React内部实现的细节,不提供向外的API,同理CompositeComponent和Link的instance也不同,为了进行区分,把CompositeComponent/DOMComponent的instance称为内部实例(internal instance)而把Link这类的实例称为外部实例(public instance),外部无法访问内部实例,但能通过getPublicInstace访问外部实例,也即组件实例,这里可以看出DOMComponent和CompositeComponent的外部实例返回类型不一致,DOMComponent返回的是node节点而CompositeComponent返回的则是组件的实例,且对于Function Component其返回值为null。其和React 的ref回调返回的实例情况一致。
至此我们可以实现React的render功能了,其接受一个element和mountNode节点,将element描述的dom树渲染到mountNode中。
function render(element, mountNode){
var rootComponent = instantiateComponent(element); // top-level internal instance
var node = rootComponent.mount(); // top-level node
mountNode.appendChild(node);
var publicInstance = rootComponent.getPublicInstance(); // top-level public instance
console.log('internal instance:', rootComponent);
console.log('public instance:', publicInstance);
return publicInstance;
}
render(app, document.querySelector('#root'));
执行后的结果是
从internal instance信息可以看出,
- public instance就是internal instance的node属性值。
- DOMComposite的node为public Instance节点,Function Component对应的publicInstance为null,Class Component对应的publicInstance是Link的实例,其完整的存储了上次渲染产生的结果,这样就可以为后续的更新操作提供便捷。
这里实现的render函数与React 的render有重要差别,React的render只有首次render时才会进行mount操作,后续render进行update操作,因此我们的render也要实现update功能。