沐鳴登錄測速_Js倒計時代碼

需求:

最近由於頁面需要,做一個倒計時的功能,具體意思就是當前時間到指定時間之間的時間段以倒計時的方式展示

代碼:

html

<span class="time" style="color:brown;font-size:45px;"></span>

js

var starttime = new Date("2028/8/16");
setInterval(function() {
var nowtime = new Date();
var time = starttime - nowtime;
var day = parseInt(time / 1000 / 60 / 60 / 24);
var hour = parseInt(time / 1000 / 60 / 60 % 24);
var minute = parseInt(time / 1000 / 60 % 60);
var seconds = parseInt(time / 1000 % 60);
$('.time').html(day + "天" + hour + "時" + minute + "分" + seconds + "秒");
}, 1200);

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10238

杏耀註冊平台官網_Js中foreach()用法及使用的坑

js中foreach是用於遍曆數組的方法,將遍歷到的元素傳遞給回調函數,遍歷的數組不能是空的要有值。

foreach 語法:

[ ].forEach(function(value,index,array){
  //code something
});

forEach()方法對數組的每個元素執行一次提供的函數。

var array = ['a', 'b', 'c'];
array.forEach(function(element) {
console.log(element);
});

輸出為:

a;
b;
c;

forEach()方法對數組的每個元素執行一次提供的函數。總是返回undefined;

var arr = [1,2,3,4];   
arr.forEach(alert);

//等價於:
var arr = [1, 2, 3, 4];
for (var k = 0, length = arr.length; k < length; k++) {
alert(array[k]);
}

forEach方法中的function回調有三個參數:
第一個參數是遍歷的數組內容,
第二個參數是對應的數組索引,
第三個參數是數組本身

forEach使用的坑

1.forEach不支持break

大家都知道,在使用for循環時可以使用break跳出循環,比如我希望找到數組中符合條件的第一個元素就跳出循環,這對於優化數組遍歷是非常棒的。很遺憾,forEach並不支持break操作,使用break會導致報錯。

let arr = [1, 2, 3, 4],
    i = 0,
    length = arr.length;
for (; i < length; i++) {
    console.log(arr[i]); //1,2
    if (arr[i] === 2) {
        break;
    };
};

arr.forEach((self,index) => {
    console.log(self);
    if (self === 2) {
        break; //報錯
    };
});

那forEach能不能跳出循環呢?可以,不過不是使用break,而是結合try catch操作

2.forEach中使用return無效

首先需要確定的,直接再for循環中使用return會報錯(函數中使用for可以return),forEach中使用return不會報錯,但rerutn並不會生效,我們來看個例子:

let arr = [1, 2, 3, 4];

function find(array, num) {
    array.forEach((self, index) => {
        if (self === num) {
            return index;
        };
    });
};
let index = find(arr, 2);// undefined

上述代碼想要找到数字2在數組中的索引,但return並不會起到終止代碼運行並返回值的作用。

當然如果我們真的要用return返回某個值,那就只能將return操作放在函數中,而不是forEach循環中,像這樣:

function find(array, num) {
    let _index;
    array.forEach((self, index) => {
        if (self === num) {
            _index = index;
        };
    });
    return _index;
};

3.forEach刪除自身元素index不會被重置

還記得文章開頭的問題嗎,那段代碼其實只會執行一次,數組也不會被刪除乾淨,這是因為forEach在遍歷跑完回調函數后,會隱性讓index自增,像這樣:

arr.forEach((item, index) => {
    arr.splice(index, 1);
    console.log(1);
    //這裏隱性讓index自增加1
    index++;
});

當第一次遍歷結束,此時數組為[2]而index變成了1,此時數組最大索引只是0,不滿足條件,所以跳出了循環。

靈機一動,有沒有什麼辦法讓此時的forEach不跳出循環呢,當然有,使用ES6的拓展運算符。

[...arr].forEach((item, index) => {
    arr.splice(index, 1);
    console.log(1);
});

通過拓展運算符重置數組arr,達到不跳出循環的目的,你會發現內部確實執行了兩次,很遺憾的是index依舊沒被重置,所以數組arr還是無法在遍歷的同時刪空自己。

因為在實際開發中,遍曆數組同時刪除某項的操作十分常見,所以對於習慣了forEach的同學,這一點一定要注意。


for與forEach的區別

那麼說到這裏,for循環與forEach有什麼區別呢?我想大家應該都能說上幾點了吧,比如:

1.for循環可以使用break跳出循環,但forEach不能。

2.for循環可以控制循環起點(i初始化的数字決定循環的起點),forEach只能默認從索引0開始。

3.for循環過程中支持修改索引(修改 i),但forEach做不到(底層控制index自增,我們無法左右它)。

數組遍歷並刪除自身

好了,我們回歸問題的本質,怎麼在遍歷一個數組的同時,並刪除符合條件的所有項呢?

其實很簡單,我們可以使用for循環,像這樣:

let arr = [1, 2, 1],
    i = 0,
    length = arr.length;

for (; i < length; i++) {
    // 刪除數組中所有的1
    if (arr[i] === 1) {
        arr.splice(i, 1);
        //重置i,否則i會跳一位
        i--;
    };
};
console.log(arr);//[2]

刪除符合條件的所有項,不就是過濾嗎?過濾,本能想到filter方法,也很簡單:

var arr1 = arr.filter((index) => {
    return index !== 1;
});
console.log(arr1); //[2]

更少的代碼量,只是得新建一個變量來接受filter方法的返回值。

你說,我這個人就比較倔強,forEach因為index索引無法重置,對於刪除數組項真的很困難,那我非要用forEach去做這個功能行不行,當然行,只是我們得讓數組反過來遍歷:

arr.slice().reverse().forEach(function (item, index, arr1) {
    if (item === 1) {
        arr.splice(arr1.length - 1 - index, 1);
    }
});
console.log(arr);//[2]

當然我們並不推薦這麼做,因為數組方法繁多,並沒有絕對的好與不好,什麼情況下使用對的方法才是更棒的做法,這裏使用forEach雖然達到了目的,但代碼閱讀體驗並不太好。

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10235

沐鳴平台網址_TypeScript Interface vs Type知多少

接口和類型別名非常相似,在大多情況下二者可以互換。在寫TS的時候,想必大家都問過自己這個問題,我到底應該用哪個呢?希望看完本文會給你一個答案。知道什麼時候應該用哪個,首先應該了解二者之間的相同點和不同點,再做出選擇。

接口 vs 類型別名 相同點

1. 都可以用來描述對象或函數

interface Point {
  x: number
  y: number
}

interface SetPoint {
  (x: number, y: number): void;
}
type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

2. 都可以擴展

兩者的擴展方式不同,但並不互斥。接口可以擴展類型別名,同理,類型別名也可以擴展接口。

接口的擴展就是繼承,通過 extends 來實現。類型別名的擴展就是交叉類型,通過 & 來實現。

// 接口擴展接口
interface PointX {
    x: number
}

interface Point extends PointX {
    y: number
}
// 類型別名擴展類型別名
type PointX = {
    x: number
}

type Point = PointX & {
    y: number
}
// 接口擴展類型別名
type PointX = {
    x: number
}
interface Point extends PointX {
    y: number
}
// 類型別名擴展接口
interface PointX {
    x: number
}
type Point = PointX & {
    y: number
}

接口 vs 類型別名不同點

1. 類型別名更通用(接口只能聲明對象,不能重命名基本類型)

類型別名的右邊可以是任何類型,包括基本類型、元祖、類型表達式(&或|等類型運算符);而在接口聲明中,右邊必須為結構。例如,下面的類型別名就不能轉換成接口:

type A = number
type B = A | string

2. 擴展時表現不同

擴展接口時,TS將檢查擴展的接口是否可以賦值給被擴展的接口。舉例如下:

interface A {
    good(x: number): string,
    bad(x: number): string
}
interface B extends A {
    good(x: string | number) : string,
    bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
                           // Types of property 'bad' are incompatible.
                           // Type '(x: number) => number' is not assignable to type '(x: number) => string'.
                           // Type 'number' is not assignable to type 'string'.
}

但使用交集類型時則不會出現這種情況。我們將上述代碼中的接口改寫成類型別名,把 extends 換成交集運算符 &,TS將盡其所能把擴展和被擴展的類型組合在一起,而不會拋出編譯時錯誤。

type A = {
    good(x: number): string,
    bad(x: number): string
}
type B = A & {
     good(x: string | number) : string,
     bad(x: number): number 
}

3. 多次定義時表現不同

接口可以定義多次,多次的聲明會合併。但是類型別名如果定義多次,會報錯。

interface Point {
    x: number
}
interface Point {
    y: number
}
const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

const point: Point = {x:1, y:1} // 正確
type Point = {
    x: number // Duplicate identifier 'A'.
}

type Point = {
    y: number // Duplicate identifier 'A'.
}

到底應該用哪個

如果接口和類型別名都能滿足的情況下,到底應該用哪個是我們關心的問題。感覺哪個都可以,但是強烈建議大家只要能用接口實現的就優先使用接口,接口滿足不了的再用類型別名。

為什麼會這麼建議呢?其實在TS的wiki中有說明。具體的文章地址在這裏。

以下是Preferring Interfaces Over Intersections的譯文:

大多數時候,對於聲明一個對象,類型別名和接口表現的很相似。

interface Foo { prop: string }

type Bar = { prop: string };

然而,當你需要通過組合兩個或者兩個以上的類型實現其他類型時,可以選擇使用接口來擴展類型,也可以通過交叉類型(使用 & 創造出來的類型)來完成,這就是二者開始有區別的時候了。

  • 接口會創建一個單一扁平對象類型來檢測屬性衝突,當有屬性衝突時會提示,而交叉類型只是遞歸的進行屬性合併,在某種情況下可能產生 never 類型
  • 接口通常表現的更好,而交叉類型做為其他交叉類型的一部分時,直觀上表現不出來,還是會認為是不同基本類型的組合
  • 接口之間的繼承關係會緩存,而交叉類型會被看成組合起來的一個整體
  • 在檢查一個目標交叉類型時,在檢查到目標類型之前會先檢查每一個組分

上述的幾個區別從字面上理解還是有些繞,下面通過具體的列子來說明。

interface Point1 {
    x: number
}

interface Point extends Point1 {
    x: string // Interface 'Point' incorrectly extends interface 'Point1'.
              // Types of property 'x' are incompatible.
              // Type 'string' is not assignable to type 'number'.
}
type Point1 = {
    x: number
}

type Point2 = {
    x: string
}

type Point = Point1 & Point2 // 這時的Point是一個'number & string'類型,也就是never

從上述代碼可以看出,接口繼承同名屬性不滿足定義會報錯,而相交類型就是簡單的合併,最後產生了 number & string 類型,可以解釋譯文中的第一點不同,其實也就是我們在不同點模塊中介紹的擴展時表現不同。

再來看下面例子:

interface PointX {
    x: number
}

interface PointY {
    y: number
}

interface PointZ {
    z: number
}

interface PointXY extends PointX, PointY {
}

