桂马

桂马 查看完整档案

武汉编辑湖北中医药大学  |  医学信息工程 编辑lutongnet  |  软件工程师 编辑 chenchangyuan.cn/ 编辑
编辑

精心做事,大气为人

个人动态

桂马 回答了问题 · 2020-06-15

linux切换root登录提示module is unknown,求助

是的,处理检查账户认证失败次数限制漏洞,vim /etc/pam.d/system-auth

#配置:  
auth required pam\_tally.so deny=5 unlock\_time=600  
account required pam\_tally.so

没有pam_tally.so模块,如果能去机房登录root的话就可以更改,不行的话只能emergency模式启动修复

关注 1 回答 2

桂马 提出了问题 · 2020-06-11

linux切换root登录提示module is unknown,求助

服务器基线配置问题修复,xshell登录普通账户后,切换不了root

关注 1 回答 2

桂马 赞了文章 · 2020-01-10

用 Web 实现一个简易的音频编辑器

banner

前言

市面上,音频编辑软件非常多,比如 cubase、sonar 等等。虽然它们功能强大,但是在 Web 上的应用却显得心有余而力不足。因为 Web 应用的大多数资源都是存放在网络服务器中的,用 cubase 这些软件,首先要把音频文件下载下来,修改完之后再上传到服务器,最后还要作更新操作,操作效率极其低下。如果能让音频直接在 Web 端进行编辑并更新到服务器,则可以大大提高运营人员的工作效率。下面就为大家介绍一下如何运用 Web 技术实现高性能的音频编辑器。

本篇文章总共分为 3 章:

  • 第 1 章:声音相关的理论知识
  • 第 2 章:音频编辑器的实现方法
  • 第 3 章:音频编辑器的性能优化

第 1 章 - 声音相关的理论知识

理论是实践的依据和根基,了解理论可以更好的帮助我们实践,解决实践中遇到的问题。

1.1 什么是声音

物体振动时激励着它周围的空气质点振动,由于空气具有可压缩性,在质点的相互作用下,振动物体四周的空气就交替地产生压缩与膨胀,并且逐渐向外传播,从而形成声波。声波通过介质(空气、固体、液体)传入到人耳中,带动听小骨振动,经过一系列的神经信号传递后,被人所感知,形成声音。我们之所以能听到钢琴、二胡、大喇叭等乐器发出的声音,就是因为乐器里的某些部件通过振动产生声波,经过空气传播到我们人耳中。

1.2 声音的因素

为什么人们的声音都不一样,为什么有些人的声音很好听,有些人的声音却很猥琐呢?这节介绍一下声音的 3 大因素:频率、振幅和音色,了解这些因素之后大家就知道原因了。

1.2.1 频率

声音既然是声波,就会有振幅和频率。频率越大音高越高,声音就会越尖锐,比如女士的声音频率就普遍比男士的大,所以她们的声音会比较尖锐。人的耳朵通常只能听到 20Hz 到 20kHz 频率范围内的声波。

1.2.2 振幅

声波在空气中传播时,途经的空气会交替压缩和膨胀,从而引起大气压强变化。振幅越大,大气压强变化越大,人耳听到的声波就会越响。人耳可听的声压(声压:声波引起的大气压强变化值)范围为 (2 * 10 ^ - 5)Pa~20Pa,对应的分贝数为 0~120dB。它们之间的换算公式为 20 * log( X / (2 * 10 ^ -5) ),其中 X 为声压。相比较用大气压强来表示声音振幅强度,用分贝表示会更加直观。我们平时在形容物体的声音强度时,一般也都会用分贝,而不会说这个大喇叭发出了多少多少帕斯卡的声压(但听起来好像很厉害得样子)。

1.2.3 音色

频率和振幅都不是决定一个人声音是猥琐还是动听的主要因素,决定声音是否好听的主要因素为音色,音色是由声波中的谐波决定的。自然界中物体振动产生的声波,都不是单一频率单一振幅的波(如正弦波),而是可以分解为 1 个基波加上无数个谐波。基波和谐波都是正弦波,其中谐波的频率是基波的整数倍,振幅比基波小,相位也各不相同。如钢琴中央 dou,它的基波频率为 261,其他无数个谐波频率为 261 的整数倍。声音好听的人,在发声时,声带产生的谐波比较“好听”,而声音猥琐的人,声带产生的谐波比较“猥琐”。

1.3 声音的录制、编辑、回放

不管是欧美的钢琴、小提琴,还是中国的唢呐、二胡、大喇叭,我们不可能想听的时候都叫演奏家们去为我们现场演奏,如果能将这些好听声音存储起来,我们就可以在想听的时候进行回放了。传统的声音录制方法是通过话筒等设备把声音的振动转化成模拟的电流,经过放大和处理,然后记录到磁带或传至音箱等设备发声。这种方法失真较大, 且消除噪音困难, 也不易被编辑和修改,数字化技术可以帮我们解决模拟电流带来的问题。这节我们就来介绍下数字化技术是如何做到的。

1.3.1 录制

声音是一段连续无规则的声波,由无数个正弦波组成。数字化录制过程就是采集这段声波中离散的点的幅值,量化和编码后存储在计算机中。整个过程的基本原理为:声音经过麦克风后根据振幅的不同形成一段连续的电压变化信号,这时用脉冲信号采集到离散的电压变化,最后将这些采集到的结果进行量化和编码后存储到计算机中。采样脉冲频率一般为 44.1kHz,这是因为人耳一般只能听到声波中 20-20kHz 频率正弦波部分,根据采样定律,要从采样值序列完全恢复原始的波形,采样频率必须大于或等于原始信号最高频率的 2 倍。因此,如果要保留原始声波中 20kHz 以内的所有正弦波,采样频率一定要大于等于 40kHz。

1.3.2 编辑

声音数字化后就可以非常方便的对声音进行编辑,如展示声音波形图,截取音频,添加静音效果、渐入淡出效果,通过离散型傅里叶变换查看声音频谱图(各个谐波的分布图)或者进行滤波操作(滤除不想要的谐波部分),这些看似复杂的操作却只需要对量化后的数据简单进行的计算即可实现。

1.3.3 回放

回放过程就是录制过程的逆过程,将录制或者编辑过的音频数据进行解码,去量化还原成离散的电压信号送入大喇叭中。大喇叭如何将电压信号还原成具体的声波振幅,这个没有深入学习,只能到这了。

第2章-音频编辑器的实现方法

通过第 1 章的理论知识,我们知道了什么是声音以及声音的录制和回放,其中录制保存下来的声音数据就叫音频,通过编辑音频数据就能得到我们想要的回放声音效果。这章我们就开始介绍如何用浏览器实现音频编辑工具。浏览器提供了 AudioContext 对象用于处理音频数据,本章首先会介绍下 AudioContext 的基本使用方法,然后介绍如何用 svg 绘制音频波形以及如何对音频数据进行编辑。

2.1 AudioContext 介绍

AudioContext 对音频数据处理过程是一个流式处理过程,从音频数据获取、数据加工、音频数据播放,一步一步流式进行。AudioContext 对象则提供流式加工所需要的方法和属性,如 context.createBufferSource 方法返回一个音频数据缓存节点用于存储音频数据,这是整个流式的起点;context.destination 属性为整个流式的终点,用于播放音频。每个方法都会返回一个 AudioNode 节点对象,通过 AudioNode.connect 方法将所有 AudioNode 节点连接起来。

下面通过一个简单的例子来解锁 AudioContext:

  • 为了方便起见,我们不使用服务器上的音频文件,而使用 FileReader 读取本地音频文件
  • 使用 AudioContext 的 decodeAudioData 方法对读到的音频数据进行解码
  • 使用 AudioContext 的 createBufferSource 方法创建音频源节点,并将解码结果赋值给它
  • 使用 AudioContext 的 connect 方法连接音频源节点到播放终端节点 - AudioContext 的 destination 属性
  • 使用 AudioContext 的 start 方法开始播放
    // 读取音频文件.mp3 .flac .wav等等
    const reader = new FileReader();
    // file 为读取到的文件,可以通过<input type="file" />实现
    reader.readAsArrayBuffer(file);
    reader.onload = evt => {
        // 编码过的音频数据
        const encodedBuffer = evt.currentTarget.result;
        // 下面开始处理读取到的音频数据
        // 创建环境对象
        const context = new AudioContext();
        // 解码
        context.decodeAudioData(encodedBuffer, decodedBuffer => {
            // 创建数据缓存节点
            const dataSource = context.createBufferSource();
            // 加载缓存
            dataSource.buffer = decodedBuffer;
            // 连接播放器节点destination,中间可以连接其他节点,比如音量调节节点createGain(),
            // 频率分析节点(用于傅里叶变换)createAnalyser()等等
            dataSource.connect(context.destination);
            // 开始播放
            dataSource.start();
        })
    }

2.1 什么是音频波形

音频编辑器通过音频波形图形化音频数据,使用者只要编辑音频波形就能得到对应的音频数据,当然内部实现是将对波形的操作转为对音频数据的操作。所谓音频波形,就是时域上,音频(声波)振幅随着时间的变化情况,即 X 轴为时间,Y 轴为振幅。

2.2 绘制波形

我们知道,音频的采样频率为 44.1kHz,所以一段 10 分钟的音频总共会有 10 60 44100 = 26460000,超过 2500 万个数据点。
我们在绘制波形时,即使仅用 1 个像素代表 1 个点的振幅,波形的宽度也将近 2500 万像素,不仅绘制速度慢,而且非常不利于波形分析。
因此,下面介绍一种近似算法来减少绘制的像素点:我们首先将每秒钟采集的 44100 个点平均分成 100 份,相当于 10 毫秒一份,每一份有 441 个点,
算出它们的最大值和最小值。用最大值代表波峰,用最小值代表波谷,然后用线连接所有的波峰和波谷。音频数据在被量化后,值的范围为 [-1,1],
所以我们这里取到的波峰波谷都是在 [-1,1] 的区间内的。
由于数值太小,画出来的波形不美观,我们统一将这些值乘以一个系数比如 64,这样就能很清晰得观察到波形的变化了。
绘制波形可以用 canvas,也可以用 svg,这里我选择使用 svg 进行绘制,因为 svg 是矢量图,可以简化波形缩放算法。

代码实现

  • 为了方便使用 svg 进行绘制,引入 svg.js,并初始化 svg 对象 draw
  • 我们的绘制算法是将每秒钟采集的 44100 个点平均分成 100 份,每份是10毫秒共441个数据点,用它们的最大值和最小值作为这个时间点的波峰和波谷。

然后使用svg.js将所有的波峰波谷通过折线 polyline 连接起来形成最后的波形图。由于音频数据点经过量化处理,范围为[-1,1],为了让波形更加美观,我们
会把波峰、波谷统一乘上一个增幅系数来加大 polyline 线条的幅度

  • 初始化变量 perSecPx(每秒钟绘制像素点的个数)为100,height 波峰波谷的增幅系数为128
  • 以10毫秒为单位获取所有的波峰波谷数据点 peaks,计算方法就是简单得计算出它们各自的最大值和最小值
  • 初始化波形图的宽度 svgWidth = 音频时长(buff.duration) * 每秒钟绘制像素点的个数(perSecPx)
  • 遍历 peaks,将所有的波峰波谷乘上系数并通过 polyline(折线)连接起来
const SVG = require('svg.js');
// 创建svg对象
const draw = SVG(document.getElementById('draw'));
// 波形svg对象
let polyline;
// 波形宽度
let svgWidth;
// 展示波形函数
// buffer - 解码后的音频数据
function displayBuffer(buff) {
    // 每秒绘制100个点,就是将每秒44100个点分成100份,
    // 每一份算出最大值和最小值来代表每10毫秒内的波峰和波谷
    const perSecPx = 100;
    // 波峰波谷增幅系数
    const height = 128;
    const halfHight = height / 2;
    const absmaxHalf = 1 / halfHight;
    // 获取所有波峰波谷
    const peaks = getPeaks(buff, perSecPx);
    // 设置svg的宽度
    svgWidth = buff.duration * perSecPx;
    draw.size(svgWidth);
    const points = [];
    for (let i = 0; i < peaks.length; i += 2) {
        const peak1 = peaks[i] || 0;
        const peak2 = peaks[i + 1] || 0;
        // 波峰波谷乘上系数
        const h1 = Math.round(peak1 / absmaxHalf);
        const h2 = Math.round(peak2 / absmaxHalf);
        points.push([i, halfHight - h1]);
        points.push([i, halfHight - h2]);
    }
    // 连接所有的波峰波谷
    const  polyline = draw.polyline(points);
    polyline.fill('none').stroke({ width: 1 });
}
// 获取波峰波谷
function getPeaks(buffer, perSecPx) {
    const { numberOfChannels, sampleRate, length} = buffer;
    // 每一份的点数=44100 / 100 = 441
    const sampleSize = ~~(sampleRate / perSecPx);
    const first = 0;
    const last = ~~(length / sampleSize)
    const peaks = [];
    // 为方便起见只取左声道
    const chan = buffer.getChannelData(0);
    for (let i = first; i <= last; i++) {
        const start = i * sampleSize;
        const end = start + sampleSize;
        let min = 0;
        let max = 0;
        for (let j = start; j < end; j ++) {
            const value = chan[j];
            if (value > max) {
                max = value;
            }
            if (value < min) {
                min = value;
            }
        }
    }
    // 波峰
    peaks[2 * i] = max;
    // 波谷
    peaks[2 * i + 1] = min;
    return peaks;
}

2.3 缩放操作

有时候,需要对某些区域进行放大或者对整体波形进行缩小操作。由于音频波形是通过 svg 绘制的,缩放算法就会变得非常简单,只需直接对 svg 进行缩放即可。

代码实现

  • 利用svg矢量图特性,我们只要将连接波分波谷的折线宽度乘上系数 scaleX 即可实现缩放功能,scaleX 大于1则放大,scaleX 小于1则缩小。

其实这是一种伪缩放,因为波形的精度始终是10毫秒,只是将折线图拉开了。

function zoom(scaleX) {
    draw.width(svgWidth * scaleX);
    polyline.width(svgWidth * scaleX);
}

2.4 裁剪操作

这节主要介绍下裁剪操作的实现,其他的操作也都是类似的对音频数据作计算。
所谓裁剪,就是从原始音频中去除不要的部分,如噪音部分,或者截取想要的部分,如副歌部分。要实现对音频文件进行裁剪,
首先我们需要对它 有足够的认识。
解码后的音频数据其实是一个 AudioBuffer对象 ,
它会被赋值给 AudioBufferSourceNode 音频源节点的 buffer 属性,并由 AudioBufferSourceNode
将其带进 AudioContext 的处理流里,其中 AudioBufferSourceNode 节点可以通过 AudioContext 的 createBufferSource 方法生成。
看到这里有点懵的同学可以回到 2.1 一节再回顾一下 AudioContext 的基本用法。
AudioBuffer 对象有 sampleRate(采样速率,一般为44.1kHz)、numberOfChannels(声道数)、
duration(时长)、length(数据长度)4 个属性,还有 1 个比较重要的方法 getChannelData ,返回 1 个 Float32Array 类型的数组。我们就是通过改变这个 Float32Array 里的数据来对
音频进行裁剪或者其他操作。裁剪的具体步骤:

  • 首先获取到待处理音频的通道数和采样率
  • 根据裁剪的开始时间点、结束时间点、采样率算出被裁剪的长度:长度 lengthInSamples = (endTime - startTime) * sampleRate,然后通过 AudioContext 的 createBuffer

方法创建一个长度为 lengthInSamples 的 AudioBuffer cutAudioBuffer 用于存放裁剪下来的音频数据,再创建一个长度为原始音频长度减去 lengthInSamples 的 AudioBuffer newAudioBuffer 用于存放裁剪后的音频数据

  • 由于音频往往是多声道的,裁剪操作需要对所有声道都作裁剪,所以我们遍历所有声道,通过 AudioBuffer 的 getChannelData 方法返回各个声道 Float32Array 类型的音频数据
  • 通过 Float32Array 的 subarray 方法获取需要被裁剪的音频数据,并通过 set 方法将数据设置到 cutAudioBuffer,同时将被裁剪之后的音频数据 set 到 newAudioBuffer中
  • 返回 newAudioBuffer 和 cutAudioBuffer
function cut(originalAudioBuffer, start, end) {
    const { numberOfChannels, sampleRate } = originalAudioBuffer;
    const lengthInSamples = (end - start) * sampleRate;
    // offlineAudioContext相对AudioContext更加节省资源
    const offlineAudioContext = new OfflineAudioContext(numberOfChannels, numberOfChannels, sampleRate);
    // 存放截取的数据
    const cutAudioBuffer = offlineAudioContext.createBuffer(
        numberOfChannels,
        lengthInSamples,
        sampleRate
    );
    // 存放截取后的数据
    const newAudioBuffer = offlineAudioContext.createBuffer(
        numberOfChannels,
        originalAudioBuffer.length - cutSegment.length,
        originalAudioBuffer.sampleRate
    );
    // 将截取数据和截取后的数据放入对应的缓存中
    for (let channel = 0; channel < numberOfChannels; channel++) {
        const newChannelData = newAudioBuffer.getChannelData(channel);
        const cutChannelData = cutAudioBuffer.getChannelData(channel);
        const originalChannelData = originalAudioBuffer.getChannelData(channel);
        const beforeData = originalChannelData.subarray(0,
            start * sampleRate - 1);
        const midData = originalChannelData.subarray(start * sampleRate,
            end * sampleRate - 1);
        const afterData = originalChannelData.subarray(
            end * sampleRate
        );
        cutChannelData.set(midData);
        if (start > 0) {
            newChannelData.set(beforeData);
            newChannelData.set(afterData, (start * sampleRate));
        } else {
            newChannelData.set(afterData);
        }
    }
    return {
        // 截取后的数据
        newAudioBuffer,
        // 截取部分的数据
        cutSelection: cutAudioBuffer
    };
};

2.5 撤销和重做操作

每一次操作前,把当前的音频数据保存起来。撤销或者重做时,再把对应的音频数据加载进来。这种方式有不小的性能开销,在第 3 章 - 性能优化章节中作具体分析。

第 3 章-音频编辑器的性能优化

3.1 存在的问题

通过第 2 章介绍的近似法用比较少的点来绘制音频波形,已基本满足波形查看功能。但是仍存在以下 2 个性能问题:

  1. 如果对波形进行缩放分析,比如将波形拉大 10 倍或者更大的时候,即使 svg 绘制的波形可以自适应不失真放大,但由于整个波形放大了 10 倍以上,需要绘制的像素点也增加了 10 倍,导致整个缩放过程非常得卡顿。
  2. 撤销和重做功能此每次操作都需要保存修改后音频数据。一份音频数据,一般都在几 M 到十几 M 不等,每次操作都保存的话,势必会撑爆内存。

3.2 性能优化方案

3.2.1 懒加载

