前端 20 个灵魂拷问,彻底搞明白你就是中级前端工程师 上篇
感觉大家比较喜欢看这种类型的文章,以后会多一些。
欢迎加入我们的前端交流二群 目前一群人数有点多 所以开放了二群 ~ 欢迎加入
里面很多小姐姐哦~~(包括思否小姐姐)
前端越往深度发展,越需要了解底层实现原理,借鉴他们的思想去实现业务需求,去实现性能优化,而且去学习新的东西时候也是在这些知识基础上去学习~ 事半功倍
为什么我会将这些问题放在中篇,本文会在介绍完这些问题后在后面给出理由
问题来了
1. 为什么会出现模块化,以及各种模块化标准
移动端 React 开源项目,从零搭建的 webpack 脚手架
前端模块化出现是必定的,一个很复杂的应用不可能所有的内容都在一个文件中~
模块化的历程:
传统的命令空间
代码实现:
index.js
(function(w){w.a = 1})(window)
原理:在 window
这个全局对象下面,挂载属性,那么全局都可以拿到这个属性的值,原则上一个 js
文件作为一个模块,就是一个 IIFE
函数
-> require.js
基于 AMD
规范
AMD 规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用 require.js 实现 AMD 规范的模块化:用 require.config()指定引用路径等,用 define()定义模块,用 require()加载模块。
代码实现:
// 简单的对象定义
define({
color: "black",
size: "unisize"
});
// 当你需要一些逻辑来做准备工作时可以这样定义:define(function () {
// 这里可以做一些准备工作
return {
color: "black",
size: "unisize"
}
});
// 依赖于某些模块来定义属于你自己的模块
define(["./cart", "./inventory"], function(cart, inventory) {
// 通过返回一个对象来定义你自己的模块
return {
color: "blue",
size: "large",
addToCart: function() {inventory.decrement(this);
cart.add(this);
}
}
}
);
-> sea.js
基于 CMD
规范
CMD 是另一种 js 模块化方案,它与 AMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD 推崇依赖就近、延迟执行。此规范其实是在 sea.js 推广过程中产生的。
代码实现:
define(function(require, exports, module) {var $ = require('jquery');
exports.sayHello = function() {$('#hello').toggle('slow');
};
});
seajs.config({
alias: {'jquery': 'http://modules.seajs.org/jquery/1.7.2/jquery.js'}
});
seajs.use(['./hello', 'jquery'], function(hello, $) {$('#beautiful-sea').click(hello.sayHello);
});
原理:顶部引入 sea.js
的源码文件,运行时转换代码,一开始指定入口文件,根据入口文件定义的数组(或者引入的依赖),去继续寻找对应的依赖。
-> commonJs
Node.js
原生环境支持 commonJs
模块化规范
先简单实现一个require
:
function require(/* ... */) {const module = { exports: {} };
((module, exports) => {
// Module code here. In this example, define a function.
// 模块代码在这里,在这个例子中,我们定义了一个函数
function someFunc() {}
exports = someFunc;
// At this point, exports is no longer a shortcut to module.exports, and
// this module will still export an empty default object.
// 当代码运行到这里时,exports 不再是 module.exports 的引用,并且当前的
// module 仍旧会导出一个空对象(就像上面声明的默认对象那样)
module.exports = someFunc;
// At this point, the module will now export someFunc, instead of the
// default object.
// 当代码运行到这时,当前 module 会导出 someFunc 而不是默认的对象
})(module, module.exports);
return module.exports;
}
require
就相当于把被引用的 module
拷贝了一份到当前 module
中
export
和 module.exports
暴露出来接口
export
和 module.exports
的区别:
export 是 module.exports 的引用。作为一个引用,如果我们修改它的值,实际上修改的是它对应的引用对象的值。
commonJS
用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
一句话简单总结就是,exports
-> {} <- module.exports
同时指向一个对象
-> ES6
模块化
目前最常用的模块化规范:
ES6
模块化规范原生的浏览器环境和 Node.js
环境都不识别,但是要使用,就必须要使用 babel
编译成浏览器或者 Node.js
可以识别的代码,为了节省时间,那么就会出现自动化一键打包编译代码的工具,– webpack
.
ES6
最牛逼的地方,不仅支持了静态校验,可以同步异步加载,而且统一了前后端的模块化规范,Node
和传统前端,都可以用这套规范。
ES6
模块与CommonJS
模块的差异
-
CommonJS
模块输出的是一个值的拷贝,ES6 模块输出的是值的引用 -
CommonJS
模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS
加载的是一个对象(即 module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
这也是为什么 TypeScript
支持静态类型检查的原因 因为他使用的是ES6
模块化方案
特别提示:现在
Node
也可以用ES6
模块化方案的 用experimental
即可
看看 commonJs
的
index.js
const a = require('./test1.js');
const func = require('./test2');
a.a = 2;
console.log(a.a,'test1');
func()
test2.js
const a = require('./test1')
module.exports = function(){console.log(a.a,'test2')
}
test1.js
let a={a:1}
module.exports=a
运行node index.js
输出结果
看看 ES6
的
// math.js
export let val = 1
export function add () {val++}
// test.js
import {val, add} from './math.js'
console.log(val) // 1
add()
console.log(val) // 2
React Vue
框架实现基本原理以及设计思想~
设计思想和基本原理:
- 1. 由传统的直接
DOM
操作改成了数据驱动的方式去间接替我们操作DOM
。 - 2. 每次数据改变需要重新渲染时,只对存在差异对那个部分
DOM
进行操作。—diff
算法 - 有一系列对生命周期,其实就是代码执行顺序中给定了一部分的特定函数名称进行执行,一种约定。
常见的 diff
算法,有上一个虚拟 dom
和这次更新后的虚拟 dom
去对比,然后给真实 dom
打补丁的方式,也有用真实 dom
和虚拟 dom
直接对比的方式。
从零自己编写一个 React 框架 我这篇文章附带了源码,从零自己实现了一个 React 框架
前端需要了解的常见的算法和数据结构
常见的数据结构:栈,队列,树,图,数组,单链表,双链表,图等 …
冒泡排序
比较相邻的两个元素,如果前一个比后一个大,则交换位置。
第一轮的时候最后一个元素应该是最大的一个。
按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较
function bubble_sort(arr){for(var i=0;i<arr.length-1;i++){for(var j=0;j<arr.length-i-1;j++){if(arr[j]>arr[j+1]){var swap=arr[j];
arr[j]=arr[j+1];
arr[j+1]=swap;
}
}
}
}
var arr=[3,1,5,7,2,4,9,6,10,8];
bubble_sort(arr);
console.log(arr);
快速排序
js 代码实现 解析:快速排序是对冒泡排序的一种改进,第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。然后递归调用,在两边都实行快速排序。
function quick_sort(arr){if(arr.length<=1){return arr;}
var pivotIndex=Math.floor(arr.length/2);
var pivot=arr.splice(pivotIndex,1)[0];
var left=[];
var right=[];
for(var i=0;i<arr.length;i++){if(arr[i]<pivot){left.push(arr[i]);
}else{right.push(arr[i]);
}
}
return quick_sort(left).concat([pivot],quick_sort(right));
}
var arr=[5,6,2,1,3,8,7,1,2,3,4,7];
console.log(quick_sort(arr));
时间复杂度概念:
一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。
空间复杂度概念:
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。
具体可以看这篇文章:
JavaScript 算法与数据结构
Node.js
的底层 fs
,net
,path
,stream
等模块以及 express
框架使用和操作数据库
注意,
Node.js
中很多回调函数的首个参数都是err
根据路径同步读取文件流:
// 在 macOS、Linux 和 Windows 上:fs.readFileSync('< 目录 >');
// => [Error: EISDIR: illegal operation on a directory, read < 目录 >]
异步地读取文件的全部内容:
fs.readFile('路径', (err, data) => {if (err) throw err;
console.log(data);
});
上面读取到的
data
都是buffer
数据,Buffer 类是一个全局变量,用于直接处理二进制数据。
如果路径存在,则返回 true,否则返回 false。:
fs.existsSync(path)
Node.js
中一般同步的API
都是sync
结尾,不带的一般是异步的,我们一般都用异步API
Node.js 中有四种基本的流类型:
Writable
– 可写入数据的流(例如 fs.createWriteStream())。Readable
– 可读取数据的流(例如 fs.createReadStream())。Duplex
– 可读又可写的流(例如 net.Socket)。Transform
– 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate()
)。
使用 Node.js 编写的静态资源服务器 这是我的自己编写的静态资源服务器
里面有大量的 Buffer
操作
Node
里面这些常用的模块,是走向全栈工程师的基础。越是复杂的应用,对二进制操作会越多,比如自己定义的即时通讯协议,你需要把数据一点点的从Buffer
里切出来。如果是prob
协议,那么还要反序列化。但是原理大都类似,还有涉及音视频等。
使用 Node.js
作为中间件,同构服务端渲染单页面应用,以及做转发请求等操作
为了解决单页面应用的 SEO
问题
传统的 SSR
渲染是在服务端把代码都运行好了然后通过字符串都形式传给前端渲染
现在都单页面应用是只传输一个空的 HTML
文件和很多个 js
文件 给前端,然后拿到文件后动态生成页面。这就导致搜索引擎的爬虫无法爬到网页的信息,所有有了同构。
同构就是把单页面应用,React 和 Vue
这样框架写的代码,在服务端运行一遍(并不是运行全部),然后返回字符串给前端渲染,这个时候搜索引擎就可以爬取到关键字了。前端根据服务端返回的字符串渲染生成页面后,js
文件接管后续的逻辑。这样就是一套完整的同构
React 服务端渲染源码 这个是我的 React
服务端渲染源码
客户端入口文件:
//client/index. js
import React from 'react';
import ReactDom from 'react-dom';
import {BrowserRouter, Route} from 'react-router-dom';
import {Provider} from 'react-redux';
import {getClientStore} from '../containers/redux-file/store';
import {renderRoutes} from 'react-router-config'
import routers from '../Router';
const store = getClientStore();
const App = () => {
return (<Provider store={store}>
<BrowserRouter>{renderRoutes(routers)}</BrowserRouter>
</Provider>
);
};
ReactDom.hydrate(<App />, document.getElementById('root'));
同构的入口代码:
// server/index.js
import express from 'express';
import {render} from '../utils';
import {serverStore} from '../containers/redux-file/store';
const app = express();
app.use(express.static('public'));
app.get('*', function(req, res) {if (req.path === '/favicon.ico') {res.send();
return;
}
const store = serverStore();
res.send(render(req, store));
});
const server = app.listen(3000, () => {var host = server.address().address;
var port = server.address().port;
console.log(host, port);
console.log('启动连接了');
});
render
函数:
import Routes from '../Router';
import {renderToString} from 'react-dom/server';
import {StaticRouter, Link, Route} from 'react-router-dom';
import React from 'react';
import {Provider} from 'react-redux';
import {renderRoutes} from 'react-router-config';
import routers from '../Router';
import {matchRoutes} from 'react-router-config';
export const render = (req, store) => {const matchedRoutes = matchRoutes(routers, req.path);
matchedRoutes.forEach(item => {
// 如果这个路由对应的组件有 loadData 方法
if (item.route.loadData) {item.route.loadData(store);
}
});
console.log(store.getState(),Date.now())
const content = renderToString(<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(routers)}</StaticRouter>
</Provider>
);
看起来眼花缭乱 其实就是把代码运行在服务端,然后拼接成字符串给前端
唯一有点特别的地方:
服务端代码注水:
<script>window.context={state:${JSON.stringify(store.getState())}}</script>
客户端代码脱水:
store.js
import thunk from 'redux-thunk';
import {createStore, applyMiddleware} from 'redux';
import reducers from './reducers';
export const getClientStore = () => {
// 下面这行代码就是代码脱水,createStore 是可以传入第二个参数的,去阅读源码可以了解
const defaultState = window.context ? window.context.state : {};
return createStore(reducers, defaultState, applyMiddleware(thunk));
};
export const serverStore = () => {return createStore(reducers, applyMiddleware(thunk));
};
跟我一起默念:
同构的秘诀:
1. 代码现在服务端运行
2. 返回字符串和注水后的数据给前端
3. 前端拿到字符串和注水数据后,脱水渲染,然后js
文件接管,这时候又是单页面应用的逻辑了~
经过很久考虑才觉得应该写这 5 个问题,接下来的 5 个问题会在下周更新。
为什么要挑选这五个问题
模块化规范的学习,是为了拥有改造旧轮子的能力
数据结构和算法是为了拥有编写轻量级框架和性能优化打基础
Node.js
的使用是为了向全栈发展打基础
同构是为了走向高并发场景打基础
框架的实现原理,是为了让我们学习这种设计思想,在平时业务代码书写时候,考虑时间复杂度和空间度的同时也要考虑框架底层实现。
觉得写得不错,可以给个
star
。
欢迎加入我们的二群哦~