interface Point extends PointXY, PointZ {
   
}
const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
type PointX = {
    x: number
}

type PointY = {
    y: number
}

type PointZ = {
    z: number
}

type PointXY = PointX & PointY

type Point = PointXY & PointZ

const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
                                  // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.

從報錯中可以看出,當使用接口時,報錯會準確定位到Point。
但是使用交叉類型時,雖然我們的 Point 交叉類型是 PointXY & PointZ, 但是在報錯的時候定位並不在 Point 中,而是在 Point3 中,即使我們的 Point 類型並沒有直接引用 Point3 類型。

如果我們把鼠標放在交叉類型 Point 類型上,提示的也是 type Point = PointX & PointY & PointZ,而不是 PointXY & PointZ。

這個例子可以同時解釋譯文中第二個和最後一個不同點。

結論

有的同學可能會問,如果我不需要組合只是單純的定義類型的時候,是不是就可以隨便用了。但是為了代碼的可擴展性,建議還是優先使用接口。現在不需要,誰能知道後續需不需要呢?所以,讓我們大膽的使用接口吧~

來源:https://segmentfault.com/a/1190000039834284

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10220

沐鳴註冊_一份關於vue-cli3項目常用項配置

配置全局cdn,包含js、css

開啟Gzip壓縮,包含文件js、css

去掉註釋、去掉console.log

壓縮圖片

本地代理

設置別名,vscode也能識別

配置環境變量開發模式、測試模式、生產模式

請求路由動態添加

axios配置

添加mock數據

配置全局less

只打包改變的文件

開啟分析打包日誌

vue.config.js

完整的架構配置

const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') // 去掉註釋
const CompressionWebpackPlugin = require('compression-webpack-plugin'); // 開啟壓縮
const { HashedModuleIdsPlugin } = require('webpack');

function resolve(dir) {
    return path.join(__dirname, dir)
}

const isProduction = process.env.NODE_ENV === 'production';

// cdn預加載使用
const externals = {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'axios': 'axios',
    "element-ui": "ELEMENT"
}

const cdn = {
    // 開發環境
    dev: {
        css: [
            'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
        ],
        js: []
    },
    // 生產環境
    build: {
        css: [
            'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
        ],
        js: [
            'https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js',
            'https://cdn.jsdelivr.net/npm/vue-router@3.0.1/dist/vue-router.min.js',
            'https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js',
            'https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js',
            'https://unpkg.com/element-ui/lib/index.js'
        ]
    }
}

module.exports = {

    lintOnSave: false, // 關閉eslint
    productionSourceMap: false,
    publicPath: './', 
    outputDir: process.env.outputDir, // 生成文件的目錄名稱
    chainWebpack: config => {

        config.resolve.alias
            .set('@', resolve('src'))

        // 壓縮圖片
        config.module
            .rule('images')
            .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
            .use('image-webpack-loader')
            .loader('image-webpack-loader')
            .options({ bypassOnDebug: true })

        // webpack 會默認給commonChunk打進chunk-vendors,所以需要對webpack的配置進行delete
        config.optimization.delete('splitChunks')

        config.plugin('html').tap(args => {
            if (process.env.NODE_ENV === 'production') {
                args[0].cdn = cdn.build
            }
            if (process.env.NODE_ENV === 'development') {
                args[0].cdn = cdn.dev
            }
            return args
        })
        
        config
            .plugin('webpack-bundle-analyzer')
            .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
    },

    configureWebpack: config => {
        const plugins = [];

        if (isProduction) {
            plugins.push(
                new UglifyJsPlugin({
                    uglifyOptions: {
                        output: {
                            comments: false, // 去掉註釋
                        },
                        warnings: false,
                        compress: {
                            drop_console: true,
                            drop_debugger: false,
                            pure_funcs: ['console.log']//移除console
                        }
                    }
                })
            )
            // 服務器也要相應開啟gzip
            plugins.push(
                new CompressionWebpackPlugin({
                    algorithm: 'gzip',
                    test: /\.(js|css)$/,// 匹配文件名
                    threshold: 10000, // 對超過10k的數據壓縮
                    deleteOriginalAssets: false, // 不刪除源文件
                    minRatio: 0.8 // 壓縮比
                })
            )

            // 用於根據模塊的相對路徑生成 hash 作為模塊 id, 一般用於生產環境
            plugins.push(
                new HashedModuleIdsPlugin()
            )

            // 開啟分離js
            config.optimization = {
                runtimeChunk: 'single',
                splitChunks: {
                    chunks: 'all',
                    maxInitialRequests: Infinity,
                    minSize: 1000 * 60,
                    cacheGroups: {
                        vendor: {
                            test: /[\\/]node_modules[\\/]/,
                            name(module) {
                                // 排除node_modules 然後吧 @ 替換為空 ,考慮到服務器的兼容
                                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
                                return `npm.${packageName.replace('@', '')}`
                            }
                        }
                    }
                }
            };

            // 取消webpack警告的性能提示
            config.performance = {
                hints: 'warning',
                //入口起點的最大體積
                maxEntrypointSize: 1000 * 500,
                //生成文件的最大體積
                maxAssetSize: 1000 * 1000,
                //只給出 js 文件的性能提示
                assetFilter: function (assetFilename) {
                    return assetFilename.endsWith('.js');
                }
            }

            // 打包時npm包轉CDN
            config.externals = externals;
        }

        return { plugins }
    },

    pluginOptions: {
        // 配置全局less
        'style-resources-loader': {
            preProcessor: 'less',
            patterns: [resolve('./src/style/theme.less')]
        }
    },
    devServer: {
        open: false, // 自動啟動瀏覽器
        host: '0.0.0.0', // localhost
        port: 6060, // 端口號
        https: false,
        hotOnly: false, // 熱更新
        proxy: {
            '^/sso': {
                target: process.env.VUE_APP_SSO, // 重寫路徑
                ws: true,   //開啟WebSocket
                secure: false,      // 如果是https接口,需要配置這個參數
                changeOrigin: true
            }
        }
    }
}

html模板配置cdn

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <% for (var i in
        htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style" />
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
    <% } %>
</head>

<body>
    <noscript>
        <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
            Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <% for (var i in
        htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
</body>
</html>

開啟Gzip壓縮,包含文件js、css

new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.(js|css)$/, // 匹配文件名
      threshold: 10000, // 對超過10k的數據壓縮
      deleteOriginalAssets: false, // 不刪除源文件
      minRatio: 0.8 // 壓縮比
})

去掉註釋、去掉console.log

安裝cnpm i uglifyjs-webpack-plugin -D

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
new UglifyJsPlugin({
    uglifyOptions: {
        output: {
            comments: false, // 去掉註釋
        },
        warnings: false,
        compress: {
            drop_console: true,
            drop_debugger: false,
            pure_funcs: ['console.log'] //移除console
        }
    }
})

壓縮圖片

chainWebpack: config => {
    // 壓縮圖片
    config.module
        .rule('images')
        .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
        .use('image-webpack-loader')
        .loader('image-webpack-loader')
        .options({ bypassOnDebug: true })

}

本地代理

devServer: {
    open: false, // 自動啟動瀏覽器
    host: '0.0.0.0', // localhost
    port: 6060, // 端口號
    https: false,
    hotOnly: false, // 熱更新
    proxy: {
        '^/sso': {
            target: process.env.VUE_APP_SSO, // 重寫路徑
            ws: true, //開啟WebSocket
            secure: false, // 如果是https接口,需要配置這個參數
            changeOrigin: true
        }
    }
}

設置vscode 識別別名

在vscode中插件安裝欄搜索 Path Intellisense 插件,打開settings.json文件添加 以下代碼 “@”: “${workspaceRoot}/src”,安以下添加

{
    "workbench.iconTheme": "material-icon-theme",
    "editor.fontSize": 16,
    "editor.detectIndentation": false,
    "guides.enabled": false,
    "workbench.colorTheme": "Monokai",
    "path-intellisense.mappings": {
        "@": "${workspaceRoot}/src"
    }
}

在項目package.json所在同級目錄下創建文件jsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "allowSyntheticDefaultImports": true,
        "baseUrl": "./",
        "paths": {
          "@/*": ["src/*"]
        }
    },
    "exclude": [
        "node_modules"
    ]
}

如果還沒請客官移步在vscode中使用別名@按住ctrl也能跳轉對應路徑

配置環境變量開發模式、測試模式、生產模式

在根目錄新建

.env.development

# 開發環境
NODE_ENV='development'

VUE_APP_SSO='http://http://localhost:9080'

.env.test

NODE_ENV = 'production' # 如果我們在.env.test文件中把NODE_ENV設置為test的話,那麼打包出來的目錄結構是有差異的
VUE_APP_MODE = 'test'
VUE_APP_SSO='http://http://localhost:9080'
outputDir = test

.env.production

NODE_ENV = 'production'

VUE_APP_SSO='http://http://localhost:9080'

package.json

"scripts": {
    "build": "vue-cli-service build", //生產打包
    "lint": "vue-cli-service lint",
    "dev": "vue-cli-service serve", // 開發模式
    "test": "vue-cli-service build --mode test", // 測試打包
    "publish": "vue-cli-service build && vue-cli-service build --mode test" // 測試和生產一起打包
 }

請求路由動態添加

router/index.js文件

import Vue from 'vue';
import VueRouter from 'vue-router'
Vue.use(VueRouter)

import defaultRouter from './defaultRouter'
import dynamicRouter from './dynamicRouter';

import store from '@/store';

const router = new VueRouter({
    routes: defaultRouter,
    mode: 'hash',
    scrollBehavior(to, from, savedPosition) {
        // keep-alive 返回緩存頁面後記錄瀏覽位置
        if (savedPosition && to.meta.keepAlive) {
            return savedPosition;
        }
        // 異步滾動操作
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve({ x: 0, y: 0 })
            }, 200)
        })
    }
})

// 消除路由重複警告
const selfaddRoutes = function (params) {
    router.matcher = new VueRouter().matcher;
    router.addRoutes(params);
}

// 全局路由攔截
router.beforeEach((to, from, next) => {
    const { hasRoute } = store.state; // 防止路由重複添加
    if (hasRoute) {
        next()
    } else {
        dynamicRouter(to, from, next, selfaddRoutes)
    }
})

export default router;

dynamicRouter.js

import http from '@/http/request';
import defaultRouter from './defaultRouter'
import store from '@/store'

// 重新構建路由對象
const menusMap = function (menu) {
    return menu.map(v => {
        const { path, name, component } = v
        const item = {
            path,
            name,
            component: () => import(`@/${component}`)
        }
        return item;
    })
}


// 獲取路由
const addPostRouter = function (to, from, next, selfaddRoutes) {
    http.windPost('/mock/menu') // 發起請求獲取路由
        .then(menu => {
            defaultRouter[0].children.push(...menusMap(menu));
            selfaddRoutes(defaultRouter);
            store.commit('hasRoute', true);
            next({ ...to, replace: true })
        })
}

export default addPostRouter;

defaultRouter.js 默認路由

