rwson

rwson

一个前端开发

从零开始写一个React - 初始化渲染

我们知道React组件返回的是JSX,而JSX将被babel转换,在React中是将JSX中转换成React.createElement(type, config, children)的形式。

class App extends Component {
    render() {
        return <div className="app-container">App Component</div>
    }
}

//	babel转换后输出的代码
var App = React.createClass({
    render() {
        return React.createElement("div", {
          	className: "app-container"
        }, "App Component")
    }
});

我们可以在babel中把JSXpragma转换改成自己的函数名:

{
  "presets": [
    "es2015"
  ],
  "plugins": [
    ["transform-react-jsx", {
      "pragma":  "createElement"	//	默认的是React.createElement, 这里我们还是用默认的
    }]
  ]
}

下面一起看看createElement的实现

const uselessProp = ["key", "ref", "__self", "__source"]

function createElement(type, config, ...children) {
    let props = {},
        key = null,
        ref = null;
    if (config != null) {
        ref = lodash.isUndefined(config.ref) ? null : config.ref;
        key = lodash.isUndefined(config.key) ? null : "" + config.key;
        for (let propsName in config) {
            if (uselessProp.indexOf(propsName) === -1 && hasOwnProperty(config, propsName)) {
                props[propsName] = config[propsName];
            }
        }
        //  子组件
        props.children = children;
    }
    return new ReactElement(type, key, props, ref);
}

这里createElement就是对参数进行处理,返回一个ReactElement(virtual-dom),ReactElement返回一个vnode来表示一个节点:

class ReactElement {
    constructor(type, key, props, ref) {
        this.type = type;
        this.key = key;
        this.props = props || {};
        this.ref = ref || {};
    }
}

在返回的vnode中,type用来标识组件类型,props就是组件中标签上的attributeskey用作这个vnode的唯一标识,可用于后面的更新,现在有了vnode,就需要将vnode转换成真实的DOM挂载到页面上,在React启动的时候,是通过ReactDOM.render来进行渲染组件到页面,渲染就是一个递归调用render的过程:

function render(componnet, container, callback = noop) {
    const componentInstance = instantiateReactComponent(componnet),
        markup = componentInstance.mountComponent(React.nextReactRootIndex ++);
    container.innerHTML = markup;

    if (lodash.isFunction(callback)) {
        callback.call(componentInstance);
    }
}

render中我们调用了一个instantiateReactComponent,这个函数的作用是根据vnode里面type的不同来返回不同的组件:

export function instantiateReactComponent(node) {
    //  文本节点的情况
    if (typeof node === "string" || typeof node === "number") {
        return new ReactDOMTextComponent(node);
    }

    //  浏览器默认节点的情况
    if (typeof node === "object" && typeof node.type === "string") {
        return new ReactDOMComponent(node);
    }

    //  自定义的元素节点
    if (typeof node === "object" && typeof node.type === "function") {
        return new ReactCompositeComponent(node);
    }
}

其中,ReactDOMTextComponent是纯文本组件,ReactDOMComponent是浏览器标签组件,ReactCompositeComponent是自定义的组件,后面的两种组件类型都属于virtual-dom

我们接下来看看上面三个组件的实现:

//  文本组件
export class ReactDOMTextComponent {
    constructor(text) {
        //  存下当前的字符串
        this._currentElement =  ("" + text);
        //  组件唯一id
        this._rootNodeID = null;
    }

    /**
     *  component渲染时生成的dom结构
     *  @param   {String}  rootID  [组件唯一id]
     *  @return  {String}          [HTML字符串]
     */
    mountComponent(rootID) {
        this._rootNodeID = rootID;
		return `<!-- react-text: ${this._rootNodeID} -->${this._currentElement}<!-- /react-text -->`;
    }
}

//	浏览器标签组件
export class ReactDOMComponent {
    constructor(element) {
        //  存下当前元素引用
        this._currentElement = element;
        //  组件唯一id
        this._rootNodeID = null;
        //  子组件集合
        this._renderedChildren = null;
    }

    /**
     *  component渲染时生成的dom结构
     *  @param   {String}  rootID  [组件唯一id]
     *  @return  {String}          [HTML字符串]
     */
    mountComponent(rootID) {
        this._rootNodeID = rootID;

        const { props, type } = this._currentElement,
            { children } = props,
            
			//	判断是否为单标签
            isSingleTag = SINGLE_TAG_REG.test(type);
        let tagOpen,
            tagClose,
            propKey, 
            propValue, 
            eventType,
            childrenInstances, 
            childComponentInstance, 
            childrenMarkups, 
            curRootId;

        tagOpen = [];
        tagOpen.push(`<${type}`, `data-reactid="${this._rootNodeID}"`);

        for (propKey in props) {
          
          	//	过滤掉propKey为"children"的情况
            if (hasOwnProperty(props, propKey) && propKey !== "children") {

                propValue = props[propKey];
                if (/^on[a-z]+/i.test(propKey)) {
                    /**
                     *  handleClick() {
                     *      alert("clicked");
                     *  }
                     *  
                     *  render() {
                     *      return (
                     *          <div onClick={this.handleClick}>click me</div>
                     *      );
                     *  }
                     */
                  
                  	//	取得事件名称并进行事件代理
                    eventType = propKey.replace("on", "").toLowerCase();
                    Event.delegate({
                        element: doc,
                        type: eventType,
                        selector: `[data-reactid="${this._rootNodeID}"]`,
                        handler: propValue,
                        context: null
                    });

                    //  TODO: 实现对当前元素进行事件代理
                } else if (propKey === "style") {

                    /**
                     *  render() {
                     *      return (
                     *          <div style={"background: red;"}></div>
                     *      );
                     *  }
                     *
                     *  -----------------------
                     *
                     *  render() {
                     *      return (
                     *          <div style={{background: 'red'}}>
                     *          </div>
                     *      );
                     *  }
                     */
                    if (lodash.isObject(propValue)) {
                      
						//	这里的toStyle用了一个npm包(https://www.npmjs.com/package/to-style)
                        propValue = toStyle.string(propValue);
                    }

                    tagOpen.push(`style="${propValue}"`);
                } else if (propValue === "className") {
                    tagOpen.push(`class="${propValue}"`);
                } else {
                    tagOpen.push(`${propKey}="${propValue}"`);
                }
            }
        }

        if (isSingleTag) {
            tagOpen.push("/>");
            tagClose = "";
        } else {
            tagOpen.push(">");
            tagClose = `</${type}>`;
        }

        childrenMarkups = [];
        childrenInstances = [];
      
      	//	遍历子节点,实例化->渲染子节点
        if (children && children.length) {
            children.forEach((child, key) => {
                childComponentInstance = instantiateReactComponent(child);
                childrenInstances.push(childComponentInstance);
                childComponentInstance._mountIndex = key;
              
              	//	拼一个类似于"x.y"的子节点的nodeId
                curRootId = `${this._rootNodeID}.${key}`;
                childrenMarkups.push(childComponentInstance.mountComponent.call(childComponentInstance, curRootId));
            });
        }

      	//	将子组件的集合保留起来,留作后面更新使用
        this._renderedChildren = childrenInstances;
      
      	//	返回组件的真实HTML结构
        return `${tagOpen.join(" ")} ${childrenMarkups.join(" ")} ${tagClose}`;
    }
}