缩放波形卡顿的主要原因就是所需要绘制的像素点太多,因此我们可以通过懒加载的形式减少每次绘制波形时所需要绘制的像素点。
具体方案就是,根据当前波形的滚动位置,实时计算出当前视口需要绘制波形范围。
因此,需要对第 2 章获取波峰波谷的函数 getPeaks 进行一下改造, 增加 2 个参数:

  • buffer:解码后的音频数据 AudioBuffer
  • pxPerSec:每秒钟音频数据横向需要的像素点,这里为 100,每 10 毫秒数据对应 1 组波峰波谷
  • start:当前波形视口滚动起始位置 scrollLeft
  • end:当前波形视口滚动结束位置 scrollLeft + viewWidth。
  • 具体计算时,我们只会取当前视口内对应时间段的音频的波峰和波谷。
  • 比如 start 等于 10,end 等于 100,根据我们 1 个像素对应 1 个 10 毫秒数据量波峰波谷的近似算法,就是取第 10 个 10 毫秒到第 100 个 10 毫秒的波峰波谷,即时间段为 100 毫秒到 1 秒。
function getPeaks(buffer, pxPerSec, start, end) {
    const { numberOfChannels, sampleRate } = buffer;
    const sampleWidth = ~~(sampleRate / pxPerSec);
    const step = 1;
    const peaks = [];
    for (let c = 0; c < numberOfChannels; c++) {
        const chanData = buffer.getChannelData(c);
        for (let i = start, z = 0; i < end; i += step) {
            let max = 0;
            let min = 0;
            for (let j = i * sampleWidth; j < (i + 1) * sampleWidth; j++) {
                const value = chanData[j];
                max = Math.max(value, max);
                min = Math.min(value, min);
            }
            peaks[z * 2] = Math.max(max, peaks[z * 2] || 0);
            peaks[z * 2 + 1] = Math.min(min, peaks[z * 2 + 1] || 0);
            z++;
        }
    }
    return peaks;
}

3.2.2 撤销操作的优化

其实我们只需要保存一份原始未加工过的音频数据,然后在每次编辑前,把当前执行过的指令集全部保存下来,在撤销或者重做时,再把对应的指令集对原始音频数据操作一遍。比如:对波形进行 2 次操作:第 1 次操作时裁剪掉 0-1 秒的部分,保存指令集 A 为裁剪 0-1 秒;第二次操作时,再一次裁剪 2-3 秒的部分,保存指令集 B 为裁剪 0-1 秒、裁剪 2-3 秒。撤销第 2 次操作,只要用前一次指令集 A 对原始波形作一次操作即可。通过这种保存指令集的方式,极大降低了内存的消耗。

总结

声音实质就是声波在人耳中振动被人脑感知,决定音质的因素包括振幅、频率和音色(谐波),人耳只能识别 20-20kHz 频率和 0-120db 振幅的声音。
音频数字化处理过程为:脉冲抽样,量化,编码,解码,加工,回放。
用 canvas 或者 svg 绘制声音波形时,会随着绘制的像素点上升,性能急剧下降,通过懒加载按需绘制的方式可以有效的提高绘制性能。
通过保存指令集的方式进行撤销和重做操作,可以有效的节省内存消耗。
Web Audio API 所能做的事情还有很多很多,期待大家一起去深挖。

参考

本文发布自 网易云音乐前端团队,文章未经授权禁止任何形式的转载。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们
查看原文

赞 27 收藏 13 评论 0

桂马 赞了文章 · 2019-11-05

程序员需要了解的硬核知识之磁盘

此篇文章是 《程序员需要了解的硬核知识》系列第四篇,历史文章请戳

程序员需要了解的硬核知识之内存

程序员需要了解的硬核知识之CPU

程序员需要了解的硬核知识之二进制

我们大家知道,计算机的五大基础部件是 存储器控制器运算器输入和输出设备,其中从存储功能的角度来看,可以把存储器分为内存磁盘,内存我们上面的文章已经介绍过了,那么此篇文章我们来介绍一下磁盘以及内存和磁盘的关系。

认识磁盘

首先,磁盘和内存都具有存储功能,它们都是存储设备。区别在于,内存是通过电流 来实现存储;磁盘则是通过磁记录技术来实现存储。内存是一种高速,造假昂贵的存储设备;而磁盘则是速度较慢、造假低廉的存储设备;电脑断电后,内存中的数据会丢失,而磁盘中的数据可以长久保留。内存是属于内部存储设备,硬盘是属于 外部存储设备。一般在我们的计算机中,磁盘和内存是相互配合共同作业的。

一般内存指的就是主存(负责存储CPU中运行的程序和数据);早起的磁盘指的是软磁盘(soft disk,简称软盘),就是下面这个

image.png

(2000年的时候我曾经我姑姑家最早的计算机中见到过这个,当时还不知道这是啥,现在知道了。)

如今常用的磁盘是硬磁盘(hard disk,简称硬盘),就是下面这个

image.png

程序不读入内存就无法运行

在了解磁盘前,还需要了解一下内存的运行机制是怎样的,我们的程序被保存在存储设备中,通过使用 CPU 读入来实现程序指令的执行。这种机制称为存储程序方式,现在看来这种方式是理所当然的,但在以前程序的运行都是通过改变计算机的布线来读写指令的。

计算机最主要的存储部件是内存和磁盘。磁盘中存储的程序必须加载到内存中才能运行,在磁盘中保存的程序是无法直接运行的,这是因为负责解析和运行程序内容的 CPU 是需要通过程序计数器来指定内存地址从而读出程序指令的。
image.png

磁盘构件

磁盘缓存

我们上面提到,磁盘往往和内存是互利共生的关系,相互协作,彼此持有良好的合作关系。每次内存都需要从磁盘中读取数据,必然会读到相同的内容,所以一定会有一个角色负责存储我们经常需要读到的内容。 我们大家做软件的时候经常会用到缓存技术,那么硬件层面也不例外,磁盘也有缓存,磁盘的缓存叫做磁盘缓存

磁盘缓存指的是把从磁盘中读出的数据存储到内存的方式,这样一来,当接下来需要读取相同的内容时,就不会再通过实际的磁盘,而是通过磁盘缓存来读取。某一种技术或者框架的出现势必要解决某种问题的,那么磁盘缓存就大大改善了磁盘访问的速度

image.png

Windows 操作系统提供了磁盘缓存技术,不过,对于大部分用户来说是感受不到磁盘缓存的,并且随着计算机的演进,对硬盘的访问速度也在不断演进,实际上磁盘缓存到 Windows 95/98 就已经不怎么使用了。

把低速设备的数据保存在高速设备中,需要时可以直接将其从高速设备中读出,这种缓存方式在web中应用比较广泛,web 浏览器是通过网络来获取远程 web 服务器的数据并将其显示出来。因此,在读取较大的图片的时候,会耗费不少时间,这时 web 浏览器可以把获取的数据保存在磁盘中,然后根据需要显示数据,再次读取的时候就不用重新加载了。

虚拟内存

虚拟内存是内存和磁盘交互的第二个媒介。虚拟内存是指把磁盘的一部分作为假想内存来使用。这与磁盘缓存是假想的磁盘(实际上是内存)相对,虚拟内存是假想的内存(实际上是磁盘)。

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个完整的地址空间),但是实际上,它通常被分割成多个物理碎片,还有部分存储在外部磁盘管理器上,必要时进行数据交换。

计算机中的程序都要通过内存来运行,如果程序占用内存很大,就会将内存空间消耗殆尽。为了解决这个问题,WINDOWS 操作系统运用了虚拟内存技术,通过拿出一部分硬盘来当作内存使用,来保证程序耗尽内存仍然有可以存储的空间。虚拟内存在硬盘上的存在形式就是 PAGEFILE.SYS 这个页面文件。

通过借助虚拟内存,在内存不足时仍然可以运行程序。例如,在只剩 5MB 内存空间的情况下仍然可以运行 10MB 的程序。由于 CPU 只能执行加载到内存中的程序,因此,虚拟内存的空间就需要和内存中的空间进行置换(swap),然后运行程序。

虚拟内存与内存的交换方式

刚才我们提到虚拟内存需要和内存中的部分内容做置换才可让 CPU 继续执行程序,那么做置换的方式是怎样的呢?又分为哪几种方式呢?

虚拟内存的方法有分页式分段式 两种。Windows 采用的是分页式。该方式是指在不考虑程序构造的情况下,把运行的程序按照一定大小的页进行分割,并以为单位进行置换。在分页式中,我们把磁盘的内容读到内存中称为 Page In,把内存的内容写入磁盘称为 Page Out。Windows 计算机的页大小为 4KB ,也就是说,需要把应用程序按照 4KB 的页来进行切分,以页(page)为单位放到磁盘中,然后进行置换。

image.png

为了实现内存功能,Windows 在磁盘上提供了虚拟内存使用的文件(page file,页文件)。该文件由 Windows 生成和管理,文件的大小和虚拟内存大小相同,通常大小是内存的 1 - 2 倍。

节约内存

Windows 是以图形界面为基础的操作系统。它的前身是 MS-DOC,最初的版本可以在 128kb 的内存上运行程序,但是现在想要 Windows 运行流畅的花至少要需要 512MB 的内存,但通常往往是不够的。

也许许多人认为可以使用虚拟内存来解决内存不足的情况,而虚拟内存确实能够在内存不足的时候提供补充,但是使用虚拟内存的 Page In 和 Page Out 通常伴随着低速的磁盘访问,这是一种得不偿失的表现。所以虚拟内存无法从根本上解决内存不足的情况。

为了从根本上解决内存不足的情况,要么是增加内存的容量,加内存条;要么是优化应用程序,使其尽可能变小。第一种建议往往需要衡量口袋的银子,所以我们只关注第二种情况。

注意:以下的篇幅会涉及到 C 语言的介绍,是每个程序员(不限语言)都需要知道和了解的知识。

通过 DLL 文件实现函数共有

DLL(Dynamic Link Library)文件,是一种动态链接库 文件,顾名思义,是在程序运行时可以动态加载 Library(函数和数据的集合)的文件。此外,多个应用可以共有同一个 DLL 文件。而通过共有一个 DLL 文件则可以达到节约内存的效果。

例如,假设我们编写了一个具有某些处理功能的函数 MyFunc()。应用 A 和 应用 B 都需要用到这个函数,然后在各自的应用程序中内置 MyFunc()(这个称为Static Link,静态链接)后同时运行两个应用,内存中就存在了同一个函数的两个程序,这会造成资源浪费。

image.png

为了改变这一点,使用 DLL 文件而不是应用程序的执行文件(EXE文件)。因为同一个 DLL 文件内容在运行时可以被多个应用共有,因此内存中存在函数 MyFunc()的程序就只有一个

image.png

Windows 操作系统其实就是无数个 DLL 文件的集合体。有些应用在安装时,DLL文件也会被追加。应用程序通过这些 DLL 文件来运行,既可以节约内存,也可以在不升级 EXE 文件的情况下,通过升级 DLL 文件就可以完成更新。

通过调用 _stdcall 来减少程序文件的大小

通过调用 _stdcall 来减小程序文件的方法,是用 C 语言编写应用时可以利用的高级技巧。我们来认识一下什么是 _stdcall。

_stdcall 是 standard call(标准调用)的缩写。Windows 提供的 DLL 文件内的函数,基本上都是通过 _stdcall 调用方式来完成的,这主要是为了节约内存。另一方面,用 C 语言编写的程序默认都不是 _stdcall 。C 语言特有的调用方式称为 C 调用。C 语言默认不使用 _stdcall 的原因是因为 C 语言所对应的函数传入参数是可变的,只有函数调用方才能知道到底有多少个参数,在这种情况下,栈的清理作业便无法进行。不过,在 C 语言中,如果函数的参数和数量固定的话,指定 _stdcall 是没有任何问题的。

C 语言和 Java 最主要的区别之一在于 C 语言需要人为控制释放内存空间

C 语言中,在调用函数后,需要人为执行栈清理指令。把不需要的数据从接收和传递函数的参数时使用的内存上的栈区域中清理出去的操作叫做 栈清理处理

例如如下代码

// 函数调用方
void main(){
  int a;
  a = MyFunc(123,456);
}

// 被调用方
int MyFunc(int a,int b){
  ...
}

代码中,从 main 主函数调用到 MyFunc() 方法,按照默认的设定,栈的清理处理会附加在 main 主函数这一方。在同一个程序中,有可能会多次调用,导致 MyFunc() 会进行多次清理,这就会造成内存的浪费。

汇编之后的代码如下

push 1C8h                                // 将参数 456( = 1C8h) 存入栈中
push 7Bh                                // 将参数 123( = 7Bh) 存入栈中
call @LTD+15 (MyFunc)(00401014)            // 调用 MyFunc 函数
add esp,8                                // 运行栈清理

C 语言通过栈来传递函数的参数,使用 push 是往栈中存入数据的指令,pop 是从栈中取出数据的指令。32 位 CPU 中,1次 push 指令可以存储 4 个字节(32 位)的数据。上述代码由于进行了两次 push 操作,所以存储了 8 字节的数据。通过 call 指令来调用函数,调用完成后,栈中存储的数据就不再需要了。于是就通过 add esp,8 这个指令,使存储着栈数据的 esp 寄存器前进 8 位(设定为指向高 8 位字节的地址),来进行数据清理。由于栈是在各种情况下都可以利用的内存领域,因此使用完毕后有必要将其恢复到原始状态。上述操作就是执行栈的清理工作。另外,在 C 语言中,函数的返回值,是通过寄存器而非栈来返回的。

栈执行清理工作,在调用方法处执行清理工作和在反复调用方法处执行清理工作不同,使用 _stdcall 标准调用的方式称为反复调用方法,在这种情况下执行栈清理开销比较小。

image.png

磁盘的物理结构

之前我们介绍了CPU、内存的物理结构,现在我们来介绍一下磁盘的物理结构。磁盘的物理结构指的是磁盘存储数据的形式

磁盘是通过其物理表面划分成多个空间来使用的。划分的方式有两种:可变长方式扇区方式。前者是将物理结构划分成长度可变的空间,后者是将磁盘结构划分为固定长度的空间。一般 Windows 所使用的硬盘和软盘都是使用扇区这种方式。扇区中,把磁盘表面分成若干个同心圆的空间就是 磁道,把磁道按照固定大小的存储空间划分而成的就是 扇区

image.png

扇区是对磁盘进行物理读写的最小单位。Windows 中使用的磁盘,一般是一个扇区 512 个字节。不过,Windows 在逻辑方面对磁盘进行读写的单位是扇区整数倍簇。根据磁盘容量不同功能,1簇可以是 512 字节(1 簇 = 1扇区)、1KB(1簇 = 2扇区)、2KB、4KB、8KB、16KB、32KB( 1 簇 = 64 扇区)。簇和扇区的大小是相等的。

不管是硬盘还是软盘,不同的文件是不能存储在同一簇中的,否则就会导致只有一方的文件不能删除。所以,不管多小的文件,都会占用 1 簇的空间。这样一来,所有的文件都会占用 1 簇的整数倍的空间。

我们使用软盘做实验会比较简单一些,我们先对软盘进行格式化,格式化后的软盘空间如下

image.png

接下来,我们保存一个 txt 文件,并在文件输入一个字符,这时候文件其实只占用了一个字节,但是我们看一下磁盘的属性却占用了 512 字节

image.png

然后我们继续写入一些东西,当文件大小到达 512 个字节时,已用空间也是 512 字节,但是当我们继续写入一个字符时,我们点开属性会发现磁盘空间会变为 1024 个字节(= 2 簇),通过这个实验我们可以证明磁盘是以簇为单位来保存的。

文章参考:

磁盘

磁盘缓存

虚拟内存

《程序是怎样跑起来的 第四章》

下面为自己做个宣传,欢迎关注公众号 Java建设者,号主是Java技术栈,热爱技术,喜欢阅读,热衷于分享和总结,希望能把每一篇好文章分享给成长道路上的你。关注公众号回复 002 领取为你特意准备的大礼包,你一定会喜欢并收藏的。

查看原文

赞 44 收藏 34 评论 4

桂马 赞了文章 · 2019-11-05

9个项目助你在2020年成为前端大师!

原文链接:https://dev.to/simonholdorf/9...

DEV的年度热文,读完觉得不错,所以翻译出来供大家参考,个人水平有限,文中可能会有一些翻译错误,可以在评论区指正。

本篇文章一共涉及了9个流行的框架/库,没有具体的介绍使用方法,而是给了一些非常棒的实战教程。

初学者(也许一些有经验的开发者也是一样)在读完官方文档,想写一个项目练手的时候不知道做什么项目好,或是有想法,但是无从下手。那么这篇文章将会给你带来很大的帮助。

更多文章可戳:https://github.com/YvetteLau/...

导读

无论你是编程新手还是经验丰富的开发人员。在这个行业中,我们不得不一直学习新概念和新语言或是框架,才能跟上快速变化。以React为例 —— FaceBook 四年前开源,现在它已经成为了全球JS开发者的首选。但是与此同时,Vue 和 Angular 也有自己的追求者。然后是 Svelte,Next 和 Nuxt.js,Gatsby,Gridsome,quasar 等等,如果你想成为专业的 JavaScript 开发人员,你在使用自己熟悉的框架进行开发的同时,还需要对不同的框架和库有一些了解。

为了帮助你在2020年成为一个前端大神,我收集了9个使用了不同JS框架/库的项目,你可以去构建或者将他们加入到自己未来的开发计划中。记住,没什么比实际开发一个项目更有帮助。所以,不要犹豫,试着去开发一下。

1. 使用React(with hooks)构建一个电影搜索应用

首先,你可以使用React构建一个电影搜索应用。展示如下:

k1.jpeg

你将学到什么?

构建这个项目,你可以使用较新的 Hook API 来提升你的 React 技能。示例项目使用了React组件,很多 hooks 以及一些外部的 API,当然还有一些CSS样式。

技术栈/点

  1. React(Hooks)
  2. create-react-app
  3. JSX
  4. CSS

你可以在这里看到这个示例项目:https://www.freecodecamp.org/...

2.使用Vue构建一个聊天应用

另外一个要介绍给你的很棒的项目是使用Vue构建的聊天应用程序。展示如下:

👀.png

你将学到什么?

您将学习到如何从头开始设置Vue应用,创建组件,处理状态,创建路由,连接到第三方服务,甚至是处理身份验证。

技术栈/点

  1. Vue
  2. Vuex
  3. Vue Router
  4. Vue CLI
  5. Pusher
  6. CSS

这真的是一个非常棒的项目,不管是用来学习Vue或者是提升现有的技能,以应对2020年的发展。你可以查看这个教程: https://www.sitepoint.com/pus...

3. 使用Augular8构建一款漂亮的天气应用

此示例将帮助你使用 Google 的 Angular 8 来构建一块漂亮的天气应用程序:

k3.png

你将学到什么?

该项目将教你一些宝贵的技能,例如从头开始创建应用,从设计到开发,一直到生产就绪部署。