const main = r => require.ensure([], () => r(require('@/layout/main.vue')), 'main')
const index = r => require.ensure([], () => r(require('@/view/index/index.vue')), 'index')
const about = r => require.ensure([], () => r(require('@/view/about/about.vue')), 'about')
const detail = r => require.ensure([], () => r(require('@/view/detail/detail.vue')), 'detail')
const error = r => require.ensure([], () => r(require('@/view/404/404.vue')), 'error');
const defaultRouter = [
    {
        path: "/", 
        component: main, // 布局頁
        redirect: {
            name: "index"
        },
        children:[
            {
                path: '/index',
                component: index,
                name: 'index',
                meta: {
                    title: 'index'
                }
            },
            {
                path: '/about',
                component: about,
                name: 'about',
                meta: {
                    title: 'about'
                }
            },
            {
                path: '/detail',
                component: detail,
                name: 'detail',
                meta: {
                    title: 'detail'
                }
            }
        ]
    },
    {
        path: '/404',
        component: error,
        name: '404',
        meta: {
            title: '404'
        }
    }
]
export default defaultRouter;

axios配置

import axios from "axios";
import merge from 'lodash/merge'
import qs from 'qs'

/**
 * 實例化
 * config是庫的默認值,然後是實例的 defaults 屬性,最後是請求設置的 config 參數。後者將優先於前者
 */
const http = axios.create({
    timeout: 1000 * 30,
    withCredentials: true, // 表示跨域請求時是否需要使用憑證
});

/**
 * 請求攔截
 */
http.interceptors.request.use(function (config) {
    return config;
}, function (error) {
    return Promise.reject(error);
});


/**
 * 響應攔截
 */
http.interceptors.response.use(response => {
    // 過期之類的操作
    if (response.data && (response.data.code === 401)) {
        // window.location.href = ''; 重定向
    }
    return response
}, error => {
    return Promise.reject(error)
})


/**
 * 請求地址處理
 */
http.adornUrl = (url) => {
    return url;
}

/**
 * get請求參數處理
 * params 參數對象
 * openDefultParams 是否開啟默認參數
 */
http.adornParams = (params = {}, openDefultParams = true) => {
    var defaults = {
        t: new Date().getTime()
    }
    return openDefultParams ? merge(defaults, params) : params
}


/**
 * post請求數據處理
 * @param {*} data 數據對象
 * @param {*} openDefultdata 是否開啟默認數據?
 * @param {*} contentType 數據格式
 *  json: 'application/json; charset=utf-8'
 *  form: 'application/x-www-form-urlencoded; charset=utf-8'
 */
http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => {
    var defaults = {
        t: new Date().getTime()
    }
    data = openDefultdata ? merge(defaults, data) : data
    return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
}


/**
 * windPost請求
 * @param {String} url [請求地址]
 * @param {Object} params [請求攜帶參數]
 */
http.windPost = function (url, params) {
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), qs.stringify(params))
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}


/**
 * windJsonPost請求
 * @param {String} url [請求地址]
 * @param {Object} params [請求攜帶參數]
 */
http.windJsonPost = function (url, params) {
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), http.adornParams(params))
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}


/**
 * windGet請求
 * @param {String} url [請求地址]
 * @param {Object} params [請求攜帶參數]
 */
http.windGet = function (url, params) {
    return new Promise((resolve, reject) => {
        http.get(http.adornUrl(url), { params: params })
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}

/**
 * 上傳圖片
 */
http.upLoadPhoto = function (url, params, callback) {
    let config = {}
    if (callback !== null) {
        config = {
            onUploadProgress: function (progressEvent) {
                //屬性lengthComputable主要表明總共需要完成的工作量和已經完成的工作是否可以被測量
                //如果lengthComputable為false,就獲取不到progressEvent.total和progressEvent.loaded
                callback(progressEvent)
            }
        }
    }
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), http.adornParams(params), config)
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}
export default http;

添加mock數據

const Mock = require('mockjs')

// 獲取 mock.Random 對象
const Random = Mock.Random
// mock新聞數據,包括新聞標題title、內容content、創建時間createdTime
const produceNewsData = function () {
    let newsList = []
    for (let i = 0; i < 3; i++) {
        let newNewsObject = {}
        if(i === 0){
            newNewsObject.path = '/add/article';
            newNewsObject.name  = 'add-article';
            newNewsObject.component = 'modules/add/article/article';
        }
        if(i === 1){
            newNewsObject.path = '/detail/article';
            newNewsObject.name  = 'detail-article';
            newNewsObject.component = 'modules/detail/article/article'
        }
        if(i === 2){
            newNewsObject.path = '/edit/article';
            newNewsObject.name  = 'edit-article';
            newNewsObject.component = 'modules/edit/article/article'
        }
        newsList.push(newNewsObject)
    }
    return newsList;
}
Mock.mock('/mock/menu', produceNewsData)

配置全局less

pluginOptions: {
    // 配置全局less
    'style-resources-loader': {
        preProcessor: 'less',
        patterns: [resolve('./src/style/theme.less')]
    }
}

只打包改變的文件

安裝cnpm i webpack -D

const { HashedModuleIdsPlugin } = require('webpack');
configureWebpack: config => {    
    const plugins = [];
    plugins.push(
        new HashedModuleIdsPlugin()
    )
}

開啟分析打包日誌

安裝cnpm i webpack-bundle-analyzer -D

chainWebpack: config => {
    config
        .plugin('webpack-bundle-analyzer')
        .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
}

完整代碼