//	React自定义组件
export class ReactCompositeComponent {
    constructor(element) {
        //  存放元素element对象
        this._currentElement = element;
        //  存放唯一标识
        this._rootNodeID = null;
        //  存放对应的ReactClass的实例
        this._instance = null;
    }

    mountComponent(rootID, hostContainerInfo, context) {
        this._rootNodeID = rootID;
        const { props, type } = this._currentElement,
            ReactClass = type,
            //	根据vnode中的type来实例化
            //	在instantiateReactComponent中可以看到,只有当vnode的type是一个方法类型(组件的constructor)时才会返回ReactCompositeComponent
            inst = new ReactClass(props);
        let renderedElement, renderedComponentInstance, renderedMarkup;

      	//	保持对当前组件实例的引用
        this._instance = inst;
        inst._reactInternalInstance = this;

      	//	执行生命周期中的componentWillMount
        if (lodash.isFunction(inst.componentWillMount)) {
            inst.componentWillMount();
        }

      	//	调用当前组件的render
        renderedElement = this._instance.render();
      
      	//	根据render中返回的再去执行instantiateReactComponent
        renderedComponentInstance = instantiateReactComponent(renderedElement);
        this._renderedComponent = renderedComponentInstance;
      
      	//	挂载组件
        renderedMarkup = renderedComponentInstance.mountComponent(this._rootNodeID);

      	if (lodash.isFunction(inst.componentDidMount)) {
            inst.componentDidMount();
        }

        return renderedMarkup;
    }

    receiveComponent(nextElement, newState) {}
}

上面就是我们对三种组件的一个实现,下面我们用一个图简单表示下具体的渲染过程。

渲染逻辑

接下来我们需要写一个Component基类,去给所有的子类继承:

export class Component {

    constructor() {}

    setState(newState) {
        this._reactInternalInstance.receiveComponent(null, newState);
    }

    /**
     *  组件即将被挂载到DOM上
     */
    componentWillMount() {}

    /**
     *  组件已经被挂载到DOM上
     */
    componentDidMount() {}

    /**
     *  组件即将收到新的props
     *  @param   {Object}  nextProps  [新的props]
     *  @param   {Object}  nextState  [新的state]
     */
    componentWillReceiveProps(nextProps, nextState) {}

    /**
     *  组件是否应该更新
     *  @param   {Object}  nextProps  [新的props]
     *  @param   {Object}  nextState  [新的state]
     *  @return  {Boolean}            [标记组件是否应该更新]
     */
    shouldComponentUpdate(nextProps, nextState) {
        return lodash.isEqual(this.state, nextState) || lodash.isEqual(this.props, nextProps);
    }

    /**
     *  组件即将更新
     */
    componentWillUpdate() {}

    /**
     *  组件已经更新
     */
    componentDidUpdate() {}

    /**
     *  组件即将被卸载
     */
    componentWillUnmount() {}

    /**
     *  返回组件内部的jsx
     *  @return  {JSX}  [组件布局]
     */
    render() {}
}

下面我们一起根据上面的代码来完成一个初始化渲染的demo:

import React, { Component } from "./src/react";

class InputComponnet extends Component {
    constructor() {
        super();
    }

    keyUpHandler(ev) {
        console.log(this);
    }

    render() {
        return (
			<input
		        type = "text"
                ref="textInput"
		        placeholder = { "请输入..." }
		        onKeyUp = { this.keyUpHandler.bind(this) }
		        style = {{
	                display: "block",
	                width: "200px",
	                height: "30px",
	                fontSize: "14px",
	                lineHeight: "30px"
		        }} />
    	   );
    }
}

class App extends Component {
    constructor() {
        super();
    }

    render() {
        return ( 
            <div style = { "background: red;" } >
        		App Component <InputComponnet placeholder = { "请输入..." }/> 
        	 </div>
        );
    }
}

React.render( < App / > , document.querySelector("#root"));

到这里我们的初始化渲染已经完成了,我们实现了组件中的事件绑定(事件代理),简单的生命周期(componentWillMoutcomponentDidMount),将virtual-dom转换成真实的DOM并且挂载到页面的过程。

这里可以查看相对完整的代码。

参考