React笔记
元素
定义元素
const element = <h1>哈哈哈</h1>;
react中的元素类似Java的对象.react DOM 可以确保浏览器 DOM 的数据内容与 React 元素保持一致.
元素渲染
定义一个div块,id对应const的变量,这个div块的所有内容都由react来管理,称为’根’DOM节点.
<div id="example"></div>
使用react开发只会定义一个根节点.
ReactDOM.render
使用ReactDOM.render()方法.将react元素渲染到根节点的DOM中.
1 | //定义块元素 |
1 | <div id="example"></div> |
JSX
JSX 就是用来声明 React 当中的元素.
React元素是普通的对象.React DOM 可以确保 浏览器 DOM 的数据内容与 React 元素保持一致.
1 | <style> |
1 | //添加自定义属性需要使用 data- 前缀 |
在JSX中使用js表达式.使用花括号{}定义表达式
1 | <h1>{1+1}<h1> |
JSX不能使用if else语句
1 | //使用三元运算符代替 |
我们知道在 React 组件render()
返回的是 JSX,而 JSX 将会被 babel 转换。JSX 将被转换为 React.createElement(type, config, children)
的形式。
1 | // App.js |
React.createElement()
的实现位于 /src/isomorphic/classic/element/ReactElement.js
这里的 React.createElement()
是用来生成虚拟 DOM 元素,该函数对组件的属性,事件,子组件等进行了处理,并返回值为一个 ReactElement
对象(单纯的 JavaScript 对象,仅包括 type, props, key, ref 等属性)。
这恰好说明了 JSX 中的 <h1 id='title'>hello world</h1>
实际上是 JavaScript 对象,而不是我们通常写的 HTML 标签。
样式
1 | //定义内联样式 |
注释
1 | ReactDOM.render( |
数组
1 | var arr = [ |
组件
原生HTML 元素名以小写字母开头,而自定义的React类名以大写字母开头.
1 | function HelloMessage(props) { |
1 | //使用函数定义组件 |
1 | <div id="example"></div> |
复合组件
1 | function Name(props) { |
渲染到页面
渲染到页面
单单声明了组件而没有渲染到页面上我们是看不见的(废话),所以我们需要使用 ReactDOM.render()
将其渲染到页面指定位置上。
1 | // index.html |
ReactDOM.render()
的实现位于 /src/renderers/dom/client/ReactMount.js
ReactDOM.render()
函数将会根据 ReactElement
的类型生成相对应的ReactComponent
实例,并调用其 mountComponent()
函数进行组件加载(返回 HTML片段),递归加载所有组件后,通过 setInnerHTML 将 HTML 渲染到页面上。
判断需要生成那种 ReactComponent
实例根据 ReactElement
对象的 type 属性来决定。对应 HTML 标签的 type 一般为字符串,而自定义的组件则是大写字母开头的组件函数(自定义组件需要 import,而 HTML 标签不需要)。
生成 ReactComponent
React 中生成对应的 ReactComponent
实例由 instantiateReactComponent()
完成,其实现位于 /src/renderers/shared/stack/reconciler/instantiateReactComponent.js
ReactComponent
分为 3 种:
ReactEmptyComponent
: 空组件(ReactElement 的 type 属性为 null 或 false 的组件),在浏览器中返回ReactDOMEmptyComponent
。ReactHostComponent
: 原生组件(ReactElement 为string,number 或 ReactElement 的 type 属性为 string 的组件)。createInternalComponent()
:该函数用于创建原生组件,在浏览器中返回ReactDOMComponent
。createInstanceForText()
: 该函数用于创建纯文本组件,在浏览器中返回ReactDOMTextComponent
。
ReactCompositeComponent
: 自定义组件(ReactElement 的 type 属性为 function)
可以发现 React 与平台解耦,使用 ReactEmptyComponent
与 ReactHostComponent
。而这两种组件会根据平台的不同生成不同的组件对象,在浏览器中则为 ReactDOMEmptyComponent
、ReactDOMComponent
与 ReactDOMTextComponent
。
它们通过 /src/renderers/dom/stack/client/ReactDOMStackInjection.js 进行注入。
( /src/renderers 路径下包含各个平台上不同的 ReactComponent 实现,包括 react-art/react-dom/react-native。)
State
1 | // |
从零开始:实现初始化渲染
设置 babel
首先我们需要了解 babel 如何转换 JSX:React JSX transform。
babel 可以通过transform-react-jsx
插件来设置解析 JSX 之后调用的函数,默认解析为调用 React.createElement()
。所以这就是为什么虽然在 JSX 代码中没有使用到 React,却仍然需要导入它。
通过配置 transform-react-jsx
插件的 pragma
选项可以修改解析后调用的函数。
1 | // 修改解析为调用 dom() 函数 |
babel 将会把 JSX 中的标签名作为第一个参数,把 JSX 中的标签属性作为第二个参数,将标签内容作为剩余的参数。传递这些参数给 pragma
选项设置的函数。
PS: 为了方便起见,我们使用默认的解析为 React.createElement()
实现 createElement
createElement()
接受至少 2 个参数:元素类型 type(字符串表示原生元素,函数表示自定义元素),元素设置 config。其他参数视为元素的子元素 children。并且该函数返回的是一个 ReactElement
对象,属性包括 type, props, key, ref。
1 | // element.js |
然后需要导出 createElement
,才能够通过 React.createElement()
的方式调用。
1 | // index.js |
ReactElement
需要 props, key 与 ref 参数,这三个参数将通过处理 config 与 children 得到。
我们将从 config 中获取 key 与 ref(若它们存在的话),并且根据 config 得到 props (去除一些不必要的属性),同时将 children 添加到 props 当中。
1 | export function createElement(type, config, ...children) { |
除此之外,添加对 defaultProps
的支持。defaultProps
的使用方式如下:
1 | // App.js |
当传入 App 组件的 props 中不包含 name 时,设置默认的 name 为 “ahonn”。具体实现:当 ReactElement 的 type 属性为组件函数且包含 defaultProps 时遍历 props,若 props 中不包含 defaultProps 中的属性时,设置默认的 props。
1 | export function createElement(type, config, ...children) { |
目前为止完成了将 JSX 解析为函数调用(这部分由 babel 完成),调用 React.createElement()
生成 ReactElement
对象。
接下来将实现 instantiateReactComponent()
,通过 ReactELemnt 生成相对应的 ReactComponent
实例。
实现工厂方法 instantiateReactComponent
instantiateReactComponent(element)
接受一个参数 element,该参数可以是 ReactElement 对象,string,number,false 或者 null。
我们将只考虑 Web 端,而不像 React 一样使用适配器模式进行解耦。
ReactElement 生成相应 ReactComponent 实例的规则:
- element 为 null 或 false 时,生成 ReactDOMEmptyComponent 对象实例
- element 为 string 或者 number 时,生成 ReactDOMTextComponent 对象实例
- element 为 object
- element.type 为 string 时,生成 ReactDOMComponent 对象实例
- element.type 为 function(组件函数)时,生成 ReactCompositeComponent 对象实例
1 | // virtual-dom.js |
实现 ReactComponent
现在,我们需要有不同的 ReactComponent
类以供 instantiateReactComponent()
使用。同时需要实现每个类的 mountComponent()
方法来返回对应的 HTML 片段。
ReactDOMEmptyComponent
ReactDOMEmptyComponent
表示空组件, mountComponent()
方法返回空字符串。
1 | class ReactDOMEmptyComponent { |
ReactDOMTextComponent
ReactDOMTextComponent 表示 DOM 文本组件,mountComponent()
方法返回对应的字符串。
1 | class ReactDOMTextComponent { |
ReactDOMComponent
ReactDOMComponent 表示原生组件,即浏览器支持的标签(div, p, h1, etc.)。mountConponent()
方法返回对应的 HTML 字符串。
1 | class ReactDomComponent { |
ReactDOMComponent
的 mountComponent()
方法会相对复杂一点。具体实现思路是,通过 ReactElement
的 type 与 props 属性拼接对应的 HTML 标签。处理 props 的时候需要跳过 children 属性,因为需要将子组件放在当前组件中。
当存在子组件(children)时,调用 _mountChildren(children)
将组件转换为对应的 HTML 片段。具体过程是遍历 children,转换为 ReactComponent
并调用其 mountComponent()
方法。
1 | _mountChildren(children) { |
ReactCompositeComponent
ReactCompositeComponent 表示自定义的组件,mountComponent()
方法将根据提供的组件函数(element.type)实例化,并调用该组件的 render()
方法返回 ReactElement
对象。再通过instantiateReactComponent()
生成对应的 ReactComponent
,最后执行该 ReactComponent
的mountComponent()
方法。
1 | class ReactCompositeComponent { |
通过 ReactCompositeComponent 将之前的 ReactComponent 联系起来,并递归调用 mountComponent
方法得到一段 HTML。最后 render()
通过 node.innerHTML 将 HTML 字符串填到页面上对应的容器中
实现 render
最后将之前的实现串起来,利用 innerHTML 将组件渲染到页面上。
1 | export function render(element, container) { |
到这里就基本上简单的实现了 React 中将组件渲染到页面上的部分。可以通过一个简单的例子验证一下。
1 | // index.js |
页面上将显示Hello Work!