本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
什么是React
在React的官网首页,居中写着这么一句话:React 用于构建用户界面的 JavaScript 库。
由此,我们可以知道以下两个信息:
- react是一个Js库而不是框架(有关库与框架的区别,不在本文讨论范围)。
- 它的核心专注于构建用户界面。
而与我们熟知的lodash
、jQuery
等其他Js工具库不同,React专注于构建用户视图。
什么是JSX
在React的核心概念中,这样介绍:JSX是JavaScript的语法扩展。
来看如下变量声明:
const element = <h1 style={{ color: 'red' }}>Hello, world!</h1>;
复制代码
观察以上代码可知:jsx
是一种将js
和html
混合的语法,是一种将组件的结构、数据甚至样式都聚合在一起的写法。
React与JSX有什么关系
我们常说:JSX 是 React.createElement
的语法糖。为了让大家对这句话有一个更清晰的认识,我们使用 Babel 来转译以上JSX的声明,如下图所示:
删除注释冗余后,得到如下结果:
const element = React.createElement("h1", {
style: {
color: 'red'
}
}, "Hello, world!");
复制代码
由上可知:Babel 会把 JSX 转译成一个名为 React.createElement
的函数调用。
在执行React.createElement
后,会返回一个React元素
,所谓的 React元素
其本质就是一个描述页面视图的js对象。
为了使这句话更加直观,我们在控制台中打印element
元素,如下图所示:
在删除冗余之后,我们得到以下结果:
// 注意:这是简化过的结构
{
type: 'h1',
props: {
children: 'Hello, world!',
style: {
color: 'red'
}
}
};
复制代码
在拿到上述React元素
后,使用ReactDOM.render
函数,将React元素
插入页面:
ReactDOM.render(element,document.getElementById('root'));
复制代码
代码实现
经过上述分析,我们可以梳理出以下渲染流程:
JSX
经过Babel
编译后,转化为React.createElement
的函数调用。- 调用
React.createElement
函数,生成一个React元素(描述视图结构的js对象)
。 - 调用
ReactDOM.render
将React.createElement
返回的React元素
转化为真实的DOM
插入容器。
因此我们需要写两个方法:
React.createElement
:调用后返回一个React元素(描述页面的js对象)
ReactDOM.render
:将返回的React元素
转化为真实DOM
,并插入页面
我们使用脚手架新建一个React
项目,删除冗余代码后,新建react
和react-dom
文件夹,如下图所示:
需在启动命令处禁用最新的jsx
转换,因为在React17
中Babel
编译后的结果为jsx
函数,而不是React.createElement
函数
在package.json的启动命令处,添加 cross-env DISABLE_NEW_JSX_TRANSFORM=true 参数
完成项目基本搭建后,在index.js里写入如下代码:
// 引入自己的 react
import React from './react';
// 引入自己的 react-dom
import ReactDOM from './react-dom';
// 声明JSX变量
const element = <h1 style={{ color: 'red' }}>Hello, world!</h1>;
// 将元素插入页面
ReactDOM.render(
element,
document.getElementById('root')
);
复制代码
React.createElement
代码实现如下:
function createElement(type, config, children){
let key;
let ref;
if(config){
key = config.key;
ref = config.ref;
}
const props = { ...config }
if(arguments.length > 3){
props.children = Array.prototype.slice.call(arguments, 2).map(warpToVdom)
}else{
props.children = warpToVdom(children)
}
return { type, key, ref, props }
}
复制代码
为了后续便于比较子节点,我们使用warpToVdom
函数对纯文本子节点进行包装,添加了REACT_TEXT
属性:
function warpToVdom(element){
return ['number', 'string'].includes(typeof element) ? { type: 'REACT_TEXT', props: { content: element } } : element;
}
复制代码
ReactDOM.render
代码实现如下:
function render(vdom, container){
// 抽离挂载逻辑,便于后续复用
mount(vdom, container)
}
function mount(vdom, parentDom){
const newDom = createDom(vdom)
if(newDom){
parentDom.appendChild(newDom); // 插入dom
}
}
function createDom(vdom){
if(!vdom) return null;
const { type, props, ref } = vdom;
let dom;
if(type === 'REACT_TEXT'){ // 处理文本节点
dom = document.createTextNode(props.content)
}else{
dom = document.createElement(type)
}
if(props){ // 更新属性
updateProps(dom, {}, props)
if(props.children){ // 处理子节点
const children = props.children;
if(typeof children === 'object' && children.type){ // 说明是一个react元素
mount(children, dom)
}else if(Array.isArray(children)){ // 有多个子节点
reconcileChildren(children, dom)
}
}
}
// 将真实dom挂载vdom便于dom-diff时查找
vdom.dom = dom;
return dom;
}
// 多个子节点循环挂载
function reconcileChildren(childrenVdom, parentDom){
childrenVdom.forEach(child => mount(child, parentDom))
}
// 属性更新方法
function updateProps(dom, oldProps, newProps){
for(let attr in newProps){
if(attr === 'children'){ // 子节点单独处理
continue
}else if(attr.startsWith('on')){ // 处理事件
dom[attr.toLocaleLowerCase()] = newProps[attr];
}else if(attr === 'style'){ // 处理样式
const styleObj = newProps[attr]
for(let styleName in styleObj){
dom.style[styleName] = styleObj[styleName]
}
}else{ // 处理其它属性 class id...
dom[attr] = newProps[attr]
}
}
}
复制代码
页面展示效果如下:
结语
我们从概念用法入手,梳理了react
的工作流程,并简单做了代码实现。当然,以上代码并不能展现react
的优势,在下一篇文章中,我们再继续了解它的更多概念及优化。