前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【中篇】

starzhangkiss 2019-11-03

前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【中篇】

前端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

exportmodule.exports暴露出来接口

exportmodule.exports的区别:

export 是 module.exports 的引用。作为一个引用,如果我们修改它的值,实际上修改的是它对应的引用对象的值。

commonJs用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

一句话简单总结就是,exports-> {} <- module.exports同时指向一个对象

-> ES6模块化

目前最常用的模块化规范:
前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【中篇】

ES6模块化规范原生的浏览器环境和Node.js环境都不识别,但是要使用,就必须要使用babel编译成浏览器或者Node.js可以识别的代码,为了节省时间,那么就会出现自动化一键打包编译代码的工具, - webpack.

ES6最牛逼的地方,不仅支持了静态校验,可以同步异步加载,而且统一了前后端的模块化规范,Node和传统前端,都可以用这套规范。

ES6 模块与 CommonJS 模块的差异
  1. commonJs 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用 (首次require不同路径的文件,会在require.cache中保存一份缓存,下次读取的时候就直接从缓存中读取了)
  2. 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

输出结果

前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【中篇】

看看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框架

前端需要了解的常见的算法和数据结构

常见的数据结构:栈,队列,树,图,数组,单链表,双链表,图等...

前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【中篇】

前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【中篇】

冒泡排序

比较相邻的两个元素,如果前一个比后一个大,则交换位置。
第一轮的时候最后一个元素应该是最大的一个。
按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较

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,pathstream等模块以及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

欢迎加入我们的二群哦~

我的个人微信号:CALASFxiaotan

相关推荐