點擊獲取完整代碼github(https://github.com/hangjob/vue-admin)

來源:https://segmentfault.com/a/1190000022512358

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10228

沐鳴下載_讀 Angular 代碼風格指南

原文地址:Angular 文檔

該文章擁有完整的代碼風格指南——大到如何編排文件夾,小到如何進行變量命名都涉及。但是與 ng 略有綁定,所以這裏整理一下可以單獨拿出來的通用部分。

單一職責

請堅持每個文件只定義一樣東西(例如服務或組件),並且把文件大小限制在 400 行代碼以內

小文件通常非常容易閱讀、維護,並能防止在版本控制系統里與團隊衝突

小文件還可以防止一些隱蔽的程序缺陷,當把多個組件合寫在同一個文件中時,可能造成共享變量、創建意外的閉包,或者與依賴之間產生意外耦合等情況。

總的來說,單一職責原則可以讓代碼更加可復用、更容易閱讀,減少出錯的可能性。

如果該文件是一個函數,那麼請堅持定義簡單函數,並且限制在 75 行之內。

這樣能更便於我們做單元測試。

命名

命名是一件非常重要的事情,他可以讓其他人看我們的代碼,或者是我們自己在一段時間之後再看之前的代碼時,可以迅速理解文件內容、變量含義、方法用途……。也可以在全局搜索的時候,讓我們可以迅速定位到目標

代碼給人讀的時間比給機器讀的時間多多了,因此我們需要慎重考慮命名。

可以遵循以下兩個原則:

  1. 堅持使用一致的命名規則
  2. 堅持遵循同一個模式來描述特性和類型。

文件命名

ng 推薦使用點和橫杠來分隔文件名——在描述性名字中,用橫杠來分隔單詞;用點來分隔描述性名字和類型

具體來說,就是先描述組件特性,再描述它的類型的模式,並且對所有組件使用一致的類型命名規則!!!

也就是 feature.type.ts,例如 hero.service.ts, app.module.ts, home.component.html, global.style.css。

常常使用的後綴有 *.service、*.component、*.pipe、.module、.directive。如果有必要,可以創建更多類型名,但必須注意,不要創建太多了

這樣命名文件可以讓我們來快速的識別文件中有什麼,並且輕鬆的利用編輯器或者 IDE 的模糊搜索功能找到特定文件類型。或是為自動化任務提供模式匹配。

文件名與符號名

如果將將文件命名為 hero.service.ts,那麼符號名,即類/變量名,應該命名為 HeroService。

若為 todo-list.service.ts,則該命名為 TodoListService。

也就是說,使用大寫駝峰命名法來命名類,並且需要匹配符號名與它所在的文件名,在符號名後面追加約定的類型後綴(例如 Component、Service)。

單元測試文件名

應該與測試的文件保持一致,xxx.xx.ts 的單元測試文件名應該叫做 xxx.xx.spec.ts。

結構組織與 LIFT 原則

我們應該力求項目結構組織符合 LIFT 原則:

  • Locate 快速定位代碼
  • Identify 一眼識別代碼
  • Flattest 盡量保持扁平結構
  • Try Do Not Repeat Yourself 嘗試遵循不重複自己的原則

上述四項原則重要程度從大到小。

為何?

LIFT 提供了一致的結構,它具有擴展性強模塊化的特性,它讓我們可以快速鎖定代碼,提高開發的效率。

另外,檢查應用結構是否合理的方法是問問自己:“我能快速打開與此特性有關的所有文件並開始工作嗎?”

Locate(定位)

堅持直觀、簡單和快速地定位代碼。

要想高效的工作,就必須能迅速找到文件,特別是當不知道(或不記得)文件名時——把相關的文件一起放在一個直觀的位置可以節省大量的時間。

並且富有描述性的目錄結構會讓你和後面的維護者眼前一亮!!!

可以使用上面說的,使用 特性 + 後綴 + 文件類型 的命名方式來方便我們的定位

Identify(識別)

文件的名字請達到這個程度:看到名字立刻知道它包含了什麼,代表了什麼。

文件名要具有說明性。保證文件名精準的方法就是:確保文件中只包含一個組件。

避免創建包含多個組件、服務或者混合體的文件。

為何?

花費更少的時間來查找和琢磨代碼,就會變得更有效率。較長的文件名遠勝於較短卻容易混淆的縮寫名。

Flattest(扁平)

請堅持盡可能保持扁平的目錄結構。

考慮當同一目錄下達到 7 個或更多個文件時創建子目錄。

考慮配置 IDE,以隱藏無關的文件,例如生成出來的 .js 文件和 .js.map 文件等。

沒人想要在超過七層的目錄中查找文件!!!

扁平的結構有利於搜索。

另一方面,心理學家們相信,當關注的事物超過 9 個時,人類就會開始感到吃力。所以,當一個文件夾中的文件有 10 個或更多個文件時,可能就是創建子目錄的時候了。

還是根據你自己的舒適度而定吧。除非創建新文件夾能有顯著的價值,否則盡量使用扁平結構。

Try Do Not Repeat Yourself (T-DRY)

堅持 DRY(Don’t Repeat Yourself,不重複自己)。

避免過度 DRY,以致犧牲了閱讀性。

雖然 DRY 很重要,但如果要以犧牲 LIFT 的其它原則為代價,那就不值得了。這也就是為什麼它被稱為 「Try」-DRY。

推薦的目錄結構

堅持把所有源代碼都放到名為 src 的目錄里。

堅持如果組件具有多個伴生文件 (.ts、.html、.css 和 .spec),就為它創建一個文件夾。

我習慣使用的前端目錄結構:

- src
  - app
    - moduleA // 模塊 B
      - assets
      - components
      - ...
    - moduleB // 模塊 A
    - shared // 共享模塊
  - layouts
  - assets
  - main.ts
  - ...

(完)

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10100

沐鳴登錄平台_git 常用命令

倉庫

# 在當前目錄新建一個Git代碼庫
$ git init

# 新建一個目錄,將其初始化為Git代碼庫
$ git init [project-name]

# 下載一個項目和它的整個代碼歷史
$ git clone [url] 

配置

# 显示當前的Git配置
$ git config --list

# 編輯Git配置文件
$ git config -e [--global]

# 設置提交代碼時的用戶信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]" 

增加/刪除文件

# 添加指定文件到暫存區
$ git add [file1] [file2] ...

# 添加指定目錄到暫存區,包括子目錄
$ git add [dir]

# 添加當前目錄的所有文件到暫存區
$ git add .

# 添加每個變化前,都會要求確認
# 對於同一個文件的多處變化,可以實現分次提交
$ git add -p

# 刪除工作區文件,並且將這次刪除放入暫存區
$ git rm [file1] [file2] ...

# 停止追蹤指定文件,但該文件會保留在工作區
$ git rm --cached [file]

# 改名文件,並且將這個改名放入暫存區
$ git mv [file-original] [file-renamed] 

代碼提交

# 提交暫存區到倉庫區
$ git commit -m [message]

# 提交暫存區的指定文件到倉庫區
$ git commit [file1] [file2] ... -m [message]

# 提交工作區自上次commit之後的變化,直接到倉庫區
$ git commit -a

# 提交時显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
# 如果代碼沒有任何新變化,則用來改寫上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,並包括指定文件的新變化
$ git commit --amend [file1] [file2] ... 

分支

# 列出所有本地分支
$ git branch

# 列出所有遠程分支
$ git branch -r

# 列出所有本地分支和遠程分支
$ git branch -a

# 新建一個分支,但依然停留在當前分支
$ git branch [branch-name]

# 新建一個分支,並切換到該分支
$ git checkout -b [branch]

# 新建一個分支,指向指定commit
$ git branch [branch] [commit]

# 新建一個分支,與指定的遠程分支建立追蹤關係
$ git branch --track [branch] [remote-branch]

# 切換到指定分支,並更新工作區
$ git checkout [branch-name]

# 切換到上一個分支
$ git checkout -

# 建立追蹤關係,在現有分支與指定的遠程分支之間
$ git branch --set-upstream [branch] [remote-branch]

# 合併指定分支到當前分支
$ git merge [branch]

# 選擇一個commit,合併進當前分支
$ git cherry-pick [commit]

# 刪除分支
$ git branch -d [branch-name]

# 刪除遠程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch] 

標籤

# 列出所有tag
$ git tag

# 新建一個tag在當前commit
$ git tag [tag]

# 新建一個tag在指定commit
$ git tag [tag] [commit]

# 刪除本地tag
$ git tag -d [tag]

# 刪除遠程tag
$ git push origin :refs/tags/[tagName]

# 查看tag信息
$ git show [tag]

# 提交指定tag
$ git push [remote] [tag]

# 提交所有tag
$ git push [remote] --tags

# 新建一個分支,指向某個tag
$ git checkout -b [branch] [tag] 

查看信息

# 显示有變更的文件
$ git status

# 显示當前分支的版本歷史
$ git log

# 显示commit歷史,以及每次commit發生變更的文件
$ git log --stat

# 搜索提交歷史,根據關鍵詞
$ git log -S [keyword]

# 显示某個commit之後的所有變動,每個commit佔據一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某個commit之後的所有變動,其"提交說明"必須符合搜索條件
$ git log [tag] HEAD --grep feature

# 显示某個文件的版本歷史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相關的每一次diff
$ git log -p [file]

# 显示過去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交過的用戶,按提交次數排序
$ git shortlog -sn

# 显示指定文件是什麼人在什麼時間修改過
$ git blame [file]

# 显示暫存區和工作區的差異
$ git diff

# 显示暫存區和上一個commit的差異
$ git diff --cached [file]

# 显示工作區與當前分支最新commit之間的差異
$ git diff HEAD

# 显示兩次提交之間的差異
$ git diff [first-branch]...[second-branch]

# 显示今天你寫了多少行代碼
$ git diff --shortstat "@{0 day ago}"

# 显示某次提交的元數據和內容變化
$ git show [commit]

# 显示某次提交發生變化的文件
$ git show --name-only [commit]

# 显示某次提交時,某個文件的內容
$ git show [commit]:[filename]

# 显示當前分支的最近幾次提交
$ git reflog 

遠程同步

# 下載遠程倉庫的所有變動
$ git fetch [remote]

# 显示所有遠程倉庫
$ git remote -v

# 显示某個遠程倉庫的信息
$ git remote show [remote]

# 增加一個新的遠程倉庫,並命名
$ git remote add [shortname] [url]

# 取回遠程倉庫的變化,並與本地分支合併
$ git pull [remote] [branch]

# 上傳本地指定分支到遠程倉庫
$ git push [remote] [branch]

# 強行推送當前分支到遠程倉庫,即使有衝突
$ git push [remote] --force

# 推送所有分支到遠程倉庫
$ git push [remote] --all 

撤銷

# 恢復暫存區的指定文件到工作區
$ git checkout [file]

# 恢復某個commit的指定文件到暫存區和工作區
$ git checkout [commit] [file]

# 恢復暫存區的所有文件到工作區
$ git checkout .

# 重置暫存區的指定文件,與上一次commit保持一致,但工作區不變
$ git reset [file]

# 重置暫存區與工作區,與上一次commit保持一致
$ git reset --hard

# 重置當前分支的指針為指定commit,同時重置暫存區,但工作區不變
$ git reset [commit]

# 重置當前分支的HEAD為指定commit,同時重置暫存區和工作區,與指定commit一致
$ git reset --hard [commit]

# 重置當前HEAD為指定commit,但保持暫存區和工作區不變
$ git reset --keep [commit]

# 新建一個commit,用來撤銷指定commit
# 後者的所有變化都將被前者抵消,並且應用到當前分支
$ git revert [commit]

暫時將未提交的變化移除,稍後再移入
$ git stash
$ git stash pop 

其他

# 生成一個可供發布的壓縮包
$ git archive

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10098

沐鳴登錄測速_深入理解React Diff算法

點擊進入react源碼調試倉庫。

fiber上的updateQueue經過React的一番計算之後,這個fiber已經有了新的狀態,也就是state,對於類組件來說,state是在render函數里被使用的,既然已經得到了新的state,那麼當務之急是執行一次render,得到持有新state的ReactElement。

假設render一次之後得到了大量的ReactElement,而這些ReactElement之中若只有少量需要更新的節點,那麼顯然不能全部去更新它們,此時就需要有一個diff過程來決定哪些節點是真正需要更新的。

源碼結構

我們以類組件為例,state的計算髮生在類組件對應的fiber節點beginWork中的updateClassInstance函數中,在狀態計算完畢之後,緊跟着就是去調finishClassComponent執行diff、打上effectTag(即新版本的flag)。

打上effectTag可以標識這個fiber發生了怎樣的變化,例如:新增(Placement)、更新(Update)、刪除(Deletion),這些被打上flag的fiber會在complete階段被收集起來,形成一個effectList鏈表,只包含這些需要操作的fiber,最後在commit階段被更新掉。

function updateClassComponent(
   current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes,) {
   ...
   // 計算狀態
   shouldUpdate = updateClassInstance(
     current,
     workInProgress,
     Component,
     nextProps,
     renderLanes,
   );
   
   ...
   
   // 執行render,進入diff,為fiber打上effectTag
   const nextUnitOfWork = finishClassComponent(
     current, 
     workInProgress,
     Component,
     shouldUpdate,
     hasContext,
     renderLanes,
     );
     return nextUnitOfWork;
 }

在finishClassComponent函數中,調用reconcileChildFibers去做diff,而reconcileChildFibers實際上就是ChildReconciler,這是diff的核心函數,
該函數針對組件render生成的新節點的類型,調用不同的函數進行處理。

function ChildReconciler(shouldTrackSideEffects) {
 
   ...
   function reconcileSingleElement(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      element: ReactElement,
      lanes: Lanes,
   ): Fiber {
     // 單節點diff
   }
   
  function reconcileChildrenArray(
     returnFiber: Fiber,
     currentFirstChild: Fiber | null,
     newChildren: Array<*>,
     lanes: Lanes,
  ): Fiber | null {
    // 多節點diff
  }
   
   ...
 
   function reconcileChildFibers(
     returnFiber: Fiber,
     currentFirstChild: Fiber | null,
     newChild: any, lanes: Lanes,
   ): Fiber | null {
     const isObject = typeof newChild === 'object' && newChild !== null;
     if (isObject) {
       // 處理單節點
       switch (newChild.$$typeof) {
         case REACT_ELEMENT_TYPE:
           return placeSingleChild(
             reconcileSingleElement(
             returnFiber,
             currentFirstChild,
             newChild,
             lanes,
           ),
        );
        
        case REACT_PORTAL_TYPE:
        ...
        
        case REACT_LAZY_TYPE:
        ...
      
      }
    }
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // 處理文本節點
    }
    if (isArray(newChild)) {
      // 處理多節點
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
     );
   }
   
   ...
   
 }
 
 return reconcileChildFibers;
 
}

Diff的主體

關於Diff的參与者,在reconcileChildren函數的入參中可以看出

workInProgress.child = reconcileChildFibers(
 workInProgress,
 current.child,
 nextChildren,
 renderLanes,
 );
  • workInProgress:作為父節點傳入,新生成的第一個fiber的return會被指向它。
  • current.child:舊fiber節點,diff生成新fiber節點時會用新生成的ReactElement和它作比較。
  • nextChildren:新生成的ReactElement,會以它為標準生成新的fiber節點。
  • renderLanes:本次的渲染優先級,最終會被掛載到新fiber的lanes屬性上。

可以看出,diff的兩個主體是:oldFiber(current.child)和newChildren(nextChildren,新的ReactElement),它們是兩個不一樣的數據結構。

比如現在有組件<Example/>,它計算完新的狀態之後,要基於這兩個東西去做diff,分別是現有fiber樹中(current樹)<Example/>對應fiber的所有子fiber節點<Example/>的render函數的執行結果,即那些ReactElements

<Example/>對應fiber的所有子fiber節點:oldFiber

 current樹中
 <Example/> fiber | | A --sibling---> B --sibling---> C

<Example/>的render函數的執行結果,newChildren

 current fiber 對應的組件render的結果
 [
    {$$typeof: Symbol(react.element), type: "div", key: "A" },
    {$$typeof: Symbol(react.element), type: "div", key: "B" }, 
    {$$typeof: Symbol(react.element), type: "div", key: "B" },
 ]

Diff的基本原則

對於新舊兩種結構來說,場景有節點自身更新、節點增刪、節點移動三種情況。面對複雜的情況,即使最前沿的算法,複雜度也極高。面對這種情況,React以如下策略應對:

  • 即使兩個元素的子樹完全一樣,但前後的父級元素不同,依照規則div元素及其子樹會完全銷毀,並重建一個p元素及其子樹,不會嘗試復用子樹。
