学习和阅读 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初学者的主观的见解,更多的特性和优劣须要深刻学习后才能了解。