學(xué)習(xí)和閱讀 vue 源碼有段時間了,最近在嘗試去學(xué)習(xí) react窑滞,由于眼前項目使用不上 react力穗,并不想一股腦的學(xué)習(xí)它的 API(長時間不用還是會忘)舞虱,所以此次的學(xué)習(xí)過程打算換種方式,對于 react 涉及到的每個點嘗試逐個深入类茂,了解其解析過程及整個框架的思路耍属。
對于每個點的學(xué)習(xí)和深入,將以文章的形式產(chǎn)出巩检,主要是對于學(xué)習(xí)的內(nèi)容的記錄(所以看來內(nèi)容有點多)厚骗,方便自己以后是用時查閱和回顧。
從demo開始
在此之前兢哭,曾多次的在 react 入門的邊緣來回試探领舰,每次都止于寫一個簡單的 demo,我相信下面這個大家肯定很熟悉迟螺,本文也是從這里開始的冲秽。
npx create-react-app my-app
cd my-app
npm install
npm start
然后應(yīng)該就能跑起來(環(huán)境和安裝沒有問題的話),簡化下代碼矩父,然后面對下面的這個代碼陷入了思考锉桑,雖然以前見過也寫過很多次了。
代碼如下:
import React from 'react';
function App() {
return (
<div>
<h1>good good study, day day up</h1>
</div>
);
}
export default App;
APP
返回的乍一看很像 html窍株,當(dāng)然相信很多人都知道這個是 JSX 的語法民轴。那么問題來了:
JSX 語法寫的模版,如何生成真實的 dom夹姥?
類比我們先看看Vue
中template
- ast
- code
-vnode
- dom
的實現(xiàn)杉武。
簡單看下Vue 的parse過程
template 轉(zhuǎn)換成 ast 及 ast 轉(zhuǎn)換成 code 的過程推薦幾篇文章:
以下用一個簡單的例子來簡單說明下 parse 的過程
template 如下:
<template>
<div>
<h1>good good study, day day up</h1>
</div>
</template>
對應(yīng)生成的 ast 如下:
// 簡化版,主要是看下結(jié)構(gòu)
{
//...
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
}
對應(yīng)生成的 code
如下:
with(this){
return _c(
'div',
[
_c(
'h1',
[_v("good good study, day day up")]
)
]
)
}
最終得到的結(jié)果就是這樣的渲染函數(shù)辙售。
我們再看看react的實現(xiàn)轻抱。首先直接看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旦部,會發(fā)現(xiàn)很像祈搜,所以這里可以先總結(jié)一下:
react 也是通過一層轉(zhuǎn)換较店,把我們寫的 JSX 模版,轉(zhuǎn)換成對應(yīng)的函數(shù)容燕。
所以這就算完了梁呈?來,接著來蘸秘,JSX 是如何轉(zhuǎn)換的官卡?
JSX 的轉(zhuǎn)換過程
了解 Vue parse
過程的就知道,轉(zhuǎn)換是發(fā)生在編譯的階段:在首次$mount
的時候會執(zhí)行compileToFunctions
(其中主要就是模版到渲染函數(shù)的過程)醋虏。
那 React 呢寻咒,嘗試去看了 React
和 ReactDOM
的源碼,根本找不到任何轉(zhuǎn)換的代碼颈嚼。而且大家也看到了main.chunk.js
的代碼毛秘,我們寫的 JSX 已經(jīng)轉(zhuǎn)換成對應(yīng)的函數(shù)了。所以再此之前阻课,已經(jīng)完成了轉(zhuǎn)換叫挟。
好了不賣關(guān)子了,這里用的是 babel
解析器(什么是Babel限煞,Babel能做什么)抹恳,我們首先找到工程中配置的地方。
尋找 babel 配置的入口
由于本人對于工程配置及工程化不是很了解晰骑,所以我這里也是找了很久适秩,要想找到 babel 配置的入口,需先執(zhí)行(最好找個demo工程執(zhí)行硕舆,該命令不可逆)
yarn eject
找到 /config/webpack.config.js
, 相關(guān)代碼如下:
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
,試了看各個解析器的源碼抚官,但是仍然困難重重(各種引用)扬跋,這里也是換了種方式來學(xué)習(xí)解析的過程。
嘗試手寫一個 JSX 的插件凌节。
手寫 JSX 的插件
這里大家網(wǎng)上搜應(yīng)該能搜出一堆關(guān)于babel 插件的代碼钦听,我這里也是找到一個基礎(chǔ)的例子。
console的插件的例子
以下是一個將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的函數(shù)執(zhí)行語句則不處理
if (path.node.callee.name !== 'log') return
// t.CallExpression 和 t.MemberExpression分別代表生成對于type的節(jié)點倍奢,path.replaceWith表示要去替換節(jié)點,這里我們只改變CallExpression第一個參數(shù)的值朴上,第二個參數(shù)則用它自己原來的內(nèi)容,即本來有的參數(shù)
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)
處理結(jié)果:
const a = 3 * 103.5 * 0.8;
console.log(a);
const b = a + 105 - 12;
console.log(b);
看了代碼后應(yīng)該差不多能了解插件的編寫過程卒煞,大致如下:code 首先會解析成 AST痪宰,然后會遍歷整個 AST 樹,每個節(jié)點都有其特定的屬性,插件的vistor對象的處理函數(shù)會在解析的過程中被調(diào)用衣撬,插件要做的事情就是在合適的地方(這里是CallExpression
),符合條件的情況下(這里是 path.node.callee.name === 'log'
),對解析結(jié)果進(jìn)行更改乖订。知道原理以后,嘗試著寫 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)
大致的結(jié)構(gòu)就是這樣乍构,期望達(dá)到的目標(biāo)code對應(yīng)的輸出如下:
var html = React.createElement(
"div",
null,
React.createElement("h1", null, "good good study, day day up")
)
以上代碼執(zhí)行后,會報錯扛点,因為并不是js的標(biāo)準(zhǔn)語法哥遮,無法正常解析,所以這里首先需要引入一個插件 plugin-syntax-jsx
占键,讓解析器其能識別該種語法昔善。
引入插件,修改的代碼如下:
babel.transform(code, {
plugins: [
'@babel/plugin-syntax-jsx',
{
visitor: visitor
}
]
})
執(zhí)行的結(jié)果為:
var html = <div>
<h1>good good study, day day up</h1>
</div>;
這里能看到我們能正常識別 JSX 模版畔乙,只是輸出并不是我們需要的,我們需要把它轉(zhuǎn)換成我們的函數(shù)翩概。接下來的一步就是需要找到合適的時機(jī)牲距。
尋找時機(jī)
這里我們只是知道我們能正常識別了,但是在解析的過程中钥庇,其對應(yīng)的 AST 具體長什么樣子呢牍鞠?
這里也是推薦一個網(wǎng)站,https://astexplorer.net/
這里就能看到整個 AST 樹的結(jié)構(gòu)(這里還沒去看解析成 AST 生成的過程评姨,目測和 Vue 中 parseHTML 的過程原理一樣难述,這里后續(xù)會花點時間看下 babal 生成 AST 的過程),應(yīng)該很快就能找到我們想要的關(guān)鍵信息-JSXElement
吐句,對照以上的 AST 和關(guān)鍵信息胁后,就當(dāng)前這個例子,我們就思考下‘合適的時機(jī)‘-JSXElement的變量賦值:
- VariableDeclarator
init.type === 'JSXElement'
加入‘時機(jī)’代碼
加入‘時機(jī)’后代碼如下:
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)
得到的結(jié)果如下:
start
var html = <div>
<h1>good good study, day day up</h1>
</div>;
當(dāng)然這里只是輸入標(biāo)簽的信息嗦枢,其中還有很多其他的節(jié)點信息攀芯,其他的信息那么也就是 JSX 的語法規(guī)則了,如循環(huán)文虏、class侣诺、條件語句、邏輯代碼等語法規(guī)則了氧秘。本文只做簡單的實現(xiàn)年鸳。接下來要做的就是要整合節(jié)點的信息,生成對應(yīng)的函數(shù)代碼丸相。
生成代碼
... 未完待續(xù)
(這里涉及到babel-types
的使用搔确,由于對此塊不是很熟悉,文章先進(jìn)行到這里,后續(xù)寫好會更新上來)
那了解了JSX的解析過程后妥箕,我們思考下滥酥,這個與vue的parse
的過程差別在哪?
- vue 是編譯階段生成對應(yīng)的渲染函數(shù)畦幢,react 是babel解析階段就生成了對應(yīng)的函數(shù)
- 看過 vue parse階段源碼的同學(xué)應(yīng)該知道坎吻,vue 做了很多處理瀏覽器‘怪異’行為的操作(為了保持和瀏覽器行為的一致性),如:標(biāo)簽換行會有空格符宇葱、
canBeLeftOpenTag
標(biāo)簽如:p瘦真,會補(bǔ)全關(guān)閉標(biāo)簽等,也就是大家可以像寫普通的html來寫template
黍瞧。而 react 的 JSX 就有很多的語法規(guī)則诸尽,如class
必須寫className
、標(biāo)簽之前的換行后的空格會被忽略等等(仍在學(xué)習(xí)JSX語法中印颤,后續(xù)會繼續(xù)補(bǔ)充完善這塊的區(qū)別)您机。
就第二點區(qū)別,可以看出來年局,如果是原有的html項目,想要遷移成 vue 或 react矢否,遷移成 Vue 的成本會小很多僵朗,Vue 不僅在寫法上验庙,還有對于瀏覽器特殊行為的處理上,都保持了和 html 規(guī)范的統(tǒng)一云矫。若要遷移成 react ,可能改造成本就會比較大让禀。
以上只是react初學(xué)者的主觀的看法巡揍,更多的特性和優(yōu)劣需要深入學(xué)習(xí)后才能了解腮敌。