舊
<div>
 <span>a</span>
 <span>b</span>
</div>

新
<p>
 <span>a</span>
 <span>b</span>
</p>
  • 使用tag(標籤名)和 key識別節點,區分出前後的節點是否變化,以達到盡量復用無變化的節點。
舊
<p key="a">aa</p>
<h1 key="b">bb</h1>

新
<h1 key="b">bb</h1>
<p key="a">aa</p>

因為tag 和 key的存在,所以React可以知道這兩個節點只是位置發生了變化。

場景

上面說到diff算法應對三種場景:節點更新、節點增刪、節點移動,但一個fiber的子元素有可能是單節點,也有可能是多節點。所以依據這兩類節點可以再細分為:

  • 單節點更新、單節點增刪。
  • 多節點更新、多節點增刪、多節點移動。

什麼是節點的更新呢?對於DOM節點來說,在前後的節點類型(tag)和key都相同的情況下,節點的屬性發生了變化,是節點更新。若前後的節點tag或者key不相同,Diff算法會認為新節點和舊節點毫無關係。

以下例子中,key為b的新節點的className發生了變化,是節點更新。

舊
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>

新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'b'}>bb</div>

以下例子中,新節點的className雖然有變化,但key也變化了,不屬於節點更新

舊
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>

新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'bbb'}>bb</div>

以下例子中,新節點的className雖然有變化,但tag也變化了,不屬於節點更新

舊
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>

新
<div className={'a'} key={'a'}>aa</div>
<p className={'bcd'} key={'b'}>bb</p>

下面來分開敘述一下單節點和多節點它們各自的更新策略。

單節點

若組件產出的元素是如下的類型:

<div key="a">aa</div>

那麼它最終產出的ReactElement為下面這樣(省略了一些與diff相關度不大的屬性)

{
   $$typeof: Symbol(react.element), type: "div", key: "a" 
   ...
}

單節點指newChildren為單一節點,但是oldFiber的數量不一定,所以實際有如下三種場景:

為了降低理解成本,我們用簡化的節點模型來說明問題,字母代表key。

  • 單箇舊節點
舊: A
新: A
  • 多箇舊節點
舊: A - B - C
新: B
  • 沒有舊節點
舊: --
新: A

對於單節點的diff,其實就只有更新操作,不會涉及位移和位置的變化,單節點的更新會調用reconcileSingleElement函數處理。該函數中對以上三種場景都做了覆蓋。但實際上面的情況對於React來說只是兩種,oldFiber鏈是否為空。因此,在實現上也只處理了這兩種情況。

oldFiber鏈不為空

遍歷它們,找到key相同的節點,然後刪除剩下的oldFiber節點,再用匹配的oldFiber,newChildren中新節點的props來生成新的fiber節點。

   function reconcileSingleElement(
     returnFiber: Fiber,
     currentFirstChild: Fiber | null,
     element: ReactElement,
     lanes: Lanes
   ): Fiber {
     const key = element.key;
     let child = currentFirstChild;
     while (child !== null) {
        if (child.key === key) {
          switch (child.tag) {
            case Fragment:
            ...
            
            case Block:
            ...
            
            default: {
              if (child.elementType === element.type) {
                 // 先刪除剩下的oldFiber節點
                deleteRemainingChildren(returnFiber, child.sibling);
                // 基於oldFiber節點和新節點的props新建新的fiber節點
                const existing = useFiber(child, element.props);
                existing.ref = coerceRef(returnFiber, child, element);
                existing.return = returnFiber; return existing;
              }
              break;
            }
         }
         
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // 沒匹配到說明新的fiber節點無法從oldFiber節點新建
        // 刪除掉所有oldFiber節點
        deleteChild(returnFiber, child);
     }
     child = child.sibling;
   }
   
 ...
 
 }

oldFiber鏈為空

對於沒有oldFiber節點的情況,只能新建newFiber節點。邏輯不複雜。

   function reconcileSingleElement(
     returnFiber: Fiber,
     currentFirstChild: Fiber | null,
     element: ReactElement,
     lanes: Lanes
   ): Fiber {
     const key = element.key;
     let child = currentFirstChild;
     while (child !== null) {
     
        // oldFiber鏈非空的處理
        ...
     } if (element.type === REACT_FRAGMENT_TYPE) {
        // 處理Fragment類型的節點
        ... 
     } else {
        // 用產生的ReactElement新建一個fiber節點
        const created = createFiberFromElement(element, returnFiber.mode, lanes);
        created.ref = coerceRef(returnFiber, currentFirstChild, element);
        created.return = returnFiber;
        return created;
     }
   }

單節點的更新就是這樣的處理,真正比較複雜的情況是多節點的diff。因為它涉及到節點的增刪和位移。

多節點

若組件最終產出的DOM元素是如下這樣:

<div key="a">aa</div>
<div key="b">bb</div>
<div key="c">cc</div>
<div key="d">dd</div>

那麼最終的newChildren為下面這樣(省略了一些與diff相關度不大的屬性)

[
 {$$typeof: Symbol(react.element), type: "div", key: "a" },
 {$$typeof: Symbol(react.element), type: "div", key: "b" },
 {$$typeof: Symbol(react.element), type: "div", key: "c" },
 {$$typeof: Symbol(react.element), type: "div", key: "d" }
]

多節點的變化有以下四種可能性。

  • 節點更新
舊: A - B - C
新: `A - B - C`
  • 新增節點
舊: A - B - C
新: A - B - C - `D - E`
  • 刪除節點
舊: A - B - C - `D - E`
新: A - B - C
  • 節點移動
舊: A - B - C - D - E
新: A - B - `D - C - E`

多節點的情況一定是屬於這四種情況的任意組合,這種情況會調用reconcileChildrenArray進行diff。按照以上四種情況,它會以newChildren為主體進行最多三輪遍歷,但這三輪遍歷並不是相互獨立的,事實上只有第一輪是從頭開始的,之後的每一輪都是上輪結束的斷點繼續。實際上在平時的實踐中,節點自身的更新是最多的,所以Diff算法會優先處理更新的節點。因此四輪遍歷又可以按照場景分為兩部分:

第一輪是針對節點自身屬性更新,剩下的兩輪依次處理節點的新增、移動,而重點又在移動節點的處理上,所以本文會着重講解節點更新和節點移動的處理,對刪除和新增簡單帶過。

節點更新

第一輪從頭開始遍歷newChildren,會逐個與oldFiber鏈中的節點進行比較,判斷節點的key或者tag是否有變化。

  • 沒變則從oldFiber節點clone一個props被更新的fiber節點,新的props來自newChildren中的新節點,這樣就實現了節點更新。
  • 有變化說明不滿足復用條件,立即中斷遍歷進入下邊的遍歷。Diff算法的複雜度也因為這個操作大幅降低。
let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
   ...
   // 更新節點,對於DOM節點來說,updateSlot內部會判斷
   // key 和 tag。任意一個不同,則返回null
   const newFiber = updateSlot( returnFiber,
     oldFiber,
     newChildren[newIdx],
     lanes,
   );
   // newFiber為null則說明當前的節點不是更新的場景,中止這一輪循環
   if (newFiber === null) {
     if (oldFiber === null) {
        oldFiber = nextOldFiber;
     }
     break;
   }
    ...
 }

我們來看一個例子,假設新舊的節點如下:

舊: A – B – C – D – E
新: A – B – D – C

在本輪遍歷中,會遍歷A – B – D – C。A和B都是key沒變的節點,可以直接復用,但當遍歷到D時,發現key變化了,跳出當前遍歷。例子中A 和 B是自身發生更新的節點,後面的D 和 C我們看到它的位置相對於oldFiber鏈發生了變化,會往下走到處理移動節點的循環中。

關於移動節點的參照物

為了方便說明,把保留在原位的節點稱為固定節點。經過這次循環的處理,可以看出固定節點是A 和 B。在newChildren中,最靠右的固定節點的位置至關重要,對於後續的移動節點的處理來說,它的意義是提供參考位置。所以,每當處理到最後一個固定節點時,要記住此時它的位置,這個位置就是lastPlacedIndex。關鍵代碼如下:

let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
 ...
 // 跳出邏輯
 
 ...
 // 如果不跳出,記錄最新的固定節點的位置
 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
 
 ...}

placeChild方法實際上是移動節點的方法,但當節點無需移動的時候,會返回當前節點的位置,對於固定節點來說,因為無需移動,所以返回的就是固定節點的index。

節點刪除

我們沒有提到對刪除節點的處理,實際上刪除節點比較簡單。

舊: A – B – C – D – E
新: A – B – C

因為遍歷的是newChildren,當它遍歷結束,但oldFiber鏈還沒有遍歷完,那麼說明剩下的節點都要被刪除。直接在oldFiber節點上標記Deletion的effectTag來實現刪除。

if (newIdx === newChildren.length) {
   // 新子節點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除
   deleteRemainingChildren(returnFiber, oldFiber);
   return resultingFirstChild;
}

deleteRemainingChildren調用了deleteChild,值得注意的是,刪除不僅僅是標記了effectTag為Deletion,還會將這個被刪除的fiber節點添加到父級的effectList中。

function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
   ...
   const last = returnFiber.lastEffect;
   // 將要刪除的child添加到父級fiber的effectList中,並添加上effectTag為刪除
   if (last !== null) {
     last.nextEffect = childToDelete;
     returnFiber.lastEffect = childToDelete;
   } else {
     returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
   }
   childToDelete.nextEffect = null;
   childToDelete.effectTag = Deletion;
}

節點新增

新增節點的場景也很好理解,當oldFiber鏈遍歷完,但newChildren還沒遍歷完,那麼餘下的節點都屬於新插入的節點,會新建fiber節點並以sibling為指針連成fiber鏈。

舊: A – B – C
新: A – B – C – D – E

插入的邏輯(省略了相關度不高的代碼)

