JSX の本質#
JSX とは一体何か、まずは React 公式サイトが示す定義を見てみましょう:
JSX は JavaScript の一種の構文拡張で、テンプレート言語に非常に近いですが、JavaScript の能力を十分に備えています。
JSX について、Facebook は「構文拡張」と定義し、同時に十分に備えている JS の能力を強調しています。しかし、実際には HTML に非常に似ており、馴染みのある JavaScript とは異なります。これはどう説明すればよいのでしょうか?
JSX 構文と JavaScript の関係#
実際、答えは非常にシンプルです。見てくださいReact 公式サイト
JSX は React.createElement () にコンパイルされ、React.createElement () は「React Element」と呼ばれる JS オブジェクトを返します。
つまり、実際には JSX は一度コンパイルされてから、React.createElement()
の呼び出しによってReact Element
の JS オブジェクトに変わるのです。
では、コンパイルはどのように行われるのでしょうか?実際、ECMAScript 2015+ バージョン
のコードに対しては、通常Babel
というツールを使用して古いバージョンのブラウザとの互換性を持たせる必要があります。
例えば、ES2015+
で非常に便利なテンプレート文字列の構文糖:
var text = 'World'
console.log(`Hello ${text}!`) //Hello World!
Babel はこのコードをほとんどの低バージョンのブラウザでも認識できる ES5 コードに変換することができます:
var text = 'World'
console.log('Hello'.concat(text, '!')) //Hello World!
同様に、Babel も JSX 構文を JavaScript コードに変換する能力を持っています。 では、Babel は具体的に JSX をどのように処理するのでしょうか?【例子】
見ると、JSX のタグはすべて対応する React.createElement の呼び出しに変換されています。つまり、実際には JSX は React.createElement を書くことに他ならず、見た目は HTML のようですが、内部は JS なのです。
JSX の本質は React.createElement という JavaScript 呼び出しの構文糖であり、これが React 公式が述べた「JSX は JavaScript の能力を十分に備えている」という言葉に完璧に呼応しています。
上の図のように、JSX の利点は明らかです。JSX コードは階層が明確で、ネスト関係がはっきりしています。しかし、React.createElement を使用すると、見た目が JSX よりもはるかに混乱し、読みづらく、書くのも大変です。
JSX 構文糖はフロントエンド開発者が最も馴染みのある HTML タグのような構文を使用して仮想 DOM を作成することを可能にし、学習コストを下げると同時に、開発効率と開発体験を向上させます。
JSX はどのように DOM にマッピングされるのか?#
まずは createElement のソースコードを見てみましょう。ここに注釈付きのソースコードがあります。
export function createElement(type, config, children) {
var propName
//保留名を抽出
var props = {}
var key = null
var ref = null
var self = null
var source = null
//タグの属性が空でない場合、タグに属性値があることを示す。特別処理:keyとrefを個別の変数に割り当てる
if (config != null) {
//合理的なrefがある
if (hasValidRef(config)) {
ref = config.ref
}
//合理的なkeyがある
if (hasValidKey(config)) {
key = '' + config.key
}
self = config.__self === undefined ? null : config.__self
source = config.__source === undefined ? null : config.__source
//configの残りの属性で、かつ原生属性(RESERVED_PROPSオブジェクトの属性)でないものは、新しいpropsオブジェクトに追加
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName] //configからkey/refを除去し、他の属性をpropsオブジェクトに追加
}
}
}
//子要素は1つ以上の引数になり得る、それらは新しく割り当てられたpropsオブジェクトに転送される。
// 子要素の数(第3引数以降の引数はすべて子要素、兄弟ノード)
var childrenLength = arguments.length - 2
if (childrenLength === 1) {
props.children = children
} else if (childrenLength > 1) {
var childArray = Array(childrenLength) //配列を宣言
//順次childrenを配列にpush
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2]
}
{
//arrayを凍結し、元のchildArrayを返し、変更できないようにする。ライブラリのコアオブジェクトが変更されるのを防ぎ、凍結オブジェクトはパフォーマンスを大幅に向上させる。
if (Object.freeze) {
Object.freeze(childArray)
}
}
props.children = childArray //親コンポーネントはthis.props.childrenを通じて子コンポーネントの値を取得
}
//子コンポーネントにデフォルト値を設定。一般的にはコンポーネントに対して。
//class com extends React.componentの場合、com.defaultPropsは現在のコンポーネントの静的メソッドを取得。
if (type && type.defaultProps) {
//現在のコンポーネントにデフォルトのdefaultPropsがある場合、現在のコンポーネントのデフォルト内容をdefaultPropsに定義。
var defaultProps = type.defaultProps
for (propName in defaultProps) {
if (props[propName] === undefined) {
//親コンポーネントの対応する値がundefinedの場合、デフォルト値をpropsの属性として割り当てる。
props[propName] = defaultProps[propName]
}
}
}
{
//refまたはkeyが存在する場合
if (key || ref) {
//typeがコンポーネントの場合
var displayName =
typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type
if (key) {
defineKeyPropWarningGetter(props, displayName)
}
if (ref) {
defineRefPropWarningGetter(props, displayName)
}
}
}
//props:1.configの属性値 2.childrenの属性(文字列/配列)3.defaultの属性値
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
}
パラメータ分析#
function createElement(type, config, children)
は 3 つのパラメータtype, config, children
を持ち、それぞれの意味は次の通りです。
type:ノードのタイプを識別するために使用されます。これは「h1」や「div」のような標準 HTML タグの文字列であるか、React コンポーネントのタイプまたは React フラグメントのタイプである可能性があります。
config:オブジェクト形式で渡され、コンポーネントのすべての属性がキーと値のペアとして config オブジェクトに格納されます。
children:オブジェクト形式で渡され、コンポーネントタグ間のネストされた内容、すなわち「子ノード」「子要素」と呼ばれるものを記録します。
例えば、以下の呼び出し例があります。
React.createElement(
'ul',
{
// 属性のキーと値のペアを渡す
className: 'list',
// 第3引数以降に渡されるパラメータはすべてchildren
},
React.createElement(
'li',
{
key: '1',
},
'1'
),
React.createElement(
'li',
{
key: '2',
},
'2'
)
)
それに対応する DOM 構造は以下の通りです。
<ul className="list">
<li key="1">1</li>
<li key="2">2</li>
</ul>
パラメータを理解した後、次に進みましょう。
config パラメータの分解#
if (config != null) {
//合理的なrefがある
if (hasValidRef(config)) {
ref = config.ref
}
//合理的なkeyがある
if (hasValidKey(config)) {
key = '' + config.key
}
self = config.__self === undefined ? null : config.__self
source = config.__source === undefined ? null : config.__source
//configの残りの属性で、かつ原生属性(RESERVED_PROPSオブジェクトの属性)でないものは、新しいpropsオブジェクトに追加
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName] //configからkey/refを除去し、他の属性をpropsオブジェクトに追加
}
}
}
ここでは、入力パラメータの config をref, key, self, source, props
のいくつかの属性に分解し、その後は children の処理部分に進みます。
子要素の抽出#
上記の config を分解した後は、子要素を処理するコードが続きます。ここでは第 2 引数以降のすべての引数をprops.children
配列に格納します。
// 子要素は1つ以上の引数になり得る、それらは新しく割り当てられたpropsオブジェクトに転送される。
// 子要素の数(第3引数以降の引数はすべて子要素、兄弟ノード)
var childrenLength = arguments.length - 2
if (childrenLength === 1) {
props.children = children
} else if (childrenLength > 1) {
var childArray = Array(childrenLength) //配列を宣言
//順次childrenを配列にpush
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2]
}
{
//arrayを凍結し、元のchildArrayを返し、変更できないようにする。ライブラリのコアオブジェクトが変更されるのを防ぎ、凍結オブジェクトはパフォーマンスを大幅に向上させる。
if (Object.freeze) {
Object.freeze(childArray)
}
}
props.children = childArray //親コンポーネントはthis.props.childrenを通じて子コンポーネントの値を取得
}
次に、親コンポーネントが children に props を渡した場合の処理に進みます。子コンポーネントがデフォルト値を設定しており、親コンポーネントが props を渡さなかった場合(つまり値がundefined
の場合)、提供されたデフォルト値を使用します。
//子コンポーネントにデフォルト値を設定。一般的にはコンポーネントに対して。
//class com extends React.componentの場合、com.defaultPropsは現在のコンポーネントの静的メソッドを取得。
if (type && type.defaultProps) {
//現在のコンポーネントにデフォルトのdefaultPropsがある場合、現在のコンポーネントのデフォルト内容をdefaultPropsに定義。
var defaultProps = type.defaultProps
for (propName in defaultProps) {
if (props[propName] === undefined) {
//親コンポーネントの対応する値がundefinedの場合、デフォルト値をpropsの属性として割り当てる。
props[propName] = defaultProps[propName]
}
}
}
次に、key
とref
が割り当てられているかどうかをチェックし、存在する場合はdefineKeyPropWarningGetter
とdefineRefPropWarningGetter
の 2 つの関数を実行します。その後、一連の組み立てられたデータをReactElement
に渡します。
ここでdefineKeyPropWarningGetter
とdefineRefPropWarningGetter
の 2 つの関数の役割は、ref と key が取得されるときにエラーを報告することです。
ReactElement#
createElement()
が最終的に呼び出すメソッドで、実際にはReactElement()
メソッドは渡された一連のデータにtype
、source
、self
などのマーク属性を追加し、JS オブジェクトを直接返します。
JSX で使用される HTML に似たノードは、Babel の助けを借りてネストされたReactElement
オブジェクトに変換されます。これらの情報は、後でアプリケーションのツリー構造を構築する際に非常に重要であり、React はこれらのタイプのデータを提供することでプラットフォームの制約から解放されます。