前言
“步入前端两年半,自觉菜鸡懒又烂。” 近来想着写写一些前端学习的心得,左思右想。还是从 React 入笔。为什么是 React?身为小白,React 庞大的技术栈确实给我很多编程思想上的启迪,也让我了解到更多前端领域的知识。时至今日,自己仍在探索 React 的路上。在此,感谢一路上并肩作战的战友和捎带一段车程的司机们。
零、初识 React
刚开始接触 React,大概是去年这个时候。当时 React 在 Github 的 stars 飞升,超过了之前如火中天的 AngularJS 。一开始会觉得 React 通过 jsx 实现 HTML 模板的做法并不比 AngularJS 的模板好用,因为按照传统的前端开发模式,HTML 负责结构层,CSS 负责渲染层,JavaScript 负责行为交互层,这似乎有所违背。但后来对 Virtual DOM 有了进一步的了解,才发现这是 React 的精髓所在。
一、Virtual DOM
Virtual DOM?说到 Virtual DOM 一开始是听说它的性能很高,是啊,渲染性能确实很高,但是只是单纯使用不去思考,那我又能获得什么呢?一个库的使用方法?了解一门前端热门的库?思前想后,我觉得 Virtual DOM 给我的启迪大致有以下两方面:性能优化策略(diff 算法)和 颠覆传统 DOM 编程思想。
1.性能优化策略
- 传统的浏览器渲染流程:浏览器中的渲染工作主要是由 渲染引擎 完成的。大致流程如下(不考虑 阻塞 等特殊情况)。
- 解析 HTML 文件,构建 DOM 树;
- 解析 CSS 文件,构建 CSSOM ;
- 合并 DOM 树和 CSSOM;
- 布局(计算 DOM 节点在屏幕上的精确位置),绘制(计算对应节点的样式);
- 重排(DOM结构变化),重绘(CSS变化);
- 很明显,通过上面的描述,可以看出每一次 DOM 结构的改变浏览器都会进行极大的计算量,因此,Virtual DOM 做了大致以下两个方面的处理:
- 在浏览器内存中,维护着一课与页面 DOM 结构一致的对象树
依赖 diff 算法极大提高了 DOM 操作的性能
简单地说,Virtual DOM 通过批量处理 DOM 操作,可以理解为对多次 DOM 操作起了一次缓存作用,从而合并多次的 DOM 操作为一次计算。(实现方法是将一个事件循环中发出的 DOM 操作全部收集起来,不立即在页面上产生效果,而是在事件循环的结尾,才向页面作用)
那么,Virtual DOM 的计算过程是怎样优化的呢(即浏览器内存中的 DOM)?不得不说,React 做了一次大胆的尝试。传统标准的 Diff 算法 复杂度达到了 O(n^3),这就意味着要展示 1000 个节点,就要依次执行上十亿次的比较。这是绝对无法满足性能需求的。而 React 开发团队通过制定大胆的策略,使得 Diff 算法 复杂度降到 O(n)。但是,React 的优化算法是基于以下三个前提的。
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
- 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分
于是,React 基于以上三个前提策略,分别对 Tree diff,Component diff,Element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建性能。
- Tree diff:只会对同一父节点下的所有子节点进行比较,当发现节点不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较
- Component diff:React 是基于组件构建,如果同一类型的组件,则按照原策略继续比较 Virtual DOM tree,如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点(另外,React 提供了一个函数钩子
shouldComponentUpdate()
来判断该组件是否需要进行 diff) - Element diff:当节点处于同一层级时,React 提供了三种节点操作,分别为插入(INSERT——MARKUP)、移动(MOVE_EXISTING)、删除(REMOVE_NODE)
2.抽象——编程思想的颠覆?
一开始思考 React 跟 Vue 的区别的时候,就只有直观的架构模式 MVC,MVVM 的区别,但是 MVC 只要加个 事件监听 跟 数据劫持 不也可以实现 数据双向绑定?当然,后来就想到了 Vue 比较适合中小型项目,而 React 比较适合大型项目。但是,究其根本,似乎没有明确的理由。近来又似乎找到了答案。
- UI 层与业务逻辑的低耦:肯定很多人都有同感,高耦合的 DOM 编程模式,对于项目的维护以及团队合作都很不友好。而 Virtual DOM 对真实 DOM 进行了一层抽象,它帮助我们去操作真实 DOM,而我们通过操作 Virtual DOM 来控制页面 UI。
- 当系统复杂度上升,模块间的低耦也是势在必行。现在的前端开发更加注重用户体验,很多传统在服务端解决的功能也被迁移到客户端,因此前端开发的复杂度势必会逐渐上升,React 的 VIrtual DOM 可以说是一个革命性的创新。
二、模块化开发
模块化开发在软件工程里边也算是一个老生长谈的话题了,而在 JavaScript 这门语言中,一直都是通过
<script></script>
标签在 HTML 文档中引入文件,而且模块(文件)间的依赖关系也没有通过很多的方法去处理。或许是前期前端开发的改革注重于新功能的实现,所以很长一段时间都花在浏览器 API 的优化上面了,但是随着前端开发工程化,模块化开发成了一个不可以回避的问题。
1.模块化规范
前端的模块化规范有 AMD 和 CMD(国内提出的),后端的模块化规范有 CommonJS(Nodejs 采用这种规范)。
- AMD 规范(Asynchronous Module Definition):一个浏览器端模块化开发的规范,主要的思想有以下三点。
- 模块将被异步加载
- 模块加载不影响后面语句的运行
- 所有依赖某些模块的语句均放置在回调函数中
- AMD 是 require.js 在推广过程中对模块定义的规范化的产出。AMD 规范只定义了一个全局函数
define
:
define(id?, dependencies?, factory); // id:模块名字; dependcies: 依赖模块字面量; factory: 模块初始化需要执行的函数或对象
- CMD规范(Common Module Definition):这是国内发展出来的,该规范明确了模块的基本书写格式和基本交互规则。AMD 是依赖关系前置,CMD 是按需加载。在 CMD 规范中,一个模块就是一个文件,代码书写格式如下:
define(factory);
-
factory
为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:require
、exports
和module
:
// require 是可以把其他模块导入进来的一个参数
// exports 是可以把模块内的一些属性和方法导出的一个参数
define(function(require, exports, module) {
// 模块代码
})
- CommonJS 规范是服务端模块的规范,Node.js 采用了这个规范。Node.js 首先采用了 js 模块化的概念,根据 CommonJS 规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为
global
属性。如下代码:
// module.exports 就是模块外部与内部通信的桥梁
// 加载模块使用 require 方法,该方法读取一个文件并执行,最后返回文件内部的 module.exports 对象
let i = 1;
let max = 30;
module.exports = function () {
for(i -= 1; i++ < max; ) {
console.log(i);
}
max *=1.1;
}
2.React 开发中使用的模块化
- 组件化开发:想必很多前端开发的道友都会有这样一个感触,就是同一个项目中经常有很多相似的模块(结构相似或样式相似,甚至一模一样),那么我们就会考虑到这些模块复用的问题。这个时候我们就会联想到类似 Bootstrap , Foundation 等前端框架,这些框架提供了很多便利的组件,提高了开发效率。但是我们会想到一个问题就是,第一这些组件都是别人开发好的,第二复用组件的方式就是引入 代码库,复制粘贴 HTML 代码。想解决这些问题,一开始就会想到用 模板引擎 (比如 Nunjuck Template)来解决这个问题,但是我们就会想到引入分模块引入样式表的问题,当然也可以通过自己封装一个类来实现模块化管理(比如正则匹配url,执行相关文件的函数)。而我们看看下面 React 的组件化
jsx
文件代码结构:
import '../welcome.scss'
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
正如前面所说的,React 是 js 主导的一门框架,所以我们可以在
jsx
文件中通过 js 的模块化机制引入其他文件(比如 scss 文件),这样就可以很好的解决组件化开发的问题(样式表在组件内部编写)。当然,js 的模块化机制只是针对 js 的,所以类似 css,scss,图片的引入,我们就得借助 Loader (比如 webpack 的 style-loader, css-loader, sass-loader, url-loader)进行转化,把其他形式的文件转化成 js 可以进行模块化管理的文件。而且我们可以实现一个文件一个自定义组件的效果,从而借助文件结构更方便的管理组件结构。ES6:一开始 React 出现的时候,使用的是 ES5 语法,但是这种通过 原型链 实现面向对象编程的语法对于跟常见的面向对象语言语法还是有很多不同,因此对开发的编码效率以及代码的可读性也不是很友好(个人觉得),因此 React 官方文档也推荐开发者使用 ES6 推出 的
class
语法糖。此外,ES6 的export
和import
Module 语法糖也帮助解决 js 文件依赖关系的模块化管理问题。当然了,在接触 React 的时候接触到了 ES6 这些新的语法特性,就不由得去了解一下 ES6 的其他语法特性,其中很多语法特性应用到 React 项目中,比如 箭头表达式(Arrow functions),
let
和const
命令,还要对 数组方法及字符串方法 的巩固应用。也通过 React 的编码规范了解了 建造者模式 等设计模式,对于 JavaScript 的学习之路有了进一步的规划。SCSS:SCSS 是 CSS 的一门预编译语言,由于 CSS 是一门面向设计的语言,所以对于庞大的项目来说,CSS 代码的模块化管理还有编码规范方面问题很难解决,于是就会有 SASS, LESS 等预编译语言的出现。SCSS 是 SASS 的一个改进版本,主要是语法方面向 CSS 靠近。使用 SCSS 模块化管理的方法很简单,如下:
.welcome{
h1{
...
}
p{
...
}
}
// 编译结果
.welcome h1{...}
.welcome p{...}
- 很明显可以看出,我们只要保证顶层的类与其他模块不同,则编译出来的 CSS 代码就不会有模块化冲突的问题了。当然,我们通常以组件名给顶层类命名,便于管理。但是这样就会有一个问题,不同模块可能存在相似或相同的样式表,那这样编译出来的样式就会存在冗余的部分了,对此的解决方法,目前想到的就是:
- 设置 Common.scss ,定义好全局复用的样式效果(比如字体大小的几款样式,样色,阴影效果,动画效果)
- 组件划分尽量划分到不能再细分的组件,这样代码的复用率比较高
3.包管理工具(npm)和自动化打包工具(webpack)
npm:(node package manager),顾名思义即 NodeJS 的一个包管理工具。NodeJS 除了官方提供的 核心模块 ,还有大量的第三方模块,因此 npm 的诞生就是为了更好管理和使用这些第三方模块的。使用的方法也很简单,安装需要的模块并将此模块作为项目的依赖模块的信息写入
package.json
文件中,方便其他开发者直接安装模块,也方便自己后期查看依赖模块信息(比如版本信息)。在需要引用该模块的文件中,通过requrie('')
语句进行调用即可。webpack:一个自动化打包工具,webpack 也就花了两三天看看文档,写个小 demo 测试一下而已。要说感触的话,主要是觉得
loader
,plugin
对开发过程很友好,而且打包文件进行了一系列优化处理,比如代码压缩、图片进行base64
处理等。当然,刚使用 webpack 的时候也想过这样一些问题。
webpack 的打包原理?
webpack 打包慢的处理方式?
webpack-dev-server 的监听原理?
第一个问题,由于 webpack 是依赖于 NodeJS 的,因此模块规范就是 CommonJS。一个文件就是一个模块,
require
就是引入模块,module.exports
就是对外暴露的接口。而打包的方式也就是常见的 管道模式(即require
的文件中的require
继续打包)。另外,同一个模块不会被引入两次,因为在入口文件中,每个require
都配置了一个id
,因此即使同个模块被引入多次,由于其id
一致,故不会被打包多次。第二个问题,目前想到也就以下两种方法。
- 使用
webpack --watch
命令代替webpack
,这样在第一次打包之后打包速度会比较快
将常用的库等静态资源打包成一个文件,开发时不再动态打包
第三个问题,官方文档有给出答案。也就是建立一个小的服务器进行监听。
The webpack-dev-server is a little Node.js Express server, which uses the webpack-dev-middleware to serve a webpack bundle. It also has a little runtime which is connected to the server via Sock.js.
三、React-router
react-router 简单地说,就是 URL 与 Components 的映射,其实现原理深究无非就是 UI 与 URL 的同步实现,下图便很清晰地解释其实现原理。而 react-router 有一点是觉得比较有趣的。就是一开始以为 URL 的更新是通过
window.location
进行设置,但是后来意识到window.location
对象在更新时会出现页面跳转的现象。于是查了一下,发现 单页面应用 的 URL 更细是通过window.history.pushState()
方法实现的,这样一来,history
的go()
,back()
等问题自然也能够得到解决。后来又发现,
window.history
对象是高版本浏览器才有的,于是查了一下,发现 react-router 是基于一个开源的第三方 js 库 history 实现的。主要的兼容方式为:
- 老版本浏览器的 history:主要通过
hash
来实现,对应createHashHistory
- 高版本浏览器:通过 HTML5 里面的
history
,对应createBrowerHistory
- Node 环境下:主要存储在
memeory
里面,对于createMemoryHistory