最近开始学 React,发现与 Vue 和 Angular 相比,React 非常容易上手且实现原理简单。在此记录一下学习 React 的心路历程:
JSX 语法
这是一种非常棒的语法,可以用 JS 来表达 HTML,虽说是一种语法糖,但是甜得大家不要不要的,其实只要知道 jsx 最终会被 babel 转义成标准 JS 语法即可,例如:
const title = <h1 className="title">Hello, world!</h1>;
打开 Babel REPL 输入上面代码,发现最终被转换成下面这个样子:
const title = React.createElement(
'h1', // 标签名
{ className: 'title' }, // 属性对象
'Hello, world!' // 子元素
);
到这里,大家应该大概知道其结构了,为了加深理解,我们再来一个复杂点的案例:
const element = (
<div className="title" width="200" height="100">
hello<span className="content">world!</span>
</div>
)
被转换为:
const element = React.createElement(
"div", // 标签名
{ // 属性对象
className: "title",
width: "200",
height: "100",
},
"hello", // 第一个子元素
React.createElement( // 第二个子元素
"span",
{ className: "content" },
"world!"
)
);
可以看出来,jsx 的表现力是非常强的,语法简洁,是前端界的一大杀器。
虚拟 DOM
我们已经知道,jsx 片段会被转译成用 React.createElement 方法包裹的代码,那 React.createElement 方法究竟是什么呢?其实很简单,就是把参数转换为一个对象而已:
function createElement(type, attrs, ...children) {
return { type, props: { attrs, children } }
}
- 第一个参数是 DOM 节点的标签名,它的值可能是 div,h1,span 等
- 第二个参数是一个对象,里面包含了节点所有的属性,可能包含了 className,id 等
- 从第三个参数开始,就是它的子节点
所以,jsx 最终会被转化成拥有这种结构的对象而已,这种结构的对象还有一个非常牛逼的称呼:虚拟 DOM!
const title = <h1 className="title">Hello, world!</h1>;
其实就是:
{
type: 'h1',
props: { attrs: { className: 'title' }, children: [ 'Hello, world!' ] }
}
当只有一个子节点的时候,通常 children 就不写成数组了,而是直接取数组中的第一个元素,即:
{
type: 'h1',
props: { attrs: { className: 'title' }, children: 'Hello, world!' }
}
所以,我们的 React.createElement 方法可以稍微改一下:
function createElement(type, attrs, children) {
return {
type,
props: {
attrs,
children: arguments.length > 3 ? Array.prototype.slice.call(arguments, 2) : children,
},
}
}
函数组件
函数组件就是一个接收属性对象并返回一个 React 元素的函数。例如:
function Title(props) {
return <h1 className="title">标题</h1>
}
这个定义已经非常清楚了,但是注意下面的函数也是函数组件:
function MyTitle(props) {
return <Title />
}
因为 <Title />
这种语法相当于执行了 Title 函数,得到的仍然是一个 React 元素。
类组件
类组件就是一个实现了 render 方法的 React.Component 子类。
class TitleComponent extends React.Component {
render() {
return <h1 className="title">标题</h1>
}
}
你可能会问,React.Component 父类长啥样啊,看下 TypeScript 类型声明就知道了:
class Component<P, S> {
readonly props: Readonly<P> ;
state: Readonly<S>;
constructor(props: Readonly<P> | P);
setState<K extends keyof S>(
state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
callback?: () => void
): void;
forceUpdate(callback?: () => void): void;
render(): ReactNode;
}
React.Component 父类结构如下:
class Component {
constructor(props) {
this.props = props
this.state = {}
}
// 状态更新
setState(nextState, callback) {
// ...
}
// 强制刷新
forceUpdate() {
// ...
}
render() {
throw new Error('此方法为抽象方法,需要子类实现')
}
}
组件
组件分为内置原生组件和自定义组件:
- 内置组件是 div、span 等合法 HTML 标签,其虚拟 DOM 中的 type 是字符串
- 自定义组件就是函数或类组件,其虚拟 DOM 中的 type 不再是字符串了,而是函数
Render 方法
render 方法的作用是把虚拟 DOM 变成真实 DOM 并插入到容器内部,函数结构为:
function render(vdom, container) {
// ...
}
语法如下:
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
那 render 函数内部究竟做了什么黑魔法,竟然能够把虚拟 DOM 映射到真实 DOM 呢?还记得虚拟 DOM 的数据结构吗?
{ type: 'xxx', props: { attrs: {}, children: [] }}
其实很简单,就几行代码就可以实现了:
function render(vdom, container) {
if (typeof vdom === 'string') { // 当vdom为字符串时,渲染结果是一段文本
return container.appendChild(document.createTextNode(vdom))
}
const {type, props: {attrs = {}, children}} = vdom
const dom = document.createElement(type)
Object.keys(attrs).forEach(key => {
const value = attrs[key]
if (key === 'style') {
for (let attr in value) dom.style[attr] = value[attr]
} else {
dom[key.startsWith('on') ? key.toLocaleLowerCase() : key] = value
}
});
[].concat(children).forEach(child => render(child, dom)) // 递归渲染子节点
return container.appendChild(dom) // 将渲染结果挂载到真正的DOM上
}
生命周期
生命周期分旧版和新版,各有一张图来描述,下面是旧版生命周期:
分为了初始化、挂载、更新和卸载四个阶段,还是比较清晰的,而新版生命周期把除了 componentWillUnmount 之外的所有含有 will 的函数都去掉了,例如:
-
componentWillMount
-
componentWillReceiveProps
-
componentWillUpdate
增加了:
-
getDerivedStateFromProps 静态方法
-
getSnapshotBeforeUpdate 实例方法
下面的是新版声明周期示意图: