• 博文
  • 归档
  • 创建NodeJS命令行包指南

    2019-04-26

    次访问

    原文地址

    受到启发要创建一个NodeJS命令行脚本来解决特定的问题?您想要将命令行作为可安装包发布吗?这应该很简单,对吧?幸运的是,它确实很简单!

    下面是关于创建NodeJS命令行包的简明指南。

    本指南将引导您创建、映射和链接NodeJS命令行脚本。

    创建node包

    首先,我们需要创建NodeJS包,比如只包含一个package.json文件的目录。我们可以简单地分两步来做:
    1、创建一个空目录;
    2、在目录下运行npm init。

    这对于创建NodeJS命令行包并不是什么新鲜事,也不是特殊的,因为它是任何NodeJS包的起点。下面让我们创建NodeJS命令行脚本。

    创建NodeJS命令行脚本

    你可能已经知道我们可以通过运行node script.js来执行一段NodeJS脚本,这在大多数情况下是可行的,但是NodeJS命令行脚本除了包含一个特殊的shell指令外,就是一个常规的JavaScript文件,这个稍后再详细介绍。首先,让我们创建一个JavaScript文件,它将成为NodeJS命令行脚本。

    创建JavaScript文件

    npm官方文档和流行的NodeJS项目通常将JavaScript命令行文件命名为cli.js。这个命名非常好,因为它的名字就说明了它的作用。

    JavaScript文件转成NodeJS命令行脚本

    和其他shell脚本类似,我想通过本地安装的node来运行我们的JavaScript文件,所以我们在JavaScript文件的顶部添加一个shebang字符序列:

    1
    #!/usr/bin/env node

    这样,我们就告诉*nix系统,JavaScript文件的解释器应该是/usr/bin/env node,它查找本地安装的node。
    在Windows系统中,这行会被忽略,因为它被看成注释,但是它是必须的,因为在Windows机器上npm会在NodeJS命令行包安装的时候读取它。

    让JavaScript命令行文件可执行

    在大多数情况下,不允许执行新文件。在创建将要执行的NodeJS命令行脚本时,我们需要修改它的文件权限。在*nix系统中,您可以这样做:

    1
    chmod +x cli.js           # 让cli.js文件可执行

    现在我们给脚本文件加点儿代码,我们将创建一个简单的Hello World,并打印一些提供的变量。

    给NodeJS命令行文件增加代码

    1
    2
    3
    4
    #!/usr/bin/env node

    const [,,...args] = process.argv // 取变量
    console.log(`Hello World ${args}`) // 打印Hello World和变量

    现在我们可以运行它,在Linux 和 Mac OS X系统运行./cli.js,在Windows系统运行node.cmd cli.js。
    试试看!试试传递一些变量给它。
    nodecli

    到现在为止,我们在Linux和Mac OS X上可以像普通的javascript文件一样运行我们的NodeJS命令行文件,但是在Windows中我们仍然需要增加node.cmd。此外,我们使用文件名来执行命令行脚本,这并不好。在下一节中,我们将避开这些问题。

    命令行脚本映射到命令名

    到现在为止,我们将JavaScript文件变成了NodeJS命令行文件,然而,我们希望给它一个更有意义的名字,而不是NodeJS命令行脚本文件的名字。为此,我们需要配置package.json来映射命令行脚本和命令名。npm官网是这么说的:

    在package.json中提供一个bin字段,表示命令名到本地文件名的映射。

    这意味着我们可以为本地的JavaScript脚本指定一个命令名称,比如我们像让cli.js脚本映射到say-hello命令,我们可以像上面提到的那样增加bin字段到package.json:

    node-bin-sayhello

    我们为bin字段分配了一个对象,其中键成为命令名,值是映射到NodeJS命令行脚本文件的对象。这种格式允许我们作为开发人员提供多个脚本映射。但是,如果我们想提供一个与它的文件同名的NodeJS命令行脚本,我们可以设置一个字符串表示本地文件路径,而不是一个对象。

    命令命名

    我们可以为命令选择任何名称,但我们不希望它与现有的流行命令名称(如ls、cd、dir等)发生冲突。如果我们使用一个现有的名称,脚本很可能不会被执行,而是执行现有现有的命令(结果可能不同)。

    链接开发命令

    npm link命令允许我们在本地“符号链接一个包文件夹”,根据我们的需要,它将在本地安装package.json里bin字段中列出的任何命令。换句话说,npm link就像一个NodeJS包安装模拟器。值得一提的是,npm link有更广泛的用途,超出了本指南的范围。

    npm link命令是从我们想符号链接的NodeJS包目录中使用的:

    1
    npm link

    执行后,我们将看到命令被全局符号链接。现在,我们可以用它自己的命令名称say-hello来执行NodeJS命令行脚本:

    node-sayhello

    非常简洁有没有?这样我们就能在npm publish之前本地运行NodeJS命令行脚本。

    npm link说明

    在底层,npm链接(也适用于npm安装)符号链接package.json bin字段中指定的所有文件。npmjs文档说:

    在安装时,npm将符号链接文件到prefix/bin中用于全局安装,或者./node_modules/.bin/用于本地安装。

    在*nix系统上,npm链接过程类似于为指定的命令文件创建快捷方式,该命令文件将由shell执行,然后由node执行(因为指定了#!/usr/bin/env node)。
    在Windows上,npm会做相同的事情(如果指定了运行环境),然而它还会创建{command-name}.cmd来让node执行我们特殊的命令行文件/

    保持目录整洁

    当我们的符号链接命令测试通过后,可能会想删除它。
    为此,我们可以在包目录下运行下面的代码:

    1
    npm unlink                   // 不再安装

    总结

    以上就是关于创建NodeJS命令行包的简明指南。
    通过这四个步骤,我们已经具备了发布NodeJS包的基础知识,该包将安装命令行包。
    现在,你可以commit,push你的NodeJS命令行包代码来解放你的创造力了。如果您这样做了,请在评论中通过GitHub链接给我留言,这样我就可以偷看了。

    推荐

    最后,这些是我在自己的命令行项目中用到的工具:

    • meow – 简单的命令行工具
    • chalk - 终端字符样式
    • yargs - 命令行options解析
    ...more
  • lottie-web应用优化

    2019-04-20

    次访问

    Web动画之AE+Bodymovin文中的应用过程最近找到了2点优化空间:

    1、图片资源加载2次,增加了CDN流量。优化方向:图片加载次数减小到1次;
    2、设计输出的图片资源多且零散,有9张小图片。优化方向:多张图片合并成1张雪碧图,减少网络请求。

    加载次数

    loadTwice
    上图可以看到,同一张图加载了2次,且返回的status都是200。

    首先分析同一张图片加载2次的原因。

    查看源码发现图片加载主要有预加载逻辑和页面元素创建逻辑两处:
    creatimagedata
    creatcontent

    预加载逻辑中会调用creatimagedata,这里会将所有的图片提前下载,后面创建svg页面元素时调用了creatcontent也会加载图片。

    点击图片查看请求Headers,发现Request Headers中的Cache-Control值为no-cache,表示客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定。协商缓存的概念和协商过程这里就不叙述了,但很明显,这里的协商缓存没有命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串。

    这就体现了协商缓存的缺陷:
    如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。

    而我们这里的资源下载速度最慢的也只有293ms,所以预加载的图片缓存并没有体现出预计的效果。所以图片被加载了2次。

    如果不做上面方案二雪碧图的优化,可以简单的把预加载逻辑去掉。那么图片就不会加载2次了。

    雪碧图

    查看lottie-web的文档和源码,发现并不支持雪碧图。
    参阅开源项目lottie-web-sprite源码后,对我们用到的lottie_svg做改造:

    生成雪碧图

    生成雪碧图使用lia。步骤如下:

    创建精灵图配置文件sprite_conf.js。(使用lia init命令可以自动生成配置文件,只不过因为需要使用自定义模版,所以这里手动创建)

    1
    2
    3
    4
    5
    6
    7
    8
    'use strict';

    module.exports = [{
    src: ['*.png'], // 图片素材路径匹配规则
    image: 'sprite.png', // 生成的精灵图的路径
    style: 'sprite.js', // 生成的图片素材和精灵图的位置关系数据文件的路径
    tmpl: './template.ejs' // 图片素材和精灵图的位置关系数据文件模版
    }];

    创建图片素材和精灵图的位置关系数据模版文件template.ejs。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var opt = {
    width: <%= size.width %>,
    height: <%= size.height %>,
    src: '<%= realpath %>',
    count: <%= items.length %>,
    items: [
    <% items.forEach(function(item, idx) { -%>
    {
    index: <%= idx %>,
    name: '<%= item.name %>',
    width: <%= item.size.width %>,
    height: <%= item.size.height %>,
    x: <%= item.x %>,
    y: <%= item.y %>
    },
    <% }) -%>
    ]
    }

    module.exports = opt;

    运行下面2行命令得到精灵图sprite.png以及位置关系数据文件sprite.js:

    1
    2
    npm i -g lia
    lia

    把位置关系数据文件sprite.js中的绝对路径改为相对路径:src: './sprite.png',。

    此文基础:所有文件都在同一目录层级,否则需要注意修改相对目录结构。

    修改data.json

    data.json新增字段_sprite,将sprite.js中的数据复制给_sprite。此时data.json中的数据看起来类似这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    {
    "v": "5.5.0",
    "fr": 25,
    "ip": 0,
    ...
    "assets": [{
    "id": "image_0",
    "w": 66,
    "h": 55,
    "u": "",
    "p": "img_0.png",
    "e": 0
    }
    ...
    ],
    "layers": [...],
    "markers": [],
    "_sprite": {
    "width": 733,
    "height": 568,
    "src": "./sprite.png",
    "count": 9,
    "items": [{
    "index": 0,
    "name": "img_0",
    "width": 66,
    "height": 55,
    "x": 243,
    "y": 310
    }
    ...
    ]
    }
    }

    更改源码

    更改configAnimation方法,增加preloadSprite方法和loadAssetsFromSprite方法,这些可参考lottie-web-sprite。

    另外需要更改getAssetsPath方法,如果有_spriteSrc,图片路径取雪碧图生成的base64编码。方法里的其他逻辑不变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    AnimationItem.prototype.getAssetsPath = function (assetData) {
    var path = '';
    if (this._spriteSrc) {
    this.imagePreloader.images.assetData
    var i = 0, len = this.imagePreloader.images.length;
    while (i < len) {
    if(assetData.id == this.imagePreloader.images[i].assetData.id){
    return this.imagePreloader.images[i].img.src;
    }
    i += 1;
    }
    } else if(assetData.e) {
    path = assetData.p;
    } else if(this.assetsPath){
    var imagePath = assetData.p;
    if(imagePath.indexOf('images/') !== -1){
    imagePath = imagePath.split('/')[1];
    }
    path = this.assetsPath + imagePath;
    } else {
    path = this.path;
    path += assetData.u ? assetData.u : '';
    path += assetData.p;
    }
    return path;
    };

    总结

    现在就可以应用雪碧图了。
    loadSprite

    lia自动生成的雪碧图还可以再压缩的哦

    spriteTiny

    优化效果总结(以童话故事吊牌数据为例):

    webp格式动图 HTML HTML格式优化后
    总流量 2M 258.2k 139.3k
    图片请求次数 1 18 1

    webp格式动图主要缺点是太大,接近2M。
    做成HTML吊牌后,利用第三方库实现动效,但是这种方式的缺点是设计直接输出的图多且零散,比如童话故事有9张图,第三方库会预加载图片,发起9次请求,由于CDN缓存机制导致预加载功能失效,会重复加载,一共发起18次图片资源请求。
    HTML吊牌优化后,效果最佳。主要更改了第三方库的源码,多张图片合并成1张,图片资源大小和请求次数都大幅减小。

    本文修改的是lottie-web-5.5.0版本的lottie_svg.js源码

    参考

    • lottie-web-sprite
    • 精灵图在 Lottie Web 动画中的应用 - 知乎
    • lia
    • 一文读懂前端缓存 - 知乎
    ...more
  • node自动布署

    2019-04-18

    次访问

    前端静态资源布署,可以通过xShell或者SecureCRT这种软件来上传资源,用软件的ftp上传,或者手动输入一些命令来操作,比如:

    1
    2
    3
    4
    #上传
    rz
    #解压
    unzip

    如果用node自动布署,就简便很多。

    使用node脚本,可将npm run build之后构建出来的dist文件夹压缩,然后自动上传到远端服务器,然后在远端服务器自动解压到固定的目录。

    cli

    在package.json中增加bin命令cli-deploy,指向./bin/deploy.js,在script中增加deploy命令。后面完成deploy.js后就可以直接npm run deploy来发布了。
    不了解node cli的话,可以看这篇文章A guide to create a NodeJS command-line package – Netscape – Medium

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // package.json
    "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "deploy": "vue-cli-service build && cli-deploy"
    },
    "bin": {
    "cli-deploy": "./bin/deploy.js"
    },

    也可以不使用bin,将scripts中的deploy命令改成vue-cli-service build && node ./bin/deploy.js

    压缩

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // zip.js
    const glob = require('glob')
    const fs = require('fs')
    const path = require('path')
    const yazl = require("yazl")
    const chalk = require('chalk')

    const zip = ({ sourcePath, destPath, filename }) => {
    return new Promise((resolve) => {

    const zipfile = new yazl.ZipFile()

    if (!fs.existsSync(destPath)) {
    fs.mkdirSync(destPath, 0777)
    }

    const files = glob.sync('**/*.*', {
    cwd: sourcePath
    })

    files.forEach(function(file) {
    const filePath = path.join(sourcePath, file)
    zipfile.addFile(filePath, file)
    })

    zipfile.outputStream.pipe(fs.createWriteStream(path.resolve(destPath, filename))).on('close', () => {
    console.log(chalk.cyan(
    ` ${filename} has build!`
    ))
    resolve()
    })
    zipfile.end()
    })
    }

    module.exports = {
    zip
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bin/deploy.js
    #!/usr/bin/env node
    const { zip } = require('./zip')
    const zipOptions = {
    sourcePath: path.join(__dirname, '../dist'), // 需要要打包的目录
    destPath: path.join(__dirname, '../'), // 打包存放的目录
    filename: 'dist.zip', // 打包后的名称
    }
    zip(zipOptions)

    上传

    上传这块做了比较多的调研,看了比较多的库,最后决定用scp2,不需要配置ssh免密登录,直接使用的时候指定信息就行。

    1
    2
    3
    4
    5
    6
    // bin/deploy.js
    const path = require('path')
    const client = require('scp2')
    client.scp(path.join(__dirname, '../dist.zip'), `${user}:${pass}@${host}:${reomteFloder}`, () => {
    // 这里远端解压
    })

    也可以不压缩,直接上传文件夹

    1
    2
    3
    4
    5
    6
    // bin/deploy.js
    const path = require('path')
    const client = require('scp2')
    client.scp(path.join(__dirname, '../dist/'), `${user}:${pass}@${host}:${reomteFloder}/`, () => {
    // 上传成功
    })

    解压

    选用simple-ssh连接远端,然后用ssh.exec在远端执行命令。simple-ssh只是把ssh2进一步封装了一下,让调用更简单。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // bin/deploy.js
    const SSH = require('simple-ssh')
    const host = 'xx.xx.xx.xx' // 远端主机ip
    const user = 'xxx' // 用户名
    const pass = 'xxx' // 密码
    const reomteFloder = '/xxx/xxx/xx' // 远端解压目录
    const ssh = new SSH({host, user, pass})
    ssh.exec(`rm -rf ${reomteFloder}/dist`) // 解压前先清空dist目录,避免有哈希的文件积累
    .exec(`unzip -o ${reomteFloder}/dist.zip -d ${reomteFloder}/dist`, {
    out: console.log.bind(console),
    })
    .start()
    unzip参数说明: - -o 不必先询问用户,unzip执行后覆盖原有文件 - -d <目录> 指定文件解压缩后所要存储的目录

    pm2

    有的项目需要用pm2重启,如果直接远程执行pm2命令,比如pm2 status, 会报错pm2: command not found。
    解决办法:把pm2 status改成'bash -l -c "pm2 status"'。
    参考了bash-doesnt-load-node-on-remote-ssh-command和env-problem-when-ssh-executing-command-on-remote

    总结

    至此,完成了自动压缩、上传、解压。
    cli-deploy可以部署;
    npm run deploy可以构建+部署。

    完整版本deploy.js如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #!/usr/bin/env node
    const { zip } = require('./zip')
    const path = require('path')
    const SSH = require('simple-ssh')
    const client = require('scp2')

    const zipOptions = {
    sourcePath: path.join(__dirname, '../dist'), // 需要要打包的目录
    destPath: path.join(__dirname, '../'), // 打包存放的目录
    filename: 'dist.zip', // 打包后的名称
    }
    const host = 'xx.xx.xx.xx' // 远端主机ip
    const user = 'xxx' // 用户名
    const pass = 'xxx' // 密码
    const reomteFloder = '/xxx/xxx/xx' // 远端解压目录

    const ssh = new SSH({host, user, pass})

    const upload = () => {
    client.scp(path.join(__dirname, '../dist.zip'), `${user}:${pass}@${host}:${reomteFloder}`, () => {
    ssh.exec(`unzip -o ${reomteFloder}/dist.zip -d ${reomteFloder}/dist`, {
    out: console.log.bind(console),
    })
    .start()
    })
    }

    async function deploy () {
    await zip(zipOptions)
    console.log('Build complete')
    upload()
    }

    deploy()
    ...more
  • Web动画之AE+Bodymovin

    2019-03-28

    次访问

    AE+Bodymovin简介

    设计通过AE设计动效,安装Bodymovin插件后,直接导出js或者json数据给前端。前端利用设计提供的动画数据即可实现动效。

    设计

    设计首先需要下载Bodymovin插件。
    具体的设计细节这里就不描述了,交给设计同事就好。
    需要注意的是:
    1、文件
    如果使用了图片资源或者未转成形状图层的Adobe Illustrator文件图层, 将会同时生成一个images文件夹存放这些图片资源。(建议将AI图层转换为形状图层,这样他们会被导出为矢量数据,只需在AE中导入的AI图层上右键 > 从矢量图层创建形状)

    注意,如果不同的带图片资源的动画导出到同一地址,images文件夹将会被覆盖。

    2、性能
    Bodymovin的动画都是实时渲染的,最好控制下AE工程文件体积。避免这种情况:绘制了一个巨大的形状图层,但是只通过遮罩使用其中一小部分。
    过多的节点同样会影响性能。
    Bodymovin支持的AE特性:

    • 支持预合成、形状图层、固态层、图片、空对象以及文字图层。
    • 支持遮罩和反向遮罩。也许别的模式也会支持,但是会对性能造成巨大影响。
    • 支持时间重映射。
    • 支持形状图层的形状、矩形、椭圆和星形。
    • 支持滑块效果。
    • 支持部分表达式。更多介绍可以查看这里。
    • 不支持: 图像序列、视频和音频。
    • 不要伸缩图层!不知为何,伸缩图层会破坏导出的数据,所以不要做这个操作。

    前端

    1、安装

    1
    npm install lottie-web

    或者直接从官方CDN获取。

    完整版支持渲染为svg\canvas\html三种格式,由于我们只需要svg格式(html格式性能不好,canvas格式性能够好,但是动画数据是js格式,有安全隐患,折中选择json格式数据的svg),所以只需要lottie_svg.min.js,经过gzip压缩的库文件只有48.3k。

    2、demo
    请看demo。
    请看官方文档。

    应用

    设计输出资源的目录结构是这样的:
    .
    ├── images
    │ ├── img_0.png
    │ ├── img_1.png
    │ ├── img_2.png
    │ ├── …
    ├── demo.html
    ├── lottie.js
    └── data.json
    有3个问题导致不能直接布署:
    1、资源太大;
    2、目录结构不方便上传CDN;
    3、动画尺寸未设置,默认是100%。

    资源大小问题

    由于不完全是矢量图的设计,所以有images图片资源,直接点击demo.html就可以看到效果。但是设计直接导出的资源比较大,主要有以下几点原因:

    1、lottie.js未压缩,直接内嵌在html里面,有243k,如果选择官网CDN的lottie_svg.min.js就只有48k,不过公司的CDN只能压缩到62k。
    2、data.json也直接内嵌到html里面,但把json文件放到CDN上用gzip压缩后,只有2k。

    把demo.html里的lottie.js和data.json代码删除,用CDN加载lottie_svg.min.js和data.json。通过webpack压缩后的demo.html文件只有577字节。

    目录结构问题

    为了方便发布到CDN,改变了目录结构
    .
    ├── img_0.png
    ├── img_1.png
    ├── …
    ├── img_8.png
    ├── demo.html
    └── data.json
    需要把data.json里的图片资源路径改一下,assets里面的u代表图片资源路径。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 原来是这样的
    "assets": [{
    "id": "image_0",
    "w": 66,
    "h": 55,
    "u": "images/",
    "p": "img_0.png",
    "e": 0
    }]
    // 改成下面这样
    "assets": [{
    "id": "image_0",
    "w": 66,
    "h": 55,
    "u": "",
    "p": "img_0.png",
    "e": 0
    }]

    动画尺寸问题

    修改#lottie的width和height。

    完整示例

    本示例lottie_svg.min.js使用官方CDN,实际使用的是公司CDN资源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <html xmlns="http://www.w3.org/1999/xhtml">
    <meta charset="UTF-8">
    <head>
    <style>
    body{
    margin: 0px;
    height: 100%;
    overflow: hidden;
    }
    #lottie{
    width:280px;
    height:300px;
    display:block;
    overflow: hidden;
    transform: translate3d(0,0,0);
    text-align: center;
    opacity: 1;
    }
    </style>
    </head>
    <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.5.1/lottie_svg.min.js" type="text/javascript"></script>
    <div id="lottie"></div>
    <script>
    var params = {
    container: document.getElementById('lottie'),
    renderer: 'svg',
    loop: true,
    autoplay: true,
    path: 'data.json'
    };
    var anim;
    anim = lottie.loadAnimation(params);
    </script>
    </body>
    </html>

    应用总结

    1、资源大小
    输出的webp格式动图接近2M。
    这种方式就小了很多,库文件lottie_svg.min.js有63k,data.json有2k,html不到1k,所有的图片资源96.1k,不过不知道为什么图片资源会加载2次(后面分析源码搞清楚了加载两次的原因,见lottie-web应用优化,造成了192.2k的图片资源流量。不过总体来说63+2+1+192.2=258.2k,是远小于2M的,只有原来的12.6%的大小,相当于节约了87.4%的流量。

    2、开发效率高
    设计输出动效资源,开发处理下上面列过的3个问题点就可以直接布署了,动效细节完美实现。

    参考

    • lottie-web
    ...more
  • SVG线条动画

    2019-03-25

    次访问

    SVG线条动画原理
    现在有一个svg,如何让它动起来呢?主要用到两个参数:

    • stroke-dasharray:控制描边的点划线的图案范式。dasharray是一个长度和百分比的数列,数与数之间用逗号或者空白隔开,指定短划线和缺口的长度。如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。
    • stroke-dashoffset:指定dash模式到路径开始的距离。如果使用了一个 <百分比> 值, 那么这个值就代表了当前viewport的一个百分比。值可以取为负值。

    stroke-dasharray

    原理一

    利用stroke-dasharray,假如svg路径长度是100,设置stroke-dasharray: 0, 100,表示路径上的实线长度为0,空隙为100,所以一开始整个路径都是空隙,什么也看不见,然后过渡到stroke-dasharray: 100, 100,由于整个路径的长度是100,实线从0变成100,看起来就像实线动起来了。

    原理二

    利用stroke-dashoffset,假如svg路径长度是100,设置stroke-dasharray: 100, 100;stroke-dashoffset: 100,表示100实线和100空隙,线条偏移100,于是100的实线被移出路径,路径上只剩100空隙,什么也看不见,然后慢慢修改偏移量,把实线一点点挪出来,就看到实线动起来了。

    获取路径长度

    上面原理都是假设路径长度100,实际需要使用js的document.getElementById('path').getTotalLength()获取路径长度。
    svg-animation

    demo

    st1使用stroke-dashoffset,st2使用stroke-dasharray。请查看demo。

    ...more
  • JavaScript引擎基础之内联缓存

    2019-03-25

    次访问

    形状背后的主要推动力是内联缓存(Inline Caches)或ICs的概念。ICs是让JavaScript快速运行的关键因素!JavaScript引擎使用ICs来记住去哪里寻找对象的属性,从而减少昂贵的查找次数。

    下面这个函数getX接受一个对象并返回属性x:

    1
    2
    3
    function getX(o) {
    return o.x;
    }

    如果我们在JSC中运行这个函数,它会生成以下字节码:ic-1

    第一条命令get_by_id从第一个变量arg1中加载属性x,并把结果存到loc0中,第二条命令返回我们在loc0中存的内容。
    JSC还将内联缓存嵌入get_by_id命令,该命令由两个未初始化的槽组成。ic-2

    现在我们假设把对象{ x: ‘a’ }传入getX,正如我们上篇文章中所了解的,这个对象有一个带有属性x的形状,这个形状存储属性x的偏移量和值。当你第一次执行getX,get_by_id命令寻找x属性,然后发现值存在偏移为0的地方。ic-3

    嵌入到get_by_id指令中的IC将记住找到的属性的形状和偏移:ic-4

    后续运行种,IC只需要比较形状,如果一样,就直接从记忆偏移量处读取值,而且,如果JavaScript引擎遇到的对象的形状之前IC记录过,引擎就不会获取属性值,开销很大的属性寻找被直接跳过,这比每次都寻找属性明显快多了。

    高效存储数组

    数组是常见的存储类型,这些属性的值被称为数组元素,JavaScript引擎在默认情况下让数组索引属性可写、可枚举和可配置,并将数组元素与其他命名属性分开存储。

    思考下面这个数组:

    1
    2
    3
    const array = [
    '#jsconfeu',
    ];

    JavaScript引擎存储数组长度length为1,然后指向含有offset和length属性的形状。array-shape

    看起来和我们之前看到的一样,但是数组的value值存到哪里去了呢?array-elements

    每个数组都有一个单独的元素支持存储,其中包含所有数组索引的属性值。JavaScript引擎不需要为数组元素存储任何属性属性,因为通常情况下数组都是可写、可枚举和可配置的。

    异常情况下会发生什么呢?比如更改数组元素的属性值会怎样?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 请永远不要这么做
    const array = Object.defineProperty(
    [],
    '0',
    {
    value: 'Oh noes!!1',
    writable: false,
    enumerable: false,
    configurable: false,
    }
    );

    上面的代码片段定义了一个名为“0”的属性(恰好是一个数组索引),但是将其值设置为非默认值。
    在这种情况下,JavaScript引擎将整个支持存储的元素表示为字典,将数组索引映射到属性值。array-dictionary-elements

    即使只有1个数组元素有非默认属性,整个数组的后备存储器进入这种缓慢而低效的模式。避免在数组索引上使用Object.defineProperty(我不知道你为什么要这么做。这似乎是一件奇怪的、无用的事情)。

    小结

    我们已经了解了JavaScript引擎如何存储对象和数组,形状和ICs又是如何优化对象和数组的常见操作。基于这些知识,我们发现了一些实用的JavaScript编码技巧,可以帮助提高性能:

    • 始终以相同的方式初始化对象,这样它们就不会有不同的形状。
    • 不要打乱数组元素的默认值,这样可以有效地存储和操作它们。
    ...more
  • JavaScript引擎基础之形状

    2019-03-11

    次访问

    原文地址

    本文描述了所有JavaScript引擎共有的一些关键基础,不仅仅是作者所研究的V8引擎。作为JavaScript开发者,深入理解JavaScript引擎的工作方式,会帮助你推断代码的性能。

    JavaScript引擎管道

    这一切都从您编写的JavaScript代码开始,JavaScript引擎解析源码,将其转换成抽象语法树(Abstract Syntax Tree,简称AST),基于AST,解释器将其变成字节码。此时,引擎实际上正在运行JavaScript代码。js-engine-pipeline

    • JavaScript source code: JavaScript源码
    • parser: 解析器
    • Abstract Syntax Tree: 抽象语法树,简称AST
    • interpreter: 解释器
    • bytecode: 字节码
    • optimize: 优化
    • optimizing compiler: 优化编译器
    • optimized code: 优化后的代码
    • deoptimize: 反优化
    • profiling data: 分析数据

    为了让它运行的更快,字节码可以和分析数据一起送到优化编译器,优化编译器根据它的分析数据做出某些假设,然后生成高度优化的机器码。如果在某一时刻其中一个假设被证明是错的,那么优化编译器就会取消优化并返回解释器。

    JavaScript引擎的解释/编译器管道(Interpreter/compiler)

    现在,让我们放大这个管道中实际运行JavaScript代码的部分。比如代码在哪里得到解释和优化,以及主流JavaScript引擎之间的一些差异。
    通常,有一个包含解释器和优化编译器的管道,解释器快速生成未经过优化的字节码,而优化编译器需要更长的时间,但最终生成高度优化的机器码。

    优化编译器:interpreter-optimizing-compiler
    这个通用管道几乎和V8(Chrome和Node中使用的JavaScript引擎)的工作原理一样。

    V8的优化编译器:interpreter-optimizing-compiler-v8

    Ignition: 点火器
    TurboFan: 风扇

    V8里的解释器叫点火器,负责生成和执行字节码。当它运行字节码时,会收集分析数据(后面将会用来加快执行速度),当某个函数变热,比如经常执行,它的字节码和分析数据就会传到风扇(我们的优化编译器),然后基于分析数据来生成高度优化的机器码。

    SpiderMonkey(Firefox和SpiderNode中使用的Mozilla JavaScript引擎),会有一点不同。它有两个而不是一个优化编译器。解释器优化到基线编译器里面,基线编译器会生成一些优化的代码。结合运行代码时收集的分析数据,IonMonkey编译器可以生成高度优化的代码。如果假设的优化失败了,IonMonkey返回到基线代码。

    SpiderMonkey的优化编译器:interpreter-optimizing-compiler-spidermonkey

    Baseline: 基线

    Chakra(在Edge和Node-ChakraCore中使用的微软的JavaScript引擎)使用了类似的两个优化编译器。解释器优化成SimpleJIT (JIT代表即时编译器),生成优化的代码。与分析数据相结合,FullJIT生成更优化的代码。
    Chakra的优化编译器:interpreter-optimizing-compiler-chakra
    JavaScriptCore(缩写为JSC,苹果的JavaScript引擎,用于Safari和React Native),使用三种不同的优化编译器将其发挥到极致。底层解释器LLInt优化到基线编译器,然后基线编译器优化到DFG(Data Flow Graph)编译器,DFG又优化到FTL(Faster Than Light)编译器。
    JavaScriptCore的优化编译器:interpreter-optimizing-compiler-jsc
    为什么有些引擎比其他引擎有更多的优化编译器?
    一切都是关于权衡的。解释器可以快速生成字节码,但是字节码通常不是很高效。另一方面,优化编译器需要更长的时间,但最终会生成更高效的机器码。
    这是快速让代码运行(解释器)和慢一点让代码运行,但最终让代码以最优性能的方式运行(优化编译器)之间的权衡。
    一些引擎选择添加具有不同时间或效率特性的多个优化编译器,以额外的复杂性为代价对这些权衡进行更细致的控制。另一个权衡与内存使用有关。有关这方面的详细信息,请参阅我们的后续文章。

    上面我们主要描述了不同的JavaScript引擎的解释器和优化编译器管道的差异,但是除了这些差异,在高层面上看,所有JavaScript引擎都具有相同的体系结构:有一个解析器和某种解释器/编译器管道。

    JavaScript的对象模型

    让我们通过放大某些方面的实现来看看JavaScript引擎还有什么共同之处。比如,JavaScript引擎如何实现JavaScript对象模型,以及它们使用哪些技巧来加速访问JavaScript对象上的属性,事实上,所有主流引擎都实现了非常类似的功能。
    ECMAScript规范定义对象是字符串键映射到属性值的字典。object-model
    除了[[Value]]本身外,规范还定义了这些属性:

    • [[Writable]]:决定是否可以将属性重写
    • [[Enumerable]]:决定属性是否出现在for-in循环中(是否可枚举)
    • [[Configurable]]:决定是否可以将属性删除
      双方括号[[]]看起来很时髦,但这只是规范表示不直接暴露给JavaScript的属性的方式。但您仍然可以在JavaScript中使用Object.getOwnPropertyDescriptor获得任何给定对象的这些值。
    1
    2
    3
    const object = { foo: 42 };
    Object.getOwnPropertyDescriptor(object, 'foo');
    // → { value: 42, writable: true, enumerable: true, configurable: true }

    这就是JavaScript如何定义对象的,那数组呢?
    您可以将数组看作对象的特殊情况,一个区别是数组对数组索引有特殊的处理。数组索引是ECMAScript规范中的一个特殊术语。在JavaScript中数组长度限制在2³²−1以内,数组索引是该限制内任何有效的索引,比如,任何0到2³²−2之间的整数。

    另一个不同之处在于数组还有一个神奇的长度属性。

    1
    2
    3
    4
    const array = ['a', 'b'];
    array.length; // → 2
    array[2] = 'c';
    array.length; // → 3

    上面的例子中,数组在创建的时候有一个值为2的length,然后我们给第2项赋值,length就自动更新了。

    JavaScript定义数组类似于对象。例如,包括数组索引在内的所有键都显式地表示为字符串。数组中的第一个元素存储在键“0”下。array-1

    length属性只是另一个不可枚举和不可删除的属性,一旦元素被添加到数组中,JavaScript将自动更新length属性的[[Value]]值。总之,数组的表现和对象非常相似。array-2

    优化属性访问

    现在我们知道在JavaScript中对象是怎么定义的了,让我们深入了解JavaScript引擎如何高效地处理对象。
    JavaScript中访问属性是目前最常见的操作。对于JavaScript引擎来说,快速访问属性是至关重要的。

    1
    2
    3
    4
    5
    6
    7
    const object = {
    foo: 'bar',
    baz: 'qux',
    };

    // 这里我们访问了`object`的`foo`属性
    doSomething(object.foo);

    形状

    在JavaScript中,很多对象有相同的key是很常见的,这样的对象有相同的形状。

    1
    2
    3
    const object1 = { x: 1, y: 2 };
    const object2 = { x: 3, y: 4 };
    // `object1`和`object2`有相同的形状

    形状相同的对象获取相同的属性值也很常见:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function logX(object) {
    console.log(object.x);
    }

    const object1 = { x: 1, y: 2 };
    const object2 = { x: 3, y: 4 };

    logX(object1);
    logX(object2);

    考虑到这一点,JavaScript引擎可以根据对象的形状优化对象属性访问。下面是它的工作原理。
    让我们假设有一个对象,有x和y属性,它使用了我们前面讨论过的字典数据结构:它包含key的字符串,它们指向各自的值。object-model1
    如果你访问一个属性,比如object.y,JavaScript引擎在JSObject中查找键y,然后加载相应的属性值,最后返回[[value]]。
    但是这些属性值在内存中是存在哪里呢?我们可以把他们存为JSObject的一部分吗?如果我们假设以后会看到更多具有这种形状的对象,那么将包含键值对的完整字典存储在JSObject本身是很浪费的,因为所有具有相同形状的对象都重复使用属性名。这是大量重复和不必要的内存使用。作为一种优化,引擎单独存储对象的形状。shape-1
    这个形状Shape包含所有的键和值,除了[[value]],在Shape中用值的偏移量offset代替JSobject中的value,这样JavaScript就知道去哪里找到value,每个具有相同形状的JSObject都指向这个形状实例,现在,每个JSObject只需要存储这个对象特有的值。shape-2

    这个好处在有大量对象的时候变得明显,不管有多少对象,只要它们形状相同,我们只需要存储一次形状和属性。

    所有JavaScript引擎都使用形状作为优化手段,但它们并不都称它们为形状:

    • 学术论文称它们为隐藏类(Hidden Classes)
    • V8称它们为映射(Maps)
    • JavaScriptCore称它们为结构(Structures)
    • SpiderMonkey称它们为形状(Shapes)
    • 在本文中,我们将继续使用形状这个术语。

    转换链和树

    如果你有一个具有特定形状的对象,但是你给它添加了一个属性,会发生什么?JavaScript引擎如何找到新形状?

    1
    2
    3
    const object = {};
    object.x = 5;
    object.y = 6;

    这些形状在JavaScript引擎中形成所谓的转换链。这里有一个例子:shape-chain1

    这个对象一开始没有任何属性,所以它指向一个空的形状,下一个状态中,这个对象增加了x为5的属性,所以JavaScript引擎将其指向一个新的形状,这个形状有x属性,值5在第一个偏移量0处添加到JSObject,再下一个状态增加y为6的属性,所以JavaScript引擎将其指向又一个新的形状,这个形状包含x和y属性,并将值6追加到JSObject(偏移量为1)。

    属性添加的顺序影响形状,例如{ x: 4, y: 5 }的形状和{ y: 5, x: 4 }不同

    我们甚至不需要为每个形状存储完整的属性表,每个形状只需要知道它引入的新属性。
    在本例中,我们不必将关于x的信息存储在最后一个形状中,因为它可以在前面的链找到。为了实现这一功能,每个形状都链接着上一个形状。shape-chain-2

    如果你JavaScript代码中写o.x,JavaScript引擎通过遍历转换链查找属性x,直到找到引入属性x的形状。
    但是如果没有办法创建一个转换链呢?例如,如果您有两个空对象,并且为每个空对象添加了不同的属性,该怎么办?

    1
    2
    3
    4
    const object1 = {};
    object1.x = 5;
    const object2 = {};
    object2.y = 6;

    在这种情况下,我们必须用分支,而不是链,我们最终得到一个转换树:shape-tree

    这里我们创建了空对象a,然后给a增加x属性,最终我们得到只有一个value的JSObject和2个形状:一个空形状,一个只有属性x的形状。
    第二个例子一开始也只有一个空对象b,然后增加属性y。最终我们得到两个转换链和三个形状。

    这意味着我们总是从空的形状开始?不一定。
    引擎对已经包含属性的对象应用了一些优化。假设我们从空对象开始添加x,或者有一个已经包含x的对象:

    1
    2
    3
    const object1 = {};
    object1.x = 5;
    const object2 = { x: 6 };

    在第一个例子中,我们一开始是空的形状,然后链接到有x属性的形状,就像我们上文看到的那样;在object2的例子中,直接生成有x的对象,而不是从一个空对象开始并进行转换。empty-shape-bypass

    包含属性x的对象从包含x的形状开始,有效地跳过了空形状。这就是V8和SpiderMonkey所做的。这种优化缩短了转换链,使从字面构造对象更加有效。

    下面的例子是有x,y和z的3D坐标对象。

    1
    2
    3
    4
    const point = {};
    point.x = 4;
    point.y = 5;
    point.z = 6;

    如前所述,这将在内存中创建有3个形状的对象(不包括空形状)。读取对象上的x,比如你在代码中写point.x,JavaScript引擎需要遵循链接列表:它从底部的形状开始,然后逐渐上升到顶部引入x的形状。shapetable-1

    如果我们经常这样做,将会非常慢,尤其是当对象有很多属性的时候。找到该属性的时间是O(n),也就是说,对象的属性数量是线性的。为了加快搜索属性的速度,JavaScript引擎添加了叫形状表的数据结构。这个形状表是一个字典,将属性键映射到引入属性的各个形状。shapetable-2

    等一下,现在我们回到了字典查找……这是我们开始添加形状之前的位置!那么我们为什么还要为形状而烦恼呢?
    这就是形状可以用内联缓存优化的原因(内联缓存见后续文章)。

    ...more
  • FLIP你的动画

    2019-03-04

    次访问

    网页中的动效应该运行在60fps,达到这个帧率并不容易,它去取决于你做了多大的尝试,这里我将用FLIP来帮助你。

    我已经写了一个FLIP库,在这里你可以看到文档和demos。

    FLIP本质上是一个原则,而不是框架或库。这是一种思考动画的方式,尝试让浏览器渲染动画的开销尽可能小,如果一切顺利,应该能达到60fps的动画。

    基本概念

    一般动画直接开始后,在每帧可能做开销大的计算,而这里我们让动画从头开始,动态的重新计算动效属性,然后让浏览器简单的执行渲染。
    FLIP代表First,Last,Invert.Play。

    • First: 元素涉及到动效的初值状态
    • Last: 元素的最终状态
    • Invert: 指出从动画开始到结束元素要如何变化,比如width、height、opacity。然后用transform、opacity来反向设置。如果元素从开始到结束下移了90px,你就要应用transformY(-90px),让元素看起来和开始时一样,虽然实际上不是。
    • Play: 开始变化任何你改变的属性,然后去掉逆变换。因为元素处于最终位置,所以反向设置中的transform、opacity,将使它们从模拟的第一个位置轻松变换到最后一个位置。

    flip-layout-6
    flip-layout-7

    代码分解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 获取初始位置
    var first = el.getBoundingClientRect();

    // 设置到最终位置
    el.classList.add('totes-at-the-end');

    // 再次获取位置信息. 注意这会引起重排.
    var last = el.getBoundingClientRect();

    // 需要的话也可以操作其他可计算的样式。
    // 但要确保尽量使用只触发重绘的属性,比如transform、opacity
    var invert = first.top - last.top;

    // Invert.
    el.style.transform = `translateY(${invert}px)`;

    // 等待下一帧,这样我们就知道所有的样式更改都已生效
    requestAnimationFrame(function() {

    // 开始动画过程
    el.classList.add('animate-on-transforms');

    el.style.transform = '';
    });

    // 用transitionend捕获动画结束
    el.addEventListener('transitionend', tidyUpAnimations);

    然而,也可以用即将来临的Web Animations API,这个会更简单,只是需要Web Animations API polyfill,不过这个补丁很轻量,并且确实很实用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 获取初始位置
    var first = el.getBoundingClientRect();

    // 移动到最终位置
    el.classList.add('totes-at-the-end');

    // 获取最终位置
    var last = el.getBoundingClientRect();

    // Invert.
    var invert = first.top - last.top;

    // 从inverted位置移动到最终last位置.
    var player = el.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
    ], {
    duration: 300,
    easing: 'cubic-bezier(0,0,0.32,1)',
    });

    // 在动画结束时做任何需要的整理
    player.addEventListener('finish', tidyUpAnimations);

    FLIP的用途

    当你在响应到用户输入后用一些动画来响应,这绝对是非常棒的。比如,在Chrome开发峰会上,我展开用户点击的卡片,通常情况下,元素开始和结束的位置、大小都不知道,因为网页是响应式的,元素位置大小都不固定,但FLIP就很有用,它能明确的计算元素,在运行时给出正确的值。

    你能够做这种相对昂贵的预计算是因为利用了用户感知。用户和你的网页有交互之后,你有趁他们不注意的100ms时间来做这些。在这100ms内,用户会觉得网站是立刻响应了的,而你只需要在动画过程中保持60fps。

    我们可以利用这个感知期(100ms),通过javascript做getBoundingClientRect操作(或者你非要用不优雅的getComputedStyle),这样我们使动画细腻流畅,利于重绘。

    可以用transform和opacity重写动画是最好的,如果你在js或CSS中没有用这些属性,那你可以开始优化了,它们会在你改变布局属性(比如width、height、left、top)时,用开销小的属性重写后达到最好的优化效果。

    有时你为了用FLIP需要重新构思你的动画,在多数情况下,我把动画元素单独提取出来,这样我就可以不失真的制作动画了,并且尽可能多的用FLIP。你可能会觉得这样应用过度了,但我觉得不是,因为:

    1、大家想这样。我的一个同事兼好友最近做了一个关于人们想从新闻app上得到什么的调查,最多的回答(这让他很吃惊)不是离线支持、同步、通知,或类似的东西,而是浏览平滑——没有晃动、没有卡顿、没有颤抖。

    2、程序员就是这么做的。当然,这是一种主观的衡量标准,但我已经听过很多次,程序员花了好几天的时间才把过渡做得恰到好处。通过运维服务,我们我网站加载速度都很快,用户会根据他们的操作体验来评价我们的网站,而这些细节将使我们与众不同。

    注意事项

    使用FLIP时要注意以下几点:
    1、不要超过100ms的窗口期。
    记住一定不要超过100ms,如果超过了,你的应用会表现得没反应。通过DevTools关注它,了解是否超出了100ms。

    2、认真设计动画。
    想象下,你在运行一个动画,做一些位移和透明度得改变,然后你决定运行另外一个动画,它需要大量得预计算,这会打断之前运行中的动画,这种情况很糟糕。这里的关键是保证你的预计算工作是在空闲时间或上面说过的100ms窗口期内完成的,这两个动画不会互相阻断。

    3、内容会失真。
    当你使用scale和transform,一些元素会失真。上面说过,我会调整一点结构,来不失真的使用FLIP,不过这可能有争议。

    总结
    我喜欢用FLIP来考虑动画的实现,因为它让JS和CSS配合的很好,用JS来计算,但用CSS处理动画过程,你不需要用CSS来实现动画,虽然你可以只用简单的Web Animations API或JS,或什么其他的简单的方式。主要的一点是,你正在降低每帧的复杂性和成本(通常意味着transform和opacity),以尽量为用户提供最好的体验。

    所以,使用FLIP吧。

    参考

    • Aerotwist - FLIP Your Animations
    • FLIP技术给Web布局带来的变化_JavaScript, FLIP, Animation, Web动画 教程_w3cplus
    ...more
  • Debouncing和Throttling

    2019-02-27

    次访问

    原文地址

    Debounce 和 throttle是两种相似但不同的技术,用来控制函数在一定时间内执行的次数,简单说是用来限频。

    当我们的函数操作DOM事件时,对函数用使用Debounce 或 throttle非常有用,因为我们在DOM事件和函数执行之间加了我们的控制层。

    当我们通过触控板、滚轮、拖动滚动条来滚动时,会很轻易的每秒触发30次滚动事件,但是在智能手机上测试缓慢的滚动时,每秒可触发多达100次滚动事件,你的滚动回调为这样高频的执行做好准备了吗?

    2011年,Twitter网站突现了一个问题:页面在向下滚动时变得卡顿。John Resig发表了一篇关于这个问题的博文,来解释直接将昂贵的函数绑定到滚动事件上多么糟糕。John建议在滚动回调函数外包裹一个每250ms执行一次的循环,这样滚动回调不与滚动事件耦合,简单的避免了用户体验差的问题。

    现在处理类似的高频事件的方式稍微复杂点。下面介绍Debounce, Throttle, 和 requestAnimationFrame。

    Debounce

    Debounce可以把连续的多个调用分组到一个调用中。debounce

    想象下你在电梯里,门开始关闭,突然有人要进来,电梯没有改变楼层,门又开了,每次要关门时,如果有人要进来,都会再次开门,电梯在推迟移动到其他楼层,但在优化资源。

    Leading edge(或immediate)

    在事件发生间隔变的足够长之前,Debounce会一直等待,推迟回调的执行,为什么不立刻触发回调的执行,看起来就像没有用Debounce处理过,只是在快速连续的触发事件停止之前不要再次执行,就像下面这样:debounce-leading

    在underscore.js中, 这个参数叫immediate,而不是leading。

    Debounce实现

    第一次看到Debounce的js实现是2009年John Hann的博文《Debouncing Javascript Methods》,此后不久,Ben Alman创建了一个jQuery插件(不再维护),一年后,Jeremy Ashkenas将其添加到underscore.js中。后来,它被添加到Lodash中。这3种实现在内部略有不同,但它们的接口几乎相同。有一段时间underscore.js采用了Lodash的debounce/throttle实现,直到我2013年发现了_.debounce函数的一个bug,这两种实现逐渐不同。

    Lodash在_.debounce和_.throttle中添加了更多的功能。原来的immediate参数被leading和trailing替代,你可以选择1个,或两个都选,默认只用trailing。

    本文没有介绍的maxWait参数(目前仅在Lodash中)是非常有用的,实际上,在lodash源码中可以看到,Throttle函数的定义_.debounce中有maxWait。

    Debounce示例

    1、resize
    在拖动大小控制器来resize浏览器窗口时,可以触发很多次resize事件。
    完整示例请查看原文Debouncing and Throttling Explained Through Examples | CSS-Tricks

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // Based on http://www.paulirish.com/2009/throttled-smartresize-jquery-event-handler/
    $(document).ready(function(){

    var $win = $(window);
    var $left_panel = $('.left-panel');
    var $right_panel = $('.right-panel');

    function display_info($div) {
    $div.append($win.width() + ' x ' + $win.height() + '<br>');
    }

    $(window).on('resize', function(){
    display_info($left_panel);
    });

    $(window).on('resize', _.debounce(function() {
    display_info($right_panel);
    }, 400));
    });

    resize事件中我们用了默认参数trailing,因为我们只关心浏览器停止改变窗口大小后的最终值。

    2、按键输入关联的ajax请求
    为什么我们要在用户还是输入的时候每50ms向服务器发ajax请求呢?_.debounce可以帮助我们避免这种冗余的造作,只在用户停止输入的时候发送请求。

    此时,使用leading参数没有意思,我们要等最后一个字符输入完毕。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    $(document).ready(function(){

    var $statusKey = $('.status-key');
    var $statusAjax = $('.status-ajax');
    var intervalId;

    // 仅为示例模拟的假ajax
    function make_ajax_request(e){
    var that = this;
    $statusAjax.html('等待时间足够了,现在进行数据请求');

    intervalId = setTimeout(function(){
    $statusKey.html('在这里输入,当你停止输入的时候我会发现');
    $statusAjax.html('');
    $(that).val(''); // 清空输入值
    },2000);
    }

    // 当事件被触发时显示信息
    $('.autocomplete')
    .on('keydown', function (){
    $statusKey.html('等待后续输入... ');
    clearInterval(intervalId);
    })

    // 显示数据请求什么时候会发生(停止输入后)
    // 为了示例更明显,设置了超长的1.3s等待时长。实际情况下最好等待50ms到200ms
    $('.autocomplete').on('keydown', _.debounce(make_ajax_request, 1300));
    });

    类似的情形还可能是等到用户停止输入再校验其输入内容,然后显示类似“您的密码位数太短”的提示语。

    如何使用debounce和throttle以及其中的坑

    自己写一个debounce/throttle功能是很容易的,或者随便从哪个博客里复制一个。我的建议是直接用underscore或Lodash,如果你只需要_.debounce和_.throttle方法,你可以用Lodash的自定义构建输出一个2KB的压缩包,构建命令很简单:

    1
    2
    npm i -g lodash-cli
    lodash include = debounce, throttle

    也就是说,一般会通过webpack/browserify/rollup使用lodash/throttle和lodash/debounce或lodash.throttle和lodash.debounce的包。

    一个常见的坑是不止1次的调用_.debounce函数:

    1
    2
    3
    4
    5
    6
    7
    // 错误的写法
    $(window).on('scroll', function() {
    _.debounce(doSomething, 300);
    });

    // 正确的写法
    $(window).on('scroll', _.debounce(doSomething, 200));

    如果你有需要,在Lodash和underscore.js中,为需要debounce的函数创建一个变量将允许我们调用私有方法debounced_version.cancel()。

    1
    2
    3
    4
    5
    var debounced_version = _.debounce(doSomething, 200);
    $(window).on('scroll', debounced_version);

    // 如果有需要
    debounced_version.cancel();

    Throttle

    使用了_.throttle,我们不允许函数每X毫秒执行超过1次。

    throttle和debounce的主要区别是,throttle保证函数有规律的执行,至少每X毫秒执行1次。
    和debounce一样,throttle也包含在Ben的插件、underscore.js和lodash中。

    Throttle示例

    1、无限滚动
    一个常见的例子:用户向下滑动你的无限滚动页面,你需要检查用户距离页面底部有多远,如果快滑到底部了,你就要通过ajax请求更多的数据来填充页面。
    此时_.debounce没用了,它只能在用户停止滑动时触发,而我们需要在用户滑到底之前请求数据,_.throttle可以让我们不停的检查距离底部的距离。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 这是一个很简单的例子。
    // 或许你想用类似下面这样的插件
    // https://github.com/infinite-scroll/infinite-scroll/blob/master/jquery.infinitescroll.js
    $(document).ready(function(){

    // 每200ms检查一下滚动位置
    $(document).on('scroll', _.throttle(function(){
    check_if_needs_more_content();
    }, 200));

    function check_if_needs_more_content() {
    pixelsFromWindowBottomToBottom = 0 + $(document).height() - $(window).scrollTop() -$(window).height();
    // console.log($(document).height());
    // console.log($(window).scrollTop());
    // console.log($(window).height());
    // console.log(pixelsFromWindowBottomToBottom);
    if (pixelsFromWindowBottomToBottom < 200){
    // 这里会有一个数据请求
    $('body').append($('.item').clone());
    }
    }
    });

    requestAnimationFrame (rAF)

    requestAnimationFrame是另外一种限频方式,可以看成_.throttle(dosomething, 16),但是会精确很多,因为它是为了更好的精确度的浏览器原生API。

    考虑下列优缺点后,我们可以酌情用rAF API替代throttle。

    优点:
    1、目标是60fps (16ms每帧),但内部将决定如何安排渲染的最佳时间;
    2、非常简单且标准的API,将来不会改变,便于维护。

    缺点:
    1、我们需要开始或取消rAF,不像.debounce或.throttle自己在内部处理;
    2、如果浏览器页面不是激活状态,rAF将不会执行,虽然对于滚动、鼠标、键盘事件这并不重要;
    3、虽然所有的现代浏览器支持rAF,但是IE9、Opera Mini、和老的安卓并不支持,现在还需要打补丁;
    4、node.js不支持rAF,所以不能用在服务端的文件系统事件。

    一般来说,如果js函数需要重新计算元素位置,比如直接渲染或动效,我会用requestAnimationFrame。发起数据请求,增加或移除控制动效的css class,我会用_.debounce或_.throttle,这样可以更低的执行频率,比如200ms,而不是16ms。
    你或许觉得rAF应该在underscore或lodash中实现,但他们都没有,因为它用途少并容易直接使用。

    rAF示例

    Paul Lewis写的的《Leaner, Meaner, Faster Animations with requestAnimationFrame》一步一步的介绍了这个例子的逻辑,受他的启发,这里我只会介绍requestAnimation用在滚动上的例子。

    我把_.throttle限频16ms和rAF放在一起对比,实现类似的功能,但极有可能rAF在复杂的场景会表现的更好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 参考https://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-scroll-events
    var latestKnownScrollY = 0,
    ticking = false,
    item = document.querySelectorAll('.item');

    function update() {
    // 重置tick,使我们能够捕获下一个onScroll
    ticking = false;

    item[0].style.width = latestKnownScrollY + 100 + 'px';
    }

    function onScroll() {
    latestKnownScrollY = window.scrollY; // 不支持IE8
    requestTick();
    }

    function requestTick() {
    if(!ticking) {
    requestAnimationFrame(update);
    }
    ticking = true;
    }

    window.addEventListener('scroll', onScroll, false);


    // THROTTLE
    function throttled_version() {
    item[1].style.width = window.scrollY + 100 + 'px';
    }

    window.addEventListener('scroll', _.throttle(throttled_version, 16), false);

    rAF的一个更好的例子我在headroom.js库中看到过,它把逻辑解耦并封装。

    总结

    使用debounce, throttle 和 requestAnimationFrame来优化事件处理,它们略有不同,但都很有用,并相互补充。

    方法 作用
    debounce 把突然爆发的大量事件(比如连续快速的按键输入)组合成1个事件
    throttle 保证每X毫秒执行1次的持续的事件流。比如每200ms检查下滚动位置来触发CSS动效
    requestAnimationFrame 代替throttle。当你在屏幕上重新计算并渲染元素,想保证变化或动效的流畅时使用。注意:不支持IE9
    ...more
  • 页面性能优化之减少回流的10种方法

    2019-02-22

    次访问

    原文地址

    尽管web页面达到2MB的性能仍然是一个热门话题,页面越平滑,用户体验越好,转化率越高!
    也就是说,我对添加肤浅的CSS3动画或不考虑后果地操纵多个DOM元素感到内疚。在应用视觉效果时,浏览器中使用了两个术语:

    重绘Repaints

    当变化影响元素可见性而不是布局的时候会发生一次重绘,比如:opacity, background-color, visibility, 和 outline,重绘的代价很高,因为浏览器必须检查DOM中所有其他节点的可见性——一个或多个节点可能在更改的元素下面变得可见。

    重排Reflows

    重排对性能的影响巨大,需要重新计算所有元素的位置和尺寸,这会导致部分或整个文档的重新渲染,改变单个元素能影响所有的子节点、父节点和兄弟节点。

    重绘和重排时,用户和web页面都不能做其它事情,在极端情况下,css会让js执行变慢,比如滚动不稳定、界面响应不灵敏。

    触发重排途径

    添加、删除或改变元素的可见性

    首先是显而易见的:使用JavaScript更改DOM将导致回流。

    添加、删除或改变css样式

    直接应用CSS样式或更改类名都可能会改变布局。比如更改元素的宽度会影响同一DOM树及其周围的所有元素。

    CSS3 animations 和 transitions

    动画的每一帧都会引起回流。

    用offsetWidth 和 offsetHeight

    读取元素的offsetWidth 和 offsetHeight属性会触发回流来计算属性值。

    用户行为

    一些用户行为会触发回流,比如:hover、在输入框中输入文本、调整窗口大小、更改字体大小、切换样式表或字体。

    回流的影响各不相同,比如相同的操作一些浏览器表现的更好,一些元素的渲染开销会更大。幸运的是,你可以使用一些通用技巧来提升性能.

    通用技巧

    使用最佳布局方案

    不要使用内联样式和table布局!
    内联样式会在下载HTML时影响布局,并触发额外的reflow。
    table布局开销很大,因为解析器需要多次传递去计算单元格维度,使用table时应用fixed定位有一定的优化效果,因为列的宽度是基于标题行的内容。
    主页面布局应用flexbox也会有性能影响,因为在HTML下载的时候,flex items的位置和尺寸可能会变化。

    最小化CSS规则的数量

    css规则越少,重排越快,要尽量避免复杂的css选择器。
    如果您使用的是Bootstrap这样的框架,那么这一点尤其成问题——很少有站点使用了框架提供的所有样式。像Unused CSS、uCSS、grunt-uncss和gulp-uncss这样的工具可以显著减少样式定义和文件大小。

    最小化DOM层级

    稍微复杂一点——减小DOM树大小和每个分支的元素数量。文档越小越浅,回流越快。如果不需要支持古老的浏览器,可以删除不必要的包裹元素。

    更改class的DOM层级要低

    改变class的DOM层级尽可能的低(比如:没有多个深度嵌套的元素),这能减小重排的范围,只重排必要的元素,本质上,只有在对嵌套子节点的影响很小的情况下,才将类更改应用于父节点,比如包装器。

    从文档流中移除复杂的动效

    通过使用position: absolute; 或者 position: fixed;来使有动效的元素脱离文档流,这可以在不影响文档流中的其它元素的情况下更新尺寸和位置。

    更新隐藏的元素

    通过display: none;来隐藏的元素在改变时不会触发重绘和重排,可以的话,在元素可见之前进行更改。

    批量更新元素

    所有的DOM操作在同一次动作中进行能提高性能。
    下面这个简单的例子会引起3次重排:

    1
    2
    3
    4
    var myelement = document.getElementById('myelement');
    myelement.width = '100px';
    myelement.height = '200px';
    myelement.style.margin = '10px';

    我们可以把上面的操作减少到1次重排,这也更便于维护:

    1
    2
    var myelement = document.getElementById('myelement');
    myelement.classList.add('newstyles');
    1
    2
    3
    4
    5
    .newstyles {
    width: 100px;
    height: 200px;
    margin: 10px;
    }

    你也可以最小化接触DOM的时间,假设你要创建这个项目列表:

    • item1
    • item2
    • item3

    每次添加1个元素会触发7次重排-1次添加

      ,3次添加
    • ,3次添加li里面的文本。但是,可以使用DOM片段实现1次reflow,并先在内存中构建节点:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      var
      i, li,
      frag = document.createDocumentFragment(),
      ul = frag.appendChild(document.createElement('ul'));

      for (i = 1; i <= 3; i++) {
      li = ul.appendChild(document.createElement('li'));
      li.textContent = 'item ' + i;
      }

      document.body.appendChild(frag);

      限制受影响的元素

      避免大量元素受影响的情况。
      比如:一个选项卡内容控件,单击一个选项卡将激活不同的内容块。如果每个内容块的高度不同,则会影响周围的元素。
      可以为容器设置固定高度或从文档流中删除控件来提高性能。

      意识到平滑影响性能

      每帧移动1px看起来平滑,但是低端机会卡顿,每帧移动4px,需要1/4的重排,且可能只会稍微不那么平滑。

      用浏览器工具分析渲染问题

      所有主流浏览器都提供工具来强调重排如何影响性能。
      timeline

    ...more

分类归档

  • ai3
  • css2
  • doc2
  • git1
  • js15
  • node2
  • 动效6
  • 图像处理13

标签云

最近文章

  • js遍历那些事儿
  • 详解CSS Grid布局
  • Shutterstock Editor关于Canvas和WebGL图片滤镜的实现
  • javascript异步error
  • win10编译opencvjs
PREVNEXT

© 2018 - 2024 Lovelyun