jansen

jansen 查看完整档案

上海编辑常州工学院  |  软件工程 编辑平安好医生  |  前端 编辑填写个人主网站
编辑

学习不能止步,学习就是兴趣!终生学习是目标

个人动态

jansen 赞了文章 · 3月27日

傻傻分不清的Manifest

在前端,说到manifest,其实是有歧义的,就我了解的情况来说,manifest可以指代下列含义:

  1. html标签的manifest属性: 离线缓存(目前已被废弃)
  2. PWA: 将Web应用程序安装到设备的主屏幕
  3. webpack中webpack-manifest-plugin插件打包出来的manifest.json文件,用来生成一份资源清单,为后端渲染服务
  4. webpack中DLL打包时,输出的manifest.json文件,用来分析已经打包过的文件,优化打包速度和大小
  5. webpack中manifest运行时代码

下面我们来一一介绍下

html属性

<!DOCTYPE html>
<html lang="en" manifest="/tc.mymanifest">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" href="/theme.css">
  <script data-original="/main.js"></script>
  <script data-original="/main2.js"></script>
</head>
<body>
 
</body>

</html>

浏览器解析这段html标签时,就会去访问tc.mymanifest这个文件,这是一个缓存清单文件

tc.mymanifest

# v1 这是注释
CACHE MANIFEST
/theme.css
/main.js

NETWORK:
*

FALLBACK:
/html5/ /404.html

CACHE MANIFEST指定需要缓存的文件,第一次下载完成以后,文件都不会再从网络请求了,即使用户不是离线状态,除非tc.mymanifest更新了,缓存清单更新之后,才会再次下载。标记了manifest的html本身也被缓存

NETWORK指定非缓存文件,所有类似资源的请求都会绕过缓存,即使用户处于离线状态,也不会读缓存

FALLBACK指定了一个后备页面,当资源无法访问时,浏览器会使用该页面。
比如离线访问/html5/目录时,就会用本地的/404.html页面

缓存清单可以是任意后缀名,不过必须指定content-type属性为text/cache-manifest

那如何更新缓存?一般有以下几种方式:

  • 用户清空浏览器缓存
  • manifest 文件被修改(即使注释被修改)
  • 由程序来更新应用缓存

需要特别注意:用户第一次访问该网页,缓存文件之后,第二次进入该页面,发现tc.mymanifest缓存清单更新了,于是会重新下载缓存文件,但是,第二次进入显示的页面仍然执行的是旧文件,下载的新文件,只会在第三次进入该页面后执行!!!

如果希望用户立即看到新内容,需要js监听更新事件,重新加载页面

window.addEventListener('load', function (e) {

  window.applicationCache.addEventListener('updateready', function (e) {

    if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
      // 更新缓存
      // 重新加载
      window.applicationCache.swapCache();
      window.location.reload();

    } else {

    }

  }, false);

}, false);

建议对tc.mymanifest缓存清单设置永不缓存

不过,manifest也有很多缺点,比如需要手动一个个填写缓存的文件,更新文件之后需要二次刷新,如果更新的资源中有一个资源更新失败了,将导致全部更新失败,将用回上一版本的缓存

HTML5规范也废弃了这个属性,因此不建议使用

PWA

为了实现PWA应用添加至桌面的功能,除了要求站点支持HTTPS之外,还需要准备 manifest.json文件去配置应用的图标、名称等信息

<link rel="manifest" href="/manifest.json">
{ 
"name" : "Minimal PWA" , 
"short_name" : "PWA Demo" , 
"display" : "standalone" , 
"start_url" : "/" , 
"theme_color" : "#313131" , 
"background_color" : "#313131" , 
"icons" : [ 
  {
    "src": "images/touch/homescreen48.png",
    "sizes": "48x48",
    "type": "image/png"
  }
 ] 
}

通过一系列配置,就可以把一个PWA像APP一样,添加一个图标到手机屏幕上,点击图标即可打开站点

基于webpack的react开发环境

本文默认你已经了解最基本的webpack配置,如果完全不会,建议看下这篇文章

我们首先搭建一个最简单的基于webpack的react开发环境

源代码地址https://github.com/deepred5/l...

mkdir learn-dll
cd learn-dll

安装依赖

npm init -y
npm install @babel/polyfill react react-dom --save
npm install webpack webpack-cli webpack-dev-server @babel/core @babel/preset-env @babel/preset-react add-asset-html-webpack-plugin autoprefixer babel-loader clean-webpack-plugin css-loader html-webpack-plugin mini-css-extract-plugin node-sass postcss-loader sass-loader style-loader --save-dev

新建.bablerc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage", // 根据browserslis填写的浏览器,自动添加polyfill
        "corejs": 2,
      }
    ],
    "@babel/preset-react" // 编译react
  ],
  "plugins": []
}

新建postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer') // 根据browserslis填写的浏览器,自动添加css前缀
  ]
}

新建.browserslistrc

last 10 versions
ie >= 11
ios >= 9
android >= 6

新建webpack.dev.js(基本配置不再详细介绍)

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js',
    chunkFilename: '[name].chunk.js',
  },
  devServer: {
    historyApiFallback: true,
    overlay: true,
    port: 9001,
    open: true,
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },
      {
        test: /\.css$/,
        use: ['style-loader',
          'css-loader',
          'postcss-loader'
        ],
      },
      {
        test: /\.scss$/,
        use: ['style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: false,
              importLoaders: 2
            }
          },
          'sass-loader',
          'postcss-loader'
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }), // index打包模板
  ]
}

新建src目录,并新建src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>learn dll</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

新建src/Home.js

import React from 'react';
import './Home.scss';

export default () => <div className="home">home</div>

新建src/Home.scss

.home {
  color: red;
}

新建src/index.js

import React, { Component } from 'react';
import ReactDom from 'react-dom';
import Home from './Home';

class Demo extends Component {
  render() {
    return (
      <Home />
    )
  }
}

ReactDom.render(<Demo/>, document.getElementById('app'));

修改package.json

"scripts": {
  "dev": "webpack-dev-server --config webpack.dev.js"
},

最后,运行npm run dev,应该可以看见效果

新建webpack.prod.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');


module.exports = {
  mode: 'production',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, // 单独提取css文件
          'css-loader',
          'postcss-loader'
        ],
      },
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader,
        {
          loader: 'css-loader',
          options: {
            modules: false,
            importLoaders: 2
          }
        },
          'sass-loader',
          'postcss-loader'
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css',
      chunkFilename: '[id].[contenthash:8].css',
    }),
    new CleanWebpackPlugin(), // 打包前先删除之前的dist目录
  ]
};

修改package.json,添加一句"build": "webpack --config webpack.prod.js"

运行npm run build,可以看见打包出来的dist目录

html,js,css都单独分离出来了

至此,一个基于webpack的react环境搭建完成

webpack-manifest-plugin

通常情况下,我们打包出来的js,css都是带上版本号的,通过HtmlWebpackPlugin可以自动帮我们在index.html里面加上带版本号的js和css

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>learn dll</title>
<link href="main.198b3634.css" rel="stylesheet"></head>
<body>
  <div id="app"></div>
<script type="text/javascript" data-original="main.d312f172.js"></script></body>
</html>

但是在某些情况,index.html模板由后端渲染,那么我们就需要一份打包清单,知道打包后的文件对应的真正路径

安装插件webpack-manifest-plugin

npm i webpack-manifest-plugin -D

修改webpack.prod.js

const ManifestPlugin = require('webpack-manifest-plugin');
module.exports = {
    // ...
    plugins: [
      new ManifestPlugin()
    ]
};

重新打包,可以看见dist目录新生成了一个manifest.json

{
  "main.css": "main.198b3634.css",
  "main.js": "main.d312f172.js",
  "index.html": "index.html"
}

比如在SSR开发时,前端打包后,node后端就可以通过这个json数据,返回正确资源路径的html模板

const buildPath = require('./dist/manifest.json');

res.send(`
  <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>ssr</title>
<link href="${buildPath['main.css']}" rel="stylesheet"></head>
<body>
  <div id="app"></div>
<script type="text/javascript" data-original="${buildPath['main.js']}"></script></body>
</html>
`);

代码分割

我们之前的打包方式,有一个缺点,就是把业务代码和库代码都统统打到了一个main.js里面。每次业务代码改动后,main.js的hash值就变了,导致客户端又要重新下载一遍main.js,但是里面的库代码其实是没改变的!

通常情况下,reactreact-dom之类的库,都是不经常改动的。我们希望单独把这些库代码提取出来,生成一个vendor.js,这样每次改动代码,只是下载main.jsvendor.js可以充分缓存(也就是所谓的代码分割code splitting)

webpack4自带代码分割功能,只要配置:

optimization: {
  splitChunks: {
    chunks: 'all'
  }
}

