前端20個靈魂拷問 徹底搞明白你就是中級前端工程師 【中篇】

Peter譚金傑發表於2019-08-21

圖片描述

前端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模組化

目前最常用的模組化規範:
clipboard.png

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

輸出結果

clipboard.png

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

前端需要了解的常見的演算法和資料結構

常見的資料結構:棧,佇列,樹,圖,陣列,單連結串列,雙連結串列,圖等...

clipboard.png

clipboard.png

氣泡排序

比較相鄰的兩個元素,如果前一個比後一個大,則交換位置。
第一輪的時候最後一個元素應該是最大的一個。
按照步驟一的方法進行相鄰兩個元素的比較,這個時候由於最後一個元素已經是最大的了,所以最後一個元素不用比較

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

相關文章