forthealllight / blog

📖我的博客,记录学习的一些笔记,如有喜欢,欢迎star

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React内部原理,第一部分:基础渲染

forthealllight opened this issue · comments

React本质,第一部分:基础渲染


在接下来的五篇文章中,会用通俗的方式“重新构造”一个React,通过完成一个简易版本的React的构造,可以帮助我们理解React是如何实现的,以及组件的生命周期存在的原因和作用。

这一系列的文章包括:

  • 第一部分:基础渲染
  • 第二部分:componentWillMount and componentDidMount
  • 第三部分:基础更新
  • 第四部分:setState
  • 第三部分:transaction

声明:
这个系列的文章基于React15.3,所以最新的React的特性比如Fiber等是不支持的,本系列根据React的原理所构建的"Peact"尽可能的实现React的相关功能,但没有完全实现。


一、背景知识:元素(Element)和组件(Components)

在React中有三种不同的实体类型:原生的DOM元素、虚拟React元素(Virtual React Elements)和组件(Components)。

原生的DOM元素

这就是我们通常所说的dom元素,浏览器使用真实的dom元素来组织web网页,在某一时刻,React会通过调用document.createElement()方法去创建一个真实的dom元素,或者使用浏览器的DOM API去更新真实的dom元素, 更新的元素的API比如:element.insertBefore(), element.nodeValue等等.

虚拟的React元素

一个虚拟的react元素在内存中控制真实的DOM元素,在更新前如何渲染.这个react元素可以代表的是一个原生的dom元素或者是开发者自己定义的组件.

译者注:这里的虚拟React元素,就是React virtual dom的关键,主要流程为:

用户dom操作——>改变虚拟React元素——>在浏览器渲染

组件(Component)

"组件(Component)"在React中是一个特殊的术语,React可以在组件中实现不同的工作,不同的组件实现了不同的功能,比如ReactDOM提供了ReactDOMComponent实现了虚拟React元素渲染到真实的DOM元素的映射。

自定义的组合组件

在React中,自定义的组件可以通过React.createClass()或者es6形式的,class extend React.Component的方式创建一个类组件。在类组件中会有componentWillMount、shouldComponentUpdate等生命周期函数.组件的生命周期函数是React的一个难点。组件中的生命周期函数除了一些开发者常用的,还有形如mountComponent和receiveComponent等,开发者很少使用的生命钩子.

二、React是声明式的

定义React类组件是声明式的,在需要使用的时候再去实例化,在React的类组件中,我们是这样声明式的定义的:

class MyComponent extends React.Component{
    render(){
       return <div>hello</div>
    }
}

将jsx语言编译后可以得到:

class MyComponent extends React.Component{
    render(){
       return React.createElement('div',null,'hello')
    }
}

从上述代码中,我们可能有一个疑问,在组件实例化的时候,是不是在调用React.createElement之前就已经创建了一个真实的dom元素.其实并不是这样,组件在实例化后,会通过render方法来调用React.createElement去创建真实的dom元素.React声明式的创建组件的方式,可以控制真实dom的渲染.

三、模拟React实现的微型模型Peact

基于上述的虚拟react元素,以及自定义组件的原理,我们来仿造一个简单的类React实现上述功能,用Peact来表示.Peact应该有一个render方法:

Feact.render(<h1>hello world</h1>, document.getElementById('root'));

首先抛弃jsx语法,直接用Peact.createElement来代替(jsx语法最后也需要创建原生dom元素):

Feact.render(
    Feact.createElement('h1', null, 'hello world'),
    document.getElementById('root')
)

来看Peact.createElement方法的实现:

const Feact = {
    createElement(type, props, children) {
        const element = {
            type,
            props: props || {}
        };
        if (children) {
            element.props.children = children;
        }
    return element;
   }
}

在Feact.createElement方法中,返回的element对象可以表示我们所需要渲染到浏览器的元素信息.

如何实现render方法

接着来看render方法的实现,在Feact.render()方法中,参数为我们所需要渲染的信息,以及在哪里渲染.render方法是构建Feact app的最根本的方法,我们首先通过如下方式来定义render方法:

const Feact = {
    createElement() { /* as before */ },
    render(element, container) {
    const componentInstance = new  FeactDOMComponent(element);
    return componentInstance.mountComponent(container);
    }
};

当render被调用后,我们就可以得到一个完成的web网页。在render方法中,通过FeactDOMComponent方法将渲染信息映射成真实的dom元素,我们来看FeactDOMComponent方法的具体实现:

class  FeactDOMComponent{
    contructor(element){
        this._currentElement=element;
    }
    mountComponent(container){
        const domElement=document.createElement(this._currentElement.type);
        const text=this._currentElement.props.children;
        const textNode=document.createTextNode(text);
        domElement.appendChild(textNode);
        container.appendChild(domElement);
        this._hostNode = domElement;//会在第三章用到
        return domElement;
    }
}

增加自定义组件