技术栈/点

  1. Angular 8
  2. Firebase
  3. SSR
  4. 网络布局和Flexbox
  5. 移动端友好 && 响应式布局
  6. 深色模式
  7. 漂亮的用户界面

对于这个综合项目,我真正喜欢的是,不是孤立地学习东西,而是从设计到最终部署的整个开发过程。

https://medium.com/@hamedbaat...

4. 使用 Svelte 构建一个 To-Do 应用

与React,Vue和Angular相比,Svelte 还很新,但仍是热门之一。好的,To-Do应用不一定是那里最热门的项目,但这确实可以帮助你提高Svelte技能,如下:

k4.png

你将学到什么?

本教程将向你展示如何从头到尾使用Svelte3制作应用。 它利用了组件,样式和事件处理程序。

技术栈/点

  1. Svelte 3
  2. Components
  3. CSS
  4. ES6语法

Svelte 没有太多优秀的入门项目,这个是我觉得不错的一个上手项目:https://medium.com/codingthes...

5. 使用 Next.js 构建购物车

Next.js 是一个轻量级的 React 服务端渲染应用框架,该项目将向你展示如何构建一个如下所示的购物车:

k5.jpeg

你将学到什么?

在这个项目中,你将学习如何设置 Next.js 的开发环境,创建新页面和组件,获取数据,设置样式并部署一个 next 应用。

技术栈/点

  1. Next.js
  2. 组件和页面
  3. 数据获取
  4. 样式
  5. 部署
  6. SSR和SPA

你可以在此处找到该教程:https://snipcart.com/blog/nex...

6. 使用 Nuxt.js 构建一个多语言博客网站

Nuxt.js 是 Vue 服务端渲染应用框架。你可以创建一个如下所示的应用程序:

K6.jpg

你将学到什么?

这个示例项目从初始设置到最终部署一步一步教你如何使用 Nuxt.js 构建一个完整的网站。它使用了 Nuxt 提供的许多出色功能,如页面和组件以及SCSS样式。

技术栈/点

  • Nuxt.js
  • 组件和页面
  • Storyblok模块
  • Mixins
  • Vuex
  • SCSS
  • Nuxt中间件

这个项目包含了涵盖了 Nuxt.js 的许多出色功能。我个人很喜欢使用 Nuxt 进行开发,你应该尝试使用它,这将使你成为更好的 Vue 开发人员!https://www.storyblok.com/tp/...

除此之外,我还找到了一个B站的视频:https://www.bilibili.com/vide...

7. 使用 Gatsby 构建一个博客

Gatsby是一个出色的静态站点生成器,它允许使用React作为渲染引擎引擎来搭建一个静态站点,它真正具有现代web应用程序所期望的所有优点。该项目如下:

k7.png

你将学到什么?

在本教程中,你将学习如何利用 Gatsby 构建出色的博客。

技术栈/点

  1. Gatsby
  2. React
  3. GraphQL
  4. Plugins & Themes
  5. MDX / Markdown
  6. Bootstrap CSS
  7. Templates

如果你想创建博客,这个示例教你如何利用 React 和 GraphQL 来搭建。并不是说 Wordpress 是一个不好的选择,但是有了 Gatsby ,你可以在使用 React 的同时创建高性能站点!

https://blog.bitsrc.io/how-to...

8. 使用 Gridsome 构建一个博客

Gridsome 和 Vue的关系与 Gatsby 和 React 的关系一样。Gridsome 和 Gatsby 都使用 GraphQL 作为数据层,但是 Gridsome 使用的是 VueJS。这也是一个很棒的静态站点生成器,它将帮助您创建出色的博客:

k8.png

你将学到什么?

该项目将教你如何使用 Gridsome,GraphQL 和 Markdown 构建一个简单的博客,它还介绍了如何通过Netlify 部署应用程序。

技术栈/点

  1. Gridsome
  2. Vue
  3. GraphQL
  4. Markdown
  5. Netlify

当然,这不是最全面的教程,但涵盖了 Gridsome 和 Markdown 的基本概念,可能是一个很好的起点。

https://www.telerik.com/blogs...

9.使用 Quasar 构建一个类似 SoundCloud 的音频播放器

Quasar 是另一个 Vue 框架,也可以用于构建移动应用程序。 在这个项目中,你将创建一个音频播放器应用程序,如下所示:

k9.jpeg

你将学到什么?

不少项目主要关注Web应用程序,但这个项目展示了如何通过 Quasar 框架创建移动应用程序。你应该已经配置了可工作的 Cordova 设置,并配置了 android studio / xcode。 如果没有,在教程中有一个指向quasar 网站的链接,在那里你可以学习如何进行设置。

技术栈/点

  • Quasar
  • Vue
  • Cordova
  • Wavesurfer
  • UI Components

一个展示了Quasar在构建移动应用程序方面的强大功能的小项目:https://www.learningsomething...

总结

本文展示了你可以构建的9个项目,每个项目专注于一个JavaScript框架或库。现在,你可以自行决定:使用以前未使用的框架来尝试一些新的东西或是通过做一个项目来提升已有的技能,或者在2020年完成所有项目?

定稿.png

查看原文

赞 337 收藏 257 评论 20

桂马 发布了文章 · 2019-09-20

定时发送邮件

背景

甲方爸爸:新接入业务在国庆以及军运会期间需要每天巡检业务并发送邮件告知具体情况!

我司:没问题。

甲方爸爸:假期也要发噢。

我司:没问题(。。。)。

刚开始计划指定几个同事轮流发送,业务只要不被攻击一般是没有问题的。但是想一想休息日还要处理工作上的事情(非紧急的)就不爽,近几年一直在做前端的事情,后台碰的少,毕竟也接触过,所以决定搞一个定时发送邮件的程序,遂上网查找资料。

邮件类选择

在网上大致上看了下,目前有两种方案:

  1. MimeMessage
        String title = createTitle();
        String text = createText();
        Properties props = new Properties();
        props.put("mail.smtp.host", "smtp.qq.com");
        props.put("mail.transport.protocol", "smtp");
        props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.put("mail.smtp.auth", "true");
        Session session = Session.getDefaultInstance(props, 
            new javax.mail.Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {        
                 return new PasswordAuthentication(from, passwd);
                }
            });
        MimeMessage message = new MimeMessage(session);
        try {
            message.setFrom(new InternetAddress(from));
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
            message.setSubject(title);
            message.setText(text);
            System.out.println(text);
            Transport.send(message);
        } catch(Exception e) {
            e.printStackTrace();
        }
  1. SimpleMail
        mail.setHostName(host);
        mail.setAuthentication(user, passwd);
        mail.setFrom(user);
        mail.setCharset("UTF-8");
        mail.setSubject(title);
        mail.setSSLOnConnect(true);
        mail.setMsg(content);
        mail.addTo(to);
        mail.send();

在本地重构代码并进行了测试,都是正常发送和接收,个人觉得SimpleMail看起来更加简洁,所以邮件类就选它了

定时器

网上搜索一大堆,具体就不一一介绍了,我用的是Quartz
Quartz 设计有三个核心类,分别是

  • Scheduler 调度器

调度器就相当于一个容器,装载着任务和触发器。该类是一个接口,代表一个 Quartz 的独立运行容器, TriggerJobDetail 可以注册到 Scheduler 中, 两者在 Scheduler 中拥有各自的组及名称, 组及名称是 Scheduler 查找定位容器中某一对象的依据, Trigger 的组及名称必须唯一, JobDetail 的组和名称也必须唯一(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。Scheduler 定义了多个接口方法, 允许外部通过组及名称访问和控制容器中 TriggerJobDetail

  • Job任务

定义需要执行的任务。该类是一个接口,只定义一个方法 execute(JobExecutionContext context),在实现类的 execute 方法中编写所需要定时执行的 Job(任务), JobExecutionContext 类提供了调度应用的一些信息。Job 运行时的信息保存在 JobDataMap 实例中

  • Trigger 触发器

负责设置调度策略。该类是一个接口,描述触发 job 执行的时间触发规则。主要有 SimpleTriggerCronTrigger 这两个子类。当且仅当需调度一次或者以固定时间间隔周期执行调度,SimpleTrigger 是最适合的选择;而 CronTrigger 则可以通过 Cron 表达式定义出各种复杂时间规则的调度方案:如工作日周一到周五的 15:00~16:00 执行调度等

开发测试

发送者邮箱必须开启客户端POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,具体可以在邮箱设置页进行设置,密码使用授权码
  1. 创建SendMail类,将发送邮件逻辑代码进行封装
public class SendMail implements Job {

    private static String user = "11111111@qq.com";
    private static String passwd = "passwd";//授权码
    private static String to = "22222@qq.com";
    private static String host = "smtp.qq.com";
    
    public static void sendMailForSmtp(String title, String content, String[] tos, String[] ccs) throws EmailException {
        SimpleEmail mail = new SimpleEmail();
        // 设置邮箱服务器信息
        mail.setHostName(host);
        // 设置密码验证器passwd为授权码
        mail.setAuthentication(user, passwd);
        // 设置邮件发送者
        mail.setFrom(user);
        // 设置邮件编码
        mail.setCharset("UTF-8");
        // 设置邮件主题
        mail.setSubject(title);
        //SSL方式
        mail.setSSLOnConnect(true);
        // 设置邮件内容
//        mail.setMsg(content);
        // 设置邮件接收者
//        mail.addTo(to);
        mail.addTo(tos);
        mail.addCc(ccs);
        // 发送邮件
        MimeMultipart multipart = new MimeMultipart();
        //邮件正文  
        BodyPart contentPart = new MimeBodyPart();  
        try {
            contentPart.setContent(content, "text/html;charset=utf-8");
            multipart.addBodyPart(contentPart);  
            //邮件附件  
            BodyPart attachmentPart = new MimeBodyPart();
            File file = new File("C:\\lutong\\20190918002.log");
            FileDataSource source = new FileDataSource(file);  
            attachmentPart.setDataHandler(new DataHandler(source));  
            attachmentPart.setFileName(MimeUtility.encodeWord(file.getName()));
            multipart.addBodyPart(attachmentPart);
            mail.setContent(multipart);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (MessagingException e) {
            e.printStackTrace();
        }
        System.out.println(JsonUtil.toJson(mail));
        mail.send();
        System.out.println("mail send success!");
    }
    @Override
    public void execute(JobExecutionContext var1) throws JobExecutionException {
        // TODO Auto-generated method stub
        //多个接收者
        String[] tos = {"11111@qq.com","2222@qq.com"};
        //多个抄送者
        String[] ccs = {"33333@qq.com","44444@qq.com"};
        try {
            SendMail.sendMailForSmtp("title", "hello <br> ccy", tos, ccs);
        } catch (EmailException e) {
            e.printStackTrace();
        }
    }
}
  1. 创建CronTrigger,定时发送任务
public class CronTrigger {
    public static void main(String[] args){
        //初始化job
        JobDetail job = JobBuilder.newJob(SendMail.class)// 创建 jobDetail 实例,绑定 Job 实现类
                .withIdentity("ccy", "group1")//指明job名称、所在组名称
                .build();
        //定义规则
         Trigger trigger = TriggerBuilder
         .newTrigger()
         .withIdentity("ccy", "group1")//triggel名称、组
         .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))//每隔5s执行
         .build();
        Scheduler scheduler = null;
        try {
            scheduler = new StdSchedulerFactory().getScheduler();
            System.out.println("start job...");
            //把作业和触发器注册到任务调度中
            scheduler.scheduleJob(job, trigger);
            //启动
            scheduler.start();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

测试结果

mail.png

后记

技术沟通群欢迎加入
weixinqun_1.png
如果对笔者感兴趣也欢迎你加我好友一起讨论技术,夸夸白
本人微信gm4118679254

查看原文

赞 0 收藏 0 评论 0

桂马 赞了文章 · 2019-09-20

Quartz 2 定时任务(一):基本使用指南

版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009128277

1. Quartz 体系结构

Quartz 设计有三个核心类,分别是 Scheduler(调度器)Job(任务)和 Trigger (触发器),它们是我们使用 Quartz 的关键。

1)Job:定义需要执行的任务。该类是一个接口,只定义一个方法 execute(JobExecutionContext context),在实现类的 execute 方法中编写所需要定时执行的 Job(任务), JobExecutionContext 类提供了调度应用的一些信息。Job 运行时的信息保存在 JobDataMap 实例中。

2)Trigger:负责设置调度策略。该类是一个接口,描述触发 job 执行的时间触发规则。主要有 SimpleTrigger 和 CronTrigger 这两个子类。当且仅当需调度一次或者以固定时间间隔周期执行调度,SimpleTrigger 是最适合的选择;而 CronTrigger 则可以通过 Cron 表达式定义出各种复杂时间规则的调度方案:如工作日周一到周五的 15:00~16:00 执行调度等。

3)Scheduler:调度器就相当于一个容器,装载着任务和触发器。该类是一个接口,代表一个 Quartz 的独立运行容器, Trigger 和 JobDetail 可以注册到 Scheduler 中, 两者在 Scheduler 中拥有各自的组及名称, 组及名称是 Scheduler 查找定位容器中某一对象的依据, Trigger 的组及名称必须唯一, JobDetail 的组和名称也必须唯一(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。Scheduler 定义了多个接口方法, 允许外部通过组及名称访问和控制容器中 Trigger 和 JobDetail。

Scheduler 可以将 Trigger 绑定到某一 JobDetail 中, 这样当 Trigger 触发时, 对应的 Job 就被执行。一个 Job 可以对应多个 Trigger, 但一个 Trigger 只能对应一个 Job。可以通过 SchedulerFactory 创建一个 SchedulerFactory 实例。Scheduler 拥有一个 SchedulerContext,它类似于 SchedulerContext,保存着 Scheduler 上下文信息,Job 和 Trigger 都可以访问 SchedulerContext 内的信息。SchedulerContext 内部通过一个 Map,以键值对的方式维护这些上下文数据,SchedulerContext 为保存和获取数据提供了多个 put()getXxx() 的方法。可以通过 Scheduler#getContext() 获取对应的 SchedulerContext 实例。

4)JobDetail:描述 Job 的实现类及其它相关的静态信息,如:Job 名字、描述、关联监听器等信息。Quartz 每次调度 Job 时, 都重新创建一个 Job 实例, 所以它不直接接受一个 Job 的实例,相反它接收一个 Job 实现类,以便运行时通过 newInstance() 的反射机制实例化 Job。

5)ThreadPool:Scheduler 使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

Job 有一个 StatefulJob 子接口(Quartz 2 后用 @PersistJobDataAfterExecution 注解代替),代表有状态的任务,该接口是一个没有方法的标签接口,其目的是让 Quartz 知道任务的类型,以便采用不同的执行方案。

  • 无状态任务在执行时拥有自己的 JobDataMap 拷贝,对 JobDataMap 的更改不会影响下次的执行。

  • 有状态任务共享同一个 JobDataMap 实例,每次任务执行对 JobDataMap 所做的更改会保存下来,后面的执行可以看到这个更改,也即每次执行任务后都会对后面的执行发生影响。

正因为这个原因,无状态的 Job 并发执行,而有状态的 StatefulJob 不能并发执行。这意味着如果前次的 StatefulJob 还没有执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务需要考虑更多的因素,程序往往拥有更高的复杂度,因此除非必要,应该尽量使用无状态的 Job。

6)Listener:Quartz 拥有完善的事件和监听体系,大部分组件都拥有事件,如:JobListener 监听任务执行前事件、任务执行后事件;TriggerListener 监听触发器触发前事件、触发后事件;TriggerListener 监听调度器开始事件、关闭事件等等,可以注册相应的监听器处理感兴趣的事件。

2. 调度示例

使用 Quartz 进行任务调度:

package org.quartz.examples;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class QuartzTest implements Job {

    /**
     * Quartz requires a public empty constructor so that the
     * scheduler can instantiate the class whenever it needs.
     */
    public QuartzTest() {
    }

    /**
     * 该方法实现需要执行的任务
     */
    @SuppressWarnings("unchecked")
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 从 context 中获取 instName, groupName 以及 dataMap
        String instName = context.getJobDetail().getKey().getName();
        String groupName = context.getJobDetail().getKey().getGroup();
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        // 从 dataMap 中获取 myDescription, myValue 以及 myArray
        String myDescription = dataMap.getString("myDescription");
        int myValue = dataMap.getInt("myValue");
        List<String> myArray = (List<String>) dataMap.get("myArray");
        System.out.println("---> Instance = " + instName + ", group = " + groupName
                + ", description = " + myDescription + ", value =" + myValue
                + ", array item[0] = " + myArray.get(0));
        System.out.println("Runtime: " + new Date().toString() + " <---");
    }

    public static void main(String[] args) throws SchedulerException, InterruptedException {
        // 通过 schedulerFactory 获取一个调度器
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();

        // 创建 jobDetail 实例,绑定 Job 实现类
        // 指明 job 的名称,所在组的名称,以及绑定 job 类
        JobDetail job = JobBuilder.newJob(QuartzTest.class).withIdentity("job1", "group1").build();

        // 定义调度触发规则

        // SimpleTrigger,从当前时间的下 1 秒开始,每隔 1 秒执行 1 次,重复执行 2 次
        /*Trigger trigger = TriggerBuilder.newTrigger()
                // 指明 trigger 的 name 和 group
                .withIdentity("trigger1", "group1")
                // 从当前时间的下 1 秒开始执行,默认为立即开始执行(.startNow())
                .startAt(DateBuilder.evenSecondDate(new Date()))
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1) // 每隔 1 秒执行 1 次
                        .withRepeatCount(2)) // 重复执行 2 次,一共执行 3 次
                .build();*/


        // corn 表达式,先立即执行一次,然后每隔 5 秒执行 1 次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
                .build();

        // 初始化参数传递到 job
        job.getJobDataMap().put("myDescription", "Hello Quartz");
        job.getJobDataMap().put("myValue", 1990);
        List<String> list = new ArrayList<>();
        list.add("firstItem");
        job.getJobDataMap().put("myArray", list);

        // 把作业和触发器注册到任务调度中
        sched.scheduleJob(job, trigger);

        // 启动计划程序(实际上直到调度器已经启动才会开始运行)
        sched.start();

        // 等待 10 秒,使我们的 job 有机会执行
        Thread.sleep(10000);

        // 等待作业执行完成时才关闭调度器
        sched.shutdown(true);
    }
}

运行结果(设置了 sleep 10 秒,故在 0 秒调度一次,5 秒调度一次, 10 秒调度最后一次):

---> Instance = job1, group = group1, description = Hello Quartz, value =1990, array item[0] = firstItem
Runtime: Wed Apr 19 11:24:15 CST 2017 <---
---> Instance = job1, group = group1, description = Hello Quartz, value =1990, array item[0] = firstItem
Runtime: Wed Apr 19 11:24:20 CST 2017 <---
---> Instance = job1, group = group1, description = Hello Quartz, value =1990, array item[0] = firstItem
Runtime: Wed Apr 19 11:24:25 CST 2017 <---