webpack.prod.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');

module.exports = {
  mode: 'production',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ],
      },
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader,
        {
          loader: 'css-loader',
          options: {
            modules: false,
            importLoaders: 2
          }
        },
          'sass-loader',
          'postcss-loader'
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css',
      chunkFilename: '[id].[contenthash:8].css',
    }),
    new CleanWebpackPlugin(),
    new ManifestPlugin()
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

重新打包,发现新生成了一个vendor.js文件,公用的一些代码就被打包进去了

重新修改src/Home.js,然后打包,你会发现vendor.js的hash没有改变,这也是我们希望的

DLL打包

上面的打包方式,随着项目的复杂度上升后,打包速度会开始变慢。原因是,每次打包,webpack都要分析哪些是公用库,然后把他打包到vendor.js

我们可不可以在第一次构建vendor.js以后,下次打包,就直接跳过那些被打包到vendor.js里的代码呢?这样打包速度可以明显提升

这就需要DllPlugin结合DllRefrencePlugin插件的运用

dll打包原理就是:

  1. 把指定的库代码打包到一个dll.js,同时生成一份对应的manifest.json文件
  2. webpack打包时,读取manifest.json,知道哪些代码可以直接忽略,从而提高构建速度

我们新建一个webpack.dll.js

const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: {
    vendors: ['react', 'react-dom'] // 手动指定打包哪些库
  },
  output: {
    filename: '[name].[hash:8].dll.js',
    path: path.resolve(__dirname, './dll'),
    library: '[name]'
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      path: path.join(__dirname, './dll/[name].manifest.json'), // 生成对应的manifest.json,给webpack打包用
      name: '[name]',
    }),
  ],
}

添加一条命令:

"build:dll": "webpack --config webpack.dll.js"

运行dll打包

npm run build:dll

发现生成一个dll目录

修改webpack.prod.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ],
      },
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader,
        {
          loader: 'css-loader',
          options: {
            modules: false,
            importLoaders: 2
          }
        },
          'sass-loader',
          'postcss-loader'
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css',
      chunkFilename: '[id].[contenthash:8].css',
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/vendors.manifest.json') // 读取dll打包后的manifest.json,分析哪些代码跳过
    }),
    new CleanWebpackPlugin(),
    new ManifestPlugin()
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

重新npm run build,发现dist目录里,vendor.js没有了

这是因为react,react-dom已经打包到dll.js里了,webpack读取manifest.json之后,知道可以忽略这些代码,于是就没有再打包了

但这里还有个问题,打包后的index.html还需要添加dll.js文件,这就需要add-asset-html-webpack-plugin插件

npm i add-asset-html-webpack-plugin -D

修改webpack.prod.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const webpack = require('webpack');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ],
      },
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader,
        {
          loader: 'css-loader',
          options: {
            modules: false,
            importLoaders: 2
          }
        },
          'sass-loader',
          'postcss-loader'
        ],
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
    new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, './dll/*.dll.js') }), // 把dll.js加进index.html里,并且拷贝文件到dist目录
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css',
      chunkFilename: '[id].[contenthash:8].css',
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/vendors.manifest.json') // 读取dll打包后的manifest.json,分析哪些代码跳过
    }),
    new CleanWebpackPlugin(),
    new ManifestPlugin()
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

重新npm run build,可以看见dll.js也被打包进dist目录了,同时index.html也正确引用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>learn dll</title>
<link href="main.198b3634.css" rel="stylesheet"></head>
<body>
  <div id="app"></div>
<script type="text/javascript" data-original="vendors.8ec3d1ea.dll.js"></script><script type="text/javascript" data-original="main.0bc9c924.js"></script></body>
</html>

runtime

webpack中有运行时的概念,比如我们通过webpack打包后分割成了dll.js,vendors.js,main.js,那这三个代码,到底哪个先调用,哪个后调用,他们运行顺序就是由运行时代码组织(通过读取manifest数据)

通常情况下我们无需关心运行时代码,但如果希望尽可能的优化浏览器缓存,那么我们可以把运行时代码单独提取出来,这样某些文件发生改变后,一些与之相关的文件hash值并不会也随之改变。

通过配置runtimeChunk即可

optimization: {
    runtimeChunk: { name: 'manifest' }
}

小结

我们介绍了5种manifest相关的前端技术。manifest的英文含义是名单, 5种技术的确都是把manifest当做清单使用:

  1. 缓存清单
  2. PWA清单
  3. 打包资源路径清单
  4. dll打包清单
  5. 代码加载顺序清单

只不过是在不同的场景中使用特定的清单来完成某些功能

所以,学好英文是多么重要,这样才不会傻傻分不清manifest到底是干啥的!

查看原文

赞 28 收藏 18 评论 4

jansen 赞了文章 · 3月3日

移动web 1像素边框 瞧瞧大公司是怎么做的

前言

移动端web项目越来越多,要求也越来越高,好多设计师都发现了,你们前端实现的边线为什么是糊的,根本不是1像素,好吧,我只能找参考,要么征服设计,要么征服自己。

关于为什么设置的是1px,而显示出来却不是呢,这里我就不多做介绍了;
放出几个链接, 设备像素比devicePixelRatio简单介绍,还有 移动端高清、多屏适配方案 以及 iPhone 6 屏幕揭秘,相信大家看完这几个自己也就能想出解决的办法了。


哪些项目实现了

一般遇到问题,都是找一下成熟项目他们公司的代码看看,自己也翻看了好多关于移动端的知识点,特别推荐博客白色橡树腾讯移动知识库,有很多移动相关的知识,那我们先来找几个参考看看吧。

京东 首页边线几乎都为1像素边框
京东

携程
携程

大众点评
大众点评

糯米团
糯米团

翻看几个项目中发现,并不是所有的边线都是1像素,但是这些足够我们来参考了,如果电脑上的截图区分不出来,我们可以在自己的手机上查看对比一下,应该跟自己浏览器里导航栏或者工具条的1像素是一样的,而会出现模糊不清晰的状况。

实现方法

border-image 图片 实现

这篇文章是腾讯github上的解决方案border-image来实现的 链接走起 《使用border-image实现类似iOS7的1px底边》,缺点是,你需要制作图片,圆角的时候会出现模糊。

.border-image-1px {
    border-width: 1px 0px;
    -webkit-border-image: url("data:image/png;base64,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") 2 0 stretch;
}

background-image 渐变实现

除啦用图片,难道纯粹的css就不能实现吗?我的确不想使用图片,感觉制作起来很麻烦,其实百度糯米团首页就是这么做的但是这种方法有个缺点,就是不能实现圆角

.border {
      background-image:linear-gradient(180deg, red, red 50%, transparent 50%),
      linear-gradient(270deg, red, red 50%, transparent 50%),
      linear-gradient(0deg, red, red 50%, transparent 50%),
      linear-gradient(90deg, red, red 50%, transparent 50%);
      background-size: 100% 1px,1px 100% ,100% 1px, 1px 100%;
      background-repeat: no-repeat;
      background-position: top, right top,  bottom, left top;
      padding: 10px;
  }

viewport+rem实现

这篇文章的解决方案是使用viewport+rem+js来实现的 链接走起 《移动端1像素边框问题的解决方案》,里边还引入了张鑫旭大神的文章 《设备像素比devicePixelRatio简单介绍》,优点是可以直接设置1px就行了,剩下的就交给js了,而且圆角什么的都没问题。

<span style="font-size:18px;"><html>  
  
    <head>  
        <title>1px question</title>  
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">  
        <meta name="viewport" id="WebViewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">       
        <style>  
            html {  
                font-size: 1px;  
            }             
            * {  
                padding: 0;  
                margin: 0;  
            }  
              
            .bds_b {  
                border-bottom: 1px solid #ccc;  
            }  
              
            .a,  
            .b {  
                margin-top: 1rem;  
                padding: 1rem;                
                font-size: 1.4rem;  
            }  
              
            .a {  
                width: 30rem;  
            }  
              
            .b {  
                background: #f5f5f5;  
                width: 20rem;  
            }  
        </style>  
        <script>  
          
            var viewport = document.querySelector("meta[name=viewport]");  
            //下面是根据设备像素设置viewport  
            if (window.devicePixelRatio == 1) {  
                viewport.setAttribute('content', 'width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no');  
            }  
            if (window.devicePixelRatio == 2) {  
                viewport.setAttribute('content', 'width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no');  
            }  
            if (window.devicePixelRatio == 3) {  
                viewport.setAttribute('content', 'width=device-width,initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no');  
            }  
            var docEl = document.documentElement;  
            var fontsize = 10 * (docEl.clientWidth / 320) + 'px';  
            docEl.style.fontSize = fontsize;  
              
        </script>  
    </head>  
  
    <body>  
        <div class="bds_b a">下面的底边宽度是虚拟1像素的</div>  
        <div class="b">上面的边框宽度是虚拟1像素的</div>  
    </body>  
  
