阅读本文需要 3 分钟,编写本文耗时 3 小时,因为不知道该从哪个角度切入会更加合适,所以涂涂改改。纠结半天,最终确定下来今天的内容。
之前看 phodal 的低代码相关的文章,里面有一句话大概说的是,项目的复杂度就像力一样,只会发生转移而不会凭空消失。
我觉得这句话在好多地方都是适用的,比如我们要手写一个前端框架的目的就是将手写原生web项目的复杂度转移到框架中。
所以要手写前端框架,我们可以回归到前端开发的最初的起点,回想一下你学习前端开发的第一个 demo。
比如我们写一个简单的 Hello World:
<!DOCTYPE html> <html lang="en"> <body> <div id="malita"> <span>loading...</span> </div> <script> const root = document.getElementById('malita'); root.innerHTML = '<span>Hello Malita!</span>'; root.addEventListener('click', (event) => { event.target.innerHTML = '<span>Hi!</span>'; }) </script> </body> </html> 复制代码
有点前端基础的同学应该一眼就能看出上面的代码逻辑,就是在页面上显示 ‘Hello Malita!’,然后用户点击之后变成了 ‘Hi!’。
上面的需求虽然非常的简单,甚至没有前端基础的同学多看几眼也能看懂,但是如果使用现代前端框架中常常被提及的 MVC 架构去分析的话。
Hello Malita!
和 Hi!
是数据,存放在 Model
层; 是视图存放在 View
层; 点击事件和数值变化是控制层存放在 Controller
;
将上面的代码拆解之后,得到以下的代码段,夸张点说,这就是 React 和 Vue 的核心源码,这可不是不买会员就可以观看的内容哦。 (我吹的,我不会Vue)
<script> function Model() { this.text = 'Hello Malita!'; this.setText = text => this.text = text; } function View(controller) { const self = this; self.root = document.getElementById('malita'); self.root.addEventListener('click', controller.onClick) this.render = () => { const text = controller.getModel().text; self.root.innerHTML = `<span>${text}</span>`; } } function Controller(model) { const self = this; self.model = model; this.onClick = (event) => { self.model.setText('Hi!'); // 数据驱动页面,页面应该根据 model 更改响应 render const text = controller.getModel().text; event.target.innerHTML = `<span>${text}</span>`; } this.getModel = () => { return self.model; } } const model = new Model(); const controller = new Controller(model); const view = new View(controller); view.render(); </script> 复制代码
递进一步的我们来看下使用 React 如何实现上面的功能。
为了便于后续的开发,(其实是我懒得复制 cdn 链接)我们在当前项目的根目录下,安装 react 和 react-dom
npm i react react-dom 复制代码
然后在 demo 中引入
<script src="../../node_modules/react/umd/react.development.js"></script> <script src="../../node_modules/react-dom/umd/react-dom.development.js"></script> 复制代码
然后使用 React 的原始用法来实现上述的功能
<body> <div id="malita"> <span>loading...</span> </div> <script> const Hello = () => { const [text, setText] = React.useState('Hello Malita!'); return React.createElement("span", { onClick: () => { setText('Hi!'); } }, text); }; const root = ReactDOM.createRoot(document.getElementById('malita')); root.render(React.createElement(Hello)); </script> </body> 复制代码
如果你没有 React 基础,那这个用例和上面的哪些用例可以比对着,仔细看看。
使用 createElement
的语法进行 React 开发,最大的问题就是当有层级嵌套时,会变得非常的复杂。可读性极差。
比如简单的
<div><p>Hello<span>Malita!</span></p></div> 复制代码
需要编写以下代码:
React.createElement("div", null, React.createElement("p", null, "Hello", React.createElement("span", null, "Malita!")));
就相当于你在写 html
的时候,都不使用 html
的标签,而是全部使用 js 方法 document.createElement(tagName)
来编写整个页面。
所以为了更加便于书写和阅读,充分利用 html 和 js 的优势,我们进行 React 开发一般都会使用 jsx
语法。
依旧是上面的需求,我们引入 jsx
,代码变得更直观了:
<body> <div id="malita"> <span>loading...</span> </div> <script> const Hello = () => { const [text, setText] = React.useState('Hello Malita!'); return (<span onClick={()=> { setText('Hi!') }}> {text} </span>); }; const root = ReactDOM.createRoot(document.getElementById('malita')); root.render(React.createElement(Hello)); </script> </body> 复制代码
但是写完发现,浏览器是无法识别 jsx 语法的,所以我们要引入 babel 来将我们的 jsx 语法转换成 React 的原始语法。
其实 babel 的实现原理非常简单,就是通过将代码转换成抽象语法书 (AST),再转换成目标语法,记下来,这是面试题。
在项目中安装 babel-standalone
npm i babel-standalone 复制代码
然后在项目中引入,并且修改 script 的 type 为 text/babel
。
<head> + <script src="../../node_modules//babel-standalone/babel.min.js"></script> </head> <body> <div id="malita"> <span>loading...</span> </div> - <script> + <script type="text/babel"> </script> </body> 复制代码
我测试的时候使用的是谷歌浏览器 V100 ,所以里面用到的 ES6 的一些语法,它都可以识别,后续我们也会使用 babel 将我们的 es6 代码转换成 es5。
到此我们就完成了让页面运行起来的所有尝试。
值得注意的是,这不是一个 React 的零基础入门教程,文章的用意在于讲清楚前端框架的部分运行原理和工作内容,你可以把这当作一篇需求文档来对待,因为我们会在后续的内容中,用更加科学合理的方式来实现它。
在这一个系列中,我会尽量讲明“为什么使用XXX”,它解了什么问题。
感谢阅读,如果你有其他的疑问,或者对这个系列书写的角度和切入点有其他的看法和建议的话,欢迎在评论区和我互动。