乐趣区

【React源码解读】- 组件的实现

前言
react 使用也有一段时间了,大家对这个框架褒奖有加,但是它究竟好在哪里呢?让我们结合它的源码,探究一二!(当前源码为 react16,读者要对 react 有一定的了解)

回到最初
根据 react 官网上的例子,快速构建 react 项目
npx create-react-app my-app

cd my-app

npm start
打开项目并跑起来以后,暂不关心项目结构及语法糖,看到 App.js 里,这是一个基本的 react 组件 <App/> 我们 console 一下,看看有什么结果。
import React, {Component} from ‘react’;
import logo from ‘./logo.svg’;
import ‘./App.css’;

class App extends Component {

render() {
return (
<div className=”App”>
<header className=”App-header”>
<img src={logo} className=”App-logo” alt=”logo” />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
</header>
</div>
);
}
}

export default App;

console.log(<App/>)

可以看到,<App/> 组件其实是一个 JS 对象,并不是一个真实的 dom。
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。有兴趣的同学可以去阮一峰老师的 ES6 入门详细了解一下
上面有我们很熟悉的 props,ref,key, 我们稍微修改一下 console,看看有什么变化。
console.log(<App key={1} abc={2}><div> 你好,这里是 App 组件 </div></App>)

可以看到,props,key 都发生了变化,值就是我们赋予的值,props 中嵌套了 children 属性。可是为什么我们嵌入的是 div,实际上却是一个对象呢?
打开源码
/node_modules/react

首先打开 index.js
‘use strict’;

if (process.env.NODE_ENV === ‘production’) {
module.exports = require(‘./cjs/react.production.min.js’);
} else {
module.exports = require(‘./cjs/react.development.js’);
}
可以知道目前用上的是./cjs/react.development.js, 直接打开文件。根据最初的代码,我们组件 <App/> 用到了 React.Component。找到 React 暴露的接口:

接着找到 Component: Component 方法,
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

Component.prototype.setState = function (partialState, callback) {
!(typeof partialState === ‘object’ || typeof partialState === ‘function’ || partialState == null) ? invariant(false, ‘setState(…): takes an object of state variables to update or a function which returns an object of state variables.’) : void 0;
this.updater.enqueueSetState(this, partialState, callback, ‘setState’);
};

Component.prototype.forceUpdate = function (callback) {
this.updater.enqueueForceUpdate(this, callback, ‘forceUpdate’);
};

上面就是一些简单的构造函数,也可以看到,我们常用的 setState 是定义在原型上的 2 个方法。
至此,一个 <App/> 组件已经有一个大概的雏形:

到此为止了吗?这看了等于没看啊,究竟组件是怎么变成 div 的?render 吗?可是全局搜索,也没有一个 function 是 render 啊。
原来,我们的 jsx 语法会被 babel 编译的。

这下清楚了,还用到了 React.createElement
createElement: createElementWithValidation,
通过 createElementWithValidation,
function createElementWithValidation(type, props, children) {
······

var element = createElement.apply(this, arguments);

return element;
}
可以看到,return 了一个 element,这个 element 又是继承自 createElement,接着往下找:
function createElement(type, config, children) {
var propName = void 0;

// Reserved names are extracted
var props = {};

var key = null;
var ref = null;
var self = null;
var source = null;
······
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
这里又返回了一个 ReactElement 方法,再顺着往下找:
var ReactElement = function (type, key, ref, self, source, owner, props) {
var element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,

// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,

// Record the component responsible for creating this element.
_owner: owner
};

······
return element;
};
诶,这里好像返回的就是 element 对象,再看我们最初的 <App/> 的结构,是不是很像
验证一下我们的探索究竟对不对,再每一个方法上我们都打上 console,(注意,将 App 里的子元素全部删空,利于我们观察)

React.createElement、createElementWithValidation、createElement、ReactElement,通过这些方法,我们用 class 声明的 React 组件在变成真实 dom 之前都是 ReactElement 类型的 js 对象
createElementWithValidation:
首先校验 type 是否是合法的

校验了 props 是否符合设置的 proptypes

校验了子节点的 key,确保每个数组中的元素都有唯一的 key

createElement:

type 是你要创建的元素的类型,可以是 html 的 div 或者 span,也可以是其他的 react 组件,注意大小写
config 中包含了 props、key、ref、self、source 等

向 props 加入 children,如果是一个就放一个对象,如果是多个就放入一个数组。

那如果 type.defaultProps 有默认的 props 时,并且对应的 props 里面的值是 undefined,把默认值赋值到 props 中

也会对 key 和 ref 进行校验

ReactElement:
ReactElement 就比较简单了,创建一个 element 对象,参数里的 type、key、ref、props、等放进去,然后 return 了。最后调用 Object.freeze 使对象不可再改变。
组件的挂载
我们上面只是简单的探究了 <App/> 的结构和原理,那它究竟是怎么变成真实 dom 的呢

ReactDOM.render(<App />, document.getElementById(‘root’));
我们接着用 babel 编译一下:

原来 ReactDOM.render 调用的是 render 方法,一样,找暴露出来的接口。

var ReactDOM = {
······
render: function (element, container, callback) {
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
},
······
};

它返回的是一个 legacyRenderSubtreeIntoContainer 方法,这次我们直接打上 console.log

这是打印出来的结果,

legacyRenderSubtreeIntoContainer 这个方法除主要做了两件事:
清除 dom 容器元素的子元素
while (rootSibling = container.lastChild) {
{
if (!warned && rootSibling.nodeType === ELEMENT_NODE && rootSibling.hasAttribute(ROOT_ATTRIBUTE_NAME)) {
warned = true;
}
}
container.removeChild(rootSibling);
}
创建 ReactRoot 对象

源码暂时只读到了这里,关于 React16.1~3 的新功能,以及新的生命周期的使用和原理、Fiber 究竟是什么,我们将在后续文章接着介绍。
广而告之
本文发布于薄荷前端周刊,欢迎 Watch & Star ★,转载请注明出处。
欢迎讨论,点个赞再走吧 。◕‿◕。 ~

退出移动版