React内部原理,第一部分:基础渲染
forthealllight opened this issue · 6 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元素还是组件,并进行渲染。
文中有错误:Peact 与 Feact混用
文中有错误:Peact 与 Feact混用
好的
--文中表述错误(已加粗)
从上述代码中,我们可能有一个疑问,在组件实例化的时候,是不是在调用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
对象->渲染