二、「深入React源码」--- 手写实现组件

170 阅读8分钟

若对组件已经有一定了解,想要直接深入学习的,可以跳过第一节。

React推出三种定义组件的方式:函数式、es5、es6。这三种方式有什么联系和区别,及其三种方式发展中出现的原因,将在下文中写出我自己的理解。如有写的不对的地方,欢迎纠正并指导。

一、React创建组件的三种方式及其分析

创建组件的三种方式:

  1. 函数式定义的组件(无状态组件
  2. es5原生React.createClass定义的组件
  3. es6形式的extends React.Component定义的组件

1. 函数式组件

无状态函数组件形式是从 React 0.14版本开始出现的。这种组件只负责根据传入内部的props渲染,不涉及对state状态的操作。

官方指出:在大部分React代码中,大多数组件被写成无状态的组件,通过简单组合可以构建成其他的组件等;这种通过多个简单然后合并成一个大应用的设计模式被提倡。

无状态函数组件:只带有一个render方法的组件,并且该组件无state状态。可以理解为Js中的函数,入参为props属性,返回值为React元素的函数。

函数组件也是我目前工作中的主要使用形式。函数组件还有以下特点:

- 函数组件使代码的可读性更高,并可以在很大成都上减少代码冗余

- 没有组件实例,提升整体渲染性能。 函数组件是通过函数的形式去实现的,所以不会有组件实例化的过程,因此也不需要分配多余的内存。

- 组件内没有this对象。 因为没有实例化过程,没有创建实例对象,所以无法对函数组件this中的对象进行访问,如this.refthis.state等。

- 组件内无法访问生命周期。 因为函数组件无需对生命周期和状态管理,所以在底层实现时也并没有处理生命周期方法。

2. React.createClass

React.createClass是react最初推荐的创建组件的方式,这是通过es5原生的JavaScript来实现的。 与函数式组件相比,React.createClass和第三种React.Component创建的都是有状态道德组件,这些组件是会被实例化的,且可以访问组件的生命周期。随着React的发展,React.createClass自身的问题慢慢显露:

React.createClass会自绑定函数方法(不像React.Component只绑定需要关心的函数)导致不必要的性能开销,增加代码过时的可能性。

3. React.Component

React.Component是以ES6的形式来创建react的组件的,是React目前极为推荐的创建有状态组件的方式,最终会取代React.createClass形式;相对于 React.createClass可以更好实现代码复用。

4. React.createClass与React.Component区别

  • 语法不同。一个是es5创建,一个是es6创建。
  • 函数this绑定不同React.createClass创建的组件,其每一个成员函数的this都有React自动绑定,任何时候使用,直接使用this.method即可,函数中的this会被正确设置。
const Contacts = React.createClass({
    handleClick() {
        console.log(this); // React Component instance 
    },
    render() {
        return ( <div onClick={this.handleClick}></div> ); 
    } 
});

React.Component创建的组件,其成员函数不会自动绑定this,需要开发者手动绑定,否则this不能获取当前组件实例对象。

class Contacts extends React.Component {
    constructor(props) {
        super(props);
    } 
    handleClick() {
        console.log(this); // null
    }
    render() {
        return ( <div onClick={this.handleClick}></div> );
    }
}
  • 组件初始状态state的配置不同React.createClass创建的组件,其状态state是通过getInitialState方法来配置组件相关的状态;React.Component创建的组件,其状态state是在constructor中像初始化组件属性一样声明的。

5. 组件特点

组件名称以大写开头

组件必须先定义再使用

组件必须返回、且只能返回唯一的根元素 <></>

组件可以接收属性对象,用来计算返回的元素

二、函数组件

1. 实现思路

image-20210909110506405.png 1)函数组件类型为函数,那么我们需要在createDOM方法中新增判断类型function:新建方法处理dom,把原有的属性props传入函数。把虚拟DOM变为真实DOM挂载到容器。