if (oldFiber === null) {
 // 舊的遍歷完了,意味着剩下的都是新增的了
 for (; newIdx < newChildren.length; newIdx++) { // 首先創建newFiber
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    ...
    // 再將newFiber連接成以sibling為指針的單向鏈表
    if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
    } else {
        previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
  return resultingFirstChild;
}

節點移動

節點的移動是如下場景:

舊 A – B – C – D – E – F
新 A – B – D – C – E

經過第一輪遍歷的處理,固定節點為A B,最新的固定節點的位置(lastPlacedIndex)為1(B的位置)。此時oldFiber鏈中還剩C – D – E – F,newChildren中還剩D – C – E。

接下來的邏輯對於位置不一樣的節點,它自己會先更新再移動。因為此時剩餘的節點位置變了,更新又要復用oldFiber節點,所以為了在更新時方便查找,會將剩餘的oldFiber節點放入一個以key為鍵,值為oldFiber節點的map中。稱為existingChildren。

由於newChildren 和 oldFiber節點都沒遍歷完,說明需要移動位置。此刻需要明確一點,就是這些節點都在最新的固定節點的右邊

移動的邏輯是:newChildren中剩餘的節點,都是不確定要不要移動的,遍歷它們,每一個都去看看這個節點在oldFiber鏈中的位置(舊位置),遍歷到的節點有它在newChildren中的位置(新位置):

如果舊位置在lastPlacedIndex的右邊,說明這個節點位置不變。

原因是舊位置在lastPlacedIndex的右邊,而新節點的位置也在它的右邊,所以它的位置沒變化。因為位置不變,所以它成了固定節點,把lastPlacedIndex更新成新位置。

如果舊位置在lastPlacedIndex的左邊,當前這個節點的位置要往右挪。

原因是舊位置在lastPlacedIndex的左邊,新位置卻在lastPlacedIndex的右邊,所以它要往右挪,但它不是固定節點。此時無需更新lastPlacedIndex。

我們來用上邊的例子過一下這部分邏輯。

舊 A – B – C – D – E – F
新 A – B – D – C – E

位置固定部分 A – B,最右側的固定節點為B,lastPlacedIndex為1。這時剩餘oldFiber鏈為C – D – E – F,existingChildren為

{
   C: '節點C',
   D: '節點D',
   E: '節點E',
   F: '節點F'
}

newChildren的剩餘部分D – C – E繼續遍歷。

首先遍歷到D,D在oldFiber鏈中(A – B – C – D – E)的位置為3

3 > 1,oldFiber中D的位置在B的右邊,newChildren中也是如此,所以D的位置不動,此時最新的固定節點變成了D,更新lastPlacedIndex為3。並從existingChildren中刪除D,

{
   C: '節點C',
   E: '節點E',
   F: '節點F'
}

再遍歷到C,C在oldFiber鏈中(A – B – C – D – E)的索引為2

2 < 3,C原來在最新固定節點(D)的左邊,newChildren中C在D的右邊,所以要給它移動到右邊。並從existingChildren中刪除C。

{
   E: '節點E',
   F: '節點F'
}

再遍歷到E,E在oldFiber鏈中(A – B – C – D – E)的位置為4

4 > 3,oldFiber鏈中E位置在D的位置的右邊,新位置中也是如此,所以E的位置不動,此時最新的固定節點變成了E,更新lastPlacedIndex為4。並從existingChildren中刪除E,

{
   F: '節點F'
}

這個時候newChildren都處理完了,針對移動節點的遍歷結束。此時還剩一個F節點,是在oldFiber鏈中的,因為newChildren都處理完了,所以將它刪除即可。

existingChildren.forEach(child => deleteChild(returnFiber, child));

可以看到,節點的移動是以最右側的固定節點位置作為參照的。這些固定節點是指位置未發生變化的節點。每次對比節點是否需要移動之後,及時更新固定節點非常重要。

源碼

了解了上邊的多節點diff原理后,將上邊的關鍵點匹配到源碼上更方便能進一步理解。下面放出帶有詳細註釋的源碼。

 function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
): Fiber | null {
    /* * returnFiber:currentFirstChild的父級fiber節點
       * currentFirstChild:當前執行更新任務的WIP(fiber)節點
       * newChildren:組件的render方法渲染出的新的ReactElement節點
       * lanes:優先級相關
    * */
    
    // resultingFirstChild是diff之後的新fiber鏈表的第一個fiber。
    let resultingFirstChild: Fiber | null = null;
    // resultingFirstChild是新鏈表的第一個fiber。
    // previousNewFiber用來將後續的新fiber接到第一個fiber之後
    let previousNewFiber: Fiber | null = null;
    
    // oldFiber節點,新的child節點會和它進行比較
    let oldFiber = currentFirstChild;
    // 存儲固定節點的位置
    let lastPlacedIndex = 0;
    // 存儲遍歷到的新節點的索引
    let newIdx = 0;
    // 記錄目前遍歷到的oldFiber的下一個節點
    let nextOldFiber = null;
    
    // 該輪遍歷來處理節點更新,依據節點是否可復用來決定是否中斷遍歷
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
        // newChildren遍歷完了,oldFiber鏈沒有遍歷完,此時需要中斷遍歷
        if (oldFiber.index > newIdx) {
            nextOldFiber = oldFiber; oldFiber = null;
        } else {
            // 用nextOldFiber存儲當前遍歷到的oldFiber的下一個節點
            nextOldFiber = oldFiber.sibling;
        }
        // 生成新的節點,判斷key與tag是否相同就在updateSlot中
        // 對DOM類型的元素來說,key 和 tag都相同才會復用oldFiber
        // 並返回出去,否則返回null
        const newFiber = updateSlot(
            returnFiber,
            oldFiber,
            newChildren[newIdx],
            lanes,
        );
        
        // newFiber為 null說明 key 或 tag 不同,節點不可復用,中斷遍歷
        if (newFiber === null) {
            if (oldFiber === null) {
            // oldFiber 為null說明oldFiber此時也遍歷完了
            // 是以下場景,D為新增節點
            // 舊 A - B - C 
            // 新 A - B - C - D oldFiber = nextOldFiber;
            }
            break;
        }
        if (shouldTrackSideEffects) {
            // shouldTrackSideEffects 為true表示是更新過程
            if (oldFiber && newFiber.alternate === null) {
                // newFiber.alternate 等同於 oldFiber.alternate 
                // oldFiber為WIP節點,它的alternate 就是 current節點
                // oldFiber存在,並且經過更新后的新fiber節點它還沒有current節點,
                // 說明更新后展現在屏幕上不會有current節點,而更新后WIP
                // 節點會稱為current節點,所以需要刪除已有的WIP節點
                deleteChild(returnFiber, oldFiber);
                }
            }
            // 記錄固定節點的位置
            lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
            // 將新fiber連接成以sibling為指針的單向鏈表
            if (previousNewFiber === null) {
                resultingFirstChild = newFiber;
            } else {
                previousNewFiber.sibling = newFiber;
            }
            previousNewFiber = newFiber;
            // 將oldFiber節點指向下一個,與newChildren的遍歷同步移動
            oldFiber = nextOldFiber;
         }
         
        // 處理節點刪除。新子節點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除.
        if (newIdx === newChildren.length) {
            // newChildren遍歷結束,刪除掉oldFiber鏈中的剩下的節點
            deleteRemainingChildren(returnFiber, oldFiber);
            return resultingFirstChild;
        }
        
        // 處理新增節點。舊的遍歷完了,能復用的都復用了,所以意味着新的都是新插入的了
        if (oldFiber === null) {
            for (; newIdx < newChildren.length; newIdx++) {
            
                // 基於新生成的ReactElement創建新的Fiber節點
                const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
                if (newFiber === null) {
                    continue;
                }
                // 記錄固定節點的位置lastPlacedIndex
                lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); 
                // 將新生成的fiber節點連接成以sibling為指針的單向鏈表
                if (previousNewFiber === null) {
                    resultingFirstChild = newFiber;
                } else {
                    previousNewFiber.sibling = newFiber; 
                }
                previousNewFiber = newFiber;
            }
            return resultingFirstChild;
        }
        // 執行到這是都沒遍歷完的情況,把剩餘的舊子節點放入一個以key為鍵,值為oldFiber節點的map中
        // 這樣在基於oldFiber節點新建新的fiber節點時,可以通過key快速地找出oldFiber
        const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
        
        // 節點移動
        for (; newIdx < newChildren.length; newIdx++) {
            // 基於map中的oldFiber節點來創建新fiber
            const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, ); 
            if (newFiber !== null) {
                if (shouldTrackSideEffects) {
                    if (newFiber.alternate !== null) {
                        // 因為newChildren中剩餘的節點有可能和oldFiber節點一樣,只是位置換了,
                        // 但也有可能是是新增的.
                        
                        // 如果newFiber的alternate不為空,則說明newFiber不是新增的。
                        // 也就說明着它是基於map中的oldFiber節點新建的,意味着oldFiber已經被使用了,所以需
                        // 要從map中刪去oldFiber
                        existingChildren.delete(
                            newFiber.key === null ? newIdx : newFiber.key,
                        );
                     }
                  }
                  
                 // 移動節點,多節點diff的核心,這裏真正會實現節點的移動
                 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
                 // 將新fiber連接成以sibling為指針的單向鏈表
                if (previousNewFiber === null) {
                    resultingFirstChild = newFiber;
                } else {
                    previousNewFiber.sibling = newFiber; 
                }
                previousNewFiber = newFiber;
            }
         }
        if (shouldTrackSideEffects) {
           // 此時newChildren遍歷完了,該移動的都移動了,那麼刪除剩下的oldFiber
           existingChildren.forEach(child => deleteChild(returnFiber, child));
        }
        return resultingFirstChild;
 }

總結

Diff算法通過key和tag來對節點進行取捨,可直接將複雜的比對攔截掉,然後降級成節點的移動和增刪這樣比較簡單的操作。對oldFiber和新的ReactElement節點的比對,將會生成新的fiber節點,同時標記上effectTag,這些fiber會被連到workInProgress樹中,作為新的WIP節點。樹的結構因此被一點點地確定,而新的workInProgress節點也基本定型。這意味着,在diff過後,workInProgress節點的beginWork節點就完成了。接下來會進入completeWork階段。

原文來自:https://segmentfault.com/a/1190000039021724

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10095

沐鳴註冊平台官網_node.js+express 做301重定向實驗

本地項目啟動:

1.添加代碼

 if(req.url === '/apply'){
            res.writeHead(301,{
                'Location':'https://www.baidu.com/'
            })
        }

之後http://localhost:8080/apply會跳轉到https://www.baidu.com/ ,跳轉肉眼不可見

項目停掉之後,在瀏覽器地址欄輸入http://localhost:8080/apply  仍然會跳轉到https://www.baidu.com/ ,說明瀏覽器記錄了這個跳轉。

手動刪除掉瀏覽器緩存,只清楚緩存的圖片和文件就可以刪掉:

輸入http://localhost:8080/apply,不再跳轉到https://www.baidu.com/了。

我重啟項目,看一下發送的請求:

劃線表示永久重定向到緩存。打開看上線了網站的案例:

沒有把重定向緩存,所以它刪除綁定后,是不會再跳轉到自定義域名的。

關於301跳轉的問題,我們特別邀請 Baiduspider 技術專家對此做了解答。

問:我設置了 301 跳轉,多久可以生效?

答:目前百度無法承諾 301 跳轉的生效時間,因為站長感受到的生效時間會受多因素影響,比如 Baiduspider 再次抓取這個頁面發現其設置了 301 的時間、網頁的重要程度以及自身質量等。

問:301 跳轉生效后,原網頁是否會被刪除?

答:不會,會與跳轉后的新網頁同時存在。

問:原網頁新網頁都存在,相當於兩個內容重複的頁面,排名怎麼處理?

答:通常百度會認為跳轉后的新網頁更加重要,原網頁是被放棄的網頁,會讓新網頁繼承原網頁屬性,在搜索結果中會優先展現新網頁。

問:將老域名跳轉到新域名,是僅首頁 301 就可以了?還是必須所有頁面 301?

答:必須所有頁面 301 至新域名的相應頁面。

問:原域名所有頁面都跳轉至新域頁首頁會有什麼影響?