3. cronExpression 表达式

格式:[秒] [分] [时] [每月的第几日] [月] [每周的第几日] [年]

字段名必须的允许值允许的特殊字符
SecondsYES0-59,-*/
MinutesYES0-59,-*/
HoursYES0-23,-*/
Day of monthYES1-31,-*?/LW
MonthYES1-12 or JAN-DEC,-*/
Day of weekYES1-7 or SUN-SAT,-*?/L#
YearNOempty, 1970-2099,-*/

特殊字符说明:

字符含义
*用于 指定字段中的所有值。比如:* 在分钟中表示 每一分钟
?用于 指定日期中的某一天,或是 星期中的某一个星期
-用于 指定范围。比如:10-12 在小时中表示 10 点,11 点,12 点
,用于 指定额外的值。比如:MON,WED,FRI 在日期中表示 星期一, 星期三, 星期五
/用于 指定增量。比如:0/15 在秒中表示 0 秒, 15 秒, 30 秒, 45 秒5/15 在秒中表示 5 秒,20 秒,35 秒,50 秒
L在两个字段中拥有不同的含义。比如:L 在日期(Day of month)表示 某月的最后一天。在星期(Day of week)只表示 7SAT。但是,值L 在星期(Day of week)中表示 某月的最后一个星期几。 比如:6L 表示 某月的最后一个星期五。也可以在日期(Day of month)中指定一个偏移量(从该月的最后一天开始).比如:L-3 表示 某月的倒数第三天
W用于指定工作日(星期一到星期五)比如:15W 在日期中表示 到 15 号的最近一个工作日。如果第十五号是周六, 那么触发器的触发在 第十四号星期五。如果第十五号是星期日,触发器的触发在 第十六号周一。如果第十五是星期二,那么它就会工作开始在 第十五号周二。然而,如果指定 1W 并且第一号是星期六,那么触发器的触发在第三号周一,因为它不会 "jump" 过一个月的日子的边界。
LW可以在日期(day-of-month)合使用,表示 月份的最后一个工作日
#用于 指定月份中的第几天。比如:6#3 表示 月份的第三个星期五(day 6 = Friday and "#3" = the 3rd one in the month)。其它的有,2#1 表示 月份第一个星期一4#5 表示 月份第五个星期三。注意: 如果只是指定 #5,则触发器在月份中不会触发。

注意:字符不区分大小写,MONmon 相同。

3.1 cronExpression 示例

表达式含义
0 0 12 * * ?每天中午 12 点
0 15 10 ? * *每天上午 10 点 15 分
0 15 10 * * ?每天上午 10 点 15 分
0 15 10 * * ? *每天上午 10 点 15 分
0 15 10 * * ? 2005在 2005 年里的每天上午 10 点 15 分
0 * 14 * * ?每天下午 2 点到下午 2 点 59 分的每一分钟
0 0/5 14 * * ?每天下午 2 点到 2 点 55 分每隔 5 分钟
0 0/5 14,18 * * ?每天下午 2 点到 2 点 55 分, 下午 6 点到 6 点 55 分, 每隔 5 分钟
0 0-5 14 * * ?每天下午 2 点到 2 点 5 分的每一分钟
0 10,44 14 ? 3 WED3 月每周三的下午 2 点 10 分和下午 2 点 44 分
0 15 10 ? * MON-FRI每周一到周五的上午 10 点 15 分
0 15 10 15 * ?每月 15 号的上午 10 点 15 分
0 15 10 L * ?每月最后一天的上午 10 点 15 分
0 15 10 L-2 * ?每月最后两天的上午10点15分
0 15 10 ? * 6L每月的最后一个星期五的上午 10 点 15 分
0 15 10 ? * 6L 2002-20052002 年到 2005 年每个月的最后一个星期五的上午 10 点 15 分
0 15 10 ? * 6#3每月的第三个星期五的上午 10 点 15 分
0 0 12 1/5 * ?每月的 1 号开始每隔 5 天的中午 12 点
0 11 11 11 11 ?每年 11 月 11 号上午 11 点 11 分

4. Listener 示例

监听器在运行时将其注册到调度程序中,并且必须给出一个名称(或者,他们必须通过他们的 getName() 来宣传自己的名称)。

4.1 TriggerListener 和 JobListener 示例

侦听器与调度程序的 ListenerManager 一起注册,并且描述了监听器想要接收事件的作业/触发器的 Matcher。

1)注册对特定作业的 JobListener:

sched.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(new JobKey("job1", "group1")));

2)注册对特定组的所有作业的 JobListener:

sched.getListenerManager().addJobListener(new MyJobListener(), GroupMatcher.jobGroupEquals("group1"));

3)注册对两个特定组的所有作业的 JobListener:

sched.getListenerManager().addJobListener(new MyJobListener(), OrMatcher.or(GroupMatcher.jobGroupEquals("group1"), GroupMatcher.jobGroupEquals("group2")));

4)注册一个对所有作业的 JobListener:

sched.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs());

JobListener 实现类:

package org.quartz.examples;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.SchedulerException;

public class MyJobListener implements JobListener {

    @Override
    public String getName() {
        return "MyJobListener"; // 一定要设置名称
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {

    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {

    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        if (jobException != null) {
            try {
                // 立即关闭调度器
                context.getScheduler().shutdown();
                System.out.println("Error occurs when executing jobs, shut down the scheduler.");
                // 给管理员发送邮件...
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
    }
}

Job 实现类:

package org.quartz.examples;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class QuartzTest implements Job {

    /**
     * Quartz requires a public empty constructor so that the
     * scheduler can instantiate the class whenever it needs.
     */
    public QuartzTest() {
    }

    /**
     * 该方法实现需要执行的任务
     */
    @SuppressWarnings("unchecked")
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 故意运行异常,观察监听器是否正常工作
        int i = 1/0;
        
        // 从 context 中获取 instName, groupName 以及 dataMap
        String instName = context.getJobDetail().getKey().getName();
        String groupName = context.getJobDetail().getKey().getGroup();
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        // 从 dataMap 中获取 myDescription, myValue 以及 myArray
        String myDescription = dataMap.getString("myDescription");
        int myValue = dataMap.getInt("myValue");
        List<String> myArray = (List<String>) dataMap.get("myArray");
        System.out.println("---> Instance = " + instName + ", group = " + groupName
                + ", description = " + myDescription + ", value =" + myValue
                + ", array item[0] = " + myArray.get(0));
        System.out.println("Runtime: " + new Date().toString() + " <---");
    }

    public static void main(String[] args) throws SchedulerException, InterruptedException {
        // 通过 schedulerFactory 获取一个调度器
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();

        // 创建 jobDetail 实例,绑定 Job 实现类
        // 指明 job 的名称,所在组的名称,以及绑定 job 类
        JobDetail job = JobBuilder.newJob(QuartzTest.class).withIdentity("job1", "group1").build();

        // 定义调度触发规则

        // SimpleTrigger,从当前时间的下 1 秒开始,每隔 1 秒执行 1 次,重复执行 2 次
        /*Trigger trigger = TriggerBuilder.newTrigger()
                // 指明 trigger 的 name 和 group
                .withIdentity("trigger1", "group1")
                // 从当前时间的下 1 秒开始执行,默认为立即开始执行(.startNow())
                .startAt(DateBuilder.evenSecondDate(new Date()))
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1) // 每隔 1 秒执行 1 次
                        .withRepeatCount(2)) // 重复执行 2 次,一共执行 3 次
                .build();*/


        // corn 表达式,先立即执行 1 次,然后每隔 5 秒执行 1 次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
                .build();

        // 初始化参数传递到 job
        job.getJobDataMap().put("myDescription", "Hello Quartz");
        job.getJobDataMap().put("myValue", 1990);
        List<String> list = new ArrayList<>();
        list.add("firstItem");
        job.getJobDataMap().put("myArray", list);

        // 注册对特定作业的监听器
        sched.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(new JobKey("job1", "group1")));

        // 把作业和触发器注册到任务调度中
        sched.scheduleJob(job, trigger);

        // 启动计划程序(实际上直到调度器已经启动才会开始运行)
        sched.start();

        // 等待 10 秒,使我们的 job 有机会执行
        Thread.sleep(10000);

        // 等待作业执行完成时才关闭调度器
        sched.shutdown(true);
    }
}

运行结果:

[ERROR] 19 四月 11:54:35.361 上午 DefaultQuartzScheduler_Worker-1 [org.quartz.core.JobRunShell]
Job group1.job1 threw an unhandled Exception: 

java.lang.ArithmeticException: / by zero
    at org.quartz.examples.QuartzTest.execute(QuartzTest.java:27)
    at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
    at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
[ERROR] 19 四月 11:54:35.361 上午 DefaultQuartzScheduler_Worker-1 [org.quartz.core.ErrorLogger]
Job (group1.job1 threw an exception.

org.quartz.SchedulerException: Job threw an unhandled exception. [See nested exception: java.lang.ArithmeticException: / by zero]
    at org.quartz.core.JobRunShell.run(JobRunShell.java:213)
    at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
Caused by: java.lang.ArithmeticException: / by zero
    at org.quartz.examples.QuartzTest.execute(QuartzTest.java:27)
    at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
    ... 1 more

Error occurs when executing jobs, shut down the scheduler.

...注册 TriggerListener 的工作原理相同。

4.2 SchedulerListener 示例

SchedulerListener 在调度程序的 SchedulerListener 中注册。SchedulerListener 几乎可以实现任何实现 org.quartz.SchedulerListener 接口的对象。

注册对添加调度器时的 SchedulerListener:

scheduler.getListenerManager().addSchedulerListener(mySchedListener);

注册对删除调度器时的 SchedulerListener

scheduler.getListenerManager().removeSchedulerListener(mySchedListener);

5. 参考

PS:本文针对的 Quartz 版本为 Quartz 2.2.3。官方下载地址:Quartz 2.2.3 .tar.gz

查看原文

赞 8 收藏 15 评论 5

桂马 赞了文章 · 2019-09-17

常用排序算法总结

概述

在计算器科学与数学中,一个排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定排序方式进行排列的一种算法。本文将总结几类常用的排序算法,包括冒泡排序、选择排序、插入排序、快速排序和归并排序,分别使用Java代码实现,简要使用图例方式介绍其实现原理。

算法原理及实现

1、冒泡排序
  • 原理图

  • 理解
通过重复地遍历要排序的列表,比较每对相邻的项目,并在顺序错误的情况下交换它们。
  • Java Code
public class BubbleSort {

    // logic to sort the elements
    public static void bubble_srt(int array[]) {
        int n = array.length;
        int k;
        for (int m = n; m >= 0; m--) {
            for (int i = 0; i < n - 1; i++) {
                k = i + 1;
                if (array[i] > array[k]) {
                    swapNumbers(i, k, array);
                }
            }
            printNumbers(array);
        }
    }

    private static void swapNumbers(int i, int j, int[] array) {

        int temp;
        temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    private static void printNumbers(int[] input) {

        for (int i = 0; i < input.length; i++) {
            System.out.print(input[i] + ", ");
        }
        System.out.println("\n");
    }

    public static void main(String[] args) {
        int[] input = { 4, 2, 9, 6, 23, 12, 34, 0, 1 };
        bubble_srt(input);
    }
}
2、选择排序
  • 原理图

  • 理解
内部循环查找下一个最小(或最大)值,外部循环将该值放入其适当的位置。
  • Java Code
public class SelectionSort {

    public static int[] doSelectionSort(int[] arr){

        for (int i = 0; i < arr.length - 1; i++)
        {
            int index = i;
            for (int j = i + 1; j < arr.length; j++)
                if (arr[j] < arr[index]) 
                    index = j;

            int smallerNumber = arr[index];  
            arr[index] = arr[i];
            arr[i] = smallerNumber;
        }
        return arr;
    }

    public static void main(String a[]){

        int[] arr1 = {10,34,2,56,7,67,88,42};
        int[] arr2 = doSelectionSort(arr1);
        for(int i:arr2){
            System.out.print(i);
            System.out.print(", ");
        }
    }
}

冒泡排序和选择排序的区别

1、冒泡排序是比较相邻位置的两个数,而选择排序是按顺序比较,找最大值或者最小值;

2、冒泡排序每一轮比较后,位置不对都需要换位置,选择排序每一轮比较都只需要换一次位置;

3、冒泡排序是通过数去找位置,选择排序是给定位置去找数。
3、插入排序
  • 原理图

  • 理解
每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
  • Java Code
public class InsertionSort {

    public static void main(String a[]){
        int[] arr1 = {10,34,2,56,7,67,88,42};
        int[] arr2 = doInsertionSort(arr1);
        for(int i:arr2){
            System.out.print(i);
            System.out.print(", ");
        }
    }

    public static int[] doInsertionSort(int[] input){

        int temp;
        for (int i = 1; i < input.length; i++) {
            for(int j = i ; j > 0 ; j--){
                if(input[j] < input[j-1]){
                    temp = input[j];
                    input[j] = input[j-1];
                    input[j-1] = temp;
                }
            }
        }
        return input;
    }
}
4、快速排序
  • 原理图

  • 理解
将原问题分解为若干个规模更小,但结构与原问题相似的子问题,递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
  • Java Code
public class QuickSort {

    private int array[];
    private int length;

    public void sort(int[] inputArr) {

        if (inputArr == null || inputArr.length == 0) {
            return;
        }
        this.array = inputArr;
        length = inputArr.length;
        quickSort(0, length - 1);
    }

    private void quickSort(int lowerIndex, int higherIndex) {

        int i = lowerIndex;
        int j = higherIndex;
        // calculate pivot number, I am taking pivot as middle index number
        int pivot = array[lowerIndex+(higherIndex-lowerIndex)/2];
        // Divide into two arrays
        while (i <= j) {
            /**
             * In each iteration, we will identify a number from left side which 
             * is greater then the pivot value, and also we will identify a number 
             * from right side which is less then the pivot value. Once the search 
             * is done, then we exchange both numbers.
             */
            while (array[i] < pivot) {
                i++;
            }
            while (array[j] > pivot) {
                j--;
            }
            if (i <= j) {
                exchangeNumbers(i, j);
                //move index to next position on both sides
                i++;
                j--;
            }
        }
        // call quickSort() method recursively
        if (lowerIndex < j)
            quickSort(lowerIndex, j);
        if (i < higherIndex)
            quickSort(i, higherIndex);
    }

    private void exchangeNumbers(int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public static void main(String a[]){

        MyQuickSort sorter = new MyQuickSort();
        int[] input = {24,2,45,20,56,75,2,56,99,53,12};
        sorter.sort(input);
        for(int i:input){
            System.out.print(i);
            System.out.print(" ");
        }
    }
}
5、归并排序
  • 原理图

  • 理解
将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。
  • Java Code
public class MergeSort {

    private int[] array;
    private int[] tempMergArr;
    private int length;

    public static void main(String a[]){

        int[] inputArr = {45,23,11,89,77,98,4,28,65,43};
        MyMergeSort mms = new MyMergeSort();
        mms.sort(inputArr);
        for(int i:inputArr){
            System.out.print(i);
            System.out.print(" ");
        }
    }

    public void sort(int inputArr[]) {
        this.array = inputArr;
        this.length = inputArr.length;
        this.tempMergArr = new int[length];
        doMergeSort(0, length - 1);
    }

    private void doMergeSort(int lowerIndex, int higherIndex) {

        if (lowerIndex < higherIndex) {
            int middle = lowerIndex + (higherIndex - lowerIndex) / 2;
            // Below step sorts the left side of the array
            doMergeSort(lowerIndex, middle);
            // Below step sorts the right side of the array
            doMergeSort(middle + 1, higherIndex);
            // Now merge both sides
            mergeParts(lowerIndex, middle, higherIndex);
        }
    }

    private void mergeParts(int lowerIndex, int middle, int higherIndex) {

        for (int i = lowerIndex; i <= higherIndex; i++) {
            tempMergArr[i] = array[i];
        }
        int i = lowerIndex;
        int j = middle + 1;
        int k = lowerIndex;
        while (i <= middle && j <= higherIndex) {
            if (tempMergArr[i] <= tempMergArr[j]) {
                array[k] = tempMergArr[i];
                i++;
            } else {
                array[k] = tempMergArr[j];
                j++;
            }
            k++;
        }
        while (i <= middle) {
            array[k] = tempMergArr[i];
            k++;
            i++;
        }
    }
}

常见排序算法复杂度

参考链接

常用排序算法的时间复杂度和空间复杂度表格
Java Sorting Algorithms
冒泡排序和选择排序的区别
Time Complexities of all Sorting Algorithms



本文作者:taro_秋刀鱼

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

赞 31 收藏 20 评论 1

桂马 发布了文章 · 2019-06-17

前端性能优化之 JavaScript

前端性能优化之 JavaScript

前言

本文为 《高性能 JavaScript》 读书笔记,是利用中午休息时间、下班时间以及周末整理出来的,此书虽有点老旧,但谈论的性能优化话题是每位同学必须理解和掌握的,业务响应速度直接影响用户体验。

一、加载和运行

大多数浏览器使用单进程处理 UI 更新和 JavaScript 运行等多个任务,而同一时间只能有一个任务被执行

脚本位置

将所有script标签放在页面底部,紧靠</body>上方,以保证页面脚本运行之前完成解析

<html>
  <head> </head>
  <body>
    <p>Hello World</p>
    <!--  -->
    <script type="text/javascript" data-original="file.js"></script>
  </body>
</html>

defer & async

常规script脚本浏览器会立即加载并执行,异步加载使用asyncdefer
二者区别在于aysnc为无序,defer会异步根据脚本位置先后依次加载执行

<!-- file1、file2依次加载 -->
<script type="text/javascript" data-original="file1.js" defer></script>
<script type="text/javascript" data-original="file2.js" defer></script>
<!-- file1、file2无序加载 -->
<script type="text/javascript" data-original="file1.js" async></script>
<script type="text/javascript" data-original="file2.js" async></script>

动态脚本

无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。你甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的 HTTP 连接)

var script = document.createElement("script");
script.type = "text/javascript";
script.src = "file1.js";
document.getElementsByTagName("head")[0].appendChild(script);

监听加载函数

function loadScript(url, callback) {
  var script = document.createElement("script");
  script.type = "text/javascript";
  if (script.readyState) {
    //IE
    script.onreadystatechange = function() {
      if (script.readyState == "loaded" || script.readyState == "complete") {
        script.onreadystatechange = null;
        callback();
      }
    };
  } else {
    //Others
    script.onload = function() {
      callback();
    };
  }
  script.src = url;
  document.getElementsByTagName("head")[0].appendChild(script);
}

XHR 注入

前提条件为同域,此处与异步加载一样,只不过使用的是 XMLHttpRequest


总结

  • 将所有script标签放在页面底部,紧靠 body 关闭标签上方,以保证页面脚本运行之前完成解析
  • 将脚本成组打包,页面 script 标签越少加载越快,响应也就更迅速。不论外部脚本文件或者内联代码都是如此

二、数据访问

数据存储在哪里,关系到代码运行期间数据被检索到的速度.每一种数据存储位置都具有特定的读写操作负担。大多数情况下,对一个直接量和一个局部变量数据访问的性能差异是微不足道的。

在 JavaScript 中有四种基本的数据访问位置:

  • 直接量
    直接量仅仅代表自己,而不存储于特定位置。 JavaScript 的直接量包括:字符串,数字,布尔值,对象,数组,函数,正则表达式,具有特殊意义的空值,以及未定义
  • 变量
    使用 var / let 关键字创建用于存储数据值
  • 数组项
    具有数字索引,存储一个 JavaScript 数组对象
  • 对象成员
    具有字符串索引,存储一个 JavaScript 对象

总结

  • 直接量与局部变量访问速度非常快,数组项和对象成员需要更长时间
  • 局部变量比域外变量访问速度快,因为它位于作用域链的第一个对象中。变量在作用域链的位置越深,访问所需要的时间越长。全局变量总是最慢的,因为它们总位于作用域链的最后一环。
  • 避免使用 with 表达式,因为它改变了运行期上下文的作用域链,谨慎对待 try-catch 表达式中 catch 子句,因为它具有同样的效果
  • 嵌套对象成员会造成重大性能影响,尽量少用
  • 属性在原型链中的位置越深,访问速度越慢
  • 将对象成员、数组项、域外变量存入局部变量能提高 js 代码的性能

三、dom 编程

对 DOM 操作代价昂贵,在富网页应用中通常是一个性能瓶颈。通常处理以下三点

  • 访问和修改 DOM 元素
  • 修改 DOM 元素的样式,造成重绘和重新排版
  • 通过 DOM 事件处理用户响应

    一个很形象的比喻是把 DOM 看成一个岛屿,把 JavaScript(ECMAScript)看成另一个岛屿,两者之间以一座收费桥连接(参见 John Hrvatin,微软,MIX09,http://videos.visitmix.com/MI...)。每次 ECMAScript 需要访问 DOM 时,你需要过桥,交一次“过桥费”。你操作 DOM 次数越多,费用就越高。一般的建议是尽量减少过桥次数,努力停留在 ECMAScript 岛上。

DOM 访问和修改

访问或修改元素最坏的情况是使用循环执行此操作,特别是在 HTML 集合中使用循环

function innerHTMLLoop() {
  for (var count = 0; count < 15000; count++) {
    document.getElementById("here").innerHTML += "a";
  }
}

此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对 DOM 元素访问两次:一次
读取 innerHTML 属性能容,另一次写入它


优化如下

function innerHTMLLoop2() {
  var content = "";
  for (var count = 0; count < 15000; count++) {
    content += "a";
  }
  document.getElementById("here").innerHTML += content;
}

你访问 DOM 越多,代码的执行速度就越慢。因此,一般经验法则是:轻轻地触摸 DOM,并尽量保持在 ECMAScript 范围内

节点克隆

使用 DOM 方法更新页面内容的另一个途径是克隆已有 DOM 元素,而不是创建新的——即使用 element.cloneNode()(element 是一个已存在的节点)代替 document.createElement();

当布局和几何改变时发生重排版,下述情况会发生:

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变(边距、填充、边框宽度、宽、高等属性)
  • 内容改变(文本或者图片被另一个不同尺寸的所替代)
  • 最初的页面渲染
  • 浏览器窗口尺寸改变

减少重排次数

  • 改变 display 属性,临时从文档上移除然后再恢复
  • 在文档之外创建并更新一个文档片段,然后将它进行附加
  • 先创建更新节点的副本,再操作副本,最后用副本更新老节点

总结

  • 最小化 DOM 访问,在 JavaScript 端做尽可能多的事情
  • 在反复访问的地方使用局部变量存放 dom 引用
  • 谨慎处理 HTML 集合,因为它们表现‘存在性’,总对底层文档重新查询。将 length 属性缓存到一个变量中,在迭代中使用这个变量。如果经常操作这个集合,可以将集合拷贝到数组中
  • 如果可以,使用速度更快的 API,比如 document.querySelectorAll()和 firstElementChild()
  • 注意重绘和重排,批量修改风格,离线操作 DOM,缓存或减少对布局信息的访问
  • 动画中使用绝对坐标,使用拖放代理
  • 使用事件托管技术中的最小化事件句柄数量

四、算法与流程控制

代码整体结构是执行速度的决定因素之一。代码量少不一定执行快,代码量多,也不一定执行慢,性能损失与代码组织方式和具体问题解决办法直接相关。

Loops

在大多数编程语言中,代码执行时间多数在循环中度过。在一系列编程模式中,循环是最常见的模式之一,提高性能必须控制好循环,死循环和长时间循环会严重影响用户体验。

Types of Loops

  • for
  • while
  • do while
  • for in

前三种循环几乎所有编程语言都能通用,for in 循环遍历对象命名属性(包括自有属性和原型属性)

Loop Performance

循环性能争论的源头是应当选用哪种循环,在 JS 中 for-in 比其他循环明显要慢(每次迭代都要搜索实例或原型属性),除非对数目不详的对象属性进行操作,否则避免使用 for-in。除开 for-in,选择循环应当基于需求而不是性能

减少每次迭代的操作总数可以大幅提高循环的整体性能

优化循环:

  • 减少对象成员和数组项的查找,比如缓存数组长度,避免每次查找数组 length 属性
  • 倒序循环是编程语言中常用的性能优化方法

编程中经常会听到此说法,现在来验证一下,测试样例

var arr = [];
for (var i = 0; i < 100000000; i++) {
  arr[i] = i;
}
var start = +new Date();
for (var j = arr.length; j > -1; j--) {
  arr[j] = j;
}
console.log("倒序循环耗时:%s ms", Date.now() - start); //约180 ms
var start = +new Date();
for (var j = 0; j < arr.length; j++) {
  arr[j] = j;
}
console.log("正序序循环耗时:%s ms", Date.now() - start); //约788 ms

循环正反序测试

基于函数的迭代

尽管基于函数的迭代显得更加便利,它还是比基于循环的迭代要慢一些。每个数组项要关联额外的函数调用是造成速度慢的原因。在所有情况下,基于函数的迭代占用时间是基于循环的迭代的八倍,因此在关注执行时间的情况下它并不是一个合适的办法。

条件表达式

if-else VS switch

使用 if-else 或者 switch 的流行理论是基于测试条件的数量:条件数量较大,倾向使用 switch,更易于阅读
当条件体增加时,if-else 性能负担增加的程度比 switch 更多。
一般来说,if-else 适用于判断两个离散的值或者几个不同的值域,如果判断条件较多 switch 表达式将是更理想的选择

优化 if-else

  • 最小化找到正确分支:将最常见的条件放在首位
  • 查表法 当使用查表法时,必须完全消除所有条件判断,操作转换成一个数组项查询或者一个对象成员查询。

递归

会受浏览器调用栈大小的限制

迭代

任何可以用递归实现的算法可以用迭代实现。使用优化的循环替代长时间运行的递归函数可以提高性能,因为运行一个循环比反复调用一个函数的开销要低

斐波那契

function fibonacci(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

制表

//制表
function memorize(fundamental, cache) {
  cache = cache || {};
  var shell = function(args) {
    if (!cache.hasOwnProperty(args)) {
      cache[args] = fundamental(args);
    }
    return cache[args];
  };
  return shell;
}
//动态规划
function fibonacciOptimize(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  var current = 2;
  var previous = 1;
  for (var i = 3; i <= n; i++) {
    var temp = current;
    current = previous + current;
    previous = temp;
  }
  return current;
}
//计算阶乘
var res1 = fibonacci(40);
var res2 = memorize(fibonacci)(40);
var res3 = fibonacciOptimize(40);
//计算出来的res3优于res2,res2优于res1

总结

运行代码的总量越大,优化带来的性能提升越明显
正如其他编程语言,代码的写法与算法选用影响 JS 的运行时间,与其他编程语言不同,JS 可用资源有限,所以优化固然重要
  • for, while, do while 循环的性能特性相似,谁也不比谁更快或更慢
  • 除非要迭代遍历一个属性未知的对象,否则不要使用 for-in 循环
  • 改善循环的最佳方式减少每次迭代中的运算量,并减少循环迭代次数
  • 一般来说 switch 总比 if-else 更快,但总不是最好的解决方法
  • 当判断条件较多,查表法优于 if-else 和 switch
  • 浏览器的调用栈大小限制了递归算法在 js 中的应用,栈溢出导致其他代码不能正常执行
  • 如果遇到栈溢出,将方法修改为制表法,可以避免重复工作

五、字符串和正则表达式 String And Regular Expression

在 JS 中,正则是必不可少的东西,它的重要性远远超过烦琐的字符串处理

字符串链接 Stirng Concatenation

字符串连接表现出惊人的性能紧张。通常一个任务通过一个循环,向字符串末尾不断地添加内容,来创建一个字符串(例如,创建一个 HTML 表或者一个 XML 文档),但此类处理在一些浏览器上表现糟糕而遭人痛恨

MethodExample
+str = 'a' + 'b' + 'c';
+=str = 'a';
str += 'b';
str += 'c';
array.join()str = ['a','b','c'].join('');
string.concat()str = 'a';
str = str.concat('b', 'c');

当连接少量的字符串,上述的方式都很快,可根据自己的习惯使用;
当合并字符串的长度和数量增加之后,有些函数就开始发挥其作用了

+ & +=

str += "a" + "b";

此代码执行时,发生四个步骤

  1. 内存中创建了一个临时字符串
  2. 临时字符串的值被赋予'ab'
  3. 临时串与 str 进行连接
  4. 将结果赋予 str

下面的代码通过两个离散的表达式直接将内容附加在 str 上避免了临时字符串

str += "a";
str += "b";

事实上用一行代码就可以解决

str = str + "a" + "b";

赋值表达式以 str 开头,一次追加一个字符串,从左至右依次连接。如果改变了连接顺序(例如:str = 'a' + str + 'b'),你会失去这种优化,这与浏览器合并字符串时分配内存的方法有关。除 IE 外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字符串拷贝到它的尾部。如果在一个循环中,基本字符串在左端,可以避免多次复制一个越来越大的基本字符串。

Array.prototype.join

Array.prototype.join 将数组的所有元素合并成一个字符串,并在每个元素之间插入一个分隔符字符串。若传递一个空字符串,可将数组的所有元素简单的拼接起来

var start = Date.now();
var str = "I'm a thirty-five character string.",
  newStr = "",
  appends = 5000000;
while (appends--) {
  newStr += str;
}
var time = Date.now() - start;
console.log("耗时:" + time + "ms"); //耗时:1360ms
var start = Date.now();
var str = "I'm a thirty-five character string.",
  strs = [],
  newStr = "",
  appends = 5000000;
while (appends--) {
  strs[strs.length] = str;
}
newStr = strs.join("");
var time = Date.now() - start;
console.log("耗时:" + time + "ms"); //耗时:414ms

这一难以置信的改进结果是因为避免了重复的内存分配和拷贝越来越大的字符串。

String.prototype.concat

原生字符串连接函数接受任意数目的参数,并将每一个参数都追加在调用函数的字符串上

var str = str.concat(s1);
var str = str.concat(s1, s2, s3);
var str = String.prototype.concat.apply(str, array);

大多数情况下 concat 比简单的+或+=慢一些

Regular Expression Optimization 正则表达式优化

许多因素影响正则表达式的效率,首先,正则适配的文本千差万别,部分匹配时比完全不匹配所用的时间要长,每种浏览器的正则引擎也有不同的内部优化

正则表达式工作原理

  1. 编译
    当你创建了一个正则表达式对象之后(使用一个正则表达式直接量或者 RegExp 构造器),浏览器检查你的模板有没有错误,然后将它转换成一个本机代码例程,用执行匹配工作。如果你将正则表达式赋给一个变量,你可以避免重复执行此步骤。
  2. 设置起始位置
    当一个正则表达式投入使用时,首先要确定目标字符串中开始搜索的位置。它是字符串的起始位置,或者由正则表达式的 lastIndex 属性指定,但是当它从第四步返回到这里的时候(因为尝试匹配失败),此位置将位于最后一次尝试起始位置推后一个字符的位置上
  3. 匹配每个正则表达式的字元
    正则表达式一旦找好起始位置,它将一个一个地扫描目标文本和正则表达式模板。当一个特定字元匹配失败时,正则表达式将试图回溯到扫描之前的位置上,然后进入正则表达式其他可能的路径上
  4. 匹配成功或失败
    如果在字符串的当前位置上发现一个完全匹配,那么正则表达式宣布成功。如果正则表达式的所有可能路径都尝试过了,但是没有成功地匹配,那么正则表达式引擎回到第二步,从字符串的下一个字符重新尝试。只有字符串中的每个字符(以及最后一个字符后面的位置)都经历了这样的过程之后,还没有成功匹配,那么正则表达式就宣布彻底失败。

理解回溯

在大多数现代正则表达式实现中(包括 JavaScript 所需的),回溯是匹配过程的基本组成部分。它很大程度上也是正则表达式如此美好和强大的根源。然而,回溯计算代价昂贵,如果你不够小心的话容易失控。虽然回溯是整体性能的唯一因素,理解它的工作原理,以及如何减少使用频率,可能是编写高效正则表达式最重要的关键点。

正则表达式匹配过程

  • 当一个正则表达式扫描目标字符串时,它从左到右逐个扫描正则表达式的组成部分,在每个位置上测试能不能找到一个匹配。对于每一个量词和分支,都必须决定如何继续进行。如果是一个量词(诸如*,+?,或者{2,}),正则表达式必须决定何时尝试匹配更多的字符;如果遇到分支(通过|操作符),它必须从这些选项中选择一个进行尝试。
  • 每当正则表达式做出这样的决定,如果有必要的话,它会记住另一个选项,以备将来返回后使用。如果所选方案匹配成功,正则表达式将继续扫描正则表达式模板,如果其余部分匹配也成功了,那么匹配就结束了。但是如果所选择的方案未能发现相应匹配,或者后来的匹配也失败了,正则表达式将回溯到最后一个决策点,然后在剩余的选项中选择一个。它继续这样下去,直到找到一个匹配,或者量词和分支选项的所有可能的排列组合都尝试失败了,那么它将放弃这一过程,然后移动到此过程开始位置的下一个字符上,重复此过程。

示例分析

/h(ello|appy) hippo/.test("hello there, happy hippo");

此正则表达式匹配“hello hippo”或“happy hippo”。测试一开始,它要查找一个 h,目标字符串的第一个字母恰好就是 h,它立刻就被找到了。接下来,子表达式(ello|appy)提供了两个处理选项。正则表达式选择最左边的选项(分支选择总是从左到右进行),检查 ello 是否匹配字符串的下一个字符。确实匹配,然后正则表达式又匹配了后面的空格。然而在这一点上它走进了死胡同,因为 hippo 中的 h 不能匹配字符串中的下一个字母 t。此时正则表达式还不能放弃,因为它还没有尝试过所有的选择,随后它回溯到最后一个检查点(在它匹配了首字母 h 之后的那个位置上)并尝试匹配第二个分支选项。但是没有成功,而且也没有更多的选项了,所以正则表达式认为从字符串的第一个字符开始匹配是不能成功的,因此它从第二个字符开始,重新进行查找。它没有找到 h,所以就继续向后找,直到第 14 个字母才找到,它匹配 happy 的那个 h。然后它再次进入分支过程。这次 ello 未能匹配,但是回溯之后第二次分支过程中,它匹配了整个字符串“happy hippo”(如图 5-4)。匹配成功了。

回溯失控

当一个正则表达式占用浏览器上秒,上分钟或者更长时间时,问题原因很可能是回溯失控。正则表达式处理慢往往是因为匹配失败过程慢,而不是匹配成功过程慢。

var reg = /<html>[\s\S]*?<head>[\s\S]*?<title>[\s\S]*?<\/title>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/;
//优化如下
var regOptimize = /<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<title>))\2(?=([\s\S]*?<\/title>))\3(?=([\s\S]*?<\/head>))\4(?=([\s\S]*?<body>))\5(?=([\s\S]*?<\/body>))\6[\s\S]*?<\/html>/;

现在如果没有尾随的</html>那么最后一个[sS]*?将扩展至字符串结束,正则表达式将立刻失败因为没有回溯点可以返回

提高正则表达式效率的更多方法

  • 关注如何让匹配更快失败
  • 正则表达式以简单的,必需的字元开始
  • 编写量词模板,使它们后面的字元互相排斥
  • 减少分支的数量,缩小它们的范围
  • 使用非捕获组
  • 捕获感兴趣的文字,减少后处理
  • 暴露所需的字元
  • 使用适当的量词
  • 将正则表达式赋给变量,以重用它们
  • 将复杂的正则表达式拆分为简单的片断

什么时候不应该使用正则表达式

var endsWithSemicolon = /;$/.test(str);

你可能觉得很奇怪,虽说当前没有哪个浏览器聪明到这个程度,能够意识到这个正则表达式只能匹配字符串的末尾。最终它们所做的将是一个一个地测试了整个字符串。字符串的长度越长(包含的分号越多),它占用的时间也越长

var endsWithSemicolon = str.charAt(str.length - 1) == ";";

这种情况下,更好的办法是跳过正则表达式所需的所有中间步骤,简单地检查最后一个字符是不是分号:

这个例子使用 charAt 函数在特定位置上读取字符。字符串函数 slice,substr,和 substring 可用于在特定位置上提取并检查字符串的值

所有这些字符串操作函数速度都很快,当您搜索那些不依赖正则表达式复杂特性的文本字符串时,它们有助于您避免正则表达式带来的性能开销

字符串修剪

正则表达式允许你用很少的代码实现一个修剪函数,这对 JavaScript 关心文件大小的库来说十分重要。可能最好的全面解决方案是使用两个子表达式:一个用于去除头部空格,另一个用于去除尾部空格。这样处理简单而迅速,特别是处理长字符串时。

//方法 用正则表达式修剪
// trim1
String.prototype.trim = function() {
  return this.replace(/^\s+/, "").replace(/\s+$/, "");
};
//trim2
String.prototype.trim = function() {
  return this.replace(/^\s+|\s+$/g, "");
};
// trim 3
String.prototype.trim = function() {
  return this.replace(/^\s*([\s\S]*?)\s*$/, "$1");
};
// trim 4
String.prototype.trim = function() {
  return this.replace(/^\s*([\s\S]*\S)?\s*$/, "$1");
};
// trim 5
String.prototype.trim = function() {
  return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
};
//方法二 不使用正则表达式修剪
String.prototype.trim = function() {
  var start = 0;
  var end = this.length - 1;
  //ws 变量包括 ECMAScript 5 中定义的所有空白字符
  var ws =
    "\n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\ufeff";
  while (ws.indexOf(this.charAt(start)) > -1) {
    start++;
  }
  while (end > start && ws.indexOf(this.charAt(end)) > -1) {
    end--;
  }
  return this.slice(start, end + 1);
};
//方法三 混合解决方案
String.prototype.trim = function() {
  var str = this.replace(/^\s+/, ""),
    end = str.length - 1,
    ws = /\s/;
  while (ws.test(str.charAt(end))) {
    end--;
  }
  return str.slice(0, end + 1);
};

简单地使用两个子正则表达式在所有浏览器上处理不同内容和长度的字符串时,均表现出稳定的性能。因此它可以说是最全面的解决方案。混合解决方案在处理长字符串时特别快,其代价是代码稍长,在某些浏览器上处理尾部长空格时存在弱点

总结

  • 使用简单的+和+=取代数组联合,可避免(产生)不必要的中间字符串
  • 当连接数量巨大或尺寸巨大的字符串时,使用数组联合
  • 使相邻字元互斥,避免嵌套量词对一个字符串的相同部分多次匹配,通过重复利用前瞻操作的原子特性去除不必要的回溯

六、响应接口

用户倾向于重复尝试这些不发生明显变化的动作,所以确保网页应用程序的响应速度也是一个重要的性能关注点

浏览器 UI 线程

JavaScript 和 UI 更新共享的进程通常被称作浏览器 UI 线程, UI 线程围绕着一个简单的队列系统工作,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。这些任务不是运行 JavaScript 代码,就是执行 UI 更新,包括重绘和重排版.
大多数浏览器在 JavaScript 运行时停止 UI 线程队列中的任务,也就是说 JavaScript 任务必须尽快结束,以免对用户体验造成不良影响

Brendan Eich,JavaScript 的创造者,引用他的话说,“[JavaScript]运行了整整几秒钟很可能是做错了什么……”

定时器基础

定时器与 UI 线程交互的方式有助于分解长运行脚本成为较短的片断

定时器精度

所有浏览器试图尽可能准确,但通常会发生几毫秒滑移,或快或慢。正因为这个原因,定时器不可用于测量实际时间

总结

  • JavaScript 运行时间不应该超过 100 毫秒。过长的运行时间导致 UI 更新出现可察觉的延迟,从而对整体用户体验产生负面影响
  • JavaScript 运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript 长时间运行将导致用户体验混乱和脱节。
  • 同一时间只有一个定时器存在,只有当这个定时器结束时才创建一个新的定时器。以这种方式使用定时器不会带来性能问题
  • 定时器可用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务

七、Ajax

目前最常用的方法中,XMLHttpRequest(XHR)用来异步收发数据。所有现代浏览器都能够很好地支持它,而且能够精细地控制发送请求和数据接收。你可以向请求报文中添加任意的头信息和参数(包括 GET 和 POST),并读取从服务器返回的头信息,以及响应文本自身

请求数据

五种常用技术用于向服务器请求数据

  • XMLHttpRequest (XHR)
  • Dynamic script tag insertion 动态脚本标签插入
  • iframes
  • Comet
  • Multipart XHR 多部分的 XHR

XMLHttpRequest

//封装ajax
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status >= 200) {
    //
  }
};
xhr.open(type, url, true);
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(null);

动态脚本标签插入

发送数据

  • XMLHttpRequest
  • 图像灯标

数据格式

通过 Douglas Crockford 的发明与推广,JSON 是一个轻量级并易于解析的数据格式,它按照 JavaScript 对象和数组字面语法所编写

Ajax 性能向导

数据传输技术和数据格式

  • 缓存数据
  • 设置 HTTP 头
  • 本地存储数据

总结

高性能 Ajax 包括:知道你项目的具体需求,选择正确的数据格式和与之相配的传输技术

  • 减少请求数量,可合并 js 和 css 文件
  • 缩短页面的加载时间,在页面其它内容加载之后,使用 Ajax 获取少量重要文件
  • JSON 是高性能 AJAX 的基础,尤其在使用动态脚本注入时
  • 学会何时使用一个健壮的 Ajax 库,何时编写自己的底层 Ajax 代码

封装自己的 ajax 库

(function(root) {
  root.MyAjax = (config = {}) => {
    let url = config.url;
    let type = config.type || "GET";
    let async = config.async || true;
    let headers = config.headers || [];
    let contentType = config.contentType || "application/json;charset=utf-8";
    let data = config.data;
    let dataType = config.dataType || "json";
    let successFn = config.success;
    let errorFn = config.error;
    let completeFn = config.complete;
    let xhr;
    if (window.XMLHttpRequest) {
      xhr = new XMLHttpRequest();
    } else {
      xhr = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          let rsp = xhr.responseText || xhr.responseXML;
          if (dataType === "json") {
            rsp = eval("(" + rsp + ")");
          }
          successFn(rsp, xhr.statusText, xhr);
        } else {
          errorFn(xhr.statusText, xhr);
        }
        if (completeFn) {
          completeFn(xhr.statusText, xhr);
        }
      }
    };
    xhr.open(type, url, async);
    //设置超时
    if (async) {
      xhr.timeout = config.timeout || 0;
    }
    //设置请求头
    for (let i = 0; i < headers.length; ++i) {
      xhr.setRequestHeader(headers[i].name, headers[i].value);
    }
    xhr.setRequestHeader("Content-Type", contentType);
    //send
    if (
      typeof data == "object" &&
      contentType === "application/x-www-form-urlencoded"
    ) {
      let s = "";
      for (attr in data) {
        s += attr + "=" + data[attr] + "&";
      }
      if (s) {
        s = s.slice(0, s.length - 1);
      }
      xhr.send(s);
    } else {
      xhr.send(data);
    }
  };
})(window);