2)为了代码可读性更好,我们把挂载函数组件方法抽离为一个方法mountFunctionComponent。执行函数组件后,会得到div的虚拟DOM(本质是createElement返回的React元素),之后我们调用createDOM方法把虚拟DOM转为真实DOM进行挂载。

2. 实现函数组件

2.1 src/index.js

import React from "./react";
import ReactDOM from "./react-dom";
function FunctionComponent(props){
    return <div className="title" style={{ color: 'red' }}><span>{props.name}</span>{props.children}</div>;
}
let element = <FunctionComponent name="hello">world</FunctionComponent>;
ReactDOM.render(element, document.getElementById("root"));

2.2 src/react-dom.js

import { REACT_TEXT } from "./constants";

/**
 * 把虚拟DOM变成真实DOM插入容器
 * @param {*} vdom 虚拟DOM
 * @param {*} container 容器
 */
function render(vdom, container) {
    mount(vdom, container);
}

export function mount(vdom, container) {
    let newDOM = createDOM(vdom);
    container.appendChild(newDOM);
} 

/** 把虚拟DOM转为真实DOM */
export 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") {
> > >         return mountFunctionComponent(vdom);
> > >     } else { 
        dom = document.createElement(type);
    }
    // 处理属性
    if (props) { 
        updateProps(dom, {}, props); 
        if (typeof props.children == "object" && props.children.type) {
            mount(props.children, dom);
        } else if (Array.isArray(props.children)) {
            reconcileChildren(props.children, dom); 
        } 
    } 
    vdom.dom = dom; // 让虚拟dom的dom属性指向这个虚拟DOM对应的真实DOM
    return dom;
}

/** 挂载函数组件 */
> > > function mountFunctionComponent(vdom){
> > >     let { type, props }= vdom;
> > >     let renderVdom = type(props); // 获取组件将要渲染的虚拟DOM,有可能还是返回函数组件
> > >     return createDOM(renderVdom);
> > > }

/**
 * 把新的属性更新到真实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 { 
            dom[key] = newProps[key];
        }
    } 
    for(let key in oldProps){ 
        if(!newProps.hasOwnProperty(key)){
            dom[key] = null;
        }
    }
} 

function reconcileChildren(childrenVdom, parentDOM) { 
    for (let i = 0; i < childrenVdom.length; i++) { 
       let childVdom = childrenVdom[i];
        mount(childVdom, parentDOM);
    }
}

const ReactDOM = { 
    render,
};
    
export default ReactDOM;

三、类组件

1. 实现思路

类组件怎么渲染? 先创建类组件的实例,调用实例的render方法返回React元素,即虚拟DOM。

1)创建类组件实例,在react.js中添加Component类,并暴露。类在编译后,也会生成一个函数。源码中为了区分函数组件和类组件,添加了一个属性isReactComponent。子类继承父类,不但会继承实例方法,也会继承静态方法。

image-20210910114500737.png

image-20210910114748790.png

image-20210910114928429.png

2)createDOM方法中,判断组件类型,因为类组件函数组件最终都会成为函数,所以在函数的判断中,再加一层判断:isReactComponent,为真说明是类组件,执行类组件挂载方法mountClassComponent

3)mountClassComponent方法,入参为vdom。内部创建类组件的实例,调用组件的render方法得到类组件相应的虚拟DOM,可能是原生组件的虚拟DOM,也可能是类组件的虚拟DOM,也可能是函数组件的虚拟DOM。最后调用createDOM进行挂载。

2. 实现类组件

2.1 src/index.js

import React from "./react";
import ReactDOM from "./react-dom";
class ClassComponent extends React.Component{
    render(){
        return <div className="title" style={{ color: 'red' }}><span>{this.props.name}</span>{this.props.children}</div>;
    }
}
let element = <ClassComponent name="hello">world</ClassComponent>;
ReactDOM.render(element, document.getElementById("root"));

2.2 src/component.js

export class Component{
    // 函数组件和类组件编译后都会成为函数,以此来说明此组件为类组件
    static isReactComponent=true
    constructor(props){
        this.props = props;
    }
}

2.3 src/react-dom.js

import React from "./react";
import ReactDOM from "./react-dom";
function FunctionComponent(props){
    return <div className="title" style={{ color: 'red' }}><span>{props.name}</span>{props.children}</div>;
}
let element = <FunctionComponent name="hello">world</FunctionComponent>;
ReactDOM.render(element, document.getElementById("root"));

2.4 src/react-dom.js

import { REACT_TEXT } from "./constants";

/**
 * 把虚拟DOM变成真实DOM插入容器
 * @param {*} vdom 虚拟DOM
 * @param {*} container 容器
 */
function render(vdom, container) {
    mount(vdom, container);
}

export function mount(vdom, container) {
    let newDOM = createDOM(vdom);
    container.appendChild(newDOM);
} 

/** 把虚拟DOM转为真实DOM */
export 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 = document.createElement(type);
    }
    
    // 处理属性
    if (props) { 
        updateProps(dom, {}, props); 
        if (typeof props.children == "object" && props.children.type) {
            mount(props.children, dom);
        } else if (Array.isArray(props.children)) {
            reconcileChildren(props.children, dom); 
        } 
    } 
    vdom.dom = dom; // 让虚拟dom的dom属性指向这个虚拟DOM对应的真实DOM
    return dom;
}

/** 挂载类组件 */
> > > function mountClassComponent(vdom) {
> > >   let { type: ClassComponent, props } = vdom;
> > >    // 创建类组件的实例,返回组件实例对象
> > >   let classInstance = new ClassComponent(props);
> > >   let renderVdom = classInstance.render(); 
> > >   return createDOM(renderVdom);
> > > }

/** 挂载函数组件 */
function mountFunctionComponent(vdom){
    let { type, props }= vdom;
    let renderVdom = type(props); // 获取组件将要渲染的虚拟DOM,有可能还是返回函数组件
    return createDOM(renderVdom);
}

/**
 * 把新的属性更新到真实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 { 
            dom[key] = newProps[key];
        }
    } 
    for(let key in oldProps){ 
        if(!newProps.hasOwnProperty(key)){
            dom[key] = null;
        }
    }
} 

function reconcileChildren(childrenVdom, parentDOM) { 
    for (let i = 0; i < childrenVdom.length; i++) { 
       let childVdom = childrenVdom[i];
        mount(childVdom, parentDOM);
    }
}

const ReactDOM = { 
    render,
};
    
export default ReactDOM;

2.5 src/react.js

import { wrapToVdom } from "./utils";
> > > import {Component} from './Component';
import { REACT_ELEMENT } from "./constants";
/**
* 创建一个虚拟DOM == React元素
* @param {*} type 元素的类型 span div p
* @param {*} config 配置对象 className style
* @param {*} children 子元素,单个-对象/多个-数组
*/
function createElement(type, config, children) {
    let ref; // 可以通过ref获取引用此元素
    let key; // 子元素的key唯一标识
    if (config) {
    delete config.__source; // 删除暂时没用的属性
    source:bable编译时产生的属性
    delete config.__self;
    ref = config.ref; // 取出ref key
    delete config.ref; // 删除config中的ref key
    key = config.key;
    delete config.key;
}
   let props = { ...config };
   if (arguments.length > 3) {
       // 如果入参多余3个,说明有多个子元素,截取后以数组形式保存
       props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
   } else { 
       // 可能是React元素对象,也可能是string/number/null/und
       props.children = wrapToVdom(children);
   } 
   return { 
       $$typeof: REACT_ELEMENT,
       type,
       ref,
       key,
       props,
   };
}
const React = {
    createElement,
    Component,
};
export default React;