學習和閱讀 vue 源碼有段時間了,最近在嘗試去學習 react,因爲眼前項目使用不上 react,並不想一股腦的學習它的 API(長時間不用仍是會忘),因此這次的學習過程打算換種方式,對於 react 涉及到的每一個點嘗試逐個深刻,瞭解其解析過程及整個框架的思路。html
對於每一個點的學習和深刻,將以文章的形式產出,主要是對於學習的內容的記錄(因此看來內容有點多),方便本身之後是用時查閱和回顧。vue
在此以前,曾屢次的在 react 入門的邊緣來回試探,每次都止於寫一個簡單的 demo,我相信下面這個你們確定很熟悉,本文也是從這裏開始的。node
npx create-react-app my-app
cd my-app
npm install
npm start
複製代碼
而後應該就能跑起來(環境和安裝沒有問題的話),簡化下代碼,而後面對下面的這個代碼陷入了思考,雖然之前見過也寫過不少次了。react
代碼以下:webpack
import React from 'react';
function App() {
return (
<div> <h1>good good study, day day up</h1> </div>
);
}
export default App;
複製代碼
APP
返回的乍一看很像 html,固然相信不少人都知道這個是 JSX 的語法。那麼問題來了:web
JSX 語法寫的模版,如何生成真實的 dom?npm
類比咱們先看看Vue
中template
- ast
- code
-vnode
- dom
的實現。瀏覽器
template 轉換成 ast 及 ast 轉換成 code 的過程推薦幾篇文章:bash
如下用一個簡單的例子來簡單說明下 parse 的過程babel
<template>
<div>
<h1>good good study, day day up</h1>
</div>
</template>
複製代碼
// 簡化版,主要是看下結構
{
//...
parent: undefined,
children: [
{
parent: {
//...
tag: "div",
type: 1
},
children: [
// ...
text: "good good study, day day up",
type: 3
],
tag: "h1",
type: 1
}
],
tag: "div",
type: 1
}
複製代碼
對應生成的 code
以下:
with(this){
return _c(
'div',
[
_c(
'h1',
[_v("good good study, day day up")]
)
]
)
}
複製代碼
最終獲得的結果就是這樣的渲染函數。
咱們再看看react的實現。首先直接看npm star
後的main.chunk.js
文件,能夠看到以下的代碼(簡化版):
function App() {
return createElement(
"div",
{
__source: {
fileName: _jsxFileName,
lineNumber: 5
},
__self: this
},
createElement(
"h1",
{
__source: {
fileName: _jsxFileName,
lineNumber: 6
},
__self: this
},
"good good study, day day up"
)
);
}
複製代碼
對比 Vue
生成的 code,會發現很像,因此這裏能夠先總結一下:
react 也是經過一層轉換,把咱們寫的 JSX 模版,轉換成對應的函數。
因此這就算完了?來,接着來,JSX 是如何轉換的?
瞭解 Vue parse
過程的就知道,轉換是發生在編譯的階段:在首次$mount
的時候會執行compileToFunctions
(其中主要就是模版到渲染函數的過程)。
那 React 呢,嘗試去看了 React
和 ReactDOM
的源碼,根本找不到任何轉換的代碼。並且你們也看到了main.chunk.js
的代碼,咱們寫的 JSX 已經轉換成對應的函數了。因此再此以前,已經完成了轉換。
好了不賣關子了,這裏用的是 babel
解析器(什麼是Babel,Babel能作什麼),咱們首先找到工程中配置的地方。
因爲本人對於工程配置及工程化不是很瞭解,因此我這裏也是找了好久,要想找到 babel 配置的入口,需先執行(最好找個demo工程執行,該命令不可逆)
yarn eject
複製代碼
找到 /config/webpack.config.js
, 相關代碼以下:
module: {
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+ref![path]',
},
},
},
],
],
cacheDirectory: true,
cacheCompression: isEnvProduction,
compact: isEnvProduction,
},
},
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
cacheCompression: isEnvProduction,
sourceMaps: false,
},
}
複製代碼
看到這裏相信就能知道,這裏其實就是配置了 loader
,試了看各個解析器的源碼,可是仍然困難重重(各類引用),這裏也是換了種方式來學習解析的過程。
嘗試手寫一個 JSX 的插件。
這裏你們網上搜應該能搜出一堆關於babel 插件的代碼,我這裏也是找到一個基礎的例子。
如下是一個將log
處理成console.log
的插件的代碼:
const babel = require('@babel/core')
const t = require('babel-types')
const code = `
const a = 3 * 103.5 * 0.8;
log(a);
const b = a + 105 - 12;
log(b);
`
const visitor = {
CallExpression(path) {
// 這裏判斷一下若是不是log的函數執行語句則不處理
if (path.node.callee.name !== 'log') return
// t.CallExpression 和 t.MemberExpression分別表明生成對於type的節點,path.replaceWith表示要去替換節點,這裏咱們只改變CallExpression第一個參數的值,第二個參數則用它本身原來的內容,即原本有的參數
path.replaceWith(t.CallExpression(
t.MemberExpression(t.identifier('console'), t.identifier('log')),
path.node.arguments
))
}
}
const result = babel.transform(code, {
plugins: [{
visitor: visitor
}]
})
console.log(result.code)
複製代碼
處理結果:
const a = 3 * 103.5 * 0.8;
console.log(a);
const b = a + 105 - 12;
console.log(b);
複製代碼
看了代碼後應該差很少能瞭解插件的編寫過程,大體以下:code 首先會解析成 AST,而後會遍歷整個 AST 樹,每一個節點都有其特定的屬性,插件的vistor對象的處理函數會在解析的過程當中被調用,插件要作的事情就是在合適的地方(這裏是CallExpression
),符合條件的狀況下(這裏是 path.node.callee.name === 'log'
),對解析結果進行更改。知道原理之後,嘗試着寫 JSX 解析的插件。
const code = `
var html = <div>
<h1>good good study, day day up</h1>
</div>
`
const visitor = {
}
const result = babel.transform(code, {
plugins: [
{
visitor: visitor
}
]
})
console.log(result.code)
複製代碼
大體的結構就是這樣,指望達到的目標code對應的輸出以下:
var html = React.createElement(
"div",
null,
React.createElement("h1", null, "good good study, day day up")
)
複製代碼
以上代碼執行後,會報錯,由於並非js的標準語法,沒法正常解析,因此這裏首先須要引入一個插件 plugin-syntax-jsx
,讓解析器其能識別該種語法。
引入插件,修改的代碼以下:
babel.transform(code, {
plugins: [
'@babel/plugin-syntax-jsx',
{
visitor: visitor
}
]
})
複製代碼
執行的結果爲:
var html = <div>
<h1>good good study, day day up</h1>
</div>;
複製代碼
這裏能看到咱們能正常識別 JSX 模版,只是輸出並非咱們須要的,咱們須要把它轉換成咱們的函數。接下來的一步就是須要找到合適的時機。
這裏咱們只是知道咱們能正常識別了,可是在解析的過程當中,其對應的 AST 具體長什麼樣子呢?
這裏也是推薦一個網站,astexplorer.net/
這裏就能看到整個 AST 樹的結構(這裏還沒去看解析成 AST 生成的過程,目測和 Vue 中 parseHTML 的過程原理同樣,這裏後續會花點時間看下 babal 生成 AST 的過程),應該很快就能找到咱們想要的關鍵信息-JSXElement
,對照以上的 AST 和關鍵信息,就當前這個例子,咱們就思考下‘合適的時機‘-JSXElement的變量賦值:
init.type === 'JSXElement'
加入‘時機’後代碼以下:
const babel = require('@babel/core')
const code = ` var html = <div> <h1>good good study, day day up</h1> </div> `
const visitor = {
VariableDeclarator(path) {
if (path.node.init.type === 'JSXElement'){
console.log('start')
// deal
}
}
}
const result = babel.transform(code, {
plugins: [
'@babel/plugin-syntax-jsx',
{
visitor: visitor
}
]
})
console.log(result.code)
複製代碼
獲得的結果以下:
start
var html = <div>
<h1>good good study, day day up</h1>
</div>;
複製代碼
固然這裏只是輸入標籤的信息,其中還有不少其餘的節點信息,其餘的信息那麼也就是 JSX 的語法規則了,如循環、class、條件語句、邏輯代碼等語法規則了。本文只作簡單的實現。接下來要作的就是要整合節點的信息,生成對應的函數代碼。
... 未完待續
(這裏涉及到babel-types
的使用,因爲對此塊不是很熟悉,文章先進行到這裏,後續寫好會更新上來)
那瞭解了JSX的解析過程後,咱們思考下,這個與vue的parse
的過程差異在哪?
canBeLeftOpenTag
標籤如:p,會補全關閉標籤等,也就是你們能夠像寫普通的html來寫template
。而 react 的 JSX 就有不少的語法規則,如class
必須寫className
、標籤以前的換行後的空格會被忽略等等(仍在學習JSX語法中,後續會繼續補充完善這塊的區別)。就第二點區別,能夠看出來,若是是原有的html項目,想要遷移成 vue 或 react,遷移成 Vue 的成本會小不少,Vue 不只在寫法上,還有對於瀏覽器特殊行爲的處理上,都保持了和 html 規範的統一。若要遷移成 react ,可能改形成本就會比較大。
以上只是react初學者的主觀的見解,更多的特性和優劣須要深刻學習後才能瞭解。