</html></span>  

box-shadow 实现

利用阴影我们也可以实现,那么我们来看看阴影,优点是圆角不是问题,缺点是颜色不好控制。

div{
    -webkit-box-shadow:0 1px 1px -1px rgba(0, 0, 0, 0.5);
}

transform: scale(0.5) 实现 推荐相当灵活

其实我们刚才列举了那么多例子,无非就是把1px缩放都0.5px的状态下,而0.5px并不是所有都支持,再根据媒体查询设置不同的缩放比例就可以了,那么我们就开始玩儿缩放吧。

1.用height:1px的div,然后根据媒体查询设置transform: scaleY(0.5);

div{
    height:1px;
    background:#000;
    -webkit-transform: scaleY(0.5);
    -webkit-transform-origin:0 0;
    overflow: hidden;
}

2.用::after::befor,设置border-bottom:1px solid #000,然后在缩放-webkit-transform: scaleY(0.5);可以实现两根边线的需求

div::after{
    content:'';width:100%;
    border-bottom:1px solid #000;
    transform: scaleY(0.5);}

3.用::after设置border:1px solid #000; width:200%; height:200%,然后再缩放scaleY(0.5); 优点可以实现圆角京东就是这么实现的,缺点是按钮添加active比较麻烦。

.div::after {
    content: '';
    width: 200%;
    height: 200%;
    position: absolute;
    top: 0;
    left: 0;
    border: 1px solid #bfbfbf;
    border-radius: 4px;
    -webkit-transform: scale(0.5,0.5);
    transform: scale(0.5,0.5);
    -webkit-transform-origin: top left;
}
查看原文

赞 54 收藏 97 评论 0

jansen 赞了文章 · 3月3日

Promise | 手写原理(代码版)

文 / 景朝霞
来源公号 / 朝霞的光影笔记
ID / zhaoxiajingjing
❥❥❥❥点个赞,让我知道你来过~❥❥❥❥

Promise的好处

Promise 可以解决的问题

  1. 把你从回调地狱中解救出来
  2. 让你优雅的捕获错误
  3. 为你分担异步并发的难题
// 此处使用node举例,不会不要紧,先混个脸熟。再见就不陌生了呀
let fs = require('fs');
// 异步读取文件
fs.readFile('./name', function (err, data){
    if(err){}
    fs.readFile(data, function (err, address){
        if(err){}
        fs.readFile(address, function (err, product){
            // 1)深陷在回调地域中不能抽身
            if(err){
                // 2)捕获错误。OMG,我哪里错了?!?!告诉我,肯定改~
            }
        });
    });
});

// 3) 你的名字、你的地址。都告诉我,惊喜才会送到你面前呀~
fs.readFile('./name', function (err, data){});
fs.readFile('./address', function (err, data){});

Promise 使用的例子

状态变化

  • Promise 有3种状态
  • pending 等待态
  • fulfilled 成功态
  • rejected 失败态

【等待态 -> 成功态】 or 【等待态 -> 失败态】二选一,你来定。

图片描述

  • Promise 是个类,接收一个函数参数 executor 执行器,一上来就执行了。这里是同步的。
  • 每一个Promise的实例上都有一个then方法。是基于回调实现的。
console.log('一封情书');

let p = new Promise((resolve, reject)=>{
    console.log('executor请说出你的选择:');
    resolve('你中意我~(*^▽^*)');
    reject('你发了好人卡(╥﹏╥)o');
});

p.then((value)=>{
    console.log('成功态', value);
}, (reason) => {
    console.log('失败态', reason);
});

console.log('纸短情长');

图片描述

链式调用

console.log('-----一封情书-----');

let p = new Promise((resolve, reject) => {
    console.log('executor 请说出你的选择:');
    resolve('你中意我~(*^▽^*)');
    reject('你发了好人卡(╥﹏╥)o');
});

p.then((value) => {
    console.log('成功态---', value);
}, (reason) => {
    console.log('失败态---', reason);
}).then((value) => {
    console.log('---爱你一万年~(*^▽^*)');
}, (reason) => { 
    console.log('---伤心总是难免的o(╥﹏╥)o');
});
console.log('~~~纸短情长~~~');

图片描述

异步请求

  • name.txt
zhaoxiajingjing
  • 3.js
let fs = require('fs');
let p = new Promise((resolve, reject)=>{
    fs.readFile('./name.txt', 'utf8', function (err, data){
        if(err){
            return reject(err);
        }
        resolve(data);
    });
});
p.then((value)=>{
    console.log('成功了', value);
}, (reason)=>{
    console.log('失败了', reason);
});

图片描述

复杂的使用 Promise

下面的例子输出什么呢?

let fs = require('fs');

function read(filePath) {
    return new Promise((resolve, reject) => {
        fs.readFile(filePath, 'utf8',function(err, data){
            if(err) {
                return reject(err);
            }
            resolve(data);
        });
    });
}

read('./name.txt')
// then-1
.then(function (data) {
    console.log('data①', data);
    return new Promise((resolve, reject) => {
        reject('错误了');
    });
})
// then-2
.then((data) => {
    console.log('data②', data);
}, err => {
    console.log('err②', err);
})
// then-3
.then((data) => {
    console.log('data③', data);
}, (err) => {
    console.log('err③', err);
});

图片描述

OK,提炼一下重点:

  1. 有三个状态。怎么变化的?
  2. executor。怎么执行的?
  3. then方法,成功态和失败态的回调。
  4. then方法的链式调用。
  5. Promise 处理异步。

实现以上内容

Promise 基本实现

  • new Promise 会传一个函数作为参数。这个函数有两个参数:resolve 成功 reject 失败,都是用于改变状态的。都在实例上,一个 Promise 一生只改变一次状态
  • 每个实例上都有一个 then 方法,异步的。对于成功态和失败态的回调:onFulfilled onRejected

代码1,传送门~

const PENDING = 'pending';
const SUCCESS = 'fulfilled';
const FAIL = 'rejected';