答:新域名首頁將會替換舊域名首頁,新域名首頁僅繼承舊域名首頁的屬性,不會產生 1+1>2 的效果。舊域名在短時間內仍然可查。但隨着時間推移,舊域名由於質量下降最終會被清除出數據庫。

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10106

沐鳴總代平台_為什麼 Vue3.0 要重寫響應式系統

面試的時候經常被問到 響應式 相關的內容,而vue3.0 更新后,面試官又有了新的武器;

面試官: 為什麼 Vue3.0 要重寫響應式系統?

懵逼樹上懵逼果,懵逼樹下你和我,面試官在問什麼,我該怎麼回答,完全不知道怎麼回事;

有些經驗的小夥伴可能會從解釋 Proxy 的好處開始簡單聊一下,比如: Proxy 是直接代理對象,而不是劫持對象的屬性;更好的數組監控;

這樣的回答,勉強算是合格吧

那到底應該怎麼答呢?

面試官背後的出題邏輯

別急,咱們先整理一下思路,孫子兵法有雲:“知己知彼,百戰不殆”;面試就像打仗,你來我往,所以我們需要換位思考,想一想,為什麼面試官會問這樣一個問題?面試官想從這個問題里得到什麼回答?這個問題可以考察哪些技術點? 想清楚這個問題,再回到自己身上,這些技術點,你都掌握了嗎?

說得直白一點,面試就像考試,你需要先 讀題、審題才能答好這道題;

為什麼很多人認為 “面試造火箭,工作擰螺絲”?因為沒有換位思考,沒有想清楚面試題背後的邏輯;

那我們想清楚這個邏輯之後,需要我們做的就是提取技術點,整理思路,做出對應解答;當然, 前提是你需要具備這些技術能力

那麼接下來,我就嘗試拆解一下這個面試題了,提取其中的知識點。

對於你來說,就是要看看這些知識點,你都掌握了多少?

為什麼 Vue3.0 要重寫響應式系統 ?

為什麼重寫?如果之前好好的,重寫就沒有意義,那之前存在什麼問題,現在是怎麼解決的?就是關鍵點了;

不知道你對 Vue2.x 的響應式掌握多少,是不是欠下了技術的債呢?沒關係,我來幫你還債,先梳理 Vue2.x 的響應式;

其實基於這個面試題,背後還有很多技術點,上面這些,是與當前題目有直接關係的,實際面試中,很有可能基於這些技術點,在進行深入交流,這裏就不擴展了,你能把現在這些問題理清楚,就算賺到了;

Vue2.x 響應式

其實關於這一點,在Vue 的官方文檔中,早已經有過說明了,而且說得非常詳細;官方文檔: https:// cn.vuejs.org/v2/guide/r eactivity.html

當你把一個普通的 JavaScript 對象傳入 Vue 實例作為 data 選項,Vue 將遍歷此對象所有的 property,並使用 Object.defineProperty 把這些 property 全部轉為getter/setter。Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。

這些 getter/setter 對用戶來說是不可見的,但是在內部它們讓 Vue 能夠追蹤依賴,在 property 被訪問和修改時通知變更。這裏需要注意的是不同瀏覽器在控制台打印數據對象時對 getter/setter 的格式化並不同,所以建議安裝 vue-devtools 來獲取對檢查數據更加友好的用戶界面。

每個組件實例都對應一個 watcher 實例,它會在組件渲染的過程中把“接觸”過的數據 property 記錄為依賴。之後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件重新渲染。

我們使用官方給的一張圖示,來梳理整個流程;


我們先來看一段代碼

響應式原理

data 中的 obj 就是一個普通的 JavaScript 對象,通過點擊 Click 按鈕,將獲取到的隨機數賦值給 this.message ,而 this.message 指向的就是 data 中 obj 對象的 message 屬性;當message 發生數據改變時,頁面中 H1 標籤的內容會隨之改變,這個過程就是就是響應式的;那麼Vue 是如何實現的呢?

首先,Vue 內部使用 Object.defineProperty() 將 Data 中的每一個成員都轉換為 getter / setter 的形式;getter 用來依賴收集,setter 用來派發更新;而模板內容,最終會被編譯為 render 函數,在 render 函數中,我們能發現 _v(_s(message)) message 被訪問了,就會觸發 getter 來進行依賴收集,而在代碼中的點擊事件中,一旦事件處理程序被觸發執行,那麼 message 則會被修改,就會觸發 setter來進行派發更新;

雖然流程理清楚了,但是總感覺少點什麼,怎麼才能更通透呢?

我們用代碼來模擬整個的實現過程;

defineProperty 模擬代碼

defineProperty 的基本用法,直接看手冊就行了: https:// developer.mozilla.org/z h-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

我們來看看代碼:

<div id="app"> 
    hello 
</div> 
<script> 
    // 模擬 Vue 中的 data 選項 
    let data = { 
      msg: 'hello' 
    } 
    // 模擬 Vue 的實例 
    let vm = {} 
    // 數據劫持:當訪問或者設置 vm 中的成員的時候,做一些干預操作 
    Object.defineProperty(vm, 'msg', { 
      // 可枚舉(可遍歷) 
      enumerable: true, 
      // 可配置(可以使用 delete 刪除,可以通過 defineProperty 重新定義) 
      configurable: true, 
      // 當獲取值的時候執行 
      get () { 
        console.log('get: ', data.msg) 
        return data.msg 
      }, 
      // 當設置值的時候執行 
      set (newValue) { 
        console.log('set: ', newValue) 
        if (newValue === data.msg) { 
          return 
        } 
        data.msg = newValue 
        // 數據更改,更新 DOM 的值 
        document.querySelector('#app').textContent = data.msg 
      } 
    }) 
    // 測試 
    vm.msg = 'Hello World' 
    console.log(vm.msg) 
</script>

你沒有看錯,加上註釋,一共 36行代碼,這就是 Vue2.x 對響應式實現的整個流程;

繼續實現多個數據的響應式

<body>
  <div id="app">
    hello
  </div>
  <script>
    // 模擬 Vue 中的 data 選項
    let data = {
      msg: 'hello',
      count: 10
    }
    // 模擬 Vue 的實例
    let vm = {}
    proxyData(data)
    function proxyData(data) {
      // 遍歷 data 對象的所有屬性
      Object.keys(data).forEach(key => {
        // 把 data 中的屬性,轉換成 vm 的 setter/setter
        Object.defineProperty(vm, key, {
          enumerable: true,
          configurable: true,
          get () {
            console.log('get: ', key, data[key])
            return data[key]
          },
          set (newValue) {
            console.log('set: ', key, newValue)
            if (newValue === data[key]) {
              return
            }
            data[key] = newValue
            // 數據更改,更新 DOM 的值
            document.querySelector('#app').textContent = data[key]
          }
        })
      })
    }
    // 測試
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>
</body>

上面的代碼只是模擬了 響應式 的原理,但Vue在實現中,肯定不會那麼簡單,接下來,我們看一下源碼呀……

Vue2 源碼解讀

首先找到響應式代碼的處理位置:

關鍵位置 作用 源碼位置
function Vue () {} Vue 構造函數 core/instance/index.js:8
Vue.prototype._init 初始化組件實例對象 core/instance/init.js:16
initState 初始化組件狀態相關成員 core/instance/state.js:48
initData 初始化用戶傳入的 data 數據 core/instance/state.js:112
observe 觀察 data core/observer/index.js:110
關鍵位置 作用 源碼位置
observe 觀察 data core/observer/index.js:110
class Observer Observer 邏輯 core/observer/index.js:37
walk 遍歷對象成員分別處理 core/observer/index.js:64
definereactive 為組件實例定義響應式數據 core/observer/index.js:135
Object.defineProperty 攔截數據的訪問和修改 core/observer/index.js:157

看完Vue2.x 響應式的代碼,我們再回過頭來思考最開始的問題, 為什麼 Vue3.0 要重寫響應式系統 ?

為什麼重寫?如果之前好好的,重寫就沒有意義,那之前存在什麼問題,換句話問就是 defineProperty 有什麼問題?

Object.defineProperty 的問題

其實, defineProperty 的問題,在Vue2.x 的手冊中,已經說過了;“哎,很多人就是不看文檔啊”

https:// cn.vuejs.org/v2/guide/r eactivity.html#%E5%AF%B9%E4%BA%8E%E6%95%B0%E7%BB%84

下面分別使用 Vue2 和 Vue3 實現了一個小功能,代碼一模一樣,功能當然也一樣,但是,在 Vue2 中就會有Bug,而運行在vue3中的,則沒有任何問題;

Vue2:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <p v-for="(v, k) in users">
      {{ v.names }}
    </p>
    <button @click="changes">更新</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, names: "路飛-v2" },
        { id: 2, names: "鳴人-v2" },
      ],
    };
  },

  methods: {
    changes() {
      // this.users[0] = {id:'0',names:'liuneng'}
      // this.users[1].names = 'lnsdsdfg'
      this.users[1] = { id: "1", names: "劉能-v2" };
    },
  },
};
</script>

<style lang="stylus" scoped></style>

Vue3:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <p v-for="(v, k) in users">
      {{ v.names }}
    </p>

    <button @click="changes">更新</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, names: "路飛-v3" },
        { id: 2, names: "鳴人-v3" },
      ],
    };
  },

  methods: {
    changes() {
      // this.users[0] = {id:'0',names:'liuneng'}
      // this.users[1].names = 'lnsdsdfg'
      this.users[1] = { id: "1", names: "劉能-v3" };
    },
  },
};
</script>

其核心點在於 defineProperty 不能很好的實現對數組下標的監控,而在 Vue2 的實現代碼中,沒有更好的方案對此進行改善,尤大索性直接放棄了實現;關於這個問題,尤大也在 github 做過回應,截個圖給大家看看;

那麼,Vue 目前還沒有解決的問題,Vue3中顯然是已經解決的了,問題是,Vue3 是如何解決的呢?前面在 Vue3 的代碼中我們使用的是傳統的 Options Api 來實現的數據響應式, 而在 Vue3 中全新的 Composition Api 也實現了響應式系統,我們先來感受一下 Composition Api 的基礎用法

Composition API 的響應式系統

ref 響應式

<template>
  <!-- 不需要.value -->
  <button @click="addNu"> Composition API: {{nu}}</button>
</template>

<script>
// 引入 ref 
import {ref} from "vue"
export default {
  setup() {
    // 定義 ref 響應式數據
    const nu = ref(1);
    // 定義函數
    function addNu(){
      nu.value++;
    }

    // 將數據和方法返回,即可在模板中直接使用
    return {
      nu,
      addNu
    };
  },
};
</script>

reactive 響應式

<template>
  <!-- 不需要.value -->
  <button @click="addNu"> Composition API: {{nu}}</button>
  <!-- reactive 響應式數據 -->
  <h2>{{revData.name}}</h2>
  <h3 @click="ageAdd">年齡:{{revData.age}}</h3>
  <p v-for="(v,k) in revData.skill"> {{v}} </p> 
</template>