八、编程实践

  • 避免二次评估,比如 eval,Function
  • 使用对象/数组直接量
  • 不要重复工作
  • 延迟加载
  • 条件预加载
  • 使用速度快的部分
  • 位操作运算符
    四种位逻辑操作符

    • 位与
      比如判断数奇偶
    num % 2 === 0; //取模与0进行判断
    num & 1; //位与1结果位1则为奇数,为0则为偶数
    • 位或
    • 位异或
    • 位非
  • 位掩码
    位掩码在计算机科学中是一种常用的技术,可同时判断多个布尔 选项,快速地将数字转换为布尔标志数组。掩码中每个选项的值都等于 2 的幂
var OPTION_A = 1;
var OPTION_B = 2;
var OPTION_C = 4;
var OPTION_D = 8;
var OPTION_E = 16;

通过定义这些选项,你可以用位或操作创建一个数字来包含多个选项:

var options = OPTION_A | OPTION_C | OPTION_D;

可以使用位与操作检查一个给定的选项是否可用

//is option A in the list?
if (options & OPTION_A) {
  //do something
}
//is option B in the list?
if (options & OPTION_B) {
  //do something
}

像这样的位掩码操作非常快,正因为前面提到的原因,操作发生在系统底层。如果许多选项保存在一起并经常检查,位掩码有助于加快整体性能

原生方法

无论你怎样优化 JavaScript 代码,它永远不会比 JavaScript 引擎提供的原生方法更快。经验不足的 JavaScript 开发者经常犯的一个错误是在代码中进行复杂的数学运算,而没有使用内置 Math 对象中那些性能更好的版本。Math 对象包含专门设计的属性和方法,使数学运算更容易。

//查看Math对象所有方法
Object.getOwnPropertyNames(Math);

总结

  • 通过避免使用 eval()和 Function()构造器避免二次评估。此外,给 setTimeout()和 setInterval()传递函数参数而不是字符串参数。
  • 创建新对象和数组时使用对象直接量和数组直接量。它们比非直接量形式创建和初始化更快。
  • 避免重复进行相同工作。当需要检测浏览器时,使用延迟加载或条件预加载
  • 当执行数学远算时,考虑使用位操作,它直接在数字底层进行操作。
  • 原生方法总是比 JavaScript 写的东西要快。尽量使用原生方法

九、创建并部署高性能 JavaScript 应用程序

  • 合并 js 文件,减少 HTTP 请求的数量
  • 以压缩形式提供 js 文件(gzip 编码)
  • 通过设置 HTTP 响应报文头使 js 文件可缓存,通过向文件名附加时间戳解决缓存问题
  • 使用CDN提供 js 文件,CDN 不仅可以提高性能,它还可以为你管理压缩和缓存

十、工具

当网页或应用程序变慢时,分析网上传来的资源,分析脚本的运行性能,使你能够集中精力在那些需要努力优化的地方。

  • 使用网络分析器找出加载脚本和其它页面资源的瓶颈所在,这有助于决定哪些脚本需要延迟加载,或者进行进一步分析
  • 尽量延迟加载脚本以使页面渲染速度更快,向用户提供更好的整体体验。
  • 使用性能分析器找出脚本运行时速度慢的部分,检查每个函数所花费的时间,以及函数被调用的次数,通过调用栈自身提供的一些线索来找出哪些地方应当努力优化

后记

能读到最后的同学也不容易,毕竟篇幅稍长。本书大概花了三周的零碎时间读完,建议大家读一读。如果大家在看书过程中存在疑问,不妨打开电脑验证书中作者的言论,或许会更加深刻。

若文中有错误欢迎大家评论指出,或者加我微信好友一起交流gm4118679254
查看原文

赞 0 收藏 0 评论 2

桂马 赞了文章 · 2019-05-27

浅谈script标签的defer和async

1. 什么鬼

今天在做一个小需的时候,忽然看到前辈一句吊炸天的代码

    <script data-original="#link("xxxx/xx/home/home.js")" type="text/javascript" async defer></script>

卧槽,竟然同时有asyncdefer属性,心想着肯定是前辈老司机的什么黑科技,两个一块儿肯定会发生什么神奇化学反应,于是赶紧怀着一颗崇敬的心去翻书翻文档,先复习一下各自的定义。

2. 调查一番

先看看asyncdefer各自的定义吧,翻开红宝书望远镜,是这么介绍的

2.1 defer

这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。

HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在DOMContentLoad时间触发前执行,因此最好只包含一个延迟脚本。

2.2 async

这个属性与defer类似,都用于改变处理脚本的行为。同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行。

第二个脚本文件可能会在第一个脚本文件之前执行。因此确保两者之间互不依赖非常重要。指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。

概括来讲,就是这两个属性都会使script标签异步加载,然而执行的时机是不一样的。引用segmentfault上的一个回答中的一张图图片描述蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

也就是说async是乱序的,而defer是顺序执行,这也就决定了async比较适用于百度分析或者谷歌分析这类不依赖其他脚本的库。从图中可以看到一个普通的<script>标签的加载和解析都是同步的,会阻塞DOM的渲染,这也就是我们经常会把<script>写在<body>底部的原因之一,为了防止加载资源而导致的长时间的白屏,另一个原因是js可能会进行DOM操作,所以要在DOM全部渲染完后再执行。

2.3 really?

然而,这张图(几乎是百度搜到的唯一答案)是不严谨的,这只是规范的情况,大多数浏览器在实现的时候会作出优化。

来看看chrome是怎么做的

《WebKit技术内幕》:

  1. 当用户输入网页URL的时候,WebKit调用其资源加载器加载该URL对应的网页。

  2. 加载器依赖网络模块建立连接,发送请求并接受答复。

  3. WebKit接收到各种网页或者资源的数据,其中某些资源可能是同步或异步获取的。

  4. 网页被交给HTML解释器转变成一系列的词语(Token)。

  5. 解释器根据词语构建节点(Node),形成DOM树。

  6. 如果节点是JavaScript代码的话,调用JavaScript引擎解释并执行。

  7. JavaScript代码可能会修改DOM树的结构。

  8. 如果节点需要依赖其他资源,例如图片、CSS、视频等,调用资源加载器来加载他们,但是他们是异步的,不会阻碍当前DOM树的继续创建;如果是JavaScript资源URL(没有标记异步方式),则需要停止当前DOM树的创建,直到JavaScript的资源加载并被JavaScript引擎执行后才继续DOM树的创建。

所以,通俗来讲,chrome浏览器首先会请求HTML文档,然后对其中的各种资源调用相应的资源加载器进行异步网络请求,同时进行DOM渲染,直到遇到<script>标签的时候,主进程才会停止渲染等待此资源加载完毕然后调用V8引擎对js解析,继而继续进行DOM解析。我的理解如果加了async属性就相当于单独开了一个进程去独立加载和执行,而defer是和将<script>放到<body>底部一样的效果。

3. 实验一发

3.1 demo

为了验证上面的结论我们来测试一下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.css" rel="stylesheet">
        <link href="http://cdn.staticfile.org/foundation/6.0.1/css/foundation.css" rel="stylesheet">
        <script data-original="http://lib.sinaapp.com/js/angular.js/angular-1.2.19/angular.js"></script>
        <script data-original="http://libs.baidu.com/backbone/0.9.2/backbone.js"></script>
        <script data-original="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
    </head>
    <body>
        ul>li{这是第$个节点}*1000
    </body>
    </html>

一个简单的demo,从各个CDN上引用了2个CSS3个JS,在body里面创建了1000个li。通过调整外部引用资源的位置和加入相关的属性利用chrome的Timeline进行验证。

3.2 放置在<head>

图片描述
异步加载资源,但会阻塞<body>的渲染会出现白屏,按照顺序立即执行脚本

3.3 放置在<body>底部

图片描述
异步加载资源,等<body>中的内容渲染完毕后且加载完按顺序执行JS

3.3 放置在<head>头部并使用async

图片描述
异步加载资源,且加载完JS资源立即执行,并不会按顺序,谁快谁先上

3.4 放置在<head>头部并使用defer

图片描述
异步加载资源,在DOM渲染后之后再按顺序执行JS

3.5 放置在<head>头部并同时使用asyncdefer

图片描述
表现和async一致,开了个脑洞,把这两个属性交换一下位置,看会不会有覆盖效果,结果发现是一致的 = =、

综上,在webkit引擎下,建议的方式仍然是把<script>写在<body>底部,如果需要使用百度谷歌分析或者不蒜子等独立库时可以使用async属性,若你的<script>标签必须写在<head>头部内可以使用defer属性

4. 兼容性

那么,揣摩一下前辈的心理,同时写上的原因是什么呢,兼容性?

上caniuse,async在IE<=9时不支持,其他浏览器OK;defer在IE<=9时支持但会有bug,其他浏览器OK;现象在这个issue里有描述,这也就是“望远镜”里建议只有一个defer的原因。所以两个属性都指定是为了在async不支持的时候启用defer,但defer在某些情况下还是有bug。

The defer attribute may be specified even if the async attribute is specified, to cause legacy Web browsers that only support defer (and not async) to fall back to the defer behavior instead of the synchronous blocking behavior that is the default.

5. 结论

其实这么讲来,最稳妥的办法还是把<script>写在<body>底部,没有兼容性问题,没有白屏问题,没有执行顺序问题,高枕无忧,不要搞什么deferasync的花啦~

目前只研究了chrome的webkit的渲染机制,Firefox和IE的有待继续研究,图片和CSS以及其他外部资源的渲染有待研究。

更多信息在 这里

参考

查看原文

赞 59 收藏 86 评论 13

桂马 赞了文章 · 2019-05-12

浅谈小程序运行机制

写作背景

接触小程序有一段时间了,总得来说小程序开发门槛比较低,但其中基本的运行机制和原理还是要懂的。“比如我在面试的时候问到一个关于小程序的问题,问小程序有window对象吗?他说有吧”,但其实是没有的。感觉他并没有了解小程序底层的一些东西,归根结底来说应该只能算会使用这个工具,但并不明白其中的道理。

小程序与普通网页开发是有很大差别的,这就要从它的技术架构底层去剖析了。还有比如习惯Vue,react开发的开发者会吐槽小程序新建页面的繁琐,page必须由多个文件组成、组件化支持不完善、每次更改 data 里的数据都得setData、没有像Vue方便的watch监听、不能操作Dom,对于复杂性场景不太好,之前不支持npm,不支持sass,less预编译处理语言。

“有的人说小程序就像被阉割的Vue”,哈哈当然了,他们从设计的出发点就不同,咱也得理解小程序设计的初衷,通过它的使用场景,它为什么采用这种技术架构,这种技术架构有什么好处,相信在你了解完这些之后,就会理解了。下面我会从以下几个角度去分析小程序的运行机制和它的整体技术架构。

了解小程序的由来

在小程序没有出来之前,最初微信WebView逐渐成为移动web重要入口,微信发布了一整套网页开发工具包,称之为 JS-SDK,给所有的 Web 开发者打开了一扇全新的窗户,让所有开发者都可以使用到微信的原生能力,去完成一些之前做不到或者难以做到的事情。

但JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题,比如受限于设备性能和网络速度,会出现白屏的可能。因此又设计了一个增强版JS-SDK,也就是“微信 Web 资源离线存储”,但在复杂的页面上依然会出现白屏的问题,原因表现在页面切换的生硬和点击的迟滞感。这个时候需要一个 JS-SDK 所处理不了的,使用户体验更好的一个系统,小程序应运而生。

  • 快速的加载
  • 更强大的能力
  • 原生的体验
  • 易用且安全的微信数据开放
  • 高效和简单的开发

小程序与普通网页开发的区别

小程序的开发同普通的网页开发相比有很大的相似性,小程序的主要开发语言也是 JavaScript,但是二者还是有些差别的。

  • 普通网页开发可以使用各种浏览器提供的 DOM API,进行 DOM 操作,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore
    中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM
    API。
  • 普通网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。
  • 网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。
  • 小程序的执行环境

clipboard.png

小程序架构

一、技术选型

一般来说,渲染界面的技术有三种:

  • 用纯客户端原生技术来渲染
  • 用纯 Web 技术来渲染
  • 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染

通过以下几个方面分析,小程序采用哪种技术方案

  • 开发门槛:Web 门槛低,Native 也有像 RN 这样的框架支持
  • 体验:Native 体验比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生体验
  • 版本更新:Web 支持在线更新,Native 则需要打包到微信一起审核发布
  • 管控和安全:Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险

由于小程序的宿主环境是微信,如果用纯客户端原生技术来编写小程序,那么小程序代码每次都需要与微信代码一起发版,这种方式肯定是不行的。

所以需要像web技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。如果用纯web技术来渲染小程序,在一些复杂的交互上可能会面临一些性能问题,这是因为在web技术中,UI渲染跟JavaScript的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。

所以最终采用了两者结合起来的Hybrid 技术来渲染小程序,可以用一种近似web的方式来开发,并且可以实现在线更新代码,同时引入组件也有以下好处:

  • 扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力
  • 体验更好,同时也减轻 WebView 的渲染工作
  • 绕过 setData、数据通信和重渲染流程,使渲染性能更好
  • 用客户端原生渲染内置一些复杂组件,可以提供更好的性能

二、双线程模型

小程序的渲染层和逻辑层分别由 2 个线程管理:视图层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS脚本。

图片描述

图片描述