class Promise{
    constructor(execuotr){
        const status = PENDING; // 我等着你给答案~
        this.value;
        this.reason;

        let resolve = (value)=>{
            if(status === PENDING) {
                this.status = SUCCESS; // 你中意我~
                this.value = value;
            }
        };

        let reject = (reason)=>{
            if(status === PENDING) {
                this.status = FAIL; // 你发了好人卡
                this.reason = reason;
            }
        };

        // 是同步的哦~
        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
    then(onFulfilled, onRejected){
        if(this.status === SUCCESS) {
            onFulfilled(this.value);  // 爱你一万年~
        }
        if(this.status === FAIL) {
            onRejected(this.reason); // 伤心总是难免的
        }
    }
}

Pomise 解决异步问题

Promise 是个容器,里面可以放一些异步的请求,请求成功了走成功态,请求失败了走失败态。当然,你要反过来走也可以哒~

代码2,传送门~

const PENDING = 'pending';
const SUCCESS = 'fulfilled';
const FAIL = 'rejected';

class Promise {
    constructor(executor){
        this.status = PENDING;
        this.value;
        this.reason;

        // 用来存储 订阅的内容的
        this.onSuccessCallbacks = [];
        this.onFailCallbacks = [];

        let resolve = (value)=>{
            if(this.status === PENDING) {
                this.status = SUCCESS;
                this.value = value;
                this.onSuccessCallbacks.forEach(fn => fn());
            }
        };
        let reject = (reason)=>{
            if(this.status === PENDING) {
                this.status = FAIL;
                this.reason = reason;
                this.onFailCallbacks.forEach(fn => fn());
            }
        };

        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
    then(onFulfilled, onRejected){
        if(this.status === SUCCESS){
            onFulfilled(this.value);
        }
        if(this.status === FAIL){
            onRejected(this.reason);
        }
        // 当Promise里面有异步请求控制状态改变时,会先走到then方法里面
        if(this.status === PENDING) {
            this.onSuccessCallbacks.push(()=>{
                onFulfilled(this.value);
            });
            this.onFailCallbacks.push(()=>{
                onRejected(this.reason);
            });
        }
    }
}

Promise 里面有异步请求时候,会先走到 then方法里面了。此时,需要把成功态回调和失败态回调先存储起来,等到异步请求回来以后变更了状态,再触发执行。

Promise 的链式

then 方法 返回一个新的 Promise

Promise 一生只能改变一次状态。那么,Promise 的链式调用then方法,说明每次都会返回一个新的Promise

代码3,传送门~

const SUCCESS = 'fulfilled';
const FAIL = 'rejected';
const PENDING = 'pending';

class Promise {
    constructor(executor) {
       // ... executor 代码
    }
    then(onFulfilled, onRejected) {
        let promise2;

        promise2 = new Promise((resolve, reject)=>{
            if (this.status === SUCCESS) {
                try { // 用 try catch 捕获同步的报错
                    // 成功态的回调的返回值 x
                    // 【问题】 如果 x 是一个promise,那么需要取得x执行后的结果
                    let x = onFulfilled(this.value);
                    resolve(x);

                } catch (e) {
                    reject(e);
                }
            }
            if (this.status === FAIL) {
                try {
                    let x = onRejected(this.reason);
                    resolve(x);
                } catch (e) {
                    reject(e);
                }
            }
            if (this.status === PENDING) {
                this.onSuccessCallbacks.push(()=>{
                    try {
                        let x = onFulfilled(this.value);
                        resolve(x);
                    } catch (e) {
                        reject(e);
                    }
                });
                this.onFailCallbacks.push(()=>{
                    try {
                        let x = onRejected(this.reason);
                        resolve(x);
                    } catch (e) {
                        reject(e);
                    }
                });
            }
        });

        return promise2;
    }
}

解析 x

如何判断 x 是Promise,还是一个普通值?【参考规范 https://promisesaplus.com "Promise A+" 2.2.7】

代码4,传送门~

const SUCCESS = 'fulfilled';
const FAIL = 'rejected';
const PENDING = 'pending';

function resolvePromise(promise2, x, resolve, reject) {
    // 死循环了,容错
    if(promise2 === x) {
        return reject('TypeError: Chaining cycle detected for promise~~~~');
    }
    // 判断 x 类型
    if(typeof x === 'function' || (typeof x === 'object' && x != null)) {
        // 这个才有可能是 promise
        try {
            let then = x.then;
            if(typeof then === 'function') {
                // 此时,认为就是个promise
                // 如果promise是成功的,那么结果向下传递,如果失败了就传到下一个失败里面去
                then.call(x, y=>{
                    resolvePromise(promise2, y, resolve, reject);
                }, r => {
                    reject(r);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            reject(e);
        }
    } else {
        // x 肯定不是一个promise
        resolve(x);
    }
}

class Promise {
    constructor(executor) {
        // ... executor 代码
    }
    then(onFulfilled, onRejected) {
        let promise2;

        promise2 = new Promise((resolve, reject) => {
            if (this.status === SUCCESS) {
                // 用定时器模拟此时promise2已经能获取到了
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            }
            // 其他情况同理,先以一个为例说明
        });
        
        return promise2;
    }
}

严谨度

Promise 给你的承诺,一句情话:一个 Promise 一生只改变一次状态

代码5,传送门~

const SUCCESS = 'fulfilled';
const FAIL = 'rejected';
const PENDING = 'pending';

function resolvePromise(promise2, x, resolve, reject) {
    // 死循环了,容错
    if(promise2 === x) {
        return reject('TypeError: Chaining cycle detected for promise~~~~');
    }
    let called;
    if(typeof x === 'function' || (typeof x === 'object' && x != null)) {
        try {
            let then = x.then;
            if(typeof then === 'function') {
                then.call(x, y=>{
                    if(called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, r => {
                    if(called) return;
                    called = true;
                    reject(r);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if(called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

class Promise {
    constructor(executor) {
        // .... executor 的代码
    }
    then(onFulfilled, onRejected) {
        let promise2;

        promise2 = new Promise((resolve, reject) => {
            if (this.status === SUCCESS) {
                // 用定时器模拟此时promise2已经能获取到了
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            }
            // 其他情况同理,先以一个为例说明
        });

        return promise2;
    }
}

值的穿透

let p = new Promise((resolve, reject)=>{
    resolve(1000);
});
p.then().then().then(data => {
    console.log(data);
});

值可以传过去

const SUCCESS = 'fulfilled';
const FAIL = 'rejected';
const PENDING = 'pending';

function resolvePromise(promise2, x, resolve, reject) {
    // ... 判断 x 的值
}

class Promise {
    constructor(executor) {
        // ... executor 代码
    }
    then(onFulfilled, onRejected) {
        // 值穿透
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
        onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err};
        // ... promise2 的判断
    }
}

测试

  • 测试这个库是否符合我们的promise A+ 规范
  • promises-aplus-tests 用来测试当前的库是否符合规范
  • npm i promises-aplus-tests -g
  • promises-aplus-tests 文件名

可测试代码,传送门~

Promise A+ 规范

Promise 是一个构造函数,是个类。默认高版本浏览器,node都自带了。不用考虑兼容性,放心大胆的使用吧!如果真不兼容,那就用es6-promise包自己是一套吧~

Promise A+

https://promisesaplus.com

容错

上面的内容,还需要一部分容错。就是当executor 里面的有一个promise的时候,执行的结果。

let Promise = require('./promise.js');
let p = new Promise((resolve, reject)=>{
    resolve(new Promise((resolve, reject)=>{
        reject(404);
    }));
});
p.then(value => console.log(1, value), reason => console.log(2, reason));

// 输出:
// 2 404

在同步执行时,resolve 的value是一个Promise,那么需要等它的结果。

const SUCCESS = 'fulfilled';
const FAIL = 'rejected';
const PENDING = 'pending';

function resolvePromise(promise2, x, resolve, reject) {
    // ... 校验x
}

class Promise {
    constructor(executor) {
        // ... code

        let resolve = (value) => {
            if (value instanceof Promise) {
                return value.then(resolve, reject);
            }

            if (this.status === PENDING) {
                this.status = SUCCESS;
                this.value = value;
                this.onSuccessCallbacks.forEach(fn => fn());
            }
        };
        // ... code
    }
    then(onFulfilled, onRejected) {
        // ... then 方法
    }
}

module.exports = Promise;

交流

公号首发:朝霞的光影笔记
在这里,一起成长~

查看原文

赞 4 收藏 2 评论 2

jansen 发布了文章 · 2019-09-17

观察者们

Intersection Observer

Intersection Observer API提供了一种异步观察目标元素与祖先元素或顶级文档viewport的“交集"中的变化的方法。

兼容性

clipboard.png

介绍

一直以来,检测元素的可视状态或者两个元素的相对可视状态都不是件容易事,毕竟大部分解决办法并非完全可靠,也极易拖慢整个网站的性能。然而,随着网页发展,对上述检测的需求也随之增加了。多种情况下都需要用到元素交集变化的信息,比如:

  • 当页面滚动时,懒加载图片或其他内容。
  • 实现“可无限滚动”网站,也就是当用户滚动网页时直接加载更多内容,无需翻页。
  • 为计算广告收益,检测其广告元素的曝光情况。
  • 根据用户是否已滚动到相应区域来灵活开始执行任务或动画。

以往我们的做法是绑定容器的scroll事件,或者设定时器不停地调用getBoundingClientRect() 获取元素位置,但是这些代码都是在主线程上运行。所以这样做的性能会有一定的影响。

我们现在的mall-core里的imglazyload就是用的传统的方式

API

var observer = new IntersectionObserver(callback, options)

以上代码会返回一个IntersectionObserver实例,callback是当元素的可见性变化时候的回调函数,options是一些配置项(可选)

返回的这个实例呢,也比较简单,有三个方法。

  • observe 观察某个元素 observer.observe(ele)
  • unobserve 停止观察某个元素 observer.unobserve(ele)
  • disconnect 关闭观察器 observer.disconnect()

Intersection observer options
传递到IntersectionObserver()构造函数的 options 对象,允许控制调用观察者的回调的环境。它也是有3个字段

  • root
    指定根(root)元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为null,则默认为浏览器视窗。用作父级元素的时候,取值为父级元素的getboundingClientRect()
  • rootMargin
    root元素的外边距。类似于css中的 margin 属性,比如 "10px 20px 30px 40px" (top, right, bottom, left)。如果有指定root参数,则rootMargin也可以使用百分比来取值。该属性值是用作root元素和target发生交集时候的计算交集的区域范围,使用该属性可以控制root元素每一边的收缩或者扩张。默认值为0。
    用图来解释

clipboard.png

通过该图就可以知道,原本被观察的元素在通过rootMargin扩大后,会提前触发callback
  • threshold
    用来指定交叉比例,决定什么时候触发回调函数,是一个数组,默认是0,

设置为 [0, 0.5, 1] 就是指当元素出现0%、50%、100%时都会触发callback

callback
当元素的可见性发生变化时,就会触发callback函数。

function callback(entries, observer) {
    // 回调接受两个参数,一个是IntersectionObserverEntry数组,一个是obsever自己
    for (var i = 0; i < entries.length; i++) {
            console.log(entries[i]);
    }
}
  • boundingClientRect 目标元素的矩形信息
  • intersectionRatio 相交区域和目标元素的比例值
  • intersectionRect/boundingClientRect 不可见时小于等于0
  • intersectionRect 目标元素和视窗(根)相交的矩形信息 可以称为相交区域
  • isIntersecting 目标元素当前是否可见 Boolean值 可见为true
  • isvisible 感觉一直都是false,官方也没有介绍
  • rootBounds 根元素的矩形信息,没有指定根元素就是当前视窗的矩形信息
  • target 观察的目标元素
  • time 返回一个记录从IntersectionObserver的时间到交叉被触发的时间的时间戳

请留意,你注册的回调函数将会在主线程中被执行。所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用 Window.requestIdleCallback() 方法。

需要注意的是
由于观察名叫交叉,所以第一思维会是和边相交,很容易理解为元素和根元素的边相交,实际上这里的触发方式并不是说一定就是和根元素的边相交,而是元素的可见性出现在根元素整体视窗内就算相交。

接下来,就用这个来做一个简易的懒加载模块

核心代码

import { useEffect } from 'react';
function loadImg(entries, observer) {
    for (var i = 0; i < entries.length; i++) {
        const img = entries[i].target;
        var src = img.getAttribute("data-src");
        console.log(entries[i]);
        if (entries[i].isIntersecting) {
            img.src = src;
            
            img.removeAttribute("data-src");
            img.classList.remove('imglazy');
            observer.unobserve(img);
            // 实验0.5,1 解开
        }
    }
}
function observerImgs(className, observer) {
    const imgs = document.querySelectorAll(`img.${className}`);
    if(!imgs.length) {
        return;
    }
    imgs.forEach((img) => {
        observer.unobserve(img);
        observer.observe(img);
    });
}
function useImgLazy(className, list) {
    useEffect(() => {
        const observer = new IntersectionObserver(loadImg,{
            // root: document.querySelector('.product_list'),
            // rootMargin: '0px',
            threshold: [0.5, 1]
        });
        observerImgs(className, observer);
        return () => {
            observer.disconnect();
        };
    }, [className, list]);
}
export default useImgLazy;

为了观察更直观,所以demo是用的0.5露出才变更src

clipboard.png

MutationObserver

MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分。

兼容性

clipboard.png

介绍

MutationObserver构造函数只有一个callback参数,callback和上面的类似,一个是被改动的MutationRecord数组,一个是观察对象。

实例拥有3个方法

  • disconnect()

阻止 MutationObserver 实例继续接收的通知,直到再次调用其observe()方法,该观察者对象包含的回调函数都不会再被调用。

  • observe()

配置MutationObserver在DOM更改匹配给定选项时,通过其回调函数开始接收通知。

  • takeRecords()

MutationObserver的通知队列中删除所有待处理的通知,并将它们返回到MutationRecord对象的新Array中,什么是待处理呢,因为这里的所有操作都是异步的,takeRecords 立刻执行。

observe 方法需要提供两个参数

  • target

DOM树中的一个要观察变化的DOM Node (可能是一个Element) , 或者是被观察的子节点树的根节点。

  • options 可选

一个可选的MutationObserverInit 对象,此对象的配置项描述了DOM的哪些变化应该提供给当前观察者的callback。当监听的时候,里面的属性至少有一个为true,否则会抛出异常。

mutationObserver.observe(content, {
    attributes: true, // Boolean - 观察目标属性的改变
    characterData: true, // Boolean - 目标节点或子节点树中节点所包含的字符数据的变化
    childList: true, // Boolean - 目标节点(如果subtree为true,则包含子孙节点)添加或删除新的子节点。默认值为false。
    subtree: true, // Boolean - 目标以及目标的后代改变都会观察,就是如果这个值为true,其他属性为true后就会都包含子节点。
    attributeOldValue: true, // Boolean - 表示需要记录改变前的目标属性值
    characterDataOldValue: true, // Boolean - 设置了characterDataOldValue可以省略characterData设置
    // attributeFilter: ['src', 'class'] // Array - 观察指定属性
});

mutationRecord数组里的属性有

MutationRecord = {
  type:如果是属性变化,返回"attributes",如果是一个CharacterData节点(Text节点、Comment节点)变化,返回"characterData",节点树变化返回"childList"
  target:返回影响改变的节点
  addedNodes:返回添加的节点列表
  removedNodes:返回删除的节点列表
  previousSibling:返回分别添加或删除的节点的上一个兄弟节点,否则返回null
  nextSibling:返回分别添加或删除的节点的下一个兄弟节点,否则返回null
  attributeName:返回已更改属性的本地名称,否则返回null
  attributeNamespace:返回已更改属性的名称空间,否则返回null
  oldValue:返回值取决于type。对于"attributes",它是更改之前的属性的值。对于"characterData",它是改变之前节点的数据。对于"childList",它是null
}

主要知道了哪些元素哪些属性发生了某些变化后,可以针对性的对某个元素做一些操作。
演示demo

应用

  • 监听JS脚本创建的DOM渲染完成
  • 监听图片/富文本编辑器/节点内容变化及处理
  • 关于vue对于MutationObserver的应用

PerformanceObserver()

PerformanceObserver 用于监测性能度量事件,在浏览器的性能时间轴记录下一个新的 performance entries 的时候将会被通知 。
它会实时的根据每个资源的加载来通知。

使用方法

    const observer = new PerformanceObserver(performanceCallBack);
    observer.observe({entryTypes: ['paint', 'resource']});  
    observer.disconnect();
    observer.takeRecords();

其中,callback里第一个参数是PerformanceObserverEntryList,有一个getEntries方法,可以取得监控的list,第二个参数是observer对象。

说到这个吧,就得说
Performance.timingperformance.getEntries()

对比如下
1:在window.onload函数里面我们进行loadEventEnd的取值会取不到,而在PerformanceObserver则不存在这样的问题;
2:使用PerformanceObserver我们发现没有navigationStart,domLoading的值。
3:PerformanceObserver更精确。

【Prompt for unload】- 用户跳转行为(在地址栏输入url后按回车,或者点击a标签跳转等)
 navigationStart、startTime // 当前浏览器窗口的前一个网页关闭开始执行的时间戳
 unloadStart // 前一个页面unload触发开始时间戳
【unload】- 前一个页面unload时间
 unloadEnd // 前一个页面unload触发结束时间戳
 redirectStart // 返回第一个HTTP跳转开始时的时间戳如果没有跳转,或者不是同一个域名内部的跳转,则返回值为0
【redirect】- 重定向
 redirectEnd // 返回最后一个HTTP跳转结束时(即跳转回应的最后一个字节接受完成时)的时间戳,如果没有跳转,或者不是同一个域名内部的跳转,则返回值为0
 fetchStart // 返回浏览器准备使用HTTP请求读取文档时的时间戳。该事件在网页查询本地缓存之前发生
【App cache】- 网页查询本地缓存
 domainLookupStart // 返回域名查询开始时的时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值
【DNS】- 域名查询
 domainLookupEnd // 返回域名查询结束时的时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值
 connectStart // 返回建立TCP链接开始向服务器发送时的时间戳。如果使用持久连接(persistent connection),则返回值等同于fetchStart属性的值
【TCP】
 secureConnectionStart // 它的值是安全连接握手之前的时刻。如果该属性不可用,则返回undefined。如果该属性可用,但没有使用HTTPS,则返回0
 connectEnd // 返回浏览器与服务器之间的连接建立时的时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束
回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0
 requestStart // 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳
【Request】 - 网络请求
 responseStart // 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳
【Response】
 responseEnd // 返回浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳
 domLoading // 返回当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的readystatechange事件触发时)的时间戳
【Processing】
 domInteractive // 返回当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳
 domContentLoadedEventStart // 返回当前网页DOMContentLoaded事件发生时(即DOM结构解析完毕、所有脚本开始运行时)的时间戳
 domContentLoadedEventEnd // 返回当前网页所有需要执行的脚本执行完成时的时间戳
 domComplete // 返回当前网页DOM结构生成时(即Document.readyState属性变为“complete”,以及相应的readystatechange事件发生时)的时间戳
 loadEventStart // 返回当前网页load事件的回调函数开始时的时间戳。如果该事件还没有发生,返回0
【onLoad】- window.onLoad触发
 loadEventEnd // 返回当前网页load事件的回调函数运行结束时的时间戳。如果该事件还没有发生,返回0。通过while循环持续判断直到loadEventEnd>0则表示完全加载完毕了!网络不再有任何数据请求、dom也渲染完毕了

demo;

前端性能中,有一些比较重要的指标,比如
白屏时间
首屏时间

那白屏时间其实就是用我们的页面第一个内容渲染出来时的时间,就是白屏时间,js、css的加载都是会阻塞页面的渲染的。

首屏时间的话的话有多种方式计算,标准也不一样,这里不多描述了。

延伸。优化首页性能~~~

查看原文

赞 0 收藏 0 评论 0

jansen 回答了问题 · 2018-05-22

解决js 关于两个关联数组去重的问题?

看了楼主给别人评论真是哭笑不得,难道你拿到的初始数据就是两个数组吗?不是你自己拆的吗?

var objArr = [{id:'id1',name:'x'},{id:'id2',name:'y'},
{id:'id3',name:'z'},{id:'id1',name:'x'}];
var myMap = new Map();
objArr.forEach((item)=>{
    myMap.set(item.id,item.name);
})
var arr1 = [...myMap.keys()];
var arr2 = [...myMap.values()];

关注 5 回答 4

jansen 赞了回答 · 2018-05-22

解决请问es7的一个 async/await, await为什么有的时候是可以省略的?

  1. 首先,awati后面应该跟Promise实例。
  2. 其次,async函数返回的是Promise实例。

具体到楼主的例子,fun1fun2前面的async是多余的,因为 fun1() => fun2() => fun3() 已经返回了Promise实例。

可以改成下面的代码。

function fun1() {
    return fun2(); // 这里的 await 为什么可以省略?加上await结果也是一样的
}

function fun2() {
    return fun3(); // 这里的 await 为什么可以省略?加上await结果也是一样的
}

function fun3() {
    return new Promise(resolve => {
        console.log('fun3');
        resolve('fufff 3 ret');
    });
}

async function testfun1() {
    const ret = await fun1();
    console.log(ret);
}

testfun1()

对比下面的例子,可能更直观

function bar () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('bar');
        }, 1000);
    });
}

async function foo () {
    let ret1 = bar();
    let ret2 = await bar();
    console.log(ret1); // Promise { 'bar' }
    console.log(ret2); // bar
}

foo();

关注 2 回答 1

jansen 发布了文章 · 2018-05-16

javascript 内存泄漏

什么是内存泄漏

简介

CPU,内存,硬盘的关系

CPU(Central Processing Unit)工作的时候:
  1、需要从存储器里取数据出来。
  2、进行运算,要不停地用存储器读写。
  3、计算出结果再返回到存储器里。
举例子形容关系
图片描述
我们的PC的APP,手机的APP都是跑在内存上的。
程序的运行需要内存。只要程序提出要求,操作系统就必须供给内存。

那么什么是内存呢?

图片描述

内存就是处于外存和CPU之间的桥梁,用于存储CPU的运算数据,这样内存就可以保持记忆功能,你写的所有代码,都是需要在内存上跑的,虚拟内存是从外存上分配的,很慢
内存的频率(mhz)越高代表内存运算更快,同一块内存我跑的更快哟,这就是为什么DDR5比DDR3快的原因
说这个的原因,就是如果我的计算机性能足够好的话,内存泄漏带来的问题就会越来越小。

那么什么是内存溢出呢?

out of memory

内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,就会出现内存溢出。

在手机上,比如任何一个app,系统初始的时候可能只会给你分配100m的内存,如果有android studio的话,可以在log上看到,这个时候你点击了某个图片列表页(为什么用图片举例,是因为图片特有的情况,图片本身如果是20Kb,长宽为300的话,渲染到手机上由于图片采用的ARGB-888色彩格式,每个像素点占用4个字节(双通道),这样图片实际占用内存就是3003004/1024/1024 = 300+k dpi为1的情况),这个时候内存就会暴涨,一旦接近临界值,程序就会去找操作系统说,我内存不够了,再给我点,系统就会又给你分配一段,完了你返回首页了,但是因为你的代码写的有问题,暴露各种全局对象啊,各种监听啊,一进一出多次,但是系统给每个app分配的内存是有上限的,直到内存不够分,泄漏导致的内存溢出。然后crash掉。以前我写rn的时候,早期的scrollview性能堪忧,出现过内存溢出的现象。

内存泄漏

memory leak

内存泄漏指的是你申请了一块内存,在使用后无法释放已申请的内存空间,比如程序会认为你可能会用到这个变量,就一直给你留着不释放,一次内存泄漏可以被忽略,但是内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

既然内存我可以申请,就可以被系统回收,在C语言中,需要程序员手动malloc去申请内存,然后free掉它,这写起来很麻烦,所以其他大多数语言都提供了自动回收的机制,那么既然自动回收了,就很容易出现各种问题。

内存泄漏的后果

通常来说问题并不是特别大,因为正常一个进程的生命周期有限,在当下的大内存快cpu的手机下,影响有限,不过还是要列举一些情况。
1:安卓手机内存管理不好,导致只要不重启,时间越长,可用内存越少,即使杀程序。具体缘由可能还和安卓开放过多权限导致无良app各种保持后台后门运行也有一定关系。
2:导致内存溢出,如果手机内存被挤占的有限,那么手机会变卡,严重的自己crash掉,如果是pc端,浏览器的内存泄漏导致的溢出会让浏览器出现假死状态,只能通过强制关闭解决,如果是在webview上,比如我开始的时候写过一个代码在ios微信浏览器上调用swiper 的3d变换导致微信直接闪退。
3:以上还是客户端的,客户端大多数情况下不会停留时间过长,所以除非是非常规操作,很少会出大问题,但是,跑在服务端的程序,通常都是一直跑几天甚至是几个月的,如果这个里面有内存泄漏引发的内存溢出的话,那么就会导致服务器宕机,必须重启。那带来的损失就很大了。

引发内存泄漏的方式

1.意外的全局变量

JavaScript 对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete删除)。如果在浏览器中,全局对象就是window对象。
如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。

那么为什么会对未声明的变量处理方式是挂window下呢?
“当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下”

摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)。” iBooks.
function foo(arg) {
  bar = 'this is hidden global variable';
}

等同于:

function foo(arg) {
  window.bar = 'this is hidden global variable';
}

另外,通过this创建意外的全局变量:

function foo() {
  this.variable = 'this is hidden global variable';
}
// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'
foo();

------------->演示

解决方案

正常的定义全局变量没有问题,但是这种是属于意外的泄漏,所以可以使用严格模式处理,规范自己的代码。

2.console.log

传递给console.log的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。
追踪线上问题,console绝非是个好的方式。因为发生问题一般在用户哪里,你没办法看用户的日志。

function aaa() {
    this.name = (Array(100000)).join('*');
    console.log(this);
}
document.getElementsByClassName('console-obj')[0].addEventListener('click', function () {
      var oo = new aaa();
});

------------->演示

解决方案

可以删除自己的console.log,但是显然,在开发环境下,我就是想看我的console.log,这样注释来注释去也挺麻烦的,所以可以判断下当前的环境是不是env,如果是product环境下的话,直接

window.console.log = function(){return 'warn:do not use my log'}

这样的手法不仅可以屏蔽console.log,还能防止别人在我们的页面下console.log调试

延伸:如何保护自己的页面安全

3.闭包(closures)

由于闭包的特性,通过闭包而能被访问到的变量,显然不会被内存回收♻️,因为被回收的话就没闭包了这个概念了。

    function foo() {
      var str = Array(10000).join('#');
      var msg = "test message";
      function unused() {
        var message = 'it is only a test message';
        str = 'unused: ' + str;
      }
      function getData() {
          return msg;
      }
      return getData;
    }
    var bar;
    document.getElementsByClassName('closure-obj')[0].addEventListener('click', function () {
        bar = foo();
    });
    // var list = [];
    // document.getElementsByClassName('closure-obj')[0].addEventListener('click', function () {
    //     list.push(foo());
    // });
  • 演示内存performance情况
  • 演示memory 情况
  • 断点演示闭包scope,call stack

闭包造成的内存泄漏占用会比其他的要多。
原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。

延伸:VO/AO,call stack

解决方案

不暴露到全局变量上,这样就不会有问题,暴露到全局变量上就手动置为null,垃圾回收器下次回来会带走它

4.dom泄漏

在 JavaScript 中,DOM 操作是非常耗时的。因为 JavaScript/ECMAScript 引擎独立于渲染引擎,而 DOM 是位于渲染引擎,相互访问需要消耗一定的资源。如 Chrome 浏览器中 DOM 位于 WebCore,而 JavaScript/ECMAScript 位于 V8 中。假如将 JavaScript/ECMAScript、DOM 分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript 每次访问 DOM 时,都需要交纳“过桥费”。因此访问 DOM 次数越多,费用越高,页面性能就会受到很大影响。

为了减少 DOM 访问次数,一般情况下,当需要多次访问同一个 DOM 方法或属性时,会将 DOM 引用缓存到一个局部变量中。

但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的 DOM 引用,这样会造成 DOM 内存泄露。

    <input type="button" value="remove" class="remove" style="display:none;">
  <input type="button" value="add" class="add">
  <div class="container">
    <ul class="wrapper"></ul>
  </div>
    // 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点,将其缓存到本地变量中,
      var wrapper = document.querySelector('.wrapper');
      var container = document.querySelector('.container');
      var removeBtn = document.querySelector('.remove');
      var addBtn = document.querySelector('.add');
      var counter = 0;
      var once = true;
      // 方法
      var hide = function(target){
        target.style.display = 'none';
      }
      var show = function(target){
        target.style.display = 'inline-block';
      }
      // 回调函数
      var removeCallback = function(){
        removeBtn.removeEventListener('click', removeCallback, false);
        addBtn.removeEventListener('click', addCallback, false);
        hide(addBtn);
        hide(removeBtn);
        container.removeChild(wrapper);
        wrapper = null;
      }
      var addCallback = function(){
        let p = document.createElement('li');
        p.appendChild(document.createTextNode("+ ++counter + ':a new line text\n"));
        wrapper.appendChild(p);
        // 显示删除操作按钮
        if(once){
          show(removeBtn);
          once = false;
        }
      }
      // 绑定事件
      removeBtn.addEventListener('click', removeCallback, false);
      addBtn.addEventListener('click', addCallback, false);

--------->演示代码

    var refA = document.getElementById('refA');
    var refB = document.getElementById('refB');
    document.body.removeChild(refA);

    // #refA不能GC回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。
    refA = null;

    // 还存在变量refB对#refA的间接引用(refB引用了#refB,而#refB属于#refA)。将变量refB对#refB的引用释放,#refA就可以被GC回收。
    refB = null;

图片描述

5.计时器/监听器

var counter = 0;
    var clock = {
      start: function () {
        // setInterval(this.step, 1000);
        if(!this.timer){
          this.timer = setInterval(this.step, 1000);
        }
      },
      step: function () {
        var date = new Date();
        var h = date.getHours();
        var m = date.getMinutes();
        var s = date.getSeconds();
        console.log('step running');
      }
    }
    // function goo(){
    //     // clock = null;
    //     clearInterval(clock.timer);
    //     console.log('click stop');
    // }
    document.querySelector('.start').addEventListener('click', function () {
      clock.start();
      // document.querySelector('.stop').addEventListener('click',);
    });
    document.querySelector('.stop').addEventListener('click', function () {
      // clock = null;
      clearInterval(clock.timer);
    });

监听器没有及时回收或者是匿名回收导致的。
bind,call,apply的区别

如何使用chrome performance

  1. 开启【Performance】项的记录
  2. 执行一次 CG,创建基准参考线
  3. 操作页面
  4. 执行一次 CG
  5. 停止记录

以上就是我们使用的时候的步骤
那么对这个performances里的各项是如何理解的呢?

前置问题1:什么是回流,什么是重绘,以及为什么回流一定会导致重绘,但是重绘不会导致回流?

中置问题2:浏览器到了渲染阶段的过程是什么?
图片描述

一次性能的记录就完整的展示的浏览器的渲染全过程。从图中也可以看出,layout后的阶段是Painting

跑一个performances

Performances 各项简介

  • FPS 每秒的帧数,绿色条约稿,表示FPS值越高,通常上面附带红色块的帧表示该帧时间过长,可能需要优化。
  • CPU CPU资源,面积图表示不同事件对CPU资源的消耗。
  • NET 这个项和以前的不一样,查询相关资料也没有找到到底显示的是什么,所以只能通过下面的具体来看,HTML文件是蓝色条,脚本文件是黄色条,样式文件是紫色条,媒体文件是绿色条,其他的是灰色条,网络请求部分更详细的信息建议查看Network。
  • HEAP 内存占用情况
  • 三条虚线:蓝色指DOMConentLoaded,绿线表示第一次绘制,红线表示load事件,很明显看到load是比较慢的。
  • summary loading代表html花的时间,scripting代表脚本的时间,rendering代表计算样式和回流花的时间,painting代表绘制的时间
  • Bottom-up 代表花费排序
  • call-tree 代表调用排序
  • event-log 代表各项事务时间线

重点看看这个event-log,以回流为例子,再次确认回流后跟着painting,看看有哪些回流,然后去看看时间节点,发现对应的页面出现。
回流操作还是挺占用时间的
以拼团列表图片高度加载导致的回流问题,可以用一个object-fit来搞定常见的情况

如何规避内存泄漏

注意代码规范,注意代码规范,注意代码规范

垃圾回收

讲讲垃圾回收,说白了,内存泄漏,溢出,就是因为js有自动垃圾回收的机制,然后自动的垃圾回收器并不能准确的回收你所不想用的东西,就会出一些问题,那么常见的垃圾回收有两种

引用计数

当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。 如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这 个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。

//赋值给o1的对象a{},赋值给o2的对象b{};
var o1 = {
  o2: {
    x: 1
  }
};
//a+1 = 1,b作为属性也+1 = 1;
var o3 = o1;
//a+1+1 = 2,b+1+1 = 2                                                 
o1 = 1;     
//a+1+1-1 = 1,b+1+1-1 = 1;
var o4 = o3.o2;
//a+1+1-1 = 1,b+1+1-1+1 = 2;
o3 = '374'; 
//a+1+1-1-1 = 0,b+1+1-1+1-1 = 1;
o4 = null; 
//b-1 = 0;

循环引用导致的问题

//o1:x{},o2:y{};
function f() {
  var o1 = {};
   //x+1 = 1;
  var o2 = {};
    //y+1 = 1;
  o1.p = o2; // o1 references o2
    //y+1+1 = 2;
  o2.p = o1; // o2 references o1. This creates a cycle.
    //x+1+1 = 2;
}
f();

图片描述

这段代码o1和o2互相引用导致引用次数回收的时候不为1,就没有办法回收。
假设没有o2.p= o1这段,那么o1在出函数的时候要给对应的对象减一,结果发现,o1有一个属性p还没解除引用,所以先去解o1.p的,这个时候o2的对象就减一次,完了后o1.p就没了,那o1就可以解除o1的对象,o2再-它自己的,都为0,没泄漏

反过来,如果上了那段代码的话,o1要解除,先走p,o1.p想解除,结果发现o2有个p,又去解o2.p,死循环,一个都解不了,还是2.

假如这个函数被重复多次调用,就会导致大量内存得 不到回收。为此,Netscape 在 Navigator 4.0 中放弃了引用计数方式,转而采用标记清除来实现其垃圾收 集机制。可是,引用计数导致的麻烦并未就此终结。到目前为止,几乎所有的浏览器都是使用的标记清楚策略,只不过垃圾收集的时间间隔稍微不同。

标记清除

当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其 标记为“离开环境”。

Mark and sweep

过去几年,JavaScript 垃圾回收(代数、增量、并行、并行垃圾收集)领域的所有改进都是对该算法(mark-and-sweep)的实现进行改进,但并没有对垃圾回收算法本身进行改进,其目标是确定一个对象是否可达。
图片描述
这样的话,循环引用将不再是问题
图片描述
尽管两个对象还是存在引用,但是他们从 root 出发已经是不可达的了。

总结

在Javascript中,彻底避免垃圾回收或者是内存泄漏是非常困难的。所以我们能做的就是减少泄漏,减少垃圾回收的频率。对一些高频使用的函数之类的东西去做一些类似的优化。综合考虑优化成本

查看原文

赞 2 收藏 4 评论 0

jansen 发布了文章 · 2018-04-28

常用的js排序算法

插入排序

图

算法描述:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤 2~5
var arr = [5, 6, 3, 1, 8, 7, 2, 4];
for(let i = 1;i<arr.length;i++){
    let myIndex = i;
    console.log('次数:'+i);
    for(let j = i-1 ; j >= 0 ; j -- ){
        console.log('单次比较数据:'+arr[myIndex]+'---'+arr[j])
        if(arr[myIndex] < arr[j]){
            [arr[myIndex],arr[j]] = [arr[j],arr[myIndex]];
            myIndex = j;
        }else{
          break;
        }
        console.log('数组'+arr);
    }
}

时间复杂度 O(n^2)
运行过程
clipboard.png

选择排序

tu

算法描述

  • 直接从待排序数组中选择一个最小(或最大)数字,放入新数组中。
  • 假定第一个数字是最小的,然后依次和后面的比较,哪个小哪个就记录当前那个的下标。
  • 记录完下标了之后替换第一个和那个最小数字的位置
  • 依次执行上述步骤,只不过最小的位置依次累加
var arr = [5, 6, 3, 1, 8, 7, 2, 4];
for(let i = 0; i < arr.length - 1;i++){
    console.log('次数'+Number(i+1))
    let minIndex = i;
    for(let j = i ;j < arr.length - 1; j++){
         console.log('单次比较数据:'+arr[minIndex]+'---'+arr[j+1])
         if(arr[minIndex] > arr[j+1]){
            minIndex = j+1;
         }
    }
    [arr[minIndex],arr[i]] = [arr[i],arr[minIndex]];
    console.log('数组'+arr);

}

时间复杂度 O(n^2)

运行过程
clipboard.png

冒泡排序

tu

就几种算法来看,感觉冒泡是比较慢的

算法描述:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
var arr = [5, 6, 3, 1, 8, 7, 2, 4];
let count = 0;
for(let i = arr.length ; i > 0; i --){
    console.log('次数'+i);
    for(let j = 1; j < i; j ++){
        console.log('单次比较数据:'+arr[j]+'----'+arr[j-1])
        if(arr[j] < arr[j-1]){
            [arr[j],arr[j-1]] = [arr[j-1],arr[j]]
        }
    }
    console.log(arr);
}

时间复杂度 O(n^2)

运行过程
clipboard.png

归并排序

t

归并排序的图可能一下看不懂,是因为图代表的是运行的过程,主要看算法描述

归并排序:其基本思想是分治策略,先进行划分,然后再进行合并。
假设要对数组C进行归并排序,步骤是:
1.先将C划分为两个数组A和B(即把数组C从中间分开)
2.再分别对数组A、B重复步骤1的操作,逐步划分,直到不能再划分为止(每个子数组只剩下一个元素),这样,划分的过程就结束了。
如: [12 20 30 21 15 33 26 19 40 25]
划分为: [12 20 30 21 15] [33 26 19 40 25]

       [12 20]      [30 21 15]       [33 26]       [19 40 25]
     [12]  [20]   [30]  [21 15]     [33]  [26]    [19]    [40 25]
     [12]  [20]   [30] [21] [15]    [33]  [26]    [19]   [40] [25]

3.然后从下层往上层不断合并数组,每一层合并相邻的两个子数组,合并的过程是每次从待合并的两个子数组中选取一个最小的元素,然后把这个元素放到合并后的数组中,不断重复直到把两个子数组的元素都放到合并后的数组为止。
4.依次类推,直到合并到最上层结束,这时数据的排序已经完成了。

var arr = [5, 6, 3, 1, 8, 7, 2, 4,9];
function mergeSort(arr){
    if(arr.length === 1){
        return arr;
    }
    let midIndex = Math.floor(arr.length / 2);
    let leftArr = arr.slice(0,midIndex);
    let rightArr = arr.slice(midIndex);
    console.log('拆分数组'+leftArr+'------'+rightArr)
    return mergeFn(mergeSort(leftArr),mergeSort(rightArr));
}.
function mergeFn(left,right){
    let tmp = [];
    console.log(left + '----' + right);
    while (left.length && right.length) {
        console.log('单次比较数据:'+left[0]+'和'+right[0]+'谁小谁所在的数组就被shift掉一个')
        if (left[0] < right[0]){
          tmp.push(left.shift());
        }
        else{
          tmp.push(right.shift());
        }
        console.log(tmp);
    }
    let arra = tmp.concat(left, right);
    console.log('本次比较完毕:'+arra);

    return arra;

}
mergeSort(arr);

时间复杂度 O(nlogn)

运行过程,看了运行过程就能看懂图了,也知道js函数里的参数有函数的情况下的执行顺序是自左向右
clipboard.png
clipboard.png

快速排序

tt

图上的运行方式是按照基准是第0号位算的,看起来稍微有点乱,不过只要知道快排是怎么算的就好了

算法描述:

  • 在数据集之中,选择一个元素作为”基准”(pivot)。
  • 所有小于”基准”的元素,都移到”基准”的左边;所有大于”基准”的元素,都移到”基准”的右边。这个操作称为分区 (partition)操作,分区操作结束后,基准元素所处的位置就是最终排序后它的位置。
  • 对”基准”左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
var arr = [5, 6, 3, 1, 8, 7, 2, 4];
function quickSort(arr){
    if(arr.length <= 1){
        return arr;
    }
    //找基准
    let midIndex = Math.floor(arr.length/2);
    //剔除基准值
    let midNum = arr.splice(midIndex,1)[0];
    console.log('基准值:'+midNum);
    let leftArr = [],rightArr=[];
    for(let i = 0 ; i < arr.length; i++){
        //小于基准的进左边,大于的进右边
        arr[i] < midNum ? leftArr.push(arr[i]) : rightArr.push(arr[i])
    }
    console.log('小于基准值的数组:'+leftArr);
    console.log('大于基准值的数组:'+rightArr);
    return quickSort(leftArr).concat(midNum,quickSort(rightArr));
}
quickSort(arr);

时间复杂度 O(nlogn)

运行过程
clipboard.png
这个运行过程是按照基准为0号位算的;

总结

可以看到,快速排序和归并排序是比较快。而且快排更容易理解更好写一些。

查看原文

赞 9 收藏 23 评论 1

jansen 赞了回答 · 2018-03-13

解决Js怎么做这题目, 4444 333 22 1 22 333 4444 用JS怎么做。我只会做1以上那半求大神

function f(n) {
  for (let i = -n; i <= n; i++) {
    if (i === 0 || i === 1) {
      continue
    }
    let k = Math.abs(i)
    console.log(k.toString().repeat(k))
  }
}

关注 16 回答 15

jansen 发布了文章 · 2017-06-07

小技巧之JSON.stringify()/parse()

JSON

JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

JSON.stringify()

用于把一个JSON对象(恩,javascript中万物皆对象),转化为一个字符串。

JSON._proto_可以查看到JSON来自于Object。

直接使用

    var obj = [
        {
            name:'Jansen1',
            age:18,
            sex:'boy'
        },
        {
            name:'Jansen2',
            age:18,
            sex:'boy'
        },
        {
            name:'Jansen3',
            age:18,
            sex:'boy'
        },
        {
            name:'Jansen4',
            age:18,
            sex:'boy'
        }
    ];
   var myStringObj = JSON.stringify(obj);

clipboard.png

可以看到,通过该操作就可以把obj转化为字符串啦。

第二个参数replacer

JSON.stringify呢不仅仅可以直接转化字符串,还能有条件的转化字符串,这个时候就要用到第二个参数了.
  • 当参数为一个数组的时候,可以通过key来筛选最终要转化的字符串JSON.stringify(obj,['name','age'])

clipboard.png

  • 当参数为一个函数的时候,转化的结果,全依赖于函数的返回值。处理顺序是如果是个数组,处理到数组第一号元素发现还是数组或是对象的时候就进去循环,直到所有的能遍历的都被处理到再进行下一个运算。JSON.stringify(obj,(key,val)=>{if(key!='sex')return val})

clipboard.png

第三个参数-space格式化参数

文本添加缩进、空格和换行符,如果space是个数字的化,最大值是10,过10为10

JSON.stringify(obj,null,4)

clipboard.png

可以看到,在控制台输出的代码变的很规整了。

JSON.parse()

JSON.parse只拥有两个参数,第一个就是把这个字符串转换为JSON对象,第二个就是筛选对象
那么现在如果再有这样的需求,把一个JSON对象下的所有属性为sex中的boy,替换为male,girl替换为female,age大于20的age条目不显示,只要20以下的小鲜肉,那么就很好处理啦

    var testJSON = [
    {
        "name": "Jansen1",
        "age": 18,
        "sex": "boy"
    },
    {
        "name": "Jansen2",
        "age": 2,
        "sex": "girl"
    },
    {
        "name": "Jansen3",
        "age": 19,
        "sex": "girl"
    },
    {
        "name": "Jansen4",
        "age": 22,
        "sex": "boy"
    },
    {
        "name": "Jansen5",
        "age": 22,
        "sex": "boy"
    },
    {
        "name": "Jansen6",
        "age": 22,
        "sex": "girl"
    },
    {
        "name": "Jansen7",
        "age": 19,
        "sex": "boy"
    },
    {
        "name": "Jansen8",
        "age": 19,
        "sex": "boy"
    }
];

var newJSON = JSON.parse(JSON.stringify(testJSON,(key,val)=>{
  if(key === 'sex'){
    return val === 'boy' ? 'male' : 'female';
  }
  if(key === 'age' && val < 20){
    return val
  }
  else if(key !== 'age'){
    return val;
  }
},4))

clipboard.png

是不是很容易呢

兼容性

这两个方法在IE8级以上都能够实用,需要兼容低版本的浏览器,可以在代码里引用json2.js

链接里的代码就是自己实现的一个JSON,当浏览器能检测到有JSON的时候,就会用现在的,如果检测不到,才会执行代码。
有兴趣的可以先想想不带参数的JSON.stringify如何实现再去看看这个链接里的写法
查看原文

赞 2 收藏 10 评论 0

认证与成就

  • 获得 18 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-01-12
个人主页被 629 人浏览