<script>
// 引入 ref 
import {reactive, ref} from "vue"
export default {
  setup() {
    // 定義 ref 響應式數據
    const nu = ref(1);

    // reactive 響應式數據
    const revData = reactive({
      name: '路飛',
      age: 22,
      skill:['橡膠機關槍','吃雞腿'],
    })

    function ageAdd(){
      revData.age++
    }

    // 定義函數
    function addNu(){
      nu.value++;
    }

    // 將數據和方法返回,即可在模板中直接使用
    return {
      nu,
      addNu,
      revData,
      ageAdd
    };
  },
};
</script>

Vue3 中的響應式是如何實現的呢?關鍵點在於Proxy 函數;

Proxy 實現原理

使用 Proxy 實現的響應式代碼,要比使用 defineProperty 的代碼簡單得多,因為 Proxy 天然的能夠對整個對象做監聽,而不需要對數據行遍歷后做監聽,同時也就解決了數組下標的問題;

我們來一段模擬代碼看一下:

<div id="app">
    hello
  </div>
  <script>
    // 模擬 Vue 中的 data 選項
    let data = {
      msg: 'hello',
      count: 0
    }

    // 模擬 Vue 實例
    const vm = new Proxy(data, {
      // 執行代理行為的函數
      // 當訪問 vm 的成員會執行
      get (target, key) {
        console.log('get, key: ', key, target[key])
        return target[key]
      },
      // 當設置 vm 的成員會執行
      set (target, key, newValue) {
        console.log('set, key: ', key, newValue)
        if (target[key] === newValue) {
          return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
      }
    })

    // 測試
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>

來自:https://zhuanlan.zhihu.com/p/346241358

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10101

沐鳴平台網址_ES11(2020)新特性:String 的 matchAll 方法、動態導入語句 import()等

Ecma標準定義了ECMAScript 2020語言。它是ECMAScript語言規範的第11版。自從1997年第一版出版以來,ECMAScript已經發展成為世界上使用最廣泛的通用編程語言之一。它被稱為嵌入在web瀏覽器中的語言,但也被廣泛應用於服務器和嵌入式應用程序。

那麼ES11又引入了那些新特性呢?

1. String 的 matchAll 方法

2. 動態導入語句 import()

3. import.meta

4. export * as ns from ‘module’

5. Promise.allSettled

6. 新增數據類型: BigInt

7. 頂層對象: globalThis

8. 空值合併運算符: ??

9. 可選鏈操作符:?.

一、matchAll

matchAll() 方法返回一個包含所有匹配正則表達式的結果的迭代器。可以使用 for…of 遍歷,或者使用 展開運算符(…) 或者 Array.from 轉換為數組.

const regexp = /t(e)(st(\d?))/g;
const str = 'test1test2';

const matchs = str.matchAll(regexp);
console.log(matchs); // RegExpStringIterator {}
console.log([...matchs])
/*
0: (4) ["test1", "e", "st1", "1", index: 0, input: "test1test2", groups: undefined]
1: (4) ["test2", "e", "st2", "2", index: 5, input: "test1test2", groups: undefined]
length: 2
/*

RegExp.exec()  和 matchAll() 區別:

在 matchAll 出現之前,通過在循環中調用 regexp.exec() 來獲取所有匹配項信息。

const regexp = RegExp('foo[a-z]*','g');
const str = 'table football, foosball';
let match;

while ((match = regexp.exec(str)) !== null) {
  console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}.`);
}
// expected output: "Found football start=6 end=14."
// expected output: "Found foosball start=16 end=24."

如果使用 matchAll ,就可以不必使用 while 循環加 exec 方式

const regexp = RegExp('foo[a-z]*','g');
const str = 'table football, foosball';
const matches = str.matchAll(regexp);

for (const match of matches) {
  console.log(`Found ${match[0]} start=${match.index} end=${match.index + match[0].length}.`);
}
// expected output: "Found football start=6 end=14."
// expected output: "Found foosball start=16 end=24."

二、import()

import 標準的用法是導入的木塊是靜態的,會使所有被帶入的模塊在加載時就別編譯,無法做到按需加載編譯,降低了首頁的加載速度。在某些場景中,你可能希望根據條件導入模塊,或者按需導入模塊,這是就可以使用動態導入代替靜態導入了

在import() 之前,我們需要更具條件導入模塊時只能使用 require() 

if (xx) {
  const module = require('/module')  
}

// 現在可以這麼寫
if (xx) {
  const module = import('/module')
}

@babel/preset-env 已經包含了 @babel/plugin-syntax-dynamic-import,因此如果要使用 import() 語法,只需要配置 @babel/preset-env 即可。

另外:import() 返回的是一個Promise 對象:

// module.js
export default {
  name: 'shenjp'
}

// index.js
if (true) {
  let module = import('./module.js');
  console.log(module); // Promise {<pending>
  module.then(data => console.log(data)); // Module {default: {name: "shenjp"}, __esModule: true, Symbol(Symbol.toStringTag): "Module"}
}

三、import.meta

import.meta對象是由ECMAScript實現的,它帶有一個null的原型對象。這個對象可以擴展,並且它的屬性都是可寫,可配置和可枚舉的。

<script type="module" src="my-module.mjs"></script>
console.log(import.meta); // { url: "file:///home/user/my-module.mjs" }

因為 import.meta 必須要在模塊內部使用,如果不加 type=”module”,控制台會報錯:Cannot use ‘import.meta’ outside a module。

在項目中需要下載 @open-wc/webpack-import-meta-loader  才能正常使用。

module: {
    rules: [
        {
            test: /\.js$/,
            use: [
                require.resolve('@open-wc/webpack-import-meta-loader'),
                {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            "@babel/preset-env",
                            "@babel/preset-react"
                        ]
                    },
                }
            ]
        }
    ]
}

效果如下:

//src/index.js
import React from 'react';
console.log(import.meta);//{index.js:38 {url: "http://127.0.0.1:3000/src/index.js"}}

四、export * as ns from ‘module’

ES2020新增了 export * as XX from ‘module’,和 import * as XX from ‘module’

// module.js
export * as ns from './info.js'

可以理解為下面兩條語句的合併:

import * as ns from './info.js';
export { ns };

需要注意的是:export * as ns from ‘module’ 並不會真的導入模塊,因此在該模塊中無法使用 ns。

五、Promise.allSettled

Promise.allSettled()方法返回一個在所有給定的promise都已經fulfilled或rejected后的promise,並帶有一個對象數組,每個對象表示對應的promise結果。

當您有多個彼此不依賴的異步任務成功完成時,或者您總是想知道每個promise的結果時,通常使用它。

想比較之下, Promise.all() 更適合做相互依賴的Promise,只要有一個失敗就結束

const promise1 = Promise.resolve(100);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'info'));
const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 200, 'name'))

Promise.allSettled([promise1, promise2, promise3]).
    then((results) => console.log(result));
/* 
    [
        { status: 'fulfilled', value: 100 },
        { status: 'rejected', reason: 'info' },
        { status: 'fulfilled', value: 'name' }
    ]
*/

可以看出,Promise.allSettled() 成功之後返回的也是一個數組,但是改數組的每一項都是一個對象,每個對象都有一個status屬性,值為 fulfilled 和 rejected .

如果status是 fulfilled,那麼改對象的另一個屬性是 value ,對應的是該Promise成功后的結果。

如果status是 rejected,那麼對象的另一個屬性是 reason,對應的是該Promise失敗的原因。

六、BigInt

BigInt 是一種数字類型的數據,它可以表示任意精度格式的整數。在此之前,JS 中安全的最大数字是 9009199254740991,即2^53-1,在控制台中輸入 Number.MAX_SAFE_INTEGER 即可查看。超過這個值,JS 沒有辦法精確表示。另外,大於或等於2的1024次方的數值,JS 無法表示,會返回 Infinity。

BigInt 即解決了這兩個問題。BigInt 只用來表示整數,沒有位數的限制,任何位數的整數都可以精確表示。為了和 Number 類型進行區分,BigInt 類型的數據必須添加後綴 n.

//Number類型在超過9009199254740991后,計算結果即出現問題
const num1 = 90091992547409910;
console.log(num1 + 1); //90091992547409900

//BigInt 計算結果正確
const num2 = 90091992547409910n;
console.log(num2 + 1n); //90091992547409911n

我們還可以使用 BigInt 對象來初始化 BigInt 實例:

console.log(BigInt(999)); // 999n 注意:沒有 new 關鍵字!!!

需要說明的是,BigInt 和 Number 是兩種數據類型,不能直接進行四則運算,不過可以進行比較操作。

console.log(99n == 99); //true
console.log(99n === 99); //false 
console.log(99n + 1);//TypeError: Cannot mix BigInt and other types, use explicit conversionss

七、GlobalThis

JS 中存在一個頂層對象,但是,頂層對象在各種實現里是不統一的。

從不同的 JavaScript 環境中獲取全局對象需要不同的語句。在 Web 中,可以通過 window、self 取到全局對象,但是在 Web Workers 中,只有 self 可以。在 Node.js 中,它們都無法獲取,必須使用 global。

var getGlobal = function () {
    if (typeof self !== 'undefined') { return self; }
    if (typeof window !== 'undefined') { return window; }
    if (typeof global !== 'undefined') { return global; }
    throw new Error('unable to locate global object');
};

ES2020 中引入 globalThis 作為頂層對象,在任何環境下,都可以簡單的通過 globalThis 拿到頂層對象。

八、空值合併運算符

ES2020 新增了一個運算符 ??。當左側的操作數為 null 或者 undefined時,返回其右側操作數,否則返回左側操作數。

在之前我們經常會使用 || 操作符,但是使用 || 操作符,當左側的操作數為 0 、 null、 undefined、 NaN、 false、 ” 時,都會使用右側的操作數。如果使用 || 來為某些變量設置默認值,可能會遇到意料之外的行為。

?? 操作符可以規避以上問題,它只有在左操作數是 null 或者是 undefined 時,才會返回右側操作數。

const someValue = 0;
const defaultValue = 100;
let value = someValue ?? defaultValue; // someValue 為 0 ,value 的值是 0

九、可選鏈操作符

可選鏈操作符 ?. 允許讀取位於連接對象鏈深處的屬性的值,而不必明確驗證鏈中的每個引用是否有效。?. 操作符的功能類似於 . 鏈式操作符,不同之處在於,在引用為空(nullish, 即 null 或者 undefined) 的情況下不會引起錯誤,該表達式短路返回值是 undefined。

例如,我們要訪問 info 對象的 animal 的 reptile 的 tortoise。但是我們不確定 animal, reptile 是否存在,因此我們需要這樣寫:

const tortoise = info.animal && info.animal.reptile && info.animal.reptile.tortoise;

因為 null.reptile 或  undefined.reptile 會拋出錯誤:TypeError: Cannot read property ‘reptile’ of undefined 或 TypeError: Cannot read property ‘reptile’ of null,為了避免報錯,如果我們需要訪問的屬性更深,那麼這個這句代碼會越來越長。

有了可選鏈之後我們就可以簡化:

const tortoise = info.animal?.reptile?.tortoise;

可以看到可選鏈操作符 ?. 和空位合併操作符一樣,都是針對的 null 和 undefined 這兩個值。

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10198