那么为什么要这样设计呢,前面也提到了管控和安全,为了解决这些问题,我们需要阻止开发者使用一些,例如浏览器的window对象,跳转页面、操作DOM、动态执行脚本的开放性接口。

我们可以使用客户端系统的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境。

这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。

这就是小程序双线程模型的由来:

  • 逻辑层:创建一个单独的线程去执行 JavaScript,在这里执行的都是有关小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等
  • 视图层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以视图层存在多个 WebView 线程
  • JSBridge 起到架起上层开发与Native(系统层)的桥梁,使得小程序可通过API使用原生的功能,且部分组件为原生组件实现,从而有良好体验

三、双线程通信

把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。

那要怎么去实现动态更改界面呢?

如上图所示,逻辑层和试图层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。

这也就是说,我们可以把 DOM 的更新通过简单的数据通信来实现。

Virtual DOM 相信大家都已有了解,大概是这么个过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。

如图所示:

clipboard.png

1. 在渲染层把 WXML 转化成对应的 JS 对象。

2. 在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。

3. 经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。

我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。

而这样一个完整的框架,离不开小程序的基础库。

四、小程序的基础库

小程序的基础库可以被注入到视图层和逻辑层运行,主要用于以下几个方面:

  • 在视图层,提供各类组件来组建界面的元素
  • 在逻辑层,提供各类 API 来处理各种逻辑
  • 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑

由于小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。

小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。

这样可以:

  • 降低业务小程序的代码包大小
  • 可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包

五、Exparser 框架

Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。

Exparser的主要特点包括以下几点:

  1. 基于Shadow
    DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
  2. 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  3. 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。

内置组件

基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。

六、运行机制

小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台状态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。
  • 小程序没有重启的概念
  • 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
  • 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁

clipboard.png

七、更新机制

小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。

八、性能优化

主要的优化策略可以归纳为三点:

  • 精简代码,降低WXML结构和JS代码的复杂性;
  • 合理使用setData调用,减少setData次数和数据量;
  • 必要时使用分包优化。

1、setData 工作原理

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

2、常见的 setData 操作错误

  1. 频繁的去 setData在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
  2. 每次 setData 都传递大量新数据由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript
  3. 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程, 后台态页面进行
    setData当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。

总结

大致从以上几个角度分析了小程序的底层架构,从小程序的由来、到双线程的出现、设计、通信、到基础库、Exparser 框架、再到运行机制、性能优化等等,都是一个个相关而又相互影响的选择。关于小程序的底层框架设计,其实涉及到的还有很多,比如自定义组件,原生组件、性能优化等方面,都不是一点能讲完的,还要多看源码,多思考。每一个框架的诞生都有其意义,我们作为开发者能做的不只是会使用这个工具,还应理解它的设计模式。只有这样才不会被工具左右,才能走的更远!

查看原文

赞 152 收藏 90 评论 6

桂马 赞了文章 · 2019-05-12

浅谈小程序运行机制

写作背景

接触小程序有一段时间了,总得来说小程序开发门槛比较低,但其中基本的运行机制和原理还是要懂的。“比如我在面试的时候问到一个关于小程序的问题,问小程序有window对象吗?他说有吧”,但其实是没有的。感觉他并没有了解小程序底层的一些东西,归根结底来说应该只能算会使用这个工具,但并不明白其中的道理。

小程序与普通网页开发是有很大差别的,这就要从它的技术架构底层去剖析了。还有比如习惯Vue,react开发的开发者会吐槽小程序新建页面的繁琐,page必须由多个文件组成、组件化支持不完善、每次更改 data 里的数据都得setData、没有像Vue方便的watch监听、不能操作Dom,对于复杂性场景不太好,之前不支持npm,不支持sass,less预编译处理语言。

“有的人说小程序就像被阉割的Vue”,哈哈当然了,他们从设计的出发点就不同,咱也得理解小程序设计的初衷,通过它的使用场景,它为什么采用这种技术架构,这种技术架构有什么好处,相信在你了解完这些之后,就会理解了。下面我会从以下几个角度去分析小程序的运行机制和它的整体技术架构。

了解小程序的由来

在小程序没有出来之前,最初微信WebView逐渐成为移动web重要入口,微信发布了一整套网页开发工具包,称之为 JS-SDK,给所有的 Web 开发者打开了一扇全新的窗户,让所有开发者都可以使用到微信的原生能力,去完成一些之前做不到或者难以做到的事情。

但JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题,比如受限于设备性能和网络速度,会出现白屏的可能。因此又设计了一个增强版JS-SDK,也就是“微信 Web 资源离线存储”,但在复杂的页面上依然会出现白屏的问题,原因表现在页面切换的生硬和点击的迟滞感。这个时候需要一个 JS-SDK 所处理不了的,使用户体验更好的一个系统,小程序应运而生。

  • 快速的加载
  • 更强大的能力
  • 原生的体验
  • 易用且安全的微信数据开放
  • 高效和简单的开发

小程序与普通网页开发的区别

小程序的开发同普通的网页开发相比有很大的相似性,小程序的主要开发语言也是 JavaScript,但是二者还是有些差别的。

  • 普通网页开发可以使用各种浏览器提供的 DOM API,进行 DOM 操作,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore
    中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM
    API。
  • 普通网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。
  • 网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。
  • 小程序的执行环境

clipboard.png

小程序架构

一、技术选型

一般来说,渲染界面的技术有三种:

  • 用纯客户端原生技术来渲染
  • 用纯 Web 技术来渲染
  • 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染

通过以下几个方面分析,小程序采用哪种技术方案

  • 开发门槛:Web 门槛低,Native 也有像 RN 这样的框架支持
  • 体验:Native 体验比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生体验
  • 版本更新:Web 支持在线更新,Native 则需要打包到微信一起审核发布
  • 管控和安全:Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险

由于小程序的宿主环境是微信,如果用纯客户端原生技术来编写小程序,那么小程序代码每次都需要与微信代码一起发版,这种方式肯定是不行的。

所以需要像web技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。如果用纯web技术来渲染小程序,在一些复杂的交互上可能会面临一些性能问题,这是因为在web技术中,UI渲染跟JavaScript的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。

所以最终采用了两者结合起来的Hybrid 技术来渲染小程序,可以用一种近似web的方式来开发,并且可以实现在线更新代码,同时引入组件也有以下好处:

  • 扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力
  • 体验更好,同时也减轻 WebView 的渲染工作
  • 绕过 setData、数据通信和重渲染流程,使渲染性能更好
  • 用客户端原生渲染内置一些复杂组件,可以提供更好的性能

二、双线程模型

小程序的渲染层和逻辑层分别由 2 个线程管理:视图层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS脚本。

图片描述

图片描述

那么为什么要这样设计呢,前面也提到了管控和安全,为了解决这些问题,我们需要阻止开发者使用一些,例如浏览器的window对象,跳转页面、操作DOM、动态执行脚本的开放性接口。

我们可以使用客户端系统的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境。

这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。

这就是小程序双线程模型的由来:

  • 逻辑层:创建一个单独的线程去执行 JavaScript,在这里执行的都是有关小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等
  • 视图层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以视图层存在多个 WebView 线程
  • JSBridge 起到架起上层开发与Native(系统层)的桥梁,使得小程序可通过API使用原生的功能,且部分组件为原生组件实现,从而有良好体验

三、双线程通信

把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。

那要怎么去实现动态更改界面呢?

如上图所示,逻辑层和试图层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。

这也就是说,我们可以把 DOM 的更新通过简单的数据通信来实现。

Virtual DOM 相信大家都已有了解,大概是这么个过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。

如图所示:

clipboard.png

1. 在渲染层把 WXML 转化成对应的 JS 对象。

2. 在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。

3. 经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。

我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。

而这样一个完整的框架,离不开小程序的基础库。

四、小程序的基础库

小程序的基础库可以被注入到视图层和逻辑层运行,主要用于以下几个方面:

  • 在视图层,提供各类组件来组建界面的元素
  • 在逻辑层,提供各类 API 来处理各种逻辑
  • 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑

由于小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。

小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。

这样可以:

  • 降低业务小程序的代码包大小
  • 可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包

五、Exparser 框架

Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。

Exparser的主要特点包括以下几点:

  1. 基于Shadow
    DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
  2. 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  3. 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。

内置组件

基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。

六、运行机制

小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台状态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。
  • 小程序没有重启的概念
  • 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
  • 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁

clipboard.png

七、更新机制

小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。

八、性能优化

主要的优化策略可以归纳为三点:

  • 精简代码,降低WXML结构和JS代码的复杂性;
  • 合理使用setData调用,减少setData次数和数据量;
  • 必要时使用分包优化。

1、setData 工作原理

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

2、常见的 setData 操作错误

  1. 频繁的去 setData在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
  2. 每次 setData 都传递大量新数据由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript
  3. 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程, 后台态页面进行
    setData当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。

总结

大致从以上几个角度分析了小程序的底层架构,从小程序的由来、到双线程的出现、设计、通信、到基础库、Exparser 框架、再到运行机制、性能优化等等,都是一个个相关而又相互影响的选择。关于小程序的底层框架设计,其实涉及到的还有很多,比如自定义组件,原生组件、性能优化等方面,都不是一点能讲完的,还要多看源码,多思考。每一个框架的诞生都有其意义,我们作为开发者能做的不只是会使用这个工具,还应理解它的设计模式。只有这样才不会被工具左右,才能走的更远!

查看原文

赞 152 收藏 90 评论 6

桂马 赞了文章 · 2019-05-12

What's New in JavaScript

前几天 Google IO 上 V8 团队为我们分享了《What's New in JavaScript》主题,分享的语速很慢推荐大家可以都去听听就当锻炼下听力了。看完之后我整理了一个文字版帮助大家快速了解分享内容,嘉宾主要是分享了以下几点:

  1. JS 解析快了 2 倍
  2. async 执行快了 11 倍
  3. 平均减少了 20% 的内存使用
  4. class fileds 可以直接在 class 中初始化变量不用写在 constructor 里
  5. 私有变量前缀
  6. string.matchAll 用来做正则多次匹配
  7. numeric seperator 允许我们在写数字的时候使用 _ 作为分隔符提高可读性
  8. bigint 新的大数字类型支持
  9. Intl.NumberFormat 本地化格式化数字显示
  10. Array.prototype.flat(), Array.prototype.flatMap() 多层数组打平方法
  11. Object.entries() 和 Object.fromEntries() 快速对对象进行数组操作
  12. globalThis 无环境依赖的全局 this 支持
  13. Array.prototype.sort() 的排序结果稳定输出
  14. Intl.RelativeTimeFormat(), Intl.DateTimeFormat() 本地化显示时间
  15. Intl.ListFormat() 本地化显示多个名词列表
  16. Intl.locale() 提供某一本地化语言的各种常量查询
  17. 顶级 await 无需写 async 的支持
  18. Promise.allSettled() 和 Promise.any() 的增加丰富 Promise 场景
  19. WeakRef 类型用来做部分变量弱引用减少内存泄露

Async 执行比之前快了11倍

开场就用 11x faster 数字把大家惊到了,也有很多同学好奇到底是怎么做到的。其实这个优化并不是最近做的,去年11月的时候 V8 团队就发了一篇文章 《Faster async functions and promises》,这里面就非常详尽的讲述了如何让 async/await 优化到这个速度的,其主要归功于以下三点:

  • TurboFan:新的 JS 编译器
  • Orinoco:新的 GC 引擎
  • Node.js 8 上的一个 await bug

2008年 Chrome 出世,10年 Chrome 引入了 Crankshaft 编译器,多年后的今天这员老将已经无法满足现有的优化需求,毕竟当时的作者也未曾料想到前端的世界会发展的这么快。关于为何使用 TurboFan 替换掉 Crankshaft,大家可以看看《Launching Ignition and TurboFan》,原文中是这么说的:

Crankshaft 仅支持优化 JavaScript 的一部分特性。它并没有通过结构化的异常处理来设计代码,即代码块并没有通过try、catch、finally等关键字划分。另外由于为每一个新的特性Cranksshaft都将要做九套不同的框架代码适应不同的平台,因此在适配新的Javascript语言特性也很困难。还有Crankshaft框架代码的设计也限制优化机器码的扩展。尽管V8引擎团队为每一套芯片架构维护超过一万行代码,Crankshaft也不过为Javascript挤出一点点性能。
via:《Javascript是如何工作的:V8引擎的内核Ignition和TurboFan》

而 TurboFan 则提供了更好的架构,能够在不修改架构的情况下添加新的优化特性,这为面向未来优化 JavaScript 语言特性提供了很好的架构支持,能让团队花费更少的时间在做处理不同平台的特性和编码上。从原文的数据对比中就可以看到,仅仅是换了个编译器优化就在 8 倍左右了…… 给 V8 的大佬们跪下了。

而 Orinoco 新的 GC 引擎则是使用单独线程异步去处理,让其不影响 JS 主线程的执行。至于最后说的 async/await 的 BUG 则是让 V8 团队引发思考,将 async/await 原本基于 3 个 Promise 的实现减少成 2 个,最终减少成 1 个!最后达到了写 async/await 比直接写 Promise 还要快的结果。

我们知道 await 后面跟的是 Promise 对象,但是即使不是 Promise JS 也会帮我们将其包装成 Promise。而在 V8 引擎中,为了实现这个包装,至少需要一个 Promise,两个微任务过程。这个在本身已经是 Promise 的情况下就有点亏大发了。而为了实现 async/await 在 await 结束之后要重新原函数的上下文并提供 await 之后的结果,规范定义此时还需要一个 Promise,这个在 V8 团队看来是没有必要的,所以他们建议规范去除这个特性

最后的最后,官方还建议我们:多使用 async/await 而不是手写 Promise 代码,多使用 JavaScript 引擎提供的 Promise 而不是自己去实现。

Numeric Seperator

随着 Babel 的出现,JS 的语法糖简直不要太多,Numeric Seperator 算是一个。简单的说来它为我们手写数字的时候提供给了分隔符的支持,让我们在写大数字的时候具有可读性。

其实是个很简单的语法糖,为什么我会单独列出来说呢,主要是因为它正好解决了我之前一个实现的痛点。我有一个需求是一堆文章数据,我要按照产品给的规则去插入广告。如图非红框是文章,红框处是广告。由于插入规则会根据产品的需(心)求(情)频繁变化,所以我们最开始使用了两个变量进行标记:

const news = [1, 3, 5, 6, 7, 9, 10, 11];
const ads = [2, 4, 8, 12];

当位置发生变化的时候我们就需要同时对两个变量进行修改,这样导致了维护上的成本。所以我想了一个办法,广告的位置标记为 1,文章的位置标记为 0,使用纯二进制的形式来表示个记录,这样子就变成了:

+---+---+---+
| 0 | 1 | 0 |
+---+---+---+
| 1 | 0 | 0 |
+---+---+---+
| 0 | 1 | 0 |
+---+---+---+
| 0 | 0 | 1 |
+---+---+---+

1 011 010 100 010 001
// 首位为常量 1
// 2-4 位记录一行多少条
// 后续按照新闻和广告的位置进行记录

最后我们使用一个变量 0b1011010100010001 就完成了两种信息的记录。这样做将很多数据集成在了一起解决了我们之前的问题,但是它带来了新的问题,大家也可以看到注释中按照空格劈开的话大家看的还比较明白,但是在段头将空格去除之后在阅读程度上就造成了非常大的困难了。而数字分隔符这个语法糖正好就能解决这个问题,0b1_011_010_100_010_001 这样阅读起来就好很多了。

Promise

虽然在大部分的场景 async/await 都可以了,但是不好意思 Promise 有些场景还是不可替代的。Promsie.all()Promise.race() 就是这种特别的存在。而 Promise.allSettled()Promise.any() 则是新增加的方法, 相较于它们的前辈,这俩拥有忽略错误达到目的的特性。

我们之前有一个需求,是需要将文件安全的存储在一个存储服务中,为了灾备我们其实有两个 S3,一个 HBase 还有本地存储。所以每次都需要诸如以下的逻辑:

for(const service of services) {
  const result = await service.upload(file);
  if(result) break;
}

但其实我并不关心错误,我的目的是只要保证有一个服务最终能执行成功即可,所以 Promise.any() 其实就可以解决这个问题。

await Promise.any( services.map(service => service.upload(file)) );

Promise.allsettled()Promise.any() 的引入丰富了 Promise 更多的可能性。说不定以后还会增加更多的特性,例如 Promise.try(), Promise.some(), Promise.reduce() ...

WeakRef

WeakRef 这个新类型我最开始是不太理解的,毕竟我总感觉 Chrome 已经长大了,肯定会自己处理垃圾了。然而事情并没有我想的那么简单,我们知道 JS 的垃圾回收主要有“标记清除”和“引用计数”两种方法。引用计数是只要变量多一次引用则计数加 1,少一次引用则计数减 1,当引用为 0 时则表示你已经没有利用价值了,去垃圾站吧!

在 WeakRef 之前其实已经有两个类似的类型存在了,那就是 WeakMap 和 WeakSet。以 WeakMap 为例,它规定了它的 Key 必须是对象,而且它的对象都是弱引用的。举个例子:

//map.js
function usageSize() {
  const used = process.memoryUsage().heapUsed;
  return Math.round(used / 1024 / 1024 * 100) / 100 + 'M';
}

global.gc();
console.log(usageSize()); // ≈ 3.23M

let arr = new Array(5 * 1024 * 1024);
const map = new Map();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 43.22M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 43.23M
//weakmap.js
function usageSize() {
  const used = process.memoryUsage().heapUsed;
  return Math.round(used / 1024 / 1024 * 100) / 100 + 'M';
}

global.gc();
console.log(usageSize()); // ≈ 3.23M

let arr = new Array(5 * 1024 * 1024);
const map = new WeakMap();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 43.22M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 3.23M

分别执行 node --expose-gc map.jsnode --expose-gc weakmap.js 就可以发现区别了。在 arr 和 Map 中都保留了数组的强引用,所以在 Map 中简单的清除 arr 变量内存并没有得到释放,因为 Map 还存在引用计数。而在 WeakMap 中,它的键是弱引用,不计入引用计数中,所以当 arr 被清除之后,数组会因为引用计数为0而被回收掉。

正如分享中所说,WeakMap 和 WeakSet 足够好,但是它要求键必须是对象,在某些场景上不太试用。所以他们暴露了更方便的 WeakRef 类型。在 Python 中也存在 WeakRef 类型,干的事情比较类似。其实我们主要注意 WeakRef 的引用是不计引用计数的,就好理解了。例如 MDN 中所说的引用计数没办法清理的循环引用问题:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

如果试用 WeakRef 来改写,由于 WeakRef 不计算引用计数,所以计数一直为 0,在下一次回收中就会被正常回收了。

function f() {
  var o = new WeakRef({});
  var o2 = o;
  o.a = o2;

  return "azerty";
}

f();

在之前的一个多进程需求中,我们需要将子进程中的数据发送到主进程中,我们使用的方式是这样写的:

const metric = 'event';
global.DATA[metric] = {};

process.on(metric, () => {
  const data = global.DATA[metric];
  delete global.DATA[metric];
  return data;
});

代码就看着比较怪,由于 global.DATA[metric] 作为强引用,如果直接在事件中 return global.DATA[metric] 的话,由于存在引用计数,那么这个全局变量是一直占用内存的。此时如果使用 WeakRef 改写一下就可以减少 delete 的逻辑了。

const metric = 'event';
global.DATA[metric] = new WeakRef({});

process.on(metric, () => {
  const ref = global.DATA[metric];
  if(ref !== undefined) {
    return ref.deref();
  }
  return ref;
});

后记

除了我上面讲的几个特性之外,还有很多其他的特性也非常一颗赛艇。例如 String.matchAll() 让我们做多次匹配的时候再也不用写 while 了!Intl 本地化类的支持,让我们可以早日抛弃 moment.js,特别是 RelativeTimeFormat 类真是解放了我们的生产力,不过目前接口的配置似乎比较定制化,不知道后续的细粒度需求支持情况会如何。

参考资料:

查看原文

赞 49 收藏 32 评论 0

桂马 赞了文章 · 2019-05-10

旗帜鲜明地抵制 CSDN 下载(盗版)站!

SegmentFault 上线付费课程以来,对于内容质量一直严格把关,讲师认真备课,课后为学员答疑,广受好评。然而近期有多位 SegmentFault 讲师反馈在 CSDN 下载频道出现了大量他的盗版课程。

clipboard.png

不查不知道,一查我们发现——我们讲师辛辛苦苦花了上百个小时录制的付费课程,在 CSDN 下载频道竟有满满一屏幕的盗版存在(相关证据我们已经找律师团队取证),同时根据他们的关键词推荐我们发现在其博客频道也有大量的盗版内容,防不胜防,让人不吐不快。

clipboard.png

昨天我在朋友圈公开这个事情后,收到了大量业内同行的反馈,我们发现不仅仅是 SegmentFault,几乎我认识的所有同行以及我们熟悉的讲师的付费内容(包括不限于课程、图书、专栏)都有被 CSDN 下载频道侵权的经历,昨天,我们用了同行的一些关键词在 CSDN 下载频道进行检索,同样发现存在有大量的盗版内容存在。

clipboard.png

同时我们也收到一些用户的反馈在 CSDN 下载频道有大量 IT 相关技术书籍的扫描版文件,毫无疑问都是盗版资源。上传时期从 2016 年(或者更早)开始到至今一直存在。

我们通过关键词在 Google 检索后,发现最早控诉 CSDN 下载频道的内容《CSDN 首页鼓励盗版图书下载》是在 2005 年发布,在知乎等社区也有大量的讨论,如《如何看待 CSDN 利用用户上传的盗版资料卖积分赚钱?》《为什么 CSDN 能做到让用户花钱买积分下载自己网站的盗版资源?》……可见已在业内引起公愤。

clipboard.png

CSDN 做为中国最早的技术社区之一,我们认可其对开发者之间线上交流做出的贡献,但是其下载频道的存在大大助长了大量盗版侵权内容的产生。并非个例,长期存在,越发泛滥,从未被解决——这代表其产品存在根本上的机制问题。

CSDN 官方对此不知情吗?

不过是睁一只眼闭一只眼罢了——CSDN 的下载频道占了总社区超过 30% 的流量。靠着盗版和侵权他人优质内容,获取平台流量,再依靠平台流量进行广告和其他形式的变现。这不是非法牟利是什么?

我们不禁质疑号称中国最大技术社区的 CSDN 究竟拥有怎样的价值观?

纵容盗版,非法牟利;

无视用户隐私——曾经明文存储用户名密码导致用户数据泄露;

甚至欺骗客户——夸大网站流量,广告数据造假。还记得去年程序员广告代码刷量的乌龙事件吗?“博客详情页PC增加广告系统刷量代码”这句话写在了代码注释里面上线了。原来客户的广告数据都是刷出来的?(心疼投放广告的客户)

其实技术圈子非常小,很多同行可能碍于面子或者各种各样的原因,没有公开地去声讨 CSDN,一些声讨可能也并没有解决问题,这更助长了其纵容盗版的气焰。 SegmentFault 作为技术社区的一员,我们深知社区发展的不易,我们有责任帮助我们的讲师维护其付费内容的版权不被侵犯。

我们已经聘请专业的律师团队取证,同时我们也呼吁被 CSDN 下载站侵犯过内容版权的同行,讲师,书籍作者,广大开发者一起发声,在评论区留下你的声音和被侵权经历,通过曝光侵权甚至违法行为,共同净化行业环境。

一起举报

到中央网信办(www.12377.cn)

国家新闻出版广电总局官网(www.sapprft.gov.cn)

举报 CSDN下载站的盗版侵权行为。

图片描述

SegmentFault CEO:高阳Sunny 2019年5月9日凌晨于北京

查看原文

赞 144 收藏 1 评论 58

桂马 赞了文章 · 2019-03-25

前端面试题-主流浏览器内核

一、浏览器市场份额

图片描述

本报告数据,来源于百度统计所覆盖的超过150万的站点,而不是baidu.com的流量数据。

二、浏览器内核

浏览器内核可以分成两部分:渲染引擎(layout engineer 或者 Rendering Engine)和 JS 引擎

浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。

2.1 渲染引擎

渲染引擎负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。

2.2 JS 引擎

JS 引擎则是解析 Javascript 语言,执行 javascript 语言来实现网页的动态效果。

最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎

三、主流浏览器内核

常见的浏览器内核可以分为四种:TridentGeckoWebkitChromium/Bink。内核看着陌生,那么转换为相对应的浏览器:IEMozilla FireFoxSafariChrome 是不是立马就觉得熟悉了。

四、Trident ([‘traɪd(ə)nt])

说起 Trident,很多人都会感到陌生,但提起IE(Internet Explorer)则无人不知无人不晓,由于其被包含在全世界使用率最高的操作系统 Windows 中,得到了极高的市场占有率,从而使得 Trident 内核(也被称为 IE 内核)长期一家独大。

但是由于微软长时间没有更新 Trident 内核,则导致了两个后果:一是 Trident 内核曾经几乎与 W3C 标准脱节(2005年),二是 Trident 内核的大量 Bug 等安全性问题没有得到及时解决,然后加上一些致力于开源的开发者和一些学者们公开自己认为 IE 浏览器不安全的观点,也有很多用户转向了其他浏览器。

4.1 Trident 内核常见浏览器

(1)IE6、IE7、IE8(Trident 4.0)、IE9(Trident 5.0)、IE10(Trident 6.0);

(2)猎豹安全浏览器:1.0-4.2版本为Trident+Webkit,4.3版本为Trident+Blink;

(3)360安全浏览器 :1.0-5.0为Trident,6.0为Trident+Webkit,7.0为Trident+Blink;

(4)360极速浏览器:7.5之前为Trident+Webkit,7.5为Trident+Blink;

(5)傲游浏览器 :傲游1.x、2.x为IE内核,3.x为IE与Webkit双核;

(6)搜狗高速浏览器:1.x为Trident,2.0及以后版本为Trident+Webkit;

4.2 兼容模式

国内很多的双核浏览器的其中一核便是 Trident,美其名曰 “兼容模式”。

4.3 EdgeHTML 内核

Window10 发布后,IE 将其内置浏览器命名为 Edge,Edge 最显著的特点就是新内核 EdgeHTML。

五、Gecko ([‘gekəʊ])

5.1 开源内核

Gecko(Firefox 内核):Netscape6 开始采用的内核,后来的 Mozilla FireFox(火狐浏览器) 也采用了该内核,Gecko 的特点是代码完全公开,因此,其可开发程度很高,全世界的程序员都可以为其编写代码,增加功能。因为这是个开源内核,因此受到许多人的青睐,Gecko 内核的浏览器也很多,这也是 Gecko 内核虽然年轻但市场占有率能够迅速提高的重要原因。

5.2 Firefox 内核

事实上,Gecko 引擎的由来跟 IE 不无关系,前面说过 IE 没有使用 W3C 的标准,这导致了微软内部一些开发人员的不满;他们与当时已经停止更新了的 Netscape 的一些员工一起创办了 Mozilla,以当时的 Mosaic 内核为基础重新编写内核,于是开发出了 Gecko。不过事实上,Gecko 内核的浏览器仍然还是 Firefox (火狐) 用户最多,所以有时也会被称为 Firefox 内核。此外 Gecko 也是一个跨平台内核,可以在Windows、 BSD、Linux 和 Mac OS X 中使用。

六、Webkit

6.1 Safari 浏览器

只要提到 Webkit,大多数人立马想到的必然是 Chrome,结果导致如今有了把 webkit 称为 chrome 内核的错误说法,即使 chrome 的内核已经是 blink,其实 Webkit 的祖先是 Safari,也就是苹果系列产品的专属浏览器。

6.2 WebKit 是 KHTML 的分支

WebKit 的前身是苹果公司使用 KDE(Linux桌面系统)开发的 KHTML 开源引擎,可以说 WebKit 是 KHTML 的一个开源分支。

七、Chromium/Blink

7.1 Chromium Fork webkit

2008 年,谷歌公司发布了 chrome 浏览器,浏览器使用的内核被命名为 chromium。chromium fork 自开源引擎 webkit,并提高了 WebKit 的代码可读性和编译速度。

7.2 V8 引擎

谷歌公司还研发了自己的 Javascript 引擎,V8,极大地提高了 Javascript 的运算速度

7.3 Blink 是 WebKit 的分支

Google 的 Chromium 项目最初一直使用 WebKit(WebCore) 作为渲染引擎,但由于后来苹果推出的 WebKit2 与 Chromium 的沙箱设计存在冲突,所以 Google 决定从 WebKit 衍生出自己的 Blink 引擎(后由 Google 和 Opera Software 共同研发)。

八、Presto ([‘prestəʊ])

8.1 Opera 浏览器

Presto 是 Opera 自主研发的渲染引擎,然而为了减少研发成本,Opera 在 2013 年 2 月宣布放弃 Presto,转而跟随 Chrome 使用 WebKit 分支的 Chromium 引擎作为自家浏览器核心引擎。

在 Chrome 于 2013 年推出 Blink 引擎之后,Opera 也紧跟其脚步表示将转而使用 Blink 作为浏览器核心引擎。

九、移动端

移动端的浏览器内核主要说的是系统内置浏览器的内核

目前移动设备浏览器上常用的内核有 WebkitBlinkTridentGecko

1、iPhone 和 iPad 等苹果 iOS 平台主要是 WebKit

2、Android 4.4 之前的 Android 系统浏览器内核是 WebKitAndroid 4.4 系统浏览器切换到了Chromium,内核是 Webkit 的分支 Blink

3、Windows Phone 8 系统浏览器内核是 Trident

十、总结

1、IE浏览器内核:Trident内核,也被称为IE内核;

2、Chrome浏览器内核:Chromium内核 → Webkit内核 → Blink内核;

3、Firefox浏览器内核:Gecko内核,也被称Firefox内核;

4、Safari浏览器内核:Webkit内核;

5、Opera浏览器内核:最初是自主研发的Presto内核,后跟随谷歌,从Webkit到Blink内核;

6、360浏览器、猎豹浏览器内核:IE+Chrome双内核;

7、搜狗、遨游、QQ浏览器内核:Trident(兼容模式)+Webkit(高速模式);

8、百度浏览器、世界之窗内核:IE内核;

阅读更多

参考文章 主流浏览器内核介绍(前端开发值得了解的浏览器内核历史)

查看原文

赞 26 收藏 21 评论 0

桂马 回答了问题 · 2019-03-21

解决vscode不能跳转到代码

vue不算单独的文件,算组件

关注 19 回答 12

桂马 发布了文章 · 2019-03-19

Vue+Vue-router+Vuex项目实战

shopping

vue + vue-router + vuex实现电商网站

效果展示

install

  • 下载代码: git clone https://github.com/chenchangyuan/shopping.git
  • 安装依赖: npm install
  • 启动项目: npm run dev
运行环境: node v9.11.1npm 5.6.0

需求分析

  1. 登录页面、商品列表页(网站首页)、购物车页(实现结算)、商品详情页
  2. 可按颜色、品牌对商品进行筛选,单击选中,再次点击取消
  3. 根据价格进行升序降序、销量降序排列
  4. 商品列表显示图片、名称、销量、颜色、单价
  5. 实时显示购物车数量(商品类别数)
  6. 购物车页面实现商品总价、总数进行结算,优惠券打折

数据存储 & 数据处理

  • product.js存放商品数据(生产环境需通过接口调用获取数据)
{
    id: 1,
    name: 'AirPods',
    brand: 'Apple',
    image: '/src/images/airPods.jpg',
    imageDetail: '/src/images/airPods_detail.jpg',
    sales: 10000,
    cost: 1288,
    color: '白色'
},
  • window.localStorage实现数据存储与验证
let username = window.localStorage.getItem('username');
let password = window.localStorage.getItem('password');
if(!util.trim(this.username) || !util.trim(this.username) ){
    window.alert('账号或密码不能为空');
    return;
}
if(username === this.username && password === this.password){
    this.login = false;
    window.localStorage.setItem('loginStatus', 'login');
    this.$store.commit('getUser', this.username);
    window.alert('登陆成功,确定进入网站首页');
    window.location.href = '/list';
}else{
    window.alert('账号或密码错误');
}

数据过滤与排序处理

filteredAndOrderedList(){
    //拷贝原数组
    let list = [...this.list];
    //品牌过滤
    if(this.filterBrand !== ''){
        list = list.filter(item => item.brand === this.filterBrand);
    }
    //颜色过滤
    if(this.filterColor !== ''){
        list = list.filter(item => item.color === this.filterColor);
    }
    //排序
    if(this.order !== ''){
        if(this.order === 'sales'){
            list = list.sort((a, b) => b.sales - a.sales);
        }else if(this.order === 'cost-desc'){
            list = list.sort((a, b) => b.cost - a.cost);
        }else if(this.order === 'cost-asc'){
            list = list.sort((a, b) => a.cost - b.cost);
        }
    }
    return list;
}

实时显示应付总额与商品数

//购物车商品总数
countAll(){
    let count = 0;
    this.cartList.forEach(item => {
        count += item.count;
    });
    return count;
},
//购物车商品总价
costAll(){
    let cost = 0;
    this.cartList.forEach(item => {
        cost += this.productDictList[item.id].cost * item.count;
    });
    return cost;
}

购物车结算处理

//通知Vuex,完成下单
handleOrder(){
    this.$store.dispatch('buy').then(() => {
        window.alert('购买成功');
    })
},

vue-router & vuex

vue-router路由管理/src/views/目录下的vue组件进行设置,router-views挂载所有路由,登录界面与商品列表页面之间header做隐藏显示处理,登录状态下刷新页面跳转至列表页,其他页面设置默认跳转

跳转处理

const router = new VueRouter(RouterConfig);

//跳转前设置title
router.beforeEach((to, from, next) => {
    window.document.title = to.meta.title;
    next();
});
//跳转后设置scroll为原点
router.afterEach((to, from, next) => {
    window.scrollTo(0, 0);
});

routers配置

//商品列表路由配置
const routers = [
    {
        path: '/list',
        meta: {
            title: '商品列表'
        },
        component: (resolve) => require(['./views/list.vue'], resolve)
    },
    {
        path: '/product/:id',
        meta: {
            title: '商品详情'
        },
        component: (resolve) => require(['./views/product.vue'], resolve)
    },
    {
        path: '/cart',
        meta: {
            title: '购物车'
        },
        component: (resolve) => require(['./views/cart.vue'], resolve)
    },
    {
        path: '/login/:loginStatus',
        meta: {
            title: '登录注册'
        },
        component: (resolve) => require(['./views/login.vue'], resolve)
    },
    {
        path: '*',
        redirect: '/login/login'
    }
];
export default routers;

vuex状态管理,各组件共享数据在state中设置,mutation实现数据同步,action异步加载

//配置Vuex状态管理
const store = new Vuex.Store({
    state: {
        //商品列表信息
        productList: [],
        //购物车数据,数组形式,数据元素为对象(商品id,购买数量count)
        cartList: [],
        //当前用户账号
        username: window.localStorage.getItem('username'),
        //登录状态
        loginStatus: !!window.localStorage.getItem('loginStatus'),
    },
    getters: {
        //品牌、颜色筛选
        brands: state => {
            const brands = state.productList.map(item => item.brand);
            return util.getFilterArray(brands);
        },
        colors: state => {
            const colors = state.productList.map(item => item.color);
            return util.getFilterArray(colors);
        }
    },
    //mutations只能以同步方式
    mutations: {
        //添加商品列表
        setProductList(state, data){
            state.productList = data;
        },
        //添加购物车
        addCart(state, id){
            const isAdded = state.cartList.find(item => item.id === id);
            //如果不存在设置购物车为1,存在count++
            if(isAdded){
                isAdded.count++;
            }else{
                state.cartList.push({
                    id: id,
                    count: 1
                })
            }
        },
        //修改购物车商品数量
        editCartCount(state, payload){
            const product = state.cartList.find(item => item.id === payload.id);
            product.count += payload.count;
        },
        //删除购物车商品
        deleteCart(state, id){
            const index = state.cartList.findIndex(item => item.id === id);
            state.cartList.splice(index, 1)
        },
        //清空购物车
        emptyCart(state){
            state.cartList = [];
        },
        getUser(state, username){
            console.log('username',username)
            state.username = username;
        },
        getLoginStatus(state, flag){
            state.loginStatus = flag;
        }
    },
    actions: {
        //异步请求商品列表,暂且使用setTimeout
        getProductList(context){
            setTimeout(() => {
                context.commit('setProductList', product_data)
            }, 500);
        },
        //购买
        buy(context){
            //生产环境使用ajax请求服务端响应后再清空购物车
            return new Promise(resolve => {
                setTimeout(() => {
                    context.commit('emptyCart');
                    resolve();
                }, 500);
            });
        },
    }
});

后记

项目地址: 阅读完本文如果对vue的理解有所帮助,请给颗star,谢谢~

笔者个人微信 gm4118679254 欢迎加好友一起交流技术

参考资料

Vue.js实战
Vue.js

查看原文

赞 8 收藏 7 评论 0

桂马 提出了问题 · 2019-03-14

npm run dev启动vue项目浏览器访问显示当前文件夹目录

求大佬解惑

图片描述

关注 2 回答 1

桂马 提出了问题 · 2019-03-14

npm run dev启动vue项目浏览器访问显示当前文件夹目录

求大佬解惑

图片描述

关注 2 回答 1

桂马 回答了问题 · 2019-03-12

解决Webstorm 2017.2关于Vue的问题

楼主解决了吗,我也遇到同样的问题,配置里面只能勾选Vue.js,没有显示install选项,怎么回事,求解

关注 6 回答 6