在render方法中不仅可以渲染单个简单编码的元素,还应该可以渲染自定义的组件, 实现Peact.createClass自定义组件的方法如下:

const Feact = {
    createClass(spec) {
        function Constructor(props) {
            this.props = props;
        }
            Constructor.prototype.render = spec.render;
            return Constructor;
        }, 

    render(element, container) {
        // our previous implementation can't
        // handle user defined components,
        // so we need to rethink this method
    }
};

const MyTitle = Feact.createClass({
    render() {
        return Feact.createElement('h1', null, this.props.message);
    }
};
    //显示声明了一个MyTitle组件,接着是创建实例化的过程,原文缺省了实例化的过程。
let Title=new MyTitle(props);
Feact.render({
    Feact.createElement(MyTitle, { message: 'hey there Feact' }),
    document.getElementById('root')
);

改进Feact.render()方法

之前定义的方法无法渲染自定义的组件,因此我们需要修改Feact.render()方法来使其可以渲染自定义组件。

Feact = {
    render(element, container) {
        const componentInstance =
            new FeactCompositeComponentWrapper(element);

        return componentInstance.mountComponent(container);
    }
}
class FeactCompositeComponentWrapper {
    constructor(element) {
        this._currentElement = element;
    }

    mountComponent(container) {
        const Component = this._currentElement.type;
        const componentInstance = new Component(this._currentElement.props);
        const element = componentInstance.render();

        const domComponentInstance = new FeactDOMComponent(element);
        return domComponentInstance.mountComponent(container);
    }
}

给予了用户去定义组件的能力,并且Feact可以根据props传递过来的值动态的更新和渲染dom节点。

改进的自定义组件

在之前自定义组件的方法中,自定义的组件只能返回原生的dom元素,不能返回组件,也就是自定义组件目前不能嵌套式的在组件中返回组件,比如我们要实现这样的组件,有可能在组件中返回组件。

const MyMessage = Feact.createClass({
    render() {
        if (this.props.asTitle) {
            return Feact.createElement(MyTitle, {
                message: this.props.message
            });
        } else {
            return Feact.createElement('p', null, this.props.message);
        }
    }
}

上述的自定义组件MyMessage可以选择性的返回组件或者是原生的dom元素。这种类型的自定义组件,按之前定义的 FeactCompositeComponentWrapper方法是无法渲染的,因此对于这种组件我们需要重新的定义 FeactCompositeComponentWrapper方法。

class FeactCompositeComponentWrapper {
    constructor(element) {
        this._currentElement = element;
    }

    mountComponent(container) {
        const Component = this._currentElement.type;
        const componentInstance =
            new Component(this._currentElement.props);
        let element = componentInstance.render();

        while (typeof element.type === 'function') {
            element = (new element.type(element.props)).render();
        }

        const domComponentInstance = new FeactDOMComponent(element);
        domComponentInstance.mountComponent(container);
    }
}

在mountComponent方法中,如果还是组件需要一个while循环,直到循环取到最底层的原生dom元素。

再次修改Feact.render()

第一个版本的Feact.render()只能渲染原生的dom节点,而第二个版本的Feact.render()只能渲染组件,因此我们需要一个通用的方法,既可以渲染原生的dom节点,又可以渲染组件。具体修改的Feact.render()方法如下所示:

const TopLevelWrapper = function(props) {
    this.props = props;
};
TopLevelWrapper.prototype.render = function() {
    return this.props;
};

const Feact = {
    render(element, container) {
        const wrapperElement =
            this.createElement(TopLevelWrapper, element);

        const componentInstance =
            new FeactCompositeComponentWrapper(wrapperElement);
        // as before
    }
};

具体实现就是用一个上层的组件TopLevelWrapper包裹,并且其render方法返回的是props,在FeactCompositeComponentWrapper中判断是原生的dom元素还是组件,并进行渲染。

commented

文中有错误:PeactFeact混用

文中有错误:PeactFeact混用

好的

--文中表述错误(已加粗)
从上述代码中,我们可能有一个疑问,在组件实例化的时候,是不是在调用React.createElement之前就已经创建了一个真实的dom元素.其实并不是这样,组件在实例化后,会通过render方法来调用React.createElement去创建真实的dom元素.React声明式的创建组件的方式,可以控制真实dom的渲染.

修正:React.createElement会创建虚拟元素

--- jsx语法最后也需要创建原生dom元素 (表述错误)

修正:jsx 语法创建的也是虚拟元素对象

--文中表述错误(已加粗)
从上述代码中,我们可能有一个疑问,在组件实例化的时候,是不是在调用React.createElement之前就已经创建了一个真实的dom元素.其实并不是这样,组件在实例化后,会通过render方法来调用React.createElement去创建真实的dom元素.React声明式的创建组件的方式,可以控制真实dom的渲染.

修正:React.createElement会创建虚拟元素

真实的dom都是dom的api创建的,感谢指正~

虚拟dom

对象->渲染