Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
forthealllight opened this issue May 10, 2018 · 6 comments
Open

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

forthealllight opened this issue May 10, 2018 · 6 comments

Comments

@forthealllight
Copy link
Owner

forthealllight commented May 10, 2018

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元素还是组件,并进行渲染。

@forthealllight forthealllight changed the title React本质,第一部分:基础渲染 React内部原理,第一部分:基础渲染 May 12, 2018
@samwangdd
Copy link

文中有错误:PeactFeact混用

@forthealllight
Copy link
Owner Author

文中有错误:PeactFeact混用

好的

@xuqinggang
Copy link

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

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

@xuqinggang
Copy link

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

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

@forthealllight
Copy link
Owner Author

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

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

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

@yaozhibo
Copy link

虚拟dom

对象->渲染

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants