Fork me on GitHub
艺术码畜的生活瞬间

加载优化(视频加载)

视频加载

与延迟加载图像资源类似,通过视频标签引入的视频资源也可进行延迟加载,但通常都会根据需求场景进行具体的处理,下面就来探讨一 些关于 视频加载的优化内容。

一、不需要自动播放

​ 由于Chrome等一些浏览器会对视频资源进行预加载,即在HTML完成加载和解析时触发DOMContentLoaded 事件开始请求视频资源,当请求完成后触发window. onload事件开始页面渲染,过程如图所示。

视频资源的加载

为了使页面更快地加载并渲染出来,可以阻止不需要自动播放的视频的预加载:其方法是通过视频标签的preload进行控制:

<video controls preload-"none" poster="default.1po9">
        <source src="simply.webm" type-"video/webm">
        <source src="simply.mp4" tye-"video/mp4">
</video>

标签的preload属性通常的默认值为auto,表示无论用户是否希望,所有视频文件都会被自动下载,这里将其设置为none,来阻止视频的自动加载。同时这里还通过poster属性为视频提供占位符图片,它的作用是当视频未加载出来时,不至于在页面中呈现一块让用户未知的空白。考虑类似边缘异常场最是必要的,因为浏览器对视烦的加载行为可能存在较大差别。

  • Chrome之前的版本中,preload的默认值是auto,从64版本以后其默人值改为了metadata, 表示仅加载视频的元数据,Firefox、 IE11和Edge等浏览器的行为类似。
  • Safari 11.0的Mac版会默认进行部分视频资源预加载,11.2的Mac版后仅可预加载元数据,但ios的Safari不会对视频预加载。
  • 若浏览器开启了流量节省模式后,preload 将默认设置为none.

​ 当浏览器支持preload的metadata属性值后,这将会是一种兼顾了 性能与体验后更优的方式,因为从体验上讲,对于不自动播放的视频场景,在单击播放之前,若能提前告知视频的播放时长、播放列表等元数据,便能带给用户更好的可控感,同时又不至于提前加载了过多资源而阻塞页面渲染。

另外, 如果你的站点中包含了同一域名下的多个视频资源,那么推荐最好将preload属性设置为metadata,或者定义poster属性值时将preload设置为none,这样能很好地避免HTTP的最大连接数,因为通常HTTP 1.1协议规定同一城名下的最大连接数为6,如果同时有超过此数量的资源请求连接,那么多余的连接便会被挂起,这无疑也会对性能造成负面影响。

二、视频代替GIF动画

​ 另一种视频的使用场最是在前面章节讲到的:应当尽量用视频代替尺寸过大的GIF动画,虽然GIF动画的应用历史和范围都很广泛,但其在输出文件大小、图像色彩质量等许多方面的表现均不如视频。GIF动画相对于视频具有三个附加的特性:没有音轨、连续循环播放、加载完自动播放,替换成视频后类似于:

<video autoplay muted loop playsinline>
        <source src-"video.webm" type-"video/webm">
        <source srC-"video .mp4"type-"video/mp4">
</video>

​ 其中在视频标签中附加的属性含义分别为:autoplay自动播放、muted 静音播放及loop循环播放,而playsinline属性则是用于在ios中指定自动播放的。虽然有了GIF图像的替代方案,但并非所有浏览器都像Chrome一样, 能自动进行延迟加载。接下来就需要进行一些配置开发, 使该场最的视频也能延迟加载。首先修改HTML标签如下:

<video autoplay muted loop playsinline width="610" height="254" poster="video-poster.jpg">
        <source data-src="video. webm" type="video/webm">
        <source data-src="video .mp4" type="video/mp4">
</video>

​ 这里进行了两处修改:首先是为视频标签添加了poster 属性,意为使用poster中指定的图片作为视频延迟加载出现前的占位;其次是使用了类似应对图像延迟加载的方式,将真实视频资源的URL放在data-src属性中,然后基于Intersection Observer用JavaScript实现对延迟加载的控制:

Document.addEventListener("DOMContentLoaded", () => {
  const lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
  if ("IntersectionObserver" in window) {
    const lazyVideoObserver = new IntersectionObserver((entries, observer) => {
      entries.forEach((video) => {
        if (video.isIntersecting) {
          for (const source in video.target.children) {
            const videoSrc = video.target.children[source];
            if (
              typeof videoSrc.tagName === "string" &&
              videoSrc.tagName === "source"
            ) {
              videoSrc.src = videoSrc.dataset.src;
            }
          }
          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });
    });

    lazyVideos.forEach((lazyVideo) => {
      lazyVideoObserver.observe(lazyVideo);
    });
  }
});

​ 对视频标签的延迟加载有点类似,需要对所有 子元素进行选代解析,将data-src上的属性值迁移到src属性上。不同的是,需要额外显示调用元素的load方法来触发加载,然后视频才会根据autoplay属性开始进行自动播放。如此便可使用低于GIF动画的流量消耗,进行资源的延迟加载。

加载优化(图像延迟加载)

图像延迟加载

前言:

​ 相要得到更好的性能体验,只靠资源压缩与恰当的文件格式选型,是很难满足期望的。我们还需要针对资源加载过程进行优化,该环节所要做的内容可概括为分清资源加载的优先级顺序,仅加载当前所必需的资源,并利用系统空闲提前加载可能会用利的资源。这便是本章将要探讨的内容:资源的优先级、延迟加载和预加载。

​ 什么是图像的延迟加载,如何高效地实现延迟加载。随着近些年视频资源越来越多的使用,也会捎带介绍视频资源的延迟加载。然后谈谈浏览器对于资源优先级的划分和控制,既然可以通过将非关键资源延迟加载来提升性能,那么是否可以利用系统使用的空闲,预先去加载可能会使用到的资源。

​ 本节介绍什么是延迟加载,以及这种优化策略产生的逻辑和实现原理。笔者认为只有先理解了一种原理或方法的缘起流变,才能知道怎样的实现方式是更高效的、更贴近业务场景的。

一、什么是延迟加载

​ 首先来想象一个场景,当浏览一个内容丰富的网站时,比如电商的商品列表页、主流视频网站的节目列表等,由于屏幕尺寸的限制,每次只能查看到视窗中的那部分内容,而要浏览完页面所包含的全部信息,就需要滚动页面,让屏幕视窗依次展示出个页面的所有局部内容。

​ 显而易见,对于首屏之外的内容,特别是图片和视频,一方面 由于资源文件很大,若是全部加载完,既费时又费力,还容易阻塞渲染引起卡顿:另一方面,就算加载完成,用户也不一定会滚动屏幕浏览到全部页面内容,如果首屏内容没能吸引住用户,那么很可能整个页面就将遭到关闭。

​ 既然如此,本着节约不浪费的原则,在首次打开网站时,应尽量只加载首屏内容所包含的资源,而首屏之外涉及的图片或视频,可以等到用户滚动视窗浏览时再去加载。

​ 以上就是延迟加载优化策略的产生逻辑,通过延迟加载“非关键”的图片及视频资源,使得页面内容更快地呈现在用户面前。这里的“非关键”资源指的就是首屏之外的图片或视频资源,相较于文本、脚本等其他资源来说,图片的资源大小不容小觑。这个优化策略在业界已经被广泛使用,接下来笔者就以天猫购物网站的商品列表页为例,具体看看延迟加载是如何实现的,如图所示。

​ 图左侧是手机端常见的电商购物平台的商品列表页,右侧是其对应的DOM树结构。其中在区域上方,整齐如排比句般的

结构,所对应的正是列表页中一行行的商品项。以其中一件商品为例, 展开它的DOM树,直到找到展示该商品图片的标签。为了方便说明,笔者将这个标签的相关细节摘录如下:

<img class="boom-item-item" autowebp="false" autopixelratio="true" forceupdate="true" data-bindkey="pic" data-itemid="700023719087" data-size="348x348" data-rewrite="{size:'348x348'}" data-lazy-type="img" data-lazy-id="lazyId-11" data-lazy-manager-id="gLazyM-1" data-in-view-range="1" src="//gw.alicdn.com/bao/uploaded/i3/3058655500/O1CN01CyNBBW1qV3AjEusqC_!!0-item_pic.jpg_360x360q75.jpg_.webp">

​ 这里主要关注其中的src属性,src 属性代表了一个CDN上的图片资源。要知道当标签的src属性被赋予了一一个URL后,它就会立刻向该URL发起资源请求。所以这个商品的标签代表的就是一个商品图片的占位符。

​ 接下来我们找到一个位于屏幕视窗外,还未加载的商品图片和已加载的图片,相比较看看二者标签上的属性值有何不同。首先保持左侧页面显示窗口不发生滚动,在DevTools工具的Elements页签下,寻找还未呈现在左侧视窗中的商品项,容易找到它的DOM结构。

​ 首先,我们依然关注标签的src属性,这里并不是图片资源的外链URL,取而代之的是一个在图像优化章节中介绍过的Base64图片,与外链URL不同的是,Base64图片已经包含了图片的完全编码,可以直接拿来渲染,而无须发起任何网络请求。

​ 这意味着该Base64图片仅仅是在真实图片显示出来前用以占位的,同时注意到所有未展示在页面视窗中的商品,其图片占位src属性值均使用了相同的Base64的值.当页面发生滚动时,之前未出现在视窗中的商品出现在视窗中后,其商品图片的真实URL会被替换到标签的src属性上,进而发起资源请求。

​ 我们知道了什么是延迟加载。以及为什么要使用延迟加载,并通过观察一个商品列表页的案例,基本清楚了延迟加载的处理过程,接下来将通过三种方法来具体实现延迟加载。

二、实现图片的延迟加载:传统方式

​ 就是事件监听的方式,通过监听scroll事件与resize 事件,并在事件的回调函费中去判断,需要进行延迟加载的图片是否进入视窗区域。

​ 首先根据前面的例子,定义出将要实现延迟加载的标签结构:

我们只需要关注三个属性。

  • class 属性,稍后会在JavaScript中使用类选择器选取需要延迟加载处理的标签。
  • src属性,加载前的占位符图片,可用Base64图片或低分辨率的图片。
  • data-src属性,通过该自定义属性保存图片真实的URL外链。

假设以三张图片为例进行延迟加载的标签列表如下:

<img class="lazy" src-"data: image/gif;base64, iVBORwOKGg.. .BJRUErkJgqs-.data-src="https://res.cloudinary.com/ .../tacos-2x.jpg"width="385" height="108" alt="Some tacos. ">

<img class="lazy" src="data:image/gif;base64, iVBORw0KGg. . . BJRU5ErkJggg==" data-src="https:// res.cloudinary.com/d. . ./modem-2x . png" width="320" height="176" alt="A 56k modem. ">

<img class="lazy" src="data:image/gif;base64, iVBORw0KGg.. . BJRU5ErkJggg==" data-src="https ://res. cloudinary. com/ ../st-paul-2x. jpg" width="400" height="267" alt="A city skyline. ">

​ 具体的JvsSerpr实现逻辑如下,在文档的DOMContentLoaded事件中,添加延迟加载处理逻辑,首先获取class属性名为lazy的所有标签,将这些标签看存在一个名为lazylmages 的数组中,表示需要进行延迟加载但还未加载的图片集合。当一个图片被加载后,便将其从lazylmages数组中移除,直到lazyImages数组为空时,表示所有待延迟加载的图片均已经加载完成,此时便可将页面滚动事件移除。

​ 接下来的关键就是判断图片是否出现在视窗中,这里使用了getBoundingClientRect()函数获取元素的相对位置,如图所示。它会返回图片元素的宽width和高height,及其与视窗的相对位置:元素上边缘与屏幕视窗顶部之间的距离top,元素左边缘和屏幕视窗左侧之间的距离left,元素下边缘和屏幕视窗顶部之间的距离bottom 以及元素右边缘和屏幕视窗左侧之间的距离right, 其具体含义可参考示意图,window.innerHeight 表示整个视窗的高度。

getBoundingClientRect()函数获取元素的相对位置

​ 对于只可上下滚动的页面,判断一个图片 元素是否出现在屏幕视窗中的方法其实显而易见,即当元素上边缘距屏幕视窗顶部的top 值小于整个视窗的高度window.innerHeight时,预加载的事件处理代码如下:

document.addEventListener("DOMContentLoaded", function () {
  //获取所有需要延迟加载的图片
  let lazyImages = [].sllce.call(document.querySelectorAll("img.1azy"));
  //限制函数频繁被调用
  let active = false;
  const lazyLoad = function () {
    if (active === false) {
      active = true;
      setTimeout(function () {
        lazyImages.forEach(function (lazyImage) {
          //判断图片是否出现在视窗中
          if (
            lazyImage.getBoundingClientRect().top <= window.innerHeight &&
            lazyImage.getBoundingClientRect().bottom >= 0 &&
            getComputedstyle(lazyImage).display !== "none"
          ) {
            // 将真实的图片URL赋值给src属性,发起请求加载资源

            lazyImage.src = lazyImage.dataset.src;

            //图片加载完成后,取消监控以防止重复加载

            lazyImage.classList.remove("1azy");

            lazyImages = lazyImages.filter(function (image) {
              return image !== lazyImage;
            });

            //所有延迟加载图片加载完成后,移除事件触发处理函数

            if (lazyImages.length === 0) {
              document.removeEventListener("scroll", lazyLoad);
              window.removeEventListener("resize", lazyload);
              window.removeEventListener("orientationchange", lazyLoad);
            }
          }
        });

        active = false;
      }, 200);
    }
  };

  document.addEventListener("scroll", lazyLoad);
  window.addEventListener("resize", lazyLoad);
  window.addEventListener("orientationchange", lazyLoad);
});

​ 由于无法控制用户随心所欲地滑动鼠标滚轮,从而造成scroll 事件被触发地过于频繁,导致过多的冗余计算影响性能。所以此处笔者将延迟加载的处理过程置于一个200ms的异步定时器中,并在每次处理完成后,通过修改标志位active 的方式来对方法的执行进行限流。

​ 即便如此也有潜在的性能问题,因为重复的setTimeout调用是浪费的,虽然进行了触发限制,但当文档滚动或窗口大小调整时,不论图片是否出现在视窗中,每200ms都会运行一次检查,并且跟踪尚未加载的图片数量,以及完全加载完后,取消绑定滚动事件的处理函数等操作都需要开发者来考虑。

​ 如此来看,虽然传统的延迟加载实现方式具有更好的浏览器兼容性,但也存在如上所述不可逾越的性能问题与编码的烦琐性,这便有了下面一种新的实现方式。

三、实现图片的延迟加载: Intersection Observer方式

​ 现代浏览器已大多支持了Intersection Observer API,可以通过它来检查目标元素的可见性,这种方式的性能和效率都比较好。

​ 关于Intersection Observer的概念和用法,可以参考阅读相关文档,这里用一句话简述:每当因页面滚动或窗口尺寸发生变化,使得目标元素(target) 与设备视窗或其他指定元素产生交集时,便会触发通过Intersection Observer API配置的回调函数,在该回调函数中进行延迟加载的逻辑处理,会比传统方式显得更加简洁而高效。

​ 以下便是Intersection Observer 方式的具体实现,此方式仅需创建一个新的Observer,并在类名为lazy的标签进入视窗后触发回调。

document.addEventListener("DOMContentLoaded", function () {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  //判断浏览器兼容性
  if (
    "Intersectionobserver" in window &&
    "IntersectionObserverEntry" in window &&
    "intersectionRatio" in window.Intersection0bserverEntry.prototype
  ) {
    //新建Intersectionobserver对象,并在其回调函数中实现关键加载逻辑
    let lazyImageObserver = new IntersectionObserver(function (
      entries,observer
    ) {
      entries.forEach(function (entry) {
        //判断图片是否出现在视窗中
        if (entry.isIntersecting) {
          let lazyImage = entry.target;

          lazyImage.src = lazyImage.dataset.src;
          //图片加载完成后,取消监控防止重复加载
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });
    lazyImages.forEach(function (lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  }
});

​ 这种方式判断元素是否出现在视窗中更为简单直观,应在实际开发中尽量使用,但其问题是并非所有浏览器都能兼容。具其体的浏览器兼容情况可在站点上进行查看,根据网站用户的硬件分布情况来权衡是否使用,以及使用后是否需要进行兼容处理。在将这种方式引入项目之前,应当确保已做到以下两点。

(1)做好尽量完备浏览器兼容性检查,对于兼容Intersection Observer API的浏览器,采用这种方式进行处理,而对于不兼容的浏览器,则切换回传统的实现方式进行处理。

(2)使用相应兼容的polyfill插件,在W3C官方Git账号下就有提供。除此之外,还有第三种通过Css属性的实现方案。

四、实现图片的延迟加载:CSS类名方式

​ 这种实现方式通过css的 backgound-image 属性来加载图片,与判断标签src属性是否有要请求图片的URL不同,Css中图片加载的行为建立在浏览器对文档分析基础之上。

​ 具体来说,当DOM树、CSSOM树及渲染树生成后,浏览器会去检查CSS以何种方式应用于文档,再决定是否请求外部资源。如果浏览器确定涉及外部资源请求的CSS规则在当前文档中不存在时,便不会去请求该资源。图片列表如下所示:

<div class="wrapper">
<div class="lazy-background one"></div>
<div class="lazy-background two"></div>
<div class="lazy-background three"></div>
</div>

​ 具体的实现方式是通过javascript来判断元素是否出现在视窗中的,当在视窗中出现的时候,为其class属性添加visible类名,而在css文件中,为同一类名元素定义出带 .visible和不带.visible的两种包含 background-image规则。

​ 不带 .visible的图片规则中的background-image属性可以是低分辨辨率的图片或Base64图片,而带.visible的图片规则中的background-image属性为为希望展示的真实图片URL.

​ 具体JavaScript的实现过程如下所示,判断图片元素是否出现在视窗内的逻辑,与上面的Intersection Observer 方式相同。同样为了确保浏览器的兼容性,在实际应用中应确保提供回退方案或polyfill。

五、原生的延迟加载支持

​ 除了上述通过开发者手动实现延迟加载逻辑的方式,从Chrome 75版本开始,已经可以通过 img 和 ifram e标签的loading属性原生支持延迟加载了,loading 属性包含以下三种取值。

  • lazy: 进行延迟加载。
  • eager: 立即加载。
  • auto:浏览器自行决定是否进行延迟加载。

若不指定任何属性值,loading 默认取值auto。

​ 之前讲到延迟加载的独发触发机,都是当目标图像文件经页面滚动出现在屏幕视窗中时,能发对图像资源的请求。但从体验上考虑,这样处理并不完美,因为当图像标签出现在屏幕视窗中时,还只是占位符图像。

​ 如果网络存在延迟或图像资源过大,那么它的请求加载过程是可以被用户感知的。更好的做法是在图像即将滚动出现在屏幕视窗之前一段距离, 就开始请求加载图像或iframe中的内容,这样能很好地缩短用户的等待加载时长。

​ 兼容性处理:通过使用新技术优化了延迟加载的实现方式,同时也应当注意新技本在不同览器之间的兼容性,在使用前需要对浏览器特性进行检查,如下所示:

<script>
if ('loading' in HTMLImageElement . prototype){
//浏览器支持loading="lazy"的延迟加载方式
} else {
//获取其他JavaScript库来实现延迟加载
}
</script>

​ 当判断浏览器支持通过属性loading=”lazy”来进行延迟加载时,我们就在JavaScript处理程序中,将真实图像资源的URL赋值在其src属性上。而对于不支持该属性配置的延迟加载方式,就需要默认将真实图像资源的URL挂在data-src 属性上,仅当延迟加载的滚动事件触发时,才将data-src属性上的值换到src属性上。

​ 这也正是我们在传统方式中实现的加载策略,其原因是如果浏览器不支持标签的loading属性,便会立刻发起对src 属性上URL资源的网络请求。当然我们也可以使用CSS类名的方式触发对资源的加载。

< img data-src= “photo.jpg” loading=”lazy” class=”lazyload” alt=”photo” />不过对于这种方式,笔者建议等到loading属性在浏览器的稳定版本中被引入后,再在项目的生产环境中使用。

参考书籍:前端性能优化

图像优化

图像优化

前端大部分的工作都围绕在JavaScript和CSS上,考虑如何更快地下载文件,如何提供给用户复杂而优雅的交互,如何高效合理地应用有限的处理和传输资源等,这些是用户感知的全部吗?

当然,他们在前端开发和性能优化中的地位举足轻重,但JavaScript和CSS对用户感知而言,并不是最重要的部分,图像才是。我们在公众号发布文章或用PPT进行演讲时,都知道一条高效传递信息的原则:字不如表,表不如图。

网站作为一种信息传递的媒介,且如今各类Web项目中,图像资源的使用占比也越来越大,更应当注重图像资源的使用方式。如果网站中的图像资源未进行恰当的优化,那么势必会导致许多问题,诸如巨量的访问请求引发传输带宽的挑战,请求大尺寸图片需要过久的等待时间等。

图像优化问题主要可以分为两方面:图像的选取和使用,图像的加载和显示。对于加载方面的策略将放在 加载优化 中深入讨论,本章将聚焦图像的选取和使用。本章内容包括:什么是图像文件,都有哪些格式的图像文件,不同格式的图像文件适用于怎样的业务场景,以及通过怎样的优化方法能够有效提升用户对图像的体验感知等。

一、图像基础

​ HTTP Archive上的数据显示,网站传输的数据中,60%的资源都是由各种图像文件组成的,当然这个数据是将各种类型网站平均之后的结果,要是单独看电商类面向消费者端页面的数据,这个比例可能会更大。如此之大的资源占比,也同样意味着存在很大的优化空间。

1.图像是否必需

​ 图像资源优化的根本思想,可以归结为两个字: 压缩。无论是选取何种图像的文件格式,还是针对同一种格式压缩至更小的尺寸,其本质都是用更小的资源开销来完成图像的传输和展示。

​ 在深入探讨之前,我们首先思考一下要达到期望的信息传递效果,是否真的需要图像?这不仅是因为图像资源与网页上的其他资源(HTML/CSS/JavaScript等)相比有更大的字节开销,出于对节省资源的考虑,对用户注意力的珍惜也很重要,如果一个页面打开后有很多图像,那么用户其实很难快速梳理出有效的信息,即便获取到了也会让用户觉得很累。

​ 一个低感官体验的网站,它的价值转化率不会很高。当然这个问题的答案不是通过自己简单想想就能得到的,我们可能需要在日常的开发中与产品经理及体验设计师不断沟通,不断思考,来趋近更优的方案。

​ 当确定了图像的展示效果必须存在时,在前端实现上也并非一定就要用图像文件,还存在一些场景可以使用更高效的方式来实现所需的效果。

  • 网站中一个图像在不同的页面或不同的交互状态下,需要呈现出不同的效果(边角的裁切、阴影或渐变),其实没有必要为不同场景准备不同效果的多份图像文件,只需用CSS将一-张图像处理为所需的不同效果即可。相对于一个图像文件的大小来讲,修改其所增加的CSS代码量可以忽略不计。
  • 如果一个图像上面需要显示文字,建议使用网页字体的形式通过前端代码进行添加,而不是使用带文字的图像,其原因一方面是包含了更多信息的图像文件一般会更大, 另一方面是图像中的文本信息带来的用户体验一般较差 (不可选择、搜索及缩放),并且在高分辨率设备上的显示效果也会打折扣。

这里列举的两个例子,为了说明当我们在选择使用某种资源之前,如果期望达到更优的性能效果,则需要先去思考这种选择是否必需。

2.矢量图和位图

​ 当确定了图像是实现展示效果的最佳方式时,接下来就是选择合适的图像格式。图像文件可以分为两类:矢量图和位图。每种类型都有其各自的优缺点和适用场景。

1.矢量图

​ 矢量图中的图形元素被定义为一个对象,包括颜色、大小、形状及屏幕位置等属性。它适合如文本、品牌logo、控件图标及二维码等构图形状较简单的几何图形。矢量图的 优点 是能够在任何缩放比例下呈现出细节同样清晰的展示效果。其缺点是对细节的展示效果不够丰富,对足够复杂的图像来说,比如要达到照片的效果,若通过SVG进行矢量图绘制,则所得文件会大得离谱,但即便如此也很难达到照片的真实效果。

​ SVG也是一种基于XML的图像格式,其全称是Scalable Vector Graphics (可缩放的矢量图形),目前几乎所有浏览器都支持SVG.我们可以在Iconfont.上找到许多矢量图,或者上传自己绘制的矢量图,在上面构建自己的矢量图标库并引入项目进行使用,如图所示。

矢量图标

标识照片的矢量图标的SVG标签格式,如图所示。

​ SVG标签所包括的部分就是该矢量图的全部内容,除了必要的绘制信息,可能还包括一些元数据,比如XML命名空间、图层及注释信息。但这些信息对浏览器绘制一个 SVG来说并不是必要的,所以在使用前可通过工县去除这些元数据来达到压缩的目的。

2.位图

​ 位图是通过对一个矩阵中的栅格进行编码来表示图像的,每个栅格只能编码表示一个特定的颜色,如果组成图像的栅格像素点越多且每个像素点所能表示的颜色范围越广,则位图图像整体的显示效果就会越逼真。虽然位图没有像矢量图那种不受分辨率影响的优秀特性,但对于复杂的照片却能提供较为真实的细节体验,如图中一幅海边的位图对于云朵及波浪的细节表现,如果用矢量图来实现是不可想象的。

海边的位图

​ 当把图像不断放大后,就会看到许多栅格像素色块,如图所示。每个像素存储的是图像局部的RGBA信息,即红绿蓝三色通道及透明度。通常浏览器会为每个颜色通道分配一个字节的存储空间,即2^8=256个色阶值。

放大后的位图局部

​ 一个像素点4个通道就是4字节,一张图像整体的大小与其包含的像素数成正比,图像包含的像素越多,所能展示的细节就越丰富,同时图像就越大。

​ 如表所示,当图像尺寸为100像素 x 100像素时,文件大小为39KB.随着图像尺寸在长和宽两个维度上同时增大,所产生像素数量的增加就不是简单的线性关系了,而是平方的抛物线增加,也就是说文件大小会迅速增加,在网络带宽一 定的前提下,下载完张图像会更慢。

图像尺寸 像素数量 文件大小
100像素 x 100像素 10,000 39KB
200像素 x 200像素 40,000 156KB
500像素 x 500像素 250,000 977KB
800像素 x 800像素 640,000 2.5MB

​ 出于对性能的考虑,在使用图像时必须考虑对图像进行压缩,采用什么样的图像格式,使用什么样的压缩算法及压缩到何种程度,这将是本章接下来详细讨论的内容,但在此之前先说明关于分辨率的两个容易混淆的概念。

3.分辨率

​ 在前端开发过程中书写CSS时,经常会为图像设置显示所需的长宽像素值,但在不同的设备屏幕上,有时候相同的图像及相同的设置,其渲染出来的图像会让人明显察觉出清晰度有差别。产生这个现象的原因涉及两种不同的分辨率:屏幕分辨率和图像分辨率。

​ 图像分辨率表示的就是该图像文件所包含的真实像素值信息,比如一个 200像素X200像素的分辨率的图像文件,它就定义了长宽各200个像素点的信息。设备分辨率则是显示器屏幕所能显示的最大像素值,比如一台13英寸的Mac Pro 笔记本电脑的显示器分辨率为2560像素 x 1600像素。这两种分辨率都用到了像素,那么它们有什么区别呢?

​ 更高的设备分辨率有助于显示更绚丽多彩的图像,这其实很话合矢量图的发挥,因为它不会因放大而失真。而对位图来说,只有图像文件包含更多的像素信息时,才能更充分地利用屏幕分辨率。为了能在不同的分辨率下使项目中所包含的图像都能得到恰当的展示效果,可以利用picture标签和srcset 属性提供图像的多个变体。

​ 用于插入图像的ing标签,有一个 srcset属性可以用来针对不同设备,提供不同分辨率的图像文件:

< img src="photo.jpg" srcset="photo@2x.jpg 2x,photo@3x.jpg 3x, photo@4x.jpg 4x" alt="photo">

​ 除了IE和其他较低版本的浏览器不支持,目前主流的大部分浏览器都已支持img标签的srcset 属性。在srcset属性中设置多种分辨率的图像文件及使用条件,浏览器在请求之前便会先对此进行解析,只选择最合适的图像文件进行下载,如果浏览器不支持,请务必在src属性中包含必要的默认图片。

​ 使用picture标签则会在多图像文件选择时,获得更多的控制维度,比如屏幕方向、设备大小、屏幕分辨率等。

<picture>
  <source media=" (min-width:800px)" srcset="photo.ipg, photo-2x.jpg 2x">
  <source media=" (min-width:450px)" srcset="photo-s.jpg photo-s-2x.jpg 2x">
  < img src="photo.jpg">
</picture>

​ 由于picture标签也是加入标准不久的元素标签,所以在使用过程中,同样应当考虑兼容性问题。

4.压缩的有损和无损

​ 压缩是降低源文件大小的有效方式,对JavaScript代码或网页的一些脚本文件而言,压缩掉的内容是一些多余的空格及不影响执行的注释,其目的是在不损坏正常执行的情况下,尽量缩小源文件的大小。对图像文件而言,由于人眼对不同颜色的敏感度存在差异,所以便可通过减少对某种颜色的编码位数来减小文件大小,甚至还可以损失部分源文件信息,以达到近似的效果,使得压缩后的文件尺寸更小。

​ 对于图像压缩,应该采用有损压缩还是无损压缩?如果都采用又该如何搭配设置呢?当结合了具体的业务需求再考虑后,关于压缩的技术选型就可以简单分成两步进行。

(1)首先确定业务所要展示图像的颜色阶数、图像显示的分率及清晰程度,当锚定了这几个参数的基准后,如果获取的图像源文件的相应参数指标过高,便可适当进行有损压缩,通过降低源文件图像质量的方法来降低图像文件大小。

​ 如果业务所要求的图像质量较高,便可跳过有损压缩,直接进入第二步无损压缩。所以是否要进行有损压缩,其实是在理解了业务需求后的一个可选选项,而非必要的。

(2)当确定了展示图像的质量后,便可利用无损压缩技术尽可能降低图像大小。和第(1)步要通过业务决策来判断是否需要所不同的是,无损压缩是应当完成的工作环节。那么最好能通过一套完善的工程方案,自动化执行来避免烦琐的人工重复工作。

二、图像格式

​ 实际上,不同的图像文件格式(JPG、PNG、GIF 等)之间的区别,在于它们进行有损压缩和无损压缩过程中采用了不同的算法组合,接下来我们将从不同的图像文件格式入手,看看它们的特点和使用场景,以及在具体业务中应该如何选取。

1.JPEG

​ JPEG可能是目前所有图像格式中出现最早,同时也是使用范围最广的一种格式。它也是一种有损压缩算法,它通过去除相关冗余图像和色彩数据等方式来获得较高的压缩率,同时还能展现出相当丰富的图像内容。

​ JPEG在网站开发中经常被用作背景图、轮播图或者一些商品的banner图,以呈现色彩丰富的内容。但由于是有损压缩,当处理Logo或图标时,需要较强线条感和强烈颜色对比,JPEG图像可能会出现一些边 界模糊的不佳体验,另外JPEG图像并不支持透明度。

​ 接下来介绍有关JPEG常用的压缩编码方式,以及在工程实践中如何自动批量处理。

1.压缩模式

​ JPEG包含了多种压缩模式,其中常见的有基于基线的、渐进式的。简单来说基线模式的JPEG加载顺序是自上而下的,当网络连接缓慢或不稳定时,其是从上往下逐渐加载完成的,如图所示。

基线JPEG

     渐进式模式是将图像文件分为多次扫描,首先展示一个低质量模糊的图像,随着扫描到的图像信息不断增多,每次扫描过后所展示的图像清晰度也会不断提升,如图所示。

渐进式JPEG

2.渐进式JPEG的优缺点

​ 渐进式JPEG的优点是显而易见的,在网络连接缓慢的情况下,首先能快速加载出一个图像质量比较模糊的预览版本。这样用户便可据此了解图像的大致内容,来决定是否继续花费时间等待完整图像的加载。这样做可以很好地提高对用户的感知性能,用户不仅知道所访问图像的大致内容,还会感知完整的图像就快加载好了。如果读者平时留心观察,应该能注意到渐进式JPEG已经在渐渐取代基线JPEG了。

​ 通过了解两种压缩的原理不难发现,渐进式JPEG的解码速度会比基线的要慢一些,因为它增加了重复的检索开销。另外,通过渐进式JPEG压缩模式得到的图像文件也不一定是最小的,比如特别小的图像。所以是否要采用渐进式JPEG,需要综合考虑文件大小、大部分用户的设备类型与网络延迟。

3.创建渐进式JPEG

​ 如果所得到的图像不是渐进式JPEG,那么我们可以通过许多第三方工具来进行处理,例如imagemin、libjpeg、 imageMagick 等。值得注意的是,这个步骤应当尽量交给构建工具来自动化完成,通过如下代码可以将该工作加入gulp处理管道:

const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
gulp.task('images',()=> 
    gulp.src('images/*.jpg')
    .pipe(imagemin({
        progressive:true
    }))
    .pipe(gulp.dest('dist'))
);

在执行构建流程后,gulp 会调用imagemin的方法把images 文件夹下的所有jpg后缀图像全部进行渐进式编码处理。

4.其他JPEG编码方式

​ 除了常见的基线与渐进式压缩编码方式,最近还出现了几种现代的JPEG编码器,它们尝试以更高的保真度及压缩后更小的文件大小为目标,同时还兼容当前主流的浏览器。其中比较出色的有Mozilla基金会推出的MozJPEG和Google提出的Guetzli。

​ MozJPEG和Guetzli也都已经有了可靠的imagemin插件支持,其使用方式与渐进式JPEG处理方式类似,这里仅列出示例代码,具体工程化构建请读者结合项目实践进行改写。

 const gulp = require('gulp');
const imagemin = require ('gulp-imagemin');
const imageminMozJPEG = require ('imagemin-mozjpeg'); //引入MozJPEG依赖包
const imageminGuetzli = require ('imagemin-guetzli'); //引入Guetzli依赖包
//MozJPEG压缩编码
gulp. task('mozjpeg', () =>
    gulp.src('image/*. jpg')
    .pipe (imagemin([
        imageminMozJPEG({quality: 85 })
    ]))
    .pipe(gulp.dest('dist'))
)
//Guetzli压缩编码
gulp.task('guetzli', () =>
    gulp.src('image/* . jpg')
    .pipe (imagemin([
        imageminGuetzli ({quality: 85 })
    ]))
    .pipe (gulp.dest('dist'))
)

​ MozJPEG引入了对逐行扫描的优化及一些栅 格量化的功能,最多能将图像文件压缩10%,而Guetzli则是找到人眼感知上无法区分的最小体积的JPEG,那么两者的优化效果具体如何,又如何评价呢?

​ 这里需要借助两个指标来进行衡量,首先是用来计算两个图像相似度的结构相似性分数(Structural Similarity index),简称SSIM,具体的计算过程可以借助第三方工具jpeg-compress 来进行,这个指标分数以原图为标准来判断测试图片与原图的相似度,数值越接近1表示和原图越相似。

​ Butteraugli则是一种基 于人类感知测量图像的差异模型,它能在人眼几乎看不出明显差异的地方,给出可靠的差别分数。如果SSIM是对图像差别的汇总,那么Butteraugli则可以帮助找出非常糟糕的部分。表列出了MozJPEG编码压缩后的数据比较。

原图大小 982 KB Q=90 / 841KB Q=85 / 562KB Q=75 /324KB
SSIM 0.999936 0.999698 0.999478
Butteraugli 1.576957 2.483837 3.66127

​ MozJPEG编码压缩后的数据比较

原图大小 982 KB Q=100 / 945KB Q=90 / 687KB Q=85 / 542KB
SSIM 0.999998 0.99971 0.999508
Butteraugli 0.40884 1.580555 2.0996

​ Guetzli编码压缩后的数据比较

不仅要考虑图像压缩的质量和保真度,还要关注压缩后的大小,没有哪种压缩编码方式在各种条件下都是最优的,需要结合实际业务进行选择。这里可以给读者一些使用建议:

  • 使用一些外部工具找到图像的最佳表现质量后,再用MozJPEG进行编码压缩。
  • Guetzli会获得更高质量的图像,压缩速度相对较慢。

​ 虽然本节介绍了关于JPEG的若干编码器,也对它们之间的差别进行了比较,但需要明确的一点是,最终压缩后的图像文件大小差异更多地取决于设置的压缩质量,而非所选择的编码器。所以在对JPEG进行编码优化时,应主要关注业务可接受的最低图像质量。

2.GIF

​ GIF是Graphics Interchange Format的缩写,也是一种比较早的图像文件格式。 由于对支持颜色数量的限制,256色远小于展示照片所需颜色的数量级,所以GIF并不适合用来呈现照片,可能用来呈现图标或Logo会更适合些, 但后来推出的PNG格式对于图形的展示效果更佳,所以当下只有在需要使用到动画时才会使用GIF。

接下来探讨一些关于GIF的优化点。

  1. 单帧的GIF转化为PNG

​ 首先可以使用npm引入ImageMagick工具来检查GIF图像文件,看其中是否包含多帧动画。如果GIF图像文件中不包含多帧动画,则会返回一个GIF字符串,如果GIF图像文件中包含动画内容,则会返回多帧信息。

​ 对于单帧图像的情况,同样可使用ImageMagick工具将其转化为更适合展示图形的PNG文件格式。对于动画的处理稍后会进一步介绍, 这里先列出代码示例:

const im = require('imagemagick');
//检查是否为动画
im.identify(['-format','%m','my.gif'],(err,output)=>{
    if (err) throw err;
    //通过output处理判断流程
})
//将gif转化为png
im.convert(['my.gif','my.png'],(err,stdout)=>{
    if (err) throw err;
    console.log('转化完成',stdout)
})
  1. GIF 动画优化

​ 由于动画包含了许多静态帧,并且每个静态帧图像上的内容在相邻的不同帧上通常不会有太多的差异,所以可通过工具来移除动画里连续帧中重复的像素信息。这里可借助gifsicle来实现:

const { execFile } = require('child_process');
const gifsicle = require('gifsicle');
execFile(gifsicle,['-o','output.gif','input.gif'],err => {
    console.log('动画压缩完成')
})
  1. 用视频替换动画

​ 当了解过GIF的相关特性后,不难发现如果单纯以展示动面这个目的来看,那么GIF可能并不是最好的呈现方式,因为动画的内容将会受到诸如图像质量、播放帧率及播放长度等因素的限制。

​ GIF展示的动画没有声音,最高支持256色的图像质量,如果动画长度较长, 即便压缩过后文件也会较大。综合考虑,建议将内容较长的GIF动画转化为视频后进行插入,因为动画也是视频的一种, 成熟的视频编码格式可以让传输的动画内容节省网络带宽开销。

​ 可以利用ffmpeg将原本的GIF文件转化为MPEG-4或WebM的视频文件格式,将一个14MB的GIF动画通过转化后得到的视频文件格式大小分别是: MPEG-4格式下867KB, WebM 格式下611KB.另外,要知道通过压缩后的动画或视频文件,在播放前都需要进行解码,可以通过Chrome的跟踪工具(chrome://tracing)查看不同格式的文件,在解码阶段的CPU占用时,文件格式与CPU耗时如表所示。

文件格式 CPU耗时(ms)
GIF 2,668
MPEG-4 1,995
WebM 2,354

​ 从表中可以看出,相比视频文件,GIF 在解码阶段也是十分耗时的,所以出于对性能的考虑,在使用GIF前应尽量谨慎选择。

3.PNG

​ PNG是一种无损压缩的高保真图片格式,它的出现弥补了GIF图像格式的一些缺点,同时规避了当时GIF中还处在专利保护期的压缩算法,所以也有人将PNG文件后缀的缩写表示成“PNG is Not GIF”。

​ 相比于JPEG, PNG支持透明度,对线条的处理更加细腻,并增强了色彩的表现力,不过唯一的不足就是文件体积太大。如果说GIF是专门为图标图形设计的图像文件格式,JPEG是专门为照片设计的图像文件格式,那么PNG对这两种类型的图像都能支持。通常在使用中会碰到PNG的几种子类型,有PNG-8、PNG-24、 PNG-32等。

1.对比GIF

​ 其中PNG-8也称为调色板PNG,除了不支持动画,其他所有GIF拥有的功能它都拥有,同时还支持完全的alpha通道透明。只要不是颜色数特别少的图像,PNG-8的压缩比表现都会更高一筹。

​ 对于颜色数少的单帧图形图像来说,更好的做法也并不是将其存为一个GIF文件,相比雪碧图都会更好一些,因为能够大大降低HTTP请求的开销,这一点后面章节会接着介绍,此处给出个优化建议: 在使用单帧图形图像时,应当尽量用 PNG-8格式来替换GIF格式。

2.对比JPEG

​ 当所处理图像中的颜色超过256种时,就需要用到JPEG或者真彩PNG,真彩PNG包括PNG-24和PNG-32二者的区别是真彩PNG-24不包括alpha透明通道,而加上8位的alpha透明通道就是真彩PNG-32。

​ JPEG是有损的。它拥有更高的压缩比,也是照片存储的实际标准,如果还是要用PNG,那么很可能是在清晰的颜色过度周围出现了不可接受的“大色块”。

3.优化PNG

​ PNG图像格式还有一个优点,就是便于扩展,它将图像的信息保存在“块”中,开发者便可以通过添加一些自定义的“块”来实现额外的功能,但所添加的自定义功能并非所有软件都能读取识别,大部分可能只是特定的作图软件在读取时使用而已。对Web显示而言,浏览器可能直接将这些多余的块自动忽略掉了,如果对显示没有作用,那么又何必要存储和传输这些信息呢?因此我们可以使用pngcrush对这些多余的块进行删除压缩,通过npm引入imagemin-pngcrush,代码如下:

const imagemin = require('imagemin');
const imageminPngcrush = require('imagemin-pngcrush');
imagemin(['images/*.png'],'build/images',{
    plugins:[imageminPngcrush()]
})
.then(()=> console.log('完成图像优化'))

其中,imageminPngcrush()中可以带入如下一些 参数进行压缩控制。

  • -rem alla: 删除所有块,保留控制alpha透明通道的块。
  • -brute: 采用多种方法进行压缩会得到较好的压缩效果,由于执行的方法较多,所以执行压缩的速度会变慢,建议在离线操作下可以添加,但有时改进效果并不明显,如果对构建流程有要求可不加。
  • -reduce: 会尝试减少调色板使用的颜色数量。

4.WebP

​ 前面介绍的三种图像文件格式,在呈现位图方面各有优劣势: GIF虽然包含的颜色阶数少,但能呈现动画: JPEG虽然不支持透明度,但图像文件的压缩比高:PNG虽然文件尺寸较大,但支持透明且色彩表现力强。

​ 开发者在使用位图时对于这样的现状就需要先考虑选型。假如有一个统的图像文件格式,具有之前格式的所有优点,WebP 就在这样的期待中诞生了。

  1. WebP的优缺点

​ WebP是Google在2010年推出的一种图像文件格式, 它的目标是以较高的视觉体验为前提的,尽可能地降低有损压缩和无损压缩后的文件尺寸,同时还要支持透明度与动画。根据WebP官方给出的实验数据,当使用WebP有损文件时,文件尺寸会比JPEG小25%~ 34%,而使用WebP无损文件时,文件尺寸会比PNG小26%。就像所有新技术一样, 具有如此多优异特性的WebP, 同样也不可避免兼容性的问题,CanlUse. com网站数据统计情况,如图所示。

​ 从图中可以发现,除了IE不支持,其他主流浏览器的最新版本都已支持WebP, 考虑到浏览器的市场占有率,这样的兼容性程度可以说是非常乐观的了。虽然还需要做一 些兼容性处理,但我们也有足够的理由在项目中积极地使用WebP.此外,由于有损压缩WebP使用了VP8视频关键帧编码,可能对较高质量(80~ 99)的图像编码来说,会比JPEG占用更多的计算资源,但在较低质量(0~50)时,依然有很大的优势。

2.如何使用WebP

​ 可以使用图像编辑软件直接导出WebP格式的图像文件,或者将原有的JPEG或PNG图像转化为WebP格式。这样的转化最好使用构建工具辅助完成,比如通过npm安装webp-loader后,在webpack中进行如下配置:

loader:[{
    test:/\.(jpe?g|png)$/I,
    loaders:[
        'file-loader',
        'webp-loader?{quality:13}'
    ],
}]

​ 这里值得注意的是,尽量不要使用低质量的JPEG格式进行WebP转化,因为低质量的JPEG中可能包含压缩的伪像,这样WebP不仅要保存图像信息,还要保存JPEG添加的失真,从而影响最终的转化效果。所以在选择转化的源图像文件时,建议使用质量最佳的。

3.兼容性处理

​ 目前WebP格式的图像并不适用于所有浏览器,所以在使用时,应当注意兼容处理好不支持的浏览器场景。

​ 通常的处理思路分为两种:一种是在前端处理浏览器兼容性的判断,可以通过浏览器的全局属性window.navigator. userAgent获取版本信息,再根据兼容支持情况,选择是否请求WebP图像格式的资源;也可以使用标签来选择显示的图像格式,在 标签中添加多个标签元素,以及一个包含旧图像格式的标签,当浏览器在解析DOM时便会对标签中包含的多个图片源依次进行检测。

​ 倘若浏览器不支持WebP格式而未能检测获取到,最后也能够通过标记兼容显示出旧图像格式,例如:

<picture>
   <source srcset="/path/image.webp" type="image/webp">
   <img src="/path/image.jpg" alt="">
</picture>

这见需要注意的标签的顺序位置,应当将包含image/webp的图像源写在旧图像格式的前面。

​ 另一种是将判断浏览器是否支持的工作放在后端处理,让服务器根据HTTP请求头的Accept字段来决定返回图像的文件格式。如果Accept 字段中包含image/webp,就返回WebP图像格式,否则就使用旧图像格式(JPEG、PNG等)返回。这样做的好处是让系统的维护性更强,无论浏览器对WebP图像格式的兼容支持发生怎样的改变,只需要服务器检查Accept字段即可,无须前端跟进相应的修改。

5.SVG

​ 前面介绍的几种图像文件格式呈现的都是位图,而SVG呈现的是矢量图。正如我们在介绍位图和矢量图时讲到的,SVG对图像的处理不是基于像素栅格的,而是通过图像的形状轮廓、屏幕位置等信息进行描述的。

1.优缺点

​ SVG这种基于XML语法描述图像形状的文件格式,就适合用来表示Logo等图标图像,它可以无限放大并且不会失真,无论分辨率多高的屏幕,个文件就可以统一适配:另外,作为文本文件,除了可以将SVG标签像写代码样写在HTML中,还可以把对图标图像的描述信息写在以.svg为后缀的文件中进行存储和引用。

​ 由于文本文件的高压缩比,最后得到的图像文件体积也会更小。要说缺点与不足,除了仅能表示矢量图,还有就是使用的学习成本和渲染成本比较高。

2.优化建议

即便SVG图像文件拥有诸多优点,但依然有可优化的空间。下面介绍一些优化建议。

(1)应保持SVG尽量精简,去除编辑器创建SVG时可能携带的一些冗余信息,比如注释、隐藏图层及元数据等。

(2)由于显示器的本质依然是元素点构成位图,所以在渲染绘制矢量图时,就会比位图的显示多一步光栅化的过程。为了使浏览器解析渲染的过程更快,建议使用预定义的SVG形状来代替自定义路径,这样会减少最终生成图像所包含标记的数量,预定义形状有等。

(3)如果必须使用自定义路径,那么也尽量少用曲线。

(4)不要在SVG中嵌入位图。

(5)使用工具优化SVG,这里介绍一款基于node.js 的优化工具svgo;它可以通过降低定义中的数字精度来缩小文件的尺寸。通过npm install -g svgo 可直接安装命令方式使用,若想用webpack进行工程化集成,可加入svgo-loader的相关配置:

module.exports = {
    rules:[
        test: /\.svg$/,
        use: [
            {loader:'file-loader'},
            {loader:'svgo-loader',options:{externalConfig:'svgo-config.yml'},
        }]
    ]
}

其中,可在 svgo-config.yml 的配置文件中定义相关优化选项:

plugins:
 - removeTitle: true
 - convertPathData: false
 - convertColors:
    shorthex: false

(6)优化过后,使用gzip压缩和(或)brotli压缩。

6.Base64

​ 准确地说,Base64 并不是一种图像文件格式, 而是种用于传输 8位字节码的编码方式,它通过将代表图像的编码直接写入HTML或CSS中来实现图像的展示,一般展示图像的方法都是通过将图像文件的URL值传给img标签的src属性,当HTML解析到img标签时,便会发起对该图像URL的网络请求:

< img src=”https://xx.cdn.com/photo.jpg">

​ 当采用Base64编码图像时,写入src的属性值不是URL值,而是类似下面的编码:data: image/png;base64, iVBORwOKGgOAAAANSUhEUgAABYAAAWCAYAADEtGw7AA.

​ 浏览器会自动解析该编码并展示出图像,而无须发起任何关于该图像的URL,这是Base64的优点,同时也隐含了对于使用的限制。由于Base64编码原理的特点,一般经过Base64编码后的图像大小,会膨胀四分之三。

​ 这对想尝试通过Base64方式尽可能减少HTTP请求次数来说是得不偿失的,较复杂的大图经过编码后,所节省的几次HTTP请求,与图像文件大小增加所带来的带宽消耗相比简直是杯水车薪。所以也只有对小图而言,Base64 才能体现出真正的性能优势。

​ 作为使用指导建议,笔者希望在考虑是否使用Base64编码时,比对如下几个条件:

  • 图像文件的实际尺寸是否很小。
  • 图像文件是否真的无法以雪碧图的形式进行引入。
  • 图像文件的更新频率是否很低,以避免在使用Base64时,增加不心必要的维护成本。

7.格式选择建议

​ 在了解了不同图像文件格式的特性后,显而易见的是不存在适用于任何场景且性能最优的图像使用方式。所以作为开发者,想要网站性能在图像方面达到最优,如何根据业务场景选择合适的文件格式也至关重要,图像文件使用策略如图所示。

图像文件使用策略

​ 这里根据使用场景的不同,以及图像文件的特性给出了一个可参考的选择策略:考虑到矢量图具有缩放不失真且表示图标时较小的文件尺寸,凡用到图标的场景应尽量使用矢量图:而对于位图的使用场景,由于在相同图像质量下其具有更高的压缩比且支持动画,所以WebP格式应该是我们的首选。

​ 考虑到新技术的兼容性问题,也需要采用传统的方式进行适配;包含动画时使用GIF,对图像要求有更高分辨率来展示细节且需要透明度时,建议使用PNG;而在其他场景下追求更高的图像压缩比时,可使用JPEG.除此之外,位图对于不同缩放比的响应式场景,建议提供多张不同尺寸的图像,让浏览器根据具体场景进行请求调用。

三、使用建议

​ 本节额外给出些使用建议来优化图像 资源的体验性能, 包括合并多张小图资源请求次数的雪碧图方案,使用Web字体的方式来替代图标文件及display:none使用的注意事项。

1.CSS Sprite

​ CSS Sprite技术就是我们常说的雪碧图,通过将多张小图标拼接成一张大图, 有效地减少HTTP请求数量以达到加速显示内容的技术。

​ 通常对于雪碧图的使用场景应当满足以下条件:首先这些图标不会随用户信息的变化而变化,它们属于网站通用的静态图标;同时单张图标体积要尽量小,这样经过拼接后其性能的提升才会比较乐观;若加载量比较大则效果会更好。

​ 不建议将较大的图片拼接成雪碧图,因为大图拼接后的单个文件体积会非常大,这样占用网络带宽的增加与请求完成所耗费时间的延长,会完全淹没通过减少HTTP请求次数所带来的性能提升。下面来看一个雪碧图实际案例,如图所示。

​ 图中截取了淘宝网一处图标导航栏及请求的相应资源,通过案例还可以看出所拼接的雪碧图是一张PNG格式的图像文件,其中的图标不只含有一种颜色, 同时也可支持颜色渐变,这通常是单色Web字体很难具备的表现力。

雪碧图的使用方式也很简单,通过CSS 的background-image 属性引入雪碧图的UrL后,再使用background-position定位所需要的单个图标在雪碧图上的起始位置,配合width和height属性来锁定具体图标的尺寸,示例代码如下:

.sprite-sheet{
    background-image: url(https://img.xxxx.com/xxx/sprite-sheet.png);
    background-size: 24px 600px;
}
.icon-1 .sprite-sheet{
    background-position: 0 0;
    height: 24px;
    width: 24px;
}
.icon-2 .sprite-sheet{
    background-position: 0 -24px;
    height: 24px;
    width: 24px;
}

​ 其中,background-position属性关于横纵偏移的设置规则指的是如何通过设置背景图的偏移,将雪碧图上所需图标的左上角起始位置移至坐标(0,0)位置。与通常数学上的直角坐标系不同,浏览器中的坐标系Y轴正方向是垂直向下的。当引入雪碧图后,整个图片的左上角起始位置在(0,0),所以要得到其中的某个图标,我们就需要将雪碧图向负轴方向进行偏移,如图所示。

雪碧图与坐标系

​ 如果使用第一行左边第一 个图标,则可通过设置background-position: 0 0来让雪碧图不偏移(两个0之间有空格,分别表示在X轴、Y轴的位置),倘若要使用第二行中间的图标,就需要将雪碧图向左上方偏移,将属性backgound-position的值设置为-24px -24px,注意是负值,如图所示。

偏移后的雪碧图

​ 使用雪碧图来提升小图标加载性能的历史由来已久。在HTTP 1.x环境下,它确实能够减少相应的HTTP请求,但需要注意当部分图标变更时,会导致已经加载的雪碧图缓存失效。同时在HTTP2中,最好的方式应该是加载单张图像文件,因为可以在一个HTTP连接上发起多次请求,所以对于是否使用此方法,需要考虑具体的使用环境和网络设置。

2.Web 字体

​ 使用Web字体有多种优点:增强网站的设计感、可读性,同时还能搜索和选取所表示的文本内容,且不受屏幕尺寸与分辨率的影响,能提供一致的视觉体验。 除此之外,由于每个字型都是特定的矢量图标,所以可以将项目中用到的矢量图标打包到一个Web字体文件中使用,以节省对图标资源的HTTP请求次数,这样做类似雪碧图优化目的。

1.字体的使用

​ 目前网络上常用的字体格式有: EOT、TTF、WOFF与WOFF2,由于存在兼容性的问题,并没有哪一种字体能够适用所有浏览器,所以在实际使用中,网站开发者会声明提供字体的多种文件格式,来达到一致性的体验效果。

在Web项目中,一般 会先通过@font-face声明使用的字体系列:

@font-face {
font-family: 'tianfont';
src: url('//at.alicdn.com/t/font_ 1307911 xxxx.eot');
src: url('//at.alicdn.com/t/fot 1307911 xxx.eot?#iefix') format('enmbederopentype'),
url('//at.alicdn.com/t/font_ 1307911 xxxx.woff2') format('woff2'),
url('//at.alicdn.com/t/font_ 1307911 xxxx.woff') format('woff'),
url('//at.alicdn.com/t/font_ 1307911_ xxxx.ttf') format ('truetype'),
url('//at.alicdn.com/t/font_ 1307911 xxxx.svg#tianfont') format('svg'),
}

​ 在上述代码中通过src字段的属性值,可以指定字体资源的位置,并且该属性值还可以提供一个用逗 号分隔的列表,列表中不同字体文件格式的资源顺序同样重要,浏览器将选取其所支持的第一个格式资源。 如果希望较新的WOFF2格式被使用,则应当将WOFF2声明在WOFF之上。

2.子集内嵌

​ 对于同一个字符,Web字体可以根据样式、粗细及拉伸等属性的不同,拥有多种变种的字型展示。如果将所有字型都打包成一个文件来请求使用, 不免就会存在许多根本用不到的字型信息浪费带宽。相较于拉丁文字体而言,包含中文字符的字体文件的大小会格外突出。字体文件是否能够按需加载,就成为一个显而易见的优化项,这便是子集内嵌。

​ 通过@font- face和unicode-range属性就可以定义所使用的字体子集,属性unicode-range用来指定所需字体在@font-face声明字体集中的子集范围,它支持三种形式:单一取值(如U+233)、范围取值(如U+233-2ff)、通配符范围( 如U+2??),取值的含义是字体集文件中的代码索引点,具体使用示例如下:

@font-face {
font-family: 'Awesome Font' ;
font-style: normal;
font-weight: 500;
src: 
  url('/fonts/awesome.woff2') format('woff2'),
  url('/fonts/awesome.woff') format('woff'),
  url('/fonts/awesome.ttf') format('ttf'),
  url('/fonts/awesome.eot') format('eot'),
  unicode-range: U+100-3ff, U+f??
}

​ 通过使用子集内嵌,以及为字体的不同样式变体采用单独的文件,用户可以仅根据需要下载字体的子集,而不必强制他们下载可能永远都不会用到的字体子集,这样对字体下载优化来说会更快速高效。不过属性unicode-range也存在兼容性的问题,对于不支持的浏览器,若想提供必要的子集字体支持,则可能需要手动处理字体文件。

3.字体文件预加载

在默认情况下,构建谊染树之前会阻塞字体文件的请求,这将可能导致部分文本谊染延迟,对此我们可使用-link re-reloao”>对字体资源进行预加载。关于预加载的详细内容,会在加载优化章节进一步 介绍。

<head>
<link rel="preload" href="/ fonts/ awesome .woff2" as=" font">
</head>
需要和@font-face对字体的定义一同使用,它只负责提示浏览器需要预加载给定的资源,而不指明如何使用。但同时需要注意的是,这样做将会无条件向网络发出字体请求,如果项目迭代将原本使用的字体文件修改或删除,也需同步删除对字体预加载的设置。

3.注意display:none的使用

​ 在使用位图时,经常会根据屏幕尺寸、权限控制等不同条件,响应式地处理资源的展示与隐藏。出于对性能的考虑,希望对于不展示的图像:尽量避免在首展时进行资源请求加载。但根据一些直觉性的编程习惯, 读者们真的确定所控制隐藏的图像,是否有发起资源请求吗?来看下面两个例子。

下面img1.jpg的图像文件是否有被浏览器发起请求?即使父级的div 设置为不显示。

<div style="display:none">
  < img src="img1.jpg">
</div>

根据HTML的解析顺序,答案是肯定的,img1.jpg 的图像文件会被请求。那么下面img2.jpg的图像文件会发起请求吗?

<div style="display:none">
  <div style="background: url(img2. jpg) "></div>
</div>

CSS解析后发现父级使用了display:none, 再去计算子级的样式就没有多大意义了,所以就不会去下载子级div的背景图像。

如果不清楚不同浏览器对display:none 关于图像加载的控制,则可以通过开发者工具进行验证。这里推荐的做法是使用或< img srcset>的方式进行响应式显示。

总结:

​ 本章首先从图像基础开始,在普及了包括图像的构成表示、分类压缩等知识之后对前端项目中常用的图像文件格式GIF、JPEG、PNG、 SVG、 WebP 及Base64进行了细致的分析介绍,包括它们之间优缺点的比较,具体场景下的技术选型,以及优化使用建议和工程实践。给出了三点与图像相关的优化技术与建议,希望同学能够明白Web项目中的图像优化是一项技术也是一门艺术, 技术指的是对于每一种图像文件的压缩和使用都有一套工程化的手段,艺术指的是当面对具体的项目实践时,如何技术选型与压缩以达到对用户最佳的体验效果,则需要在多个维度上进行权衡与取舍,并不存在明确的最佳方案。

本章最后给出一些希望同学能够记住的方法与技巧:

  • 适合用矢量图的地方首选矢量图。
  • 使用位图时首选WebP,对不支持的浏览器场景进行兼容处理。
  • 尽量为位图图像格式找到最佳质量设置。
  • 删除图像文件中多余的元数据。
  • 对图像文件进行必要的压缩。
  • 为图像提供多种缩放尺寸的响应式资源。
  • 对工程化通用图像处理流程尽量自动化。

参考书籍:web前端性能优化

前端页面的生命周期

前端页面的生命周期

性能问题呈现给用户的感受往往是简单而直接的:加载资源缓慢、运行过程卡杨或响应交互迟缓等,当把这些问题呈现到前端工程师面前时,却是另种系统级别复杂的图景。

从域名解析、TCP建立连接到HTTP的请求与响应,以及从资源请求、文件解析到关键渲染路径等,每一 个环节都有可能因为设计不当、考虑不周、运行出错而产生性能不佳的体验。作为前端工程师,为了能在遇到性能问题时快速而准确地定位问题所在,并设计可行的优化方案,熟悉前端页面的生命周期是一堂必修课。本章就从一道常见的前端面试题开始,通过对此问题的解答,来分析前端页面生命周期的各个环节,并着重分析其中关键渲染路径的具体过程和优化实践,希望以此为基础帮读者建构一套完整知识框架的图谱,而后续章节的专题性优化,也都是对此生命周期中某个局部过程的优化分析。

一、一道前端面试题

​ 我们在进行前端面试时,经常问这样一个问题: 从浏览器地址栏输入URL后,到页面渲染出来,整个过程都发生了什么?这个问题不仅能很好地分辨出面试候选人对前端知识的掌握程度,能够考查其知识体系的完整性,更重要的是,能够考查面试者在前端性能优化方面理解和掌握此过程的深入程度,与快速定位性能瓶颈及高效权衡出恰当的性能优化解决方案是正相关的。

​ 根据面试和工作的经验,将工程师的能力由低到高划分了若干等级:不堪一击、初窥门径、略有小成、驾轻就熟、融会贯通…..如果面试者的回答是:首先浏览器发起请求,然后服务器返回数据,最后脚本执行和页面渲染,那么这种程度大概在不堪- 击与初窥门径之间,属于刚入门前端,对性能优化还没什么概念。

​ 如果知道在浏览器输入URL后会建立TCP连接,并在此之上有HTTP的请求与响应,在浏览器接收到数据后,了解HTML与CSS文件如何构成渲染树,以及JS(JavaScript的简称)引擎解析和执行的基本流程,这种程度基本算是初窥门径,在面对网站较差的性能表现时,能够尝试从网络连接、关键渲染路径及JS执行过程等角度去分析和找寻可能存在的问题。

​ 其实这个问题的回答可以非常细致,能从信号与系统、计算机原理、操作系统聊到网络通信、浏览器内核,再到DNS解析、负载均衡、页面渲染等,主要关注前端方面的相关内容,为了后文表述更清楚,这里首先将整个过程划分为以下几个阶段。

(1)浏览器接收到URL,到网络请求线程的开启。
(2)一个完整的HTTP请求并的发出。
(3)服务器接收到请求并转到具体的处理后台。
(4)前后台之间的HTTP交互和涉及的缓存机制。
(5)浏览器接收到数据包后的关键渲染路径。
(6) JS引擎的解析过程。

​ 本章接下来的部分将对以上各阶段进行介绍,由于其中涉及一些知识点,认为这些知识点对理解性能问题和实施优化十分重要,需要更多的篇幅才能表述清楚,所以本章仅对其讲明原理,而后续章节将会单独详述,比如发起完整HTTP请求阶段的DNS域名解析,前后台HTTP交互阶段的数据压缩与缓存等。

二、网络请求线程开启

​ 浏览器接收到我们输入的URL到开启网络请求线程,这个阶段是在浏览器内部完成的,需要先来了解这里面涉及的一些概念。

首先是对URL的解析,它的各部分的含义如下所示。

URL结构: Protocol://Host:Port/Path?Query#Fragment
标识 名称 备注
Protocol 协议头 说明浏览器如何处理要打开的文件,常见的有HTTP、FTP、Telnet等
Host 主机域名/IP地址 所访问资源在互联网上的地址,主机域名或经过DNS解析为IP地址
Port 端口号 请求程序和响应程序之间连接用的标识
Path 目录路径 请求的目录或者文件名
Query 查询参数 请求所传递的参数
Fragment 片段 次级资源信息,通常可作为前端路由或锚点

​ 解析URL后,如果是HTTP协议,则浏览器会新建一个网络请求线程去下载所需的资源,要明白这个过程需要先了解进程和线程之间的区别,以及目前主流的多进程浏览器结构。

1.进程与线程

​ 简单来说,进程就是一个程序运行的实例,操作系统会为进程创建独立的内存,用来存放运行所需的代码和数据;而线程是进程的组成部分,每个进程至少有一个主线程及可能的若干子线程,这些线程由所属的进程进行启动和管理。由于多个线程可以共享操作系统为其所属的同一个进程所分配的资源,所以多线程的并行处理能有效提高程序的运行效率。

下面形象地展示了进程、线程和所执行任务之间的关系。从中可以总结出进程与线程之间关系的四个特点。

(1)只要某个线程执行出错,将会导致整个进程崩溃。

(2)进程与进程之间相互隔离。这保证了当一个进程挂起或崩溃的情况发生时,并不会影响其他进程的正常运行,虽然每个进程只能访问系统分配给自己的资源,但可以通过IPC机制进行进程间通信。

(3)进程所占用的资源会在其关闭后由操作系统回收。即使进程中存在某个线程产生的内存泄漏,当进程退出时,相关的内存资源也会被回收。

(4)线程之间可以共享所属进程的数据。

2.单进程浏览器

​ 在熟悉了进程和线程之间的区别后, 我们在此基础上通过了解浏览器架构模型的演变,来看网络请求线程的开启处在怎样的位置。

​ 说到底浏览器也只是一个运行在操作系统上的程序,那么它的运行单位就是进程,而早在2008年谷歌发布Chrome多进程浏览器之前,市面上几乎所有浏览器都是单进程的,它们将所有功能模块都运行在同一一个进程中。

单进程浏览器在以下方面有着较为明显的隐患。

  • 流畅性: 首先是页面内存泄漏,浏览器内核通常非常复杂,单进程浏览器打开再关闭一个页面的操作,通常会有一些内存不能完全回收,这样随着使用时间延长,占用的内存会越来越多,从而引起浏览器运行变慢:其次由于很多模块运行在同一个线程中,如JS引擎、页面渲染及插件等,那么执行某个循环任务的模块就会阻塞其他模块的任务执行,这样难免会有卡顿的现象发生。
  • 安全性: 由于插件的存在,不免其中有些恶意脚本会利用浏览器漏洞来获取系统权限,进行引发安全问题的行为。
  • 稳定性: 由于所有模块都运行在同一个进程中,对于稍复杂的JS代码,如果页面渲染引擎崩溃,就会导致整个浏览器崩溃。同样,各种不稳定的第三方插件,也是导致浏览器崩溃的隐患。

3.多进程浏览器

​ 出于对单进程浏览器存在问题的优化,Chrome 推出了多进程浏览器架构。

​ 浏览器把原先单进程内功能相对独立的模块抽离为单个进程处理对应的任务,主要分为以下几种进程。

(1)浏览器主进程: 一个浏览器只有一个主进程,负责如菜单栏、标题栏等界面显示,文件访问,前进后退,以及子进程管理等。

(2)GPU进程: GPU (图形处理单元)最初是为了实现3D的CSS效果而引入的,后来随着网页及浏览器在界面中的使用需求越来越普遍,Chrome 便在架构中加入了GPU进程。

(3)插件进程:主进程会为每个加入浏览器的插件开辟独立的子进程,由于进程间所分配的运行资源相对独立,所以即便某个插件进程意外崩溃,也不至于对浏览器和页面造成影响。另外,出于对安全因素的考虑,这里采用了沙箱模式(即图中虚线所标出的进程),在沙箱中运行的程序受到一些限制: 不能读取敏感位置的数据,也不能在硬盘上写入数据。这样即使插件运行了恶意脚本,也无法获取系统权限。

(4)网络进程:负责页面的网络资源加载,之前属于浏览器主进程中的一个模块,最近才独立出来。

(5)渲染进程:也称为浏览器内核,其默认会为每个标签窗口页开辟一个独立的进程,负责将HTML、CSS和JavaScript等资源转为可交互的页面,其中包含多个子线程,即JS引擎线程、GUI渲染线程、事件触发线程、定时触发器线程、异步HTTP请求线程等。当打开个标签页输入 URL后,所发起的网络请求就是从这个进程开始的。另外,出于对安全性的考虑,渲染进程也被放入沙箱中。

打开Chrome任务管理器,可以从中查看到当前浏览器都启动了哪些进程,如图所示。

此时仅打开了一个标签页, 除了浏览器添加插件所开辟的进程,还可以看到浏览器进程、GPU进程、网络进程,以及最近新抽离出来的一个音频服务进程。

三、建立HTTP请求

​ 这个阶段的主要工作分为两部分: DNS解析和通信链路的建立,简单说就是,首先发起请求的客户端浏览器要明确知道所要访问的服务器地址,然后建立通往该服务器地址的路径。

1.DNS 解析

​ 在前面章节讲到的URL解析,其实仅将URL拆分为代表具体含义的字段,然后以参数的形式传入网络请求线程进行进一步处理 ,首先第一步便是这 里讲到的DNS解析。其主要目的便是通过查询将URL中的Host字段转化为网络中具体的IP地址,因为域名只是为了方便帮助记忆的,IP 地址才是所访问服务器在网络中的“门牌号”。如图所示为DNS解析过程。

​ 首先查询浏览器自身的DNS缓存,如果查到IP地址就结束解析,由于缓存时间限制比较大,一般只有1分钟,同时缓存容量也有限制,所以在浏览器缓存中没找到IP地址时,就会搜索系统自身的DNS缓存:如果还未找到,接着就会尝试从系统的hosts 文件中查找。

​ 在本地主机进行的查询若都没获取到,接下来便会在本地城名服务器上查询,如果本地城名服务器没有直接的目标IP地址可供返回,则本地域名服务器便会采取选代的方式去依次查询根域名服务器、COM顶级域名服务器和权限域名服务器等,最终将所要访问的目标服务器IP地址返回本地主机,若查询不到,则返回报错信息。

​ 由此可以看出DNS解析是个很耗时的过程,若解析的域名过多,势必会延缓首屏的加载时间。本节仅对DNS解析过程进行简要的概述,而关于原理及优化方式等更为详细的介绍会在后续章节中单独展开介绍。

2.网络模型

​ 在通过DNS解析获取到目标服务器IP地址后,就可以建立网络连接进行资源的请求与响应了。但在此之前,我们需要对网络架构模型有一些基本的认识,在互联网发展初期,为了使网络通信能够更加灵活、稳定及可互操作,国际标准化组织提出了些网络架构极型,0SI模型、TCP/IP模型,二者的网络模型图如图。

​ OSI (开放系统互连)模型将网络从底层的物理层到顶层浏览器的应用层一共划分了7层,OSI各层的具体作用如表所示。

OSI模型
应用层 负责给应用程序提供接口,使其可以使用网络服务,HTTP 协议就位于该层
表示层 负责数据的编码与解码,加密和解密,压缩和解压缩
会话层 负责协调系统之间的通信过程
传输层 负责端到端连接的建立,使报文能在端到端之间进行传输。TCP/UDP 协议位于该层
网络层 为网络设备提供逻辑地址,使位于不同地理位置的主机之间拥有可访问的连接和路径
数据链路层 在不可靠的物理链路上,提供可靠的数据传输服务。包括组帧、物理编址、流量控制、差错控制、接入控制等
物理层 主要功能包括:定义网络的物理拓扑,定义物理设备的标准(如介质传输速率、网线或光纤的接口模型等),定义比特的表示和信号的传输模式

​ OSI是一种理论下的模型,它先规划了模型再填入协议,先制定了标准再推行实践,TCP/IP充分借鉴了OSI 引入的服务、接口、协议及分层等概念,建立了TCP/IP模型并广泛使用,成为目前互联网事实上的标准。

3.TCP 连接

​ 根据对网络模型的介绍,当使用本地主机连上网线接入互联网后,数据链路层和网络层就已经打通了,而要向目标主机发起HTTP请求,就需要通过传输层建立端到端的连接。

​ 传输层常见的协议有TCP协议和UDP协议,由于本章关注的重点是前端页面的资源请求,这需要面向连接、丢包重发及对数据传输的各种控制,所以接下来仅详细介绍TCP协议的“三次握手”和“四次挥手”。

​ 由于TCP是面向有连接的通信协议,所以在数据传输之前需要建立好客户端与服务器端之间的连接,即通常所说的“三次握手”,具体过程分为如下步骤。

(1)客户端生成 一个随机数seq,假设其值为t, 并将标志位SYN设为1,将这些数据打包发给服务器端后,客户端进入等待服务器端确认的状态。

(2)服务器端收到客户端发来的SYN=1的数据包后,知道这是在请求建立连接,于是服务器端将SYN与ACK均置为1,并将请求包中客户端发来的随机数t加1后赋值给ack,然后生成一个服务器端的随机数seq=k,完成这些操作后,服务器端将这些数据打包再发回给客户端,作为对客户端建立连接请求的确认应答。

(3)客户端收到服务器端的确认应答后,检查数据包中ack的字段值是否为t+1,ACK是否等于1,若都正确就将服务器端发来的随机数加1 (ack=k+1),将ACK=1的数据包再发送给服务器端以确认服务器端的应答,服务器端收到应答包后通过检查ack是否等于k+1来确认连接是否建立成功。连接建立的关系图如图所示。

当用户关闭标签页或请求完成后,TCP连接会进行“四次挥手”,具体过程如下。

(1)由客户端先向服务器发送FIN=M的指令,随后进入完成等待状态FIN_WAIT_1,表明客户端已经没有再向服务器端发送服务器端发送的数据,但若服务器端此时还有未完成的数据传递,可继续传递数据。

(2)当服务器端收到客户端的FIN报文后,会先发送ack=M+1的确认,告知客户端关闭请求已收到,但可能由于服务器端还有未完成的数据传递,所以请客户端维续等待。

(3)当服务器端确认已完成所有数据传递后, 便发送带有FIN=N的报文给客户端,准备关闭连接。

(4)客户端收到FIN=N的报文后可进行关闭操作,但为保证数据正确性,会回传给服务器端一个确认报文ack=N+1,同时服务器端也在等待客户端的最终确认,如果服务器端没有收到报文则会进行重传,只有收到报文后才会真正断开连接。而客户端在发送了确认报文一段时间后, 没有收到服务器端任何信息则认为服务器端连接已关闭,也可关闭客户端信息。连接关闭的关系图如图所示。

​ 只有连接建立成功后才可开始进行数据的传递,由于浏览器对同一域名下并发的TCP连接有限制,以及在1.0版本的HTTP协议中,一个资源的下载需对应一个TCP的请求,这样的并发限制又会涉及许多优化方案,我们将在后续章节中进行进一步介绍。

​ 这里较为详细地介绍了TCP连接建立和断开的过程,首先让读者有一个网络架构分层的概念,虽然前端工作基本围绕在应用层,但有一个全局的网络视角后,能帮助我们在定位性能瓶颈时更加准确:其次也为了说明影响前端性能体验的因素,不仅是日常编写的代码和使用的资源,网络通信中每个环节的优劣缓急都值得关注。

四、前后端的交互

​ 当TCP连接建立好之后,便可通过HTTP等协议进行前后端的通信,但在实际的网络访问中,并非浏览器与确定IP地址的服务器之间直接通信,往往会在中间加入反向代理服务器。

1.反向代理服务器

​ 对需要提供复杂功能的网站来说,通常单一的服务器资源是很难满足期望的。一般采用的方式是将多个应用服务器组成的集群由反向代理服务器提供给客户端用户使用,这些功能服务器可能具有不同类型,比如文件服务器、邮件服务器及Web应用服务器,同时也可能是相同的Web 服务部署到多个服务器上,以实现负载均衡的效果,反向代理服务器的作用如图所示。

​ 反向代理服务器根据客户的请求,从后端服务器上获取资源后提供给客户端。反向代理服务器通常的作用如下:

  • 负载均衡。
  • 安全防火墙。
  • 加密及SSL加速。
  • 数据压缩。
  • 解决跨域。
  • 对静态资源缓存。

​ 常用作反向代理服务器的有Nginx、IIS、Apache,后面会针对Nginx深入介绍一些可用于性能优化的配置。

2.后端处理流程

​ 经反向代理收到请求后,具体的服务器后台处理流程大致如下。

(1)首先会有一层统一的验证环节,如跨城验证、安全校验拦截等。如果发现是不符合规则的请求,则直接返回相应的拒绝报文。

(2)通过验证后才会进入具体的后台程序代码执行阶段,如具体的计算、 数据库查询等。

(3)完成计算后,后台会以一个HTTP响应数据包的形式发送回请求的前端,结束本次请求。

​ 只要网站涉及数据交互,这个请求和响应的过程就会频繁发生,而后端处理程序的执行需要花费时间,HTTP协议保证数据交互的同时也对传输细节有所限制。这其中就存在很大的性能优化空间,比如HTTP协议版本的升级、缓存机等。

3.HTTP 相关协议特性

​ HTTP是建立在传输层TCP协议之上的应用层协议,在TCP层面上存在长连接和短连接的区别。所谓长连接,就是在客户端与服务器端建立的TCP连接上,可以连续发送多个数据包,但需要双方发送心跳检查包来维持这个连接。

​ 短连接就是当客户端需要向服务器端发送请求时,会在网络层IP协议之上建立一个TCP连接,当请求发送并收到响应后,则断开此连接。根据前面关于TCP连接建立过程的描述,我们知道如果这个过程频繁发生,就是个很大的性能耗费,所以从HTTP的1.0版本开始对于连接的优化一直在进行。

​ 在HTTP 1.0时,默认使用短连接,浏览器的每一次 HTTP操作就会建立一个连接,任务结束则断开连接。

​ 在HTTP1.1时,默认使用长连接,在此情况下,当一个网页的打开操作完成时,其中所建立用于传输HTTP的TCP连接并不会断开关闭,客户端后续的请求操作便会继续使用这个已经建立的连接。如果我们对浏览器的开发者工具留心,在查看请求头时会发现一行Connection: keep-alive长连接并非永久保持,它有一个持续时间,可在服务器中进行配置。

​ 而在HTTP2.0到来之前,每一个资源的请求都需要开启一个 TCP连接,由于TCP本身有并发数的限制,这样的结果就是,当请求的资源变多时, 速度性能就会明显下降。为此,经常会采用的优化策略包括,将静态资源的请求进行多城名拆分,对于小图标或图片使用雪碧图等。

​ 在HTTP2.0之后,便可以在一个TCP连接上请求多个资源,分割成更小的帧请求,其速度性能便会明显上升,所以之前针对HTTP 1.1限制的优化方案也就不再需要了。

​ HTTP2.0除了一个连接可请求多个资源这种多路复用的特性,还有如下一些新特性。

(1)二进制分帧:在应用层和传输层之间,新加入了一个二进制分帧层,以实现低延迟和高吞吐量。

(2)服务器端推送:以往是一个请求带来一 个响应,现在服务器可以向客户端的一个请求发出多个响应,这样便可以实现服务器端主动向客户端推送的功能。

(3)设置请求优先级:服务器会根据请求所设置的优先级,来决定需要多少资源处理该请求。

(4) HTTP头部压缩:减少报文传输体积。

4.浏览器缓存

​ 在基于HTTP的前后端交互过程中,使用缓存可以使性能得到显著提升。具体的缓存策略分为两种:强缓存和协商缓存。

​ 强缓存就是当浏览器判断出本地缓存未过期时,直接读取本地缓存,无须发起HTTP请求,此时状态为: 200 from cache。在HTTP 1.1版本后通过头部的cache-control字段的 max-age 属性值规定的过期时长来判断缓存是否过期失效,这比之前使用expires标识的服务器过期时间更准确而且安全。

​ 协商缓存则需要浏览器向服务器发起HTTP请求,来判断浏览器本地缓存的文件是否仍未修改,若未修改则从缓存中读取,此时的状态码为: 304。具体过程是判断浏览器头部if-none-match与服务器短的 e-tag 是否匹配,来判断所访问的数据是否发生更改。这相比HTTP 1.0版本通过last- modified判断上次文件修改时间来说也更加准确。具体的浏览器缓存触发逻辑如图所示。

​ 在浏览器缓存中,强缓存优于协商缓存,若强缓存生效则直接使用强缓存,若不生效则再进行协商缓存的请求,由服务器来判断是否使用缓存,如果都失效则重新向服务器发起请求获取资源。本节仅简要说明浏览器缓存的触发过程,由于这部分对性能优化来说比较重要,所以在后续章节也会详细介绍。

五、关键渲染路径

​ 当我们经历了网络请求过程,从服务器获取到了所访问的页面文件后,浏览器如何将这些HTML、CSS及JS文件组织在一起渲染出来呢?

1.构建对象模型

​ 首先浏览器会通过解析HTML和CSS文件,来构建DOM (文档对象模型)和CSSOM (层叠样式表对象模型),为便于理解,我们以如下HTML内容文件为例,来观察文档对象模型的构建过程。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>关键渲染路径</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <p>你好<span>性能优化</span></p>
        <div>
            <img src="photo.jpg">
        </div>
    </body>
</html>

​ 浏览器接收读取到的HTML文件,其实是文件根据指定编码(UTF-8) 的原始字节,开形如3C 62 6F 79 3E 65 6C 6F 2C 20 73 7…首先需要将字节转换成字符,即原本的代码字符串,接者再将字符串转化为W3C标准规定的令牌结构,所谓令牌就是HTML中不同标签代表不同含义的组规则结构。 然后经过词法分析将令牌转化成定义了属性和规则值的对象,最后将这些标签节点根据HTML表示的父子关系,连接成树形结构,如图所示。

​ DOM树表示文档标记的属性和关系,但未包含其中各元素经过渲染后的外观呈现,这便是接下来CSSOM的职责了,与将HTML文件解析为文档对象模型的过程类似,CSS文件也会首先经历从字节到字符串,然后令牌化及词法分析后构建为层叠样式对象模型。假设CSS文件内容如下:

body{ font-size: 16px; }
p{ font-weight: bold; }
span{ color: red; }
p span{ display: none; }
img{ float: right; }

最后构建的CSSOM树如图所示。

​ 这两个对象模型的构建过程是会花费时间的,可以通过打开Chrome 浏览器的开发者工具的性能选项卡,查看到对应过程的耗时情况,如图所示。

构建过程耗时监控

2.渲染绘制

​ 当完成文档对象模型和层叠样式表对象模型的构建后,所得到的其实是描述最终渲染页面两个不同方而信息的对象:一个是展示的文档内容, 另一个是文档对象对应的样式规则,接下来就需要将两个对象模型合并为渲染树,渲染树中只包含渲染可见的节点,该HTML文档最终生成的渲染树如图所示。

渲染树

渲染绘制的步骤大致如下。

(1)从所生成DOM树的根节点开始向下遍历每个子节点,忽略所有不可见的节点(脚本标记不可见、CSS隐藏不可见), 因为不可见的节点不会出现在渲染树中。

(2)在CSSOM中为每个可见的子节点找到对应的规则并应用。

(3)布局阶段,根据所得到的渲染树,计算它们在设备视图中的具体位置和大小,这一步输出的是一个“盒模型”。

(4)绘制阶段,将每个节点的具体绘制方式转化为屏幕上的实际像素。

​ 此处所举的例子较为简单,读者要明白执行构建渲染树、布局及绘制过程所需要的时间取决于实际文档的大小。文档越大,浏览器需要处理的任务就越多,样式也复杂,绘制需要的时间就越长,所以关键渲染路径执行快慢,将直接影响首屏加载时间的性能指标。

​ 当首屏渲染完成后,用户在和网站的交互过程中,有可能通过JavaSeript 代码提供的用户操作接口更改渲染树的结构,一且DOM结构发生改变,这个渲染过程就会重新执行遍。 可见对于关键渲染路径的优化影响的不仅是首屏性能,还有交互性能。本节仅对首屏渲染过程进行了简要描述,其中细节性的优化方案,将会在后续章节中展开介绍。

总结:

​ 本章通过一 道前端工程师常见的面试题,较为详细地描述了当用户从浏览器的地址栏输入URL后,到页面渲染出来的整个过程。其实不难理解当某个较差的性能体验发生时,很有可能是这个过程中的某个环节出现了过多的性能损耗,后续我们会介绍一些辅 助的性能分析工具来帮助定位具体的性能瓶颈,其实它们也是以页面加载生命周期为“纲”进行逐步分析的,所以我们理解并掌握了这个过程,对具体的优化手段可以做到心中有数。

​ 后续的章节安排,就是选取本章介绍的页面生命周期的某个局部环节进行优化以及某些具体的优化技巧和实用工具。如果说这些是前端性能优化的“术”,那么理解页面生命周期就是“道”。

参考文章:web前端性能优化

什么是性能优化

什么是性能优化

1.性能的起因

​ 市场上的某个功能还没有能满足其需求的可选方案的时候,如果出来一个应用即使很难用,用户都要忍着用。如果这个功能确实能解决用户的某些痛点需求,且有其存在的价值,那么让用户忍受糟糕体验的背后,就存在 对产品优化和改进的空间。

2.性能的影响

​ 大部分网站体现的价值都无外乎信息的载体、交互的工具或商品流通的渠道,这就要求它们需要与更多的用户建立联系,同时还要保持所建立的联系拥有绵延不绝的用户黏性,所以网站就不能只关注自我表达,而不顾及用户是否喜欢。

2.1用户的留存

​ 我们都希望用户访问网站所进行的交互,对网站构建的内容来讲是有意义的,比如,电商网站希望用户浏览并购买商品,社交网站希望用户之间进行互动,视频网站希望用户观看视频,而这些希望都是建立在网站用户留存的基础上的。网站用户的留存情况,一般指的是用户自登录注册之日起,经过一段时间后, 仍然还在使用该网站的用户数。统计出注册用户数与留存用户数后,就可以计算出用户留存率,这个指标对网站的运营有重要的指导意义。

​ 根据Google营销平台提供的调研发现,如果网站页面的加载时间超过3s,就会有53%的移动网站的访问遭到用户抛弃。同时他们还针对加载时间分别在4s内和20s内的网站进行比较,发现加载时间在5s内的网站,用户的停留时间相比会长70%,用户在一定时间内从该网站离开的跳出率会低35%,而网站上展示广告的可见率也会高25%。

​ 虽然影响用户留存的因素不止性能这一方面, 但从上述数据可知,通过优化性能来保证留存事是必要的措施。

2.2网站的转化率

​ 从运营角度来看,网站转化率是一个非常 重要的考量指标,网站转化率指的是用户进行了某项目标行为的访问次数与总访问次数的比率。某项目标行为可以是用户注册、资源下载、商品购买等一 系列用户行为,简言之,比如在电商网站上浏览了某个商品的用户中,有多少位用户最终购买了该商品,其所占的比例就可以看作访客到消费者的转化率。

​ 根据Mobify (一家著 名的电子商务优化平台)的调研,发现商品的结账页面加载时间每减少100ms,基于该商品购买访问的转化率就会增加1.55%, 这个比率对大型电商网站来讲,其所带来的年均收入增长将会是上千万元。Google 营销平台的调研也指出,加载时间在5s以内的网站会比在20s以内的网站的广告收入多一倍。

​ 目前大部分互联网广告营销都渐趋精准化,即广告商的广告费会根据经广告导流,产生确定的用户交易后再收取。如此看来网站性能不仅影响用户体验,对于广告主和广告商的经济利益也会带来实实在在的影响。

2.3体验与传播

​ 当用户通过手机、平板电脑等移动设备经运营商网络浏览网站时,所产生的流量数据通常是根据字节数进行收费的。虽然从2G、 3G 到4G,甚至5G,运营商所收取的流量费用单价一直在下滑,但与此同时,页面所承载的内容却在不断增大,并且这一趋势似乎将持续下去。 那么用户必将为过多的流量数据支付相应的费用,若所访问网站包含的资源文件过大、组织冗余,用户便会浪费过多的网络资费,同时过大的资源传输量也会延长请求响应的时间,最终降低用户的体验度。

​ 性能问题引起的所谓用户体验差,造成的影响并非单纯是用户觉得不喜欢就放弃了使用。用户还会拒绝向自己的周边网络推荐该网站或应用,更坏的情况是用户会对低性能进行差评。口碑是互联网时代t分可靠的通行证,如果我们不重视性能问题,经过网络口碑的放大效应,网站的发展不仅会遇到瓶颈,甚至还可能会日薄西山。

3.性能评估模型

​ 我们先来约定一个原则,以用户为中心,然后根据该原则引出指导后文涉及的各种优化策略,所参照的性能模型为RAIL性能模型。这个名字的由来是四个英文单词的首字母:响应( Response)、动画(Animation)、 空闲(Idle)、加载(Load),这四个单词代表与网站或应用的生命周期相关的四个方面,这些方面会以不同的方式影响整个网站的性能。

我们将用户作为之后性能优化的中心,首先需要了解用户对于延迟的反应。用户感知延迟的时间窗口。

延迟 用户反应
0~16ms 人眼可以感知每秒60帧的动画转场,即每帧16ms,除了浏览器将每一帧画面绘制到屏幕上的时间,网站应用大约需要10ms来生成一帧
0~100ms 在该时间窗口内来响应用户的操作,才会是流畅的体验
100~300ms 用户能感知轻微的延迟
300~1000ms 所感知的延迟会被用户当作网站页面加载或更改视图过程的一部分
>1s 用户的注意力将离开之前执行的任务
>10s 用户感觉失望,可能会放弃任务

3.1响应

​ 网站性能对于响应方面的要求是,在用户感知延迟之前接收到操作的反馈。比如用户进行了文本输入,按钮单击、表单切换及启动动画等操作后,必须在100m内收到反馈,如果超过100ms的时间窗口,用户就会感知延迟。

​ 看似很基本的用户操作背后,可能会隐藏着复杂的业务逻辑处理及网络请求与数据计算。对此我们应当谨慎,将较大开销的工作放在后台异步执行,而即便后白处理要数百毫秒才能完成的操作,也应当给用户提供及时的阶段性反馈。

​ 比如在单击按钮向后台发起某项业务处理请求时,首先反馈给用户开始处理的提示,然后在处理完成的回调后反馈完成的提示。

3.2动画

​ 前端所涉及的动画不仅有炫酷的UI特效,还包括滚动和触摸拖动等交互效果,而这一方面的性能要求就是流畅。 众所周知,人眼具有视觉暂留特性,就是当光对视网膜所产生的视觉在光停止作用后,仍能保留一段时间。

​ 研究表明这是由于视神经存在反应速度造成的, 其值是1/24s,即当我们所见的物体移除后,该物体在我们眼中并不会立即消失,而会延续存在1/24s 的时间。对动画来说,无论动画帧率有多高,最后我们仅能分辨其中的30帧,但越高的帧率会带来更好的流畅体验,因此动画要尽力达到60fps的帧率,每一帧画面的生成都需要经过若干步骤,根据60fps 帧率的计算,帧图像的生成预算为16ms ( 1000ms/60 约等于 16.6ms),除去浏览器绘制新帧的时间,留给执行代码的时间仅10ms 左右。关于这个维度的具体优化策略,会在后面优化谊染过程的相关章节中详细介绍。

3.3空闲

​ 要使网站响应迅速、动画流畅,通常都需要较长的处理时间,但以用户为中心来看待性能问题,就会发现并非所有工作都需要在响应和加载阶段完成,我们完全可以利用浏览器的空闲时间处理可延迟的任务,只要让用户感受不到延迟即可。利用空闲时间处理延迟,可减少预加载的数据大小,以保证网站或应用快速完成加载。

​ 为了更加合理地利用浏览器的空闲时间,最好将处理任务按50ms为单位分组。这么做就是保证用户在发生操作后的100ms 内给出响应。

3.4 加载

​ 用户感知要求我们尽量在1s内完成页面加载,如果没有完成,用户的注意力就会分散到其他事情上,并对当前处理的任务产生中断感。需要注意的是,这里在1s内完成加载井谊染出页面的要求,并非要完成所有页面资源的加载,从用户感如体验的角度来说,只要关键谊染路径完成,用户就会认为全部加载已完成。

​ 对于其他非关键资源的加载,延迟到浏览器空闲时段再进行,是比较常见的渐进式优化策略。

4.性能优化的步骤

​ RAIL性能模型指出了用户对不同延迟时间的感知度,以用户为中心的原则,就是要让用户满意网站或应用的性能体验。

​ 不同类型的操作,需要在规定的时间窗口内完成,所以进行性能优化的步骤般是:首先可量化地评估出网站或应用的性能表现,然后立足于网站页面响应的生命周期,分析出造成较差性能表现的原因,最后进行技术改造、可行性分析等具体的优化实施。

4.1 性能测量

​ 如果把对网站的性能优化比作一场旅程, 它无疑会是漫长且可能还略带泥泞的,那么在开始之前我们有必要对网站进行性能测量,以知道优化的方向在何处。通常我们会借助.些工具来完成性能测量.

1.Chrome浏览器的Performance功能

​ 通过Chrome浏览器访问我们要进行性能测量的网站,打开开发者工具的Performance选项卡。单击左上角的“开始评估”按钮后刷新网站,该工具便开始分析页面资源加载、渲染、请求响应等各环节耗费的时间线,据此便可分析定程度的性能问题, 比如JavaScript 的执行是否会触发大量视觉变化的计算,重绘和重排(或回流)是否会被多次触发等。

2.灯塔(Lighthouse)

​ Lighthouse是一个开源的自动化审查网站页面性能的工具,可根据所提供的网站URL从性能、可访问性、渐进式Web应用、SEO (搜索引擎优化)等多个方面进行自动化分析,最终给出一份具有指导意义的报告。它既可以当作Chrome的扩展插件来使用,又可以在开发者工具中直接使用。

除此之外,还会经常用到的性能测试工具有PageSpeed Insights、WEBPAGETEST、Pingdom等。

4.2生命周期

     网站页面的生命周期,通俗地讲就是从我们在浏览器的地址栏中输入一个URL后,到整个页面渲染出来的过程。整个过程包括域名解析,建立TCP连接,前后端通过HTTP进行会话,压缩与解压缩,以及前端的关键渲染路径等,把这些阶段拆解开来看,不仅能容易地获得优化性能的启发,而且也能为今后的前端工程师之路构建完整的知识框架。

4.3优化方案

​ 经过对网站页面性能的测量及渲染过程的了解,相信你对于糟糕性能体验的原因已经比较清楚了,那么接下来便是优化性能。

(1)传输资源的优化,比如图像资源, 不同的格式类型会有不同的使用场景,在使用的过程中是否恰当。

(2)加载过程的优化,比如延迟加载,是否有不需要在首屏展示的非关键信息,占用了页面加载的时间。

(3) JavaScript 是现代大型网站中相当“昂贵”的资源,是否进行了压缩,书写是否规范,有无考虑内存泄漏等。

(4)关键渲染路径优化,比如是否存在不必要的重绘和回流。

(5)本地存储和浏览器缓存。

参考:Web前端性能优化

面试手撕代码合集

面试手撕代码合集

1.柯里化函数

function add() {
    const _args = [...arguments];
    function fn() {
      _args.push(...arguments);
      return fn; //一直重复收集参数
    }
    fn.toString = function() {
      return _args.reduce((sum, cur) => sum + cur);
    }
    return fn;
  }
console.log(add(1)(2)(3)(4).toString()) //10
console.log(add(1,2)(1, 2, 3)(2).toString()) //11

2.千位符转换

// 将金额类型转为数字类型
function toNum(str) {
    return str.replace(/\,|\¥/g, "");
}

// 保留两位小数(四舍五入)
function toPrice(num) {
    num = parseFloat(toNum(num)).toFixed(2).toString().split(".");
    num[0] = num[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + num.join(".");
}

// 保留两位小数(不四舍五入)
function toPrice1(num) {
    num = parseFloat(toNum(num).replace(/(\.\d{2})\d+$/,"$1")).toFixed(2).toString().split(".");
    num[0] = num[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + num.join(".");;
}

// 不处理小数部分
function toPrice2(num) {
    var source = toNum(num).split(".");
    source[0] = source[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + source.join(".")
}

console.log(toPrice('12312.236')) //¥12,312.24
console.log(toPrice1('12312.234')) //¥12,312.23
console.log(toPrice2('1232342312.234')) //¥1,232,342,312.234

3.用setTimeout实现setInterval(计数器)

var i = 10;
let fn = () => {
    console.log(i--);
}
function mySetInterval(fn, delay, times) {
    let timer = setTimeout(function a() {
        fn()
        times--
        timer = setTimeout(a, delay)
        if (times <= 0) {
            clearTimeout(timer)
        }
    }, delay)
}
mySetInterval(fn, 1000, 10)

4.数组扁平化

//递归实现
var arr = [1,2,[3,4,[5,6]]]
function flatten(arr){
    let result = []
    arr.forEach(item => {
        if(Array.isArray(item)){
            result = result.concat(flatten(item))
        }else{
            result.push(item)
        }
    });
    return result
}
console.log(flatten(arr)) //[ 1, 2, 3, 4, 5, 6 ]

//利用reduce函数迭代
var arr1 = [1,2,[3,4,[5,6]]]
function flatten1(arr){
    return arr.reduce((res,next) => {
        return res.concat(Array.isArray(next) ? flatten1(next) : next)
    },[])
}
console.log(flatten1(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

5.深拷贝

function deepClone(obj,hash = new WeakMap()){
    if(obj == null) return obj;
    if(obj instanceof Date) return new Date(obj);
    if(obj instanceof RegExp) return new RegExp(obj);
    if(typeof obj !== 'object') return obj;
    if(hash.get(obj)) return hash.get(obj);
    let cloneObj = new obj.constructor;
    hash.set(obj,cloneObj);
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            cloneObj[key] = deepClone(obj[key],hash);
        }
    }
    return cloneObj;
}

let obj = {name:1,address:{x:1000}};
let d = deepClone(obj);
obj.address.x = 200;
console.log(d); //{name:1,address:{x:1000}}

6.Promise.all

Promise.prototype.all = function(promises){
    let results = [];
    let promiseCount = 0;
    let promisesLength = promises.length;
    return new Promise(function(resolve,reject){
        for(let val of promises){
            Promise.resolve(val).then(function(res){
                promiseCount++;
                //results.push(res)
                results[i] = res;
                //当所有函数都正确执行了,resolve输出所有返回结果
                if(promiseCount === promisesLength){
                    return resolve(results);
                }
            },function(err){
                return reject(err);
            })
        }
    })
}

//测试
let promise1 = new Promise(function(resolve) {
    resolve(1);
  });
  let promise2 = new Promise(function(resolve) {
    resolve(2);
  });
  let promise3 = new Promise(function(resolve) {
    resolve(3);
  });
  
  let promiseAll = Promise.all([promise1, promise2, promise3]);
  promiseAll.then(function(res) {
    console.log(res);
  });//[1,2,3]

7.Promise.race

Promise.race = function(promises){
    //将可迭代对象转换为数组
    promises = Array.from(promises);
    return new Promise((resolve,reject)=>{
        if(promises.length === 0){
            //空的可迭代对象,用于pending状态
        }else{
            for(let i = 0;i < promises.length;i++){
                Promise.resolve(promises[i]).then((data)=>{
                    resolve(data);
                }).catch((reason)=>{
                    reject(reason)
                })
            }
        }
    })
}

//测试
let p1 = new Promise(function(resolve,reject){
    setTimeout(function(){
     resolve('success')
    },1000)
})
let p2 = new Promise(function(resolve,reject){
    setTimeout(function(){
     resolve('faild')
    },500)
})

Promise.race([p1,p2]).then(result=>{
console.log(result)  //  faild    faild耗时短
})

8.instanceof

function new_instance_of(leftValue,rightValue){
    let rightProto = rightValue.prototype;//取右边表达式的 prototype 值
    leftValue = leftValue.__proto__;//取左表达式的 __proto__ 值
    while(true){
        if(leftValue == null){
            return false;
        }
        if(leftValue === rightProto){
            return true;
        }
        leftValue = leftValue.__proto__;
    }
}
function Foo(){}
console.log(new_instance_of(Foo,Object))//true

9.js继承

1.原型链继承

 // 原型链继承
  function Super(){
    this.color=['red','yellow','black']
  }

  function Sub(){
  }
  //继承了color属性 Sub.prototype.color=['red','yellow','black']
  Sub.prototype=new Super()

  //创建实例 instance1.__proto__.color
  const instance1=new Sub()
  const instance2=new Sub()
  console.log(instance1.__proto__.color===instance2.__proto__.color) //true

2.构造函数继承

function Super(name,age){
    this.name = name;
    this.age = age;
    this.color = ['red','yellow','blue'];
    this.sayHi = function(){
        console.log('hi')
    }
    console.log(this)
}
function Sub(){
    //改变this指向
    Super.apply(this,arguments)
    this.height = 180;
}

var instance1 = new Sub('mengfeng',25);
var instance2 = new Sub('mengfeng123',24);
instance1.sayHi();//hi

3.实例继承

new

4.拷贝继承

//深拷贝

5.组合继承

function Super(name,age){
    this.name = name;
    this.age = age;
    this.color = ['red','yellow','blue']
}

Super.prototype.sayHi = function(){
    console.log('hi')
}

function Sub(name,age,height){
    Super.apply(this,arguments)
    this.height = height;
}

Sub.prototype = new Super('w',22);
Sub.prototype.constructor = Sub;
console.log(Sub.prototype)
Sub.prototype.sayHello = function(){
    console.log('hello')
}

var instance1 = new Sub('mengfeng',23,180);
var instance2 = new Sub('mengfeng123',24,181);
console.log(instance1)

6.寄生组合继承

function inheritPrototype(Sub,Super){
    var subPrototype=Object.create(Super.prototype)
    subPrototype.constructor=Sub
    Sub.prototype=subPrototype
    
  }
  function Super(name){
    this.name=name
  }
  Super.prototype.sayHi=function(){
    console.log(this.name)//ccdida
  }
  function Sub(name){
    Super.call(this,name)
  }
  inheritPrototype(Sub,Super)

  Sub.prototype.sayHello=function(){
    console.log('sayHello')
  }

  var instance1=new Sub('ccdida')
  console.log(instance1.__proto__)
  console.log(instance1.__proto__.__proto__)

10.对象扁平化

//对象扁平化
function flat(obj, key = "", res = {}, isArray = false) { 
    for (let [k, v] of Object.entries(obj)) { 
      if (Array.isArray(v)) { 
        let tmp = isArray ? key + "[" + k + "]" : key + k 
        flat(v, tmp, res, true) 
      } else if (typeof v === "object") { 
        let tmp = isArray ? key + "[" + k + "]." : key + k + "." 
        flat(v, tmp, res) 
      } else { 
        let tmp = isArray ? key + "[" + k + "]" : key + k 
        res[tmp] = v 
      } 
    } 
    return res 
  }
  
  var entryObj = {
    a: {
        b: {
            c: {
                dd: 'abcdd'
            }
        },
        d: {
            xx: 'adxx'
        },
        e: 'ae'
    }
}

console.log(flat(entryObj))

11.发布订阅

class EventEmitter{
    constructor(){
        this._events = {};
    }

    on(eventName, callback){
        if(this._events[eventName]){
            if(this.eventName !== "newListener"){
                this.emit("newListener", eventName)
            }
        }
        const callbacks = this._events[eventName] || [];
        callbacks.push(callback);
        this._events[eventName] = callbacks
    }

    emit(eventName, ...args){
        const callbacks = this._events[eventName] || [];
        callbacks.forEach(cb => cb(...args))
    }

    once(eventName, callback){
        const one = (...args)=>{
            callback(...args)
            this.off(eventName, one)
        }
        one.initialCallback = callback;
        this.on(eventName, one)
    }

     off(eventName, callback){
        const callbacks = this._events[eventName] || []
        const newCallbacks = callbacks.filter(fn => fn != callback && fn.initialCallback != callback /* 用于once的取消订阅 */)
        this._events[eventName] = newCallbacks;
    }
}



const events = new EventEmitter()

events.on("newListener", function(eventName){
    console.log(`eventName`, eventName)
})

events.on("hello", function(){
    console.log("hello");
})

let cb = function(){
    console.log('cb');
}
events.on("hello", cb)

events.off("hello", cb)

function once(){
    console.log("once");
}
events.once("hello", once)

events.off("hello", once)
events.emit("hello")
events.emit("hello")

12.反柯里化函数

Function.prototype.uncurrying = function() {
    var self = this;   //self为Array.prototype.push
    return function() {
        //obj = {0:1, length: 1}, arguments = [2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
        var obj = Array.prototype.shift.call(arguments); 
        
        //Array.ptototype.push(obj, 2)
        return self.apply(obj, arguments);
    }
}
var testObj = {
    length: 1,
    0: 1
}
var push = Array.prototype.push.uncurrying();
push(testObj, 2);
console.log(testObj);   //{0: 1, 1: 2, length: 2}

13.防抖

//scroll方法中的do somthing至少间隔500毫秒执行一次
window.addEventListener('scroll',function(){
    var timer;//使用闭包,缓存变量
    return function(){
    if(timer) clearTimeout(timer);
    timer = setTimeout(function(){
    console.log('do somthing')
},500)
}
}());//此处()作用 - 立即调用return后面函数,形成闭包

14.监测数组变化

// 获取Array的原型,并创建一个新的对象指向这个原型
const arrayMethods = Object.create(Array.prototype)
// 创建一个新的原型,这就是改造之后的数组原型
const ArrayProto = []
// 重新构建Array原型里面的虽有方法
Object.getOwnPropertyNames(Array.prototype).forEach(method => {
    if(typeof arrayMethods[method] === "function"){
        ArrayProto[method] = function(){
            console.log("我已经监听到数组触发了"+method+"事件")
            return arrayMethods[method].apply(this, arguments)
        }
    }else{
        ArrayProto[method] = arrayMethods[method]
    }
})


let list = [1, 2, 3]
// 将数组的原型链指向新构造的原型
list.__proto__ = ArrayProto
// 执行push事件
list.push(2)
// 输出:
我已经监听到数组触发了push事件 // 这个说明监听成功了

15.节流

//scroll方法中当间隔时间大于2s,do somthing执行一次
window.addEventListener('scroll',function(){
    var timer ;//使用闭包,缓存变量
    var startTime = new Date();
    return function(){
    var curTime = new Date();
    if(curTime - startTime >= 2000){
    timer = setTimeout(function(){
    console.log('do somthing')
    },500);
    startTime = curTime;
    }
      } }());//此处()作用 - 立即调用return后面函数,形成闭包

16.拦截全局Promise-reject

// 使用Try catch 只能拦截try语句块里面的
try {
    new Promise((resolve, reject) => {
      reject("WTF 123");
    });
  } catch (e) {
    console.log("e", e);
    throw e;
  }
  
  // 使用 unhandledrejection 来拦截全局错误  (这个是对的)
  window.addEventListener("unhandledrejection", (event) => {
    event && event.preventDefault();
    console.log("event", event);
  });

17.千位符

// 将金额类型转为数字类型
function toNum(str) {
    return str.replace(/\,|\¥/g, "");
}

// 保留两位小数(四舍五入)
function toPrice(num) {
    num = parseFloat(toNum(num)).toFixed(2).toString().split(".");
    num[0] = num[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + num.join(".");
}

// 保留两位小数(不四舍五入)
function toPrice1(num) {
    num = parseFloat(toNum(num).replace(/(\.\d{2})\d+$/,"$1")).toFixed(2).toString().split(".");
    num[0] = num[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + num.join(".");;
}

// 不处理小数部分
function toPrice2(num) {
    var source = toNum(num).split(".");
    source[0] = source[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + source.join(".")
}

console.log(toPrice('12312.236')) //¥12,312.24
console.log(toPrice1('12312.234')) //¥12,312.23
console.log(toPrice2('1232342312.234')) //¥1,232,342,312.234

18.浅拷贝

let Sclone =(obj)=>{        
    // 方法一        
    // let obj1 = {}        
    // obj1 = Object.assign({},obj)        
    // 方法二 
    let obj1 ={...obj}       
    return obj1    
}

19.数组去重

let arr =  [1,2,2,4,null,null,'3','abc',3,5,4,1,2,2,4,null,null,'3','abc',3,5,4] 

//利用key的唯一
let obj = {};
for (let i = 0; i < arr.length; i++) {
  let item = arr[i]
  if (obj[item] !== undefined) {
    arr.splice(i, 1);
    i--; // 解决删除元素后,数组塌陷问题
    continue;
  }
  obj[item] = item
}

console.log(arr)
// arr: [1, 2, 4, null, "3", "abc", 3, 5]


// 交换元素位置从而替换调 splice方法
let obj1 = {};
for (let i = 0; i < arr.length; i++) {
  let item = arr[i]
  if (obj1[item] !== undefined) {
    arr[i] = arr[arr.length-1]
    arr.length--;
    i--; 
    continue;
  }
  obj1[item] = item
}
// arr: [1, 2, 4, null, "3", "abc", 3, 5]


// Array.filter + Array.indexO
let newArr = arr.filter((item, index) => arr.indexOf(item) === index);  
// [1, 2, 4, null, "3", "abc", 3, 5


// Array.filter + Object.hasOwnProperty
let obj2 = {}
arr.filter(item => obj2.hasOwnProperty(typeof item + item) ? false : (obj2[typeof item + item] = true))


// Array.reduce + Array.includes
let newArr1 = arr.reduce((accu, cur) => {
    return accu.includes(cur) ? accu : accu.concat(cur);  // 1. 拼接方法
    // return accu.includes(cur) ? accu : [...accu, cur]; // 2. 扩展运算
}, [])


// Array.indexOf
let newArr2 = []
for (var i = 0; i < arr.length; i++) {
    if (newArr2.indexOf(arr[i]) === -1) newArr2.push(arr[i])  
}
//等同于 forEach 写法
arr.forEach( item => newArr2.indexOf(item) === -1 ? newArr2.push(item) : '')

//Array.includes
let newArr3 = []
for (var i = 0; i < arr.length; i++) {
    if (!newArr3.includes(arr[i]))  newArr3.push(arr[i])
}
//等同于 forEach 写法
arr.forEach( item => !newArr3.includes(item) ? newArr3.push(item) : '')


// new Set + 扩展运算符 || Array.from
let newArr5 = [...new Set(arr)];      // [1, 2, 4, null, "3", "abc", 3, 5]
let newArr4 = Array.from(new Set(arr));      // [1, 2, 4, null, "3", "abc", 3, 5]
let newStr = [...new Set('ababbc')].join('')  //  'abc'


// new Map
let map = new Map();
let newStr6 = [];

for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
        map.set(arr[i], true);
        newStr6.push(arr[i]);
    }
}
console.log(newArr6)  // [1, 2, 4, null, "3", "abc", 3, 5]

20.数组转为tree

let arr= [
    { id: 1, name: '部门A', parentId: 0 },
    { id: 3, name: '部门C', parentId: 1 },
    { id: 4, name: '部门D', parentId: 1 },
    { id: 5, name: '部门E', parentId: 2 },
    { id: 6, name: '部门F', parentId: 3 },
    { id: 7, name: '部门G', parentId: 2 },
    { id: 8, name: '部门H', parentId: 4 },
    { id: 18, name: '部门K', parentId: 4 },
    { id: 22, name: '部门zz', parentId: 21 }
]


function arrToTree(arr) {
  arr=JSON.parse(JSON.stringify(arr))
  const newArr = []
  // 1. 构建一个字典:能够快速根据id找到对象。
  const map = {}
  arr.forEach(item => {
    // 为了计算方便,统一添加children
    item.children = []
    // 构建一个字典
    map[item.id] = item
  })
 
  // 2. 对于arr中的每一项
  arr.forEach(item => {
    const parent = map[item.parentId]
    if (parent) {
      //    如果它有父级,把当前对象添加父级元素的children中
      parent.children.push(item)
    } else {
      //    如果它没有父级(pid:''),直接添加到newArr
      newArr.push(item)
    }
  })
  return newArr
}

console.log(arrToTree(arr))

21.ajax

function ajax(option) {//type,url,obj,timeout,success,error将所有参数换成一个对象{}
    //  0.将对象转换成字符串
    var str = objToString(option.data);
    //  1.创建一个异步对象xmlhttp;
    var xmlhttp, timer;
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest();
    } else {// code for IE6, IE5 
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    //  2.设置请求方式和请求地址; 
    // 判断请求的类型是POST还是GET
    if (option.type.toLowerCase() === 'get') {
        xmlhttp.open(option.type, option.url + "?t=" + str, true);
        //  3.发送请求;
        xmlhttp.send();
    } else {
        xmlhttp.open(option.type, option.url, true);
        // 注意:在post请求中,必须在open和send之间添加HTTP请求头:setRequestHeader(header,value);
        xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        //  3.发送请求;
        xmlhttp.send(str);
    }
    //  4.监听状态的变化;
    xmlhttp.onreadystatechange = function () {
        clearInterval(timer);
        if (xmlhttp.readyState === 4) {
            if (xmlhttp.status >= 200 && xmlhttp.status < 300 || xmlhttp.status == 304) {
                //  5.处理返回的结果;
                option.success(xmlhttp);//成功后回调;
            } else {
                option.error(xmlhttp);//失败后回调;
            }
        }
    }
    //处理obj 
    function objToString(data) {
        data.t = new Date().getTime();
        var res = [];
        for (var key in data) {
            //需要将key和value转成非中文的形式,因为url不能有中文。使用encodeURIComponent();
            res.push(encodeURIComponent(key) + " = " + encodeURIComponent(data[key]));
        }
        return res.join("&");
    }
    //判断外界是否传入了超时时间
    if (option.timeout) {
        timer = setInterval(function () {
            xmlhttp.abort();//中断请求
            clearInterval(timer);
        }, timeout);
    }
}

22.apply

Function.prototype.apply2 = function(obj,arr){
    obj = obj?Object(obj):window;
    let _fn = "fn",result;
    while (obj.hasOwnProperty(_fn)) {
      _fn = "fn" + Math.random(); // 循环判断并重新赋值
    }
    obj[_fn] = this;
    if(arr){
      result = obj[_fn](...arr);
    }else{
      result = obj[_fn]();
    }
    delete obj[_fn];
    return result;
  }

23.async和await

function asyncToGen(genFunction) {
    return function (...args) {
      const gen = genFunction.apply(this, args);
      return new Promise((resolve, reject) => {
        function step(key, arg) {
          let genResult;
          try {
            genResult = gen[key](arg);
          } catch (err) {
            return reject(err);
          }
          const { value, done } = genResult;
          if (done) {
            return resolve(value);
          }
          return Promise.resolve(value).then(
            (val) => {
              step('next', val);
            },
            (err) => {
              step('throw', err);
            },
          );
        }
        step('next');
      });
    };
  }
  const getData = () => new Promise(resolve => setTimeout(() => resolve('data'), 1000));
  function* testG() {
    const data = yield getData();
    console.log('data: ', data);
    const data2 = yield getData();
    console.log('data2: ', data2);
    return 'success';
  }
  
  const gen = asyncToGen(testG);
  gen().then(res => console.log(res));

24.bind

Function.prototype.bind2 = function (obj) {
    obj = obj ? Object(obj) : window
    let myArguments = arguments
    let self = this
    if (arguments.length > 1) {
      return function () {
        self.apply(obj, [...[...myArguments].slice(1), ...arguments])
      }
    }
    return function () {
      self.apply(obj, [...arguments])
    }
  }

25.call

Function.prototype.call2 = function(obj){
    obj = obj?Object(obj):window;
    let _fn = "fn",result;
    while (obj.hasOwnProperty(_fn)) {
      _fn = "fn" + Math.random(); // 循环判断并重新赋值
    }
    obj[_fn] = this;
    if(arguments.length>1){
      result = obj[_fn](...([...arguments].slice(1)));
    }else{
      result = obj[_fn]();
    }
    delete obj[_fn];
    return result;
  }

26.filter

Array.prototype.myFilter = function(callback, thisArg) {
    // 确认调用者必须是个数组
    if (Object.prototype.toString.call(this) !== '[object Array]') {
      throw new TypeError('this must be a array');
    }
    if (typeof callback !== 'function') {
      throw new TypeError(callback + 'is not a function');
    }
    // 返回结果的数组
    const res = [];
    // 让O成为回调函数的对象传递(强制转换对象)
    const O = Object(this);
    console.log(O)
    // >>>0 保证len为number,且为正整数
    // 无符号位移计算符
    const len = O.length >>> 0;
    // 对整个数组进行遍历
    for (let i = 0; i < len; i++) {
        // 遍历回调函数调用传参
        // call是传入(新this指向,参数)
        // thisArg新设置的this,这里无设置就是undefined
        // O[i] 是原数组的当前元素
        // i是当前index
        // O是原数组
        if (callback.call(thisArg, O[i], i, O)) {
          res.push(O[i]);
        }
    }
    // 返回结果
    return res;
  }
  console.log([30,20,16,10].myFilter((num) => { return num >= 12}));

27.forEach

Array.prototype.myForEach = function(callback, thisArg) {
    // 判断是否是数组调用,并且传入的是回调函数
    if (this == null) {
      throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== "function") {
      throw new TypeError(callback + ' is not a function');
    }
    const O = Object(this);
    const len = O.length >>> 0;
    let k = 0;
   // 循环所有数据  
   for(let i = 0; i < len; i++) {
    callback.call(thisArg, O[k], k, O);
   }
  }

28.instanceof

function new_instance_of(leftValue,rightValue){
    let rightProto = rightValue.prototype;//取右边表达式的 prototype 值
    leftValue = leftValue.__proto__;//取左表达式的 __proto__ 值
    while(true){
        if(leftValue == null){
            return false;
        }
        if(leftValue === rightProto){
            return true;
        }
        leftValue = leftValue.__proto__;
    }
}
function Foo(){}
console.log(new_instance_of(Foo,Object))//true

29.jsonp

let jsonp=(url,data={},callback='callback')=>{
    //准备好带有padding的请求url
let dataStr=url.indexOf('?')=== -1?'?':'&'
// console.log(dataStr);
for(let key in data){
    dataStr +=`${key}=${data[key]}&`
}
dataStr +=`callback=`+callback

//构造 script
let oScript=document.createElement('script')
oScript.src=url+dataStr
//appendChild () 方法可向节点的子节点列表的末尾添加新的子节点
document.body.appendChild(oScript)

// window[callback]=(data)=>{
//     console.log(data);
// }
return new Promise((resolve,reject)=>{
    window[callback]=(data)=>{
        try{
            resolve(data)
        }catch(e){
            reject(e)
        }finally{
            oScript.parentNode.removeChild(oScript)// 注意这句代码,OScript移除,细节
        }
    }
})
}

jsonp('https://photo.sina.cn/aj/index?a=1',{
    page:1,
    cate:'recommend'
}).then(response=>{
    console.log(response,'-------then');
}) 

30.map

Array.prototype.myMap = function(callback, thisArg) {
    if (this == undefined) {
      throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== 'function') {
      throw new TypeError(callback + ' is not a function');
    }
    const res = [];
    const O = Object(this);
    const len = O.length >>> 0;
    for (let i = 0; i < len; i++) {
       // 调用回调函数并传入新数组
       res[i] = callback.call(thisArg, O[i], i, O);
    }
    return res;
  }

31.new

/**
 * new 使用Js原生实现
 */
function Parent(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    }
}
const _new = function (Parent, ...rest) {
    //1.以构造器Parent的prototype为原型创建新对象
    const child = Object.create(Parent.prototype);
    //2. 将this和调用参数传给构造器执行
    const result = Parent.apply(child, rest);
    return typeof result === 'object' ? result : child;
}
const p1 = _new(Parent,'www','23');
console.log(p1);
p1.sayName(); 

32.object-create

Object.myCreate = function (proto, propertyObject = undefined) {
    if (propertyObject === null) {
      // 这里没有判断propertyObject是否是原始包装对象
      throw 'TypeError'
    } else {
      function Fn () {}
      Fn.prototype = proto
      const obj = new Fn()
      if (propertyObject !== undefined) {
        Object.defineProperties(obj, propertyObject)
      }
      if (proto === null) {
        // 创建一个没有原型对象的对象,Object.create(null)
        obj.__proto__ = null
      }
      return obj
    }
  }
  
  // 示例
  // 第二个参数为null时,抛出TypeError
  // const throwErr = Object.myCreate({a: 'aa'}, null)  // Uncaught TypeError
  // 构建一个以
  const obj1 = Object.myCreate({a: 'aa'})
  console.log(obj1)  // {}, obj1的构造函数的原型对象是{a: 'aa'}
  const obj2 = Object.myCreate({a: 'aa'}, {
    b: {
      value: 'bb',
      enumerable: true
    }
  })
  console.log(obj2)  // {b: 'bb'}, obj2的构造函数的原型对象是{a: 'aa'}

33.object-is

Object.is = function(x, y) {
    if (x === y) {
        // 当前情况下,只有一种情况是特殊的,即 +0 -0
        // 如果 x !== 0,则返回true
        // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
        return x !== 0 || 1 / x === 1 / y;
    }
    
    // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
    // x和y同时为NaN时,返回true
    return x !== x && y !== y;
}

34.promise.all

Promise.prototype.all = function(promises){
    let results = [];
    let promiseCount = 0;
    let promisesLength = promises.length;
    return new Promise(function(resolve,reject){
        for(let val of promises){
            Promise.resolve(val).then(function(res){
                promiseCount++;
                results[i] = res;
                if(promiseCount === promisesLength){
                    return resolve(results);
                }
            },function(err){
                return reject(err);
            })
        }
    })
}

let promise1 = new Promise(function(resolve) {
    resolve(1);
  });
  let promise2 = new Promise(function(resolve) {
    resolve(2);
  });
  let promise3 = new Promise(function(resolve) {
    resolve(3);
  });
  
  let promiseAll = Promise.all([promise1, promise2, promise3]);
  promiseAll.then(function(res) {
    console.log(res);
  });

35.promise

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

const resolvePromise = (promise2, x, resolve, reject) => {
  // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise  Promise/A+ 2.3.1
  if (promise2 === x) { 
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  // Promise/A+ 2.3.3.3.3 只能调用一次
  let called;
  // 后续的条件要严格判断 保证代码能和别的库一起使用
  if ((typeof x === 'object' && x != null) || typeof x === 'function') { 
    try {
      // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候)  Promise/A+ 2.3.3.1
      let then = x.then;
      if (typeof then === 'function') { 
        // 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty  Promise/A+ 2.3.3.3
        then.call(x, y => { // 根据 promise 的状态决定是成功还是失败
          if (called) return;
          called = true;
          // 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
          resolvePromise(promise2, y, resolve, reject); 
        }, r => {
          // 只要失败就失败 Promise/A+ 2.3.3.3.2
          if (called) return;
          called = true;
          reject(r);
        });
      } else {
        // 如果 x.then 是个普通值就直接返回 resolve 作为结果  Promise/A+ 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      // Promise/A+ 2.3.3.2
      if (called) return;
      called = true;
      reject(e)
    }
  } else {
    // 如果 x 是个普通值就直接返回 resolve 作为结果  Promise/A+ 2.3.4  
    resolve(x)
  }
}

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks= [];

    let resolve = (value) => {
      if(this.status ===  PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    } 

    let reject = (reason) => {
      if(this.status ===  PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    }

    try {
      executor(resolve,reject)
    } catch (error) {
      reject(error)
    }
  }

  then(onFulfilled, onRejected) {
    //解决 onFufilled,onRejected 没有传值的问题
    //Promise/A+ 2.2.1 / Promise/A+ 2.2.5 / Promise/A+ 2.2.7.3 / Promise/A+ 2.2.7.4
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    //因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后 then 的 resolve 中捕获
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    // 每次调用 then 都返回一个新的 promise  Promise/A+ 2.2.7
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        //Promise/A+ 2.2.2
        //Promise/A+ 2.2.4 --- setTimeout
        setTimeout(() => {
          try {
            //Promise/A+ 2.2.7.1
            let x = onFulfilled(this.value);
            // x可能是一个proimise
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            //Promise/A+ 2.2.7.2
            reject(e)
          }
        }, 0);
      }

      if (this.status === REJECTED) {
        //Promise/A+ 2.2.3
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e)
          }
        }, 0);
      }

      if (this.status === PENDING) {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e)
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(()=> {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0);
        });
      }
    });
  
    return promise2;
  }
}

36.promise.race

Promise.race = function(promises){
    //将可迭代对象转换为数组
    promises = Array.from(promises);
    return new Promise((resolve,reject)=>{
        if(promises.length === 0){
            //空的可迭代对象,用于pending状态
        }else{
            for(let i = 0;i < promises.length;i++){
                Promise.resolve(promises[i]).then((data)=>{
                    resolve(data);
                }).catch((reason)=>{
                    reject(reason)
                })
            }
        }
    })
}

let p1 = new Promise(function(resolve,reject){
    setTimeout(function(){
     resolve('success')
    },1000)
})

let p2 = new Promise(function(resolve,reject){
    setTimeout(function(){
     resolve('faild')
    },500)
})

Promise.race([p1,p2]).then(result=>{
console.log(result)             //  faild    faild耗时短
})

37.reduce

Array.prototype.myReduce = function(callback, initialValue) {
    // 判断调用的是否是数组,以及传入的callback是否是函数
    if (this == undefined) {
      throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== 'function') {
      throw new TypeError(callbackfn + ' is not a function');
    }
    // 空数组也是不允许的
    if (this.length == 0) {
     throw new TypeError('Reduce of empty array with no initial value');
    }
    // 让O成为回调函数的对象传递(强制转换对象)
    const O = Object(this);
    // >>>0 保证len为number,且为正整数
    const len = this.length >>> 0;
    // 保存初始值,初始值不传的时候为undefined
    let accumulator = initialValue;
    // 标志位
    let k = 0;
    // 如果第二个参数为undefined的情况,则数组的第一个有效值作为累加器的初始值
    if (accumulator === undefined) {
      // 这里是k++,就是赋值完成之后k再加1
      accumulator = O[k++];
    }
    // 此时如果有初始值,k是0,如果无初始值k是1
    for(k;k<len;k++) {
     accumulator = callback.call(this, accumulator, O[k], k, O);
    }
    return accumulator;
  }
  console.log([2,4,6].myReduce((t,n)=>{return t+n}));
  console.log([2,4,6].myReduce((t,n)=>{return t+n},10));

38.sleep

//Promise
const sleep = time => {
    return new Promise(resolve => setTimeout(resolve,time))
  }
  sleep(1000).then(()=>{
    console.log(1)
  })
  
  //Generator
  function* sleepGenerator(time) {
    yield new Promise(function(resolve,reject){
      setTimeout(resolve,time);
    })
  }
  sleepGenerator(1000).next().value.then(()=>{console.log(1)})
  
  //async
  function sleep(time) {
    return new Promise(resolve => setTimeout(resolve,time))
  }
  async function output() {
    let out = await sleep(1000);
    console.log(1);
    return out;
  }
  output();
  
  //ES5
  function sleep(callback,time) {
    if(typeof callback === 'function')
      setTimeout(callback,time)
  }
  
  function output(){
    console.log(1);
  }
  sleep(output,1000);

39.vue-Reactive

const targetMap = new WeakMap();
let activeEffect = null; // 引入 activeEffect 变量

const effect = eff => {
    activeEffect = eff; // 1. 将副作用赋值给 activeEffect
  activeEffect();     // 2. 执行 activeEffect
  activeEffect = null;// 3. 重置 activeEffect
}

const track = (target, key) => {
    if (activeEffect) {  // 1. 判断当前是否有 activeEffect
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = new Set()));
        }
        dep.add(activeEffect);  // 2. 添加 activeEffect 依赖
    }
}

const trigger = (target, key) => {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(effect => effect());
    }
};

const reactive = (target) => {
    const handler = {
        get(target, key, receiver) {
            const result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            const oldValue = target[key];
            const result = Reflect.set(target, key, value, receiver);
            if (oldValue != result) {
                trigger(target, key);
            }
            return result;
        }
    }

    return new Proxy(target, handler);
}

let product = reactive({ price: 10, quantity: 2 });
let total = 0, salePrice = 0;
// 修改 effect 使用方式,将副作用作为参数传给 effect 方法
effect(() => {
    total = product.price * product.quantity
});
effect(() => {
    salePrice = product.price * 0.9
});
console.log(total, salePrice);  // 20 9
product.quantity = 5;
console.log(total, salePrice);  // 50 9
product.price = 20;
console.log(total, salePrice);  // 100 18

40.算法笔试

1.插入排序

function insertionSort(arr) {
    var len = arr.length;
    var preIndex, current;
    for (var i = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        while(preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex+1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex+1] = current;
    }
    return arr;
}
const arr1 = [2,6,8,2,3,5,0,1,6,8]
let res = insertionSort(arr1)
console.log(arr1)//[ 0, 1, 2, 2, 3, 5, 6, 6, 8, 8]

2.堆排序

var len;    // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) {   // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}

function heapify(arr, i) {     // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;

    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }

    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

function heapSort(arr) {
    buildMaxHeap(arr);

    for (var i = arr.length-1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

const arr1 = [12,4,6,9,11,0,4,3,7,9]
let res = heapSort(arr1)
console.log(res)//[0, 3, 4,  4,  6, 7, 9, 9, 11, 12]

3.斐波那契数列

//方法一:使用递归
function fibonacci(n) {
    if (n == 1 || n == 2) {
        return 1
    };
    return fibonacci(n - 2) + fibonacci(n - 1);
}
console.log(fibonacci(3))//2

//方法二:改进递归-把前两位数字做成参数避免重复计算
function fibonacci(n) {
    function fib(n, v1, v2) {
        if (n == 1)
            return v1;
        if (n == 2)
            return v2;
        else
            return fib(n - 1, v2, v1 + v2)
    }
    return fib(n, 1, 1)
}
fibonacci(30)

//方法三:改进递归-利用闭包特性把运算结果存储在数组里,避免重复计算
var fibonacci = function () {
    let memo = [0, 1];
    let fib = function (n) {
        if (memo[n] == undefined) {
            memo[n] = fib(n - 2) + fib(n - 1)
        }
        return memo[n]
    }
    return fib;
}()
fibonacci(30)


//方法四:改进递归-摘出存储计算结果的功能函数
var memoizer = function (func) {
    let memo = [];
    return function (n) {
        if (memo[n] == undefined) {
            memo[n] = func(n)
        }
        return memo[n]
    }
};
var fibonacci=memoizer(function(n){
    if (n == 1 || n == 2) {
        return 1
    };
    return fibonacci(n - 2) + fibonacci(n - 1);
})
fibonacci(30)


//方法五:普通for循环
function fibonacci(n) {
    var n1 = 1, n2 = 1, sum;
    for (let i = 2; i < n; i++) {
        sum = n1 + n2
        n1 = n2
        n2 = sum
    }
    return sum
}
fibonacci(30)

//方法六:for循环+解构赋值
var fibonacci = function (n) {
    let n1 = 1; n2 = 1;
    for (let i = 2; i < n; i++) {
        [n1, n2] = [n2, n1 + n2]
    }
    return n2
}
fibonacci(30)

4.归并排序

function mergeSort(arr) {  // 采用自上而下的递归方法
    var len = arr.length;
    if(len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right)
{
    var result = [];

    while (left.length && right.length) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }

    while (left.length)
        result.push(left.shift());

    while (right.length)
        result.push(right.shift());

    return result;
}


const arr1 = [1,4,6,1,0,3,4,2,7,3,1]
let res = mergeSort(arr1)
console.log(res)//[0, 1, 1, 1, 2,3, 3, 4, 4, 6,7]

5.汉诺塔问题

/** 
 * @param {圆盘数:number} plates 
 * @param {起始柱子 a:string} source 
 * @param {辅助柱子 b:string} helper 
 * @param {目标柱子 c:string} dest 
 * @param {移动步骤集:Array,数组的长度就是移动的次数} moves 
 */
function hanoi(plates, source, helper, dest, moves = []) {
    if (plates <= 0) {
        return moves;
    }
    if (plates === 1) {
        moves.push([source, dest]);
    } else {
        hanoi(plates - 1, source, dest, helper, moves);
        moves.push([source, dest]);
        hanoi(plates - 1, helper, source, dest, moves);
    }
    return moves;
}

// test
console.log(hanoi(4, 'source', 'helper', 'dest')); // 输出结果如下图展示

6.合并两个有序数组

var merge = function (nums1, m, nums2, n) {
    var p = m + n - 1;//0
    var p1 = m - 1;//-1
    var p2 = n - 1;//0
      // 理论上来说,nums2应该全部填充进去,所以这里以p2作为条件
    while (p2 >= 0) {
        // nums1里面全是0的情况,比如[0], 0, [1], 1
        if (p1 < 0) {
            // 直接用nums2去填补nums1就好了
            nums1[p--] = nums2[p2--]
        // 只有nums2比nums1大才用nus2填补
        } else if (nums2[p2] > nums1[p1]) {
            nums1[p] = nums2[p2];
            p--;
            p2--;
        // 反之用nums1填补
        } else {
            nums1[p] = nums1[p1];
            p--;
            p1--;
        }
    };
    return nums1;
};

let nums1 = [1,2,3,0,0,0], m = 3,nums2 = [2,5,6],n = 3
console.log(merge(nums1, m, nums2, n))//[ 1, 2, 2, 3, 5, 6 ]

7.快速排序

function quickSort(arr, left, right) {
    var len = arr.length,
        partitionIndex,
        left = typeof left != 'number' ? 0 : left,
        right = typeof right != 'number' ? len - 1 : right;

    if (left < right) {
        partitionIndex = partition(arr, left, right);
        quickSort(arr, left, partitionIndex-1);
        quickSort(arr, partitionIndex+1, right);
    }
    return arr;
}

function partition(arr, left ,right) {     // 分区操作
    var pivot = left,                      // 设定基准值(pivot)
        index = pivot + 1;
    for (var i = index; i <= right; i++) {
        if (arr[i] < arr[pivot]) {
            swap(arr, i, index);
            index++;
        }        
    }
    swap(arr, pivot, index - 1);
    return index-1;
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
function partition2(arr, low, high) {
  let pivot = arr[low];
  while (low < high) {
    while (low < high && arr[high] > pivot) {
      --high;
    }
    arr[low] = arr[high];
    while (low < high && arr[low] <= pivot) {
      ++low;
    }
    arr[high] = arr[low];
  }
  arr[low] = pivot;
  return low;
}

function quickSort2(arr, low, high) {
  if (low < high) {
    let pivot = partition2(arr, low, high);
    quickSort2(arr, low, pivot - 1);
    quickSort2(arr, pivot + 1, high);
  }
  return arr;
}

const arr1 = [1,4,6,1,0,3,4,2,7,3,1]
let res = quickSort(arr1,0,11)
console.log(res)//[ 0, 1, 1, 1, 2, 3, 3, 4, 4, 6,7]

8.两数之和

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
const twoSum = (nums, target) => {
    const prevNums = {};                    // 存储出现过的数字,和对应的索引               
  
    for (let i = 0; i < nums.length; i++) {       // 遍历元素   
      const curNum = nums[i];                     // 当前元素   
      const targetNum = target - curNum;          // 满足要求的目标元素   
      const targetNumIndex = prevNums[targetNum]; // 在prevNums中获取目标元素的索引
      if (targetNumIndex !== undefined) {         // 如果存在,直接返回 [目标元素的索引,当前索引]
        return [targetNumIndex, i];
      } else {                                    // 如果不存在,说明之前没出现过目标元素
        prevNums[curNum] = i;                     // 存入当前的元素和对应的索引
      }
    }
  }

  let nums = [2,7,11,15], target = 17
  console.log(twoSum(nums,target))//[ 0, 3 ]

9.冒泡排序

 //双向冒泡排序
 function bubbleSort_twoway(arr) {
    var len = arr.length;    //依次将最大的数放置到数组末尾,将第二大的数放到倒数第二位...
    var flag = false;
    for(var i = 0; i < len/2; i++) {
        flag = false;
        for(var j = i; j < len - 1 - i; j++) {   //从前往后,比较相邻两个数,把大的放在后边.之前已放置成功的可以不再参与比较
            if(arr[j] > arr[j + 1]) {
                var middle = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = middle;
                flag =true;
            }
        }
        if(!flag){
            break;
        }

        for(var j = len - 1 - i; j > i; j--){
            if(arr[j] < arr[j - 1]) {
                var middle = arr[j];
                arr[j] = arr[j-1];
                arr[j-1] = middle;
                flag = true;
            }
        }
        if(!flag){
            break;
        }
    }
    return arr;
}

var defaultArr = [3, 5, 32, 15, 7, 26, 10, 55, 45, 12, 28, 88, 18];
var resultArr = bubbleSort_twoway(defaultArr);
console.table(resultArr);

10.爬台阶

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    const sqrt_5 = Math.sqrt(5);
    const fib_n = Math.pow((1 + sqrt_5) / 2, n + 1) - Math.pow((1 - sqrt_5) / 2,n + 1);
    return Math.round(fib_n / sqrt_5);
};


/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    const dp = [];
    dp[0] = 1;
    dp[1] = 1;
    for(let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};

11.数组交集

//数组功能扩展
//数组迭代函数
Array.prototype.each = function(fn){
    fn = fn || Function.K;
    var a = [];
    var args = Array.prototype.slice.call(arguments, 1);
    for(var i = 0; i < this.length; i++){
    var res = fn.apply(this,[this[i],i].concat(args));
    if(res != null) a.push(res);
    }
    return a;
   };
   //数组是否包含指定元素
   Array.prototype.contains = function(suArr){
    for(var i = 0; i < this.length; i ++){
    if(this[i] == suArr){
    return true;
    }
    }
    return false;
   }
   //不重复元素构成的数组
   Array.prototype.uniquelize = function(){
    var ra = new Array();
    for(var i = 0; i < this.length; i ++){
    if(!ra.contains(this[i])){
    ra.push(this[i]);
    }
    }
    return ra;
   };
   //两个数组的交集
   Array.intersect = function(a, b){
    return a.uniquelize().each(function(o){return b.contains(o) ? o : null});
   };
   //两个数组的差集
   Array.minus = function(a, b){
    return a.uniquelize().each(function(o){return b.contains(o) ? null : o});
   };
   //两个数组的补集
   Array.complement = function(a, b){
    return Array.minus(Array.union(a, b),Array.intersect(a, b));
   };
   //两个数组并集
   Array.union = function(a, b){
    return a.concat(b).uniquelize();
   };


var a = [1,2,3,4,5]
var b = [2,4,6,8,10]
console.log("数组a:", a);
console.log("数组b:", b);
var sa = new Set(a);
var sb = new Set(b);
// 交集
let intersect = a.filter(x => sb.has(x));
// 差集
let minus = a.filter(x => !sb.has(x));
// 补集
let complement = [...a.filter(x => !sb.has(x)), ...b.filter(x => !sa.has(x))];
// 并集
let unionSet = Array.from(new Set([...a, ...b]));
console.log("a与b的交集:", intersect);
console.log("a与b的差集:", minus);
console.log("a与b的补集:", complement);
console.log("a与b的并集:", unionSet);

12.希尔排序

function shellSort(arr) {
    var len = arr.length,
        temp,
        gap = 1;
    while(gap < len/3) {          //动态定义间隔序列
        gap =gap*3+1;
    }
    for (gap; gap > 0; gap = Math.floor(gap/3)) {
        for (var i = gap; i < len; i++) {
            temp = arr[i];
            for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
                arr[j+gap] = arr[j];
            }
            arr[j+gap] = temp;
        }
    }
    return arr;
}

var defaultArr = [3, 5, 32, 15, 7, 26, 10, 55, 45, 12, 28, 88, 18];
var resultArr = shellSort(defaultArr);
console.table(resultArr);

13.旋转数组

/**
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
 */

let reverse = function(nums, start, end){
    while(start < end){
        [nums[start++], nums[end--]] = [nums[end], nums[start]];
    }
}
let rotate = function(nums, k) {
    k %= nums.length;
    reverse(nums, 0, nums.length - 1);
    reverse(nums, 0, k - 1);
    reverse(nums, k, nums.length - 1);
    return nums;
};

14.选择排序

function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
        minIndex = i;
        for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {     // 寻找最小的数
                minIndex = j;                 // 将最小数的索引保存
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}
var defaultArr = [3, 5, 32, 15, 7, 26, 10, 55, 45, 12, 28, 88, 18];
var resultArr = selectionSort(defaultArr);
console.table(resultArr);

umi-4.0

umi 4.0

Umi 4 有什么新功能?

多构建引擎

Umi 4 同时支持 Vite 和 Webpack 两种构建方式,并尽量确保他们之间功能的一致性,让开发者可以通过一行配置进行切换。可能有些同学会喜欢 dev 用 vite,build 用 webpack 这样的组合。同时我们也在探索包括 ESMi 在内的其他构建方案的探索。

export default {
  vite: {}
}

默认快

默认快是多维度的,我们通过 MFSU V3 + Webpack 5 缓存解 Dev 时编译慢的问题;内网还有通过 Webpack 5 物理缓存和 CD 平台结合解 Build 时编译慢的问题;有使用 esbuild 做 js 和 css 的压缩器、配置和 MOCK 文件的读取、jest 的 transformer,让除构建之外的其他环节也飞快;此外还有运行时速度也有考虑。

MFSU V3

Umi 3 的 MFSU 大家可能多少有接触过,虽然有用,但 DX 不够好。用的时候会遇到一些坑,以至于很多同学都掌握了一项特殊技能,遇到问题时 rm -rf src/.umi。大家可能会遇到 monorepo 不支持、热更新导致 Tab 卡死、请求多导致页面打开慢、一些语法不支持的问题。以上问题在 MFSU V3 中全解!基于此,我们非常有信心地在 Umi 4 中默认开启 MFSU 功能。当然,如果你不喜欢,会保留手动配置 mfsu: false 关闭的口子。同时,MFSU V3 还可脱离 Umi 独立使用。

Umi Max

这是内部 Bigfish 框架的对外版本,解我们自己的问题,同时也给社区另一个集中化框架的选择,定位是中后台框架,包含了中后台相关的大量最佳实践的插件。如果有定制需求,大家可以参考他来实现内网框架的定制,比如快手团队就有基于 Umi 4 的框架定制,还有 Alita 也是基于 Umi 定制的面向移动端的框架。

$ npm i @umijs/max -D

React Router 6

我们升级了路由方案到 React Router 6,喜忧参半。好消息是,React Router 6 是 Remix 的基础库,面向框架层做了很多优化,路由实现层更优雅,Umi 得以删除大量路由渲染的代码;坏消息是,带来不少 Break Change,比如之前父路由渲染子路由用 children,得换成 。

- { props.children }
+ <Outlet />

支持 Vue

Umi 4 中提供了 Vue 支持,记得我在 Umi 2 时画过一张架构图,其中就有 Vue 的一环,Umi 3 时也有过尝试,但那会 Vue 3 还不太成熟,接入时遇到不少坑,这个坑今天总算是补上了。此功能由社区同学操刀,只需装载一个 preset 即可切换到 Vue。

export default {
  presets: ['@umijs/preset-vue'],
};

默认最快的 CSR 请求

项目构建快解的是 DX 问题,但同时也应该关注 UX。Client Loader 的目的是让应用加载默认快,避免 React 项目经典的 Render-Then-Fetch 的加载瀑布流问题。效果见下图,示例项目的从 9s 降到 6s,这 6s 还是之前截的图,上了 Preload 功能之后其实已更快。

export default function() {
  // 使用请求数据
  useClientLoaderData()
}
// 声明请求
export function clientLoader() {}

白盒文档的 Lint。

Umi 4 里内置了我们精挑细选的 lint 规则,只有质量类不开可能会导致项目问题的规则,不包含风格类的规则,不包含 TypeScript 中 type-aware 类的规则,这类规则需要跑整个项目,会导致性能问题;同时,我们通过 @rushstack/eslint-pach 的方式锁定了 config 里找 plugin 的规则,确保规则是长期稳定的。

SSR。

Umi 4 重写了 SSR 功能,目前此功能还在 beta 阶段,请勿将其用于生产环境。Umi 4 的 SSR 有以下特点,1)server 代码的构建基于 esbuild,所以极快,2)请求的处理类似 next.js 的 getServerSideProps 和 remix 的 loader,只在服务端跑,3)基于 react 18 的 suspense 和 renderToPipeableStream。实现原因,部署层目前仅实现了 vercel 的 adapter。这里有个简单的 Todos 示例:test-vercel-chencheng.vercel.app/

export default {
  ssr: { platform: 'vercel' }
}

API 路由

Umi 4 约定 src/api 目录下存放的 Serverless Function 格式的文件即为 API 路由。这部分路由会打包成不同平台支持的 Serverless Function 产物。场景比如带 token 的 API 调用、动态数据源、基于 Notion API 的 Blog、Hackernews Clone 等等。基于此,Umi 能做的事的边界就大了很多。不再只是写写中后台,实现静态页面。

export default {
  apiRoute: {},
}

微生成器

此概念来自 Modern.js。Modern.js 引入很多新概念,其中「微生成器」还是非常贴切的。他包含两个功能,1)小型脚手架,2)功能的开启与关闭。Umi 3 虽然也有 generate 命令,但只包含功能 1。Umi 4 拓展了下 generate(alias 为 g)命令。除了支持更多类型的小型脚手架生成,还支持功能的开启与关闭,以及比如 Monorepo、react 和 antd 版本等的功能切换。

$ npx umi g
? Pick generator type › - Use arrow-keys. Return to submit.
❯   创建页面 -- Create a umi page by page name
    创建组件 -- .
    创建 mock 代码 -- .
    创建 model 代码 -- .
    启用 Prettier -- Setup Prettier Configurations
    启用 Jest -- Setup Jest Configuration
    启用 E2E 测试 -- .
    启用 Tailwind CSS -- Setup Tailwind CSS configuration
    启用 SSR -- .
    启用 Low Import 研发模式 -- .
    启用权限方案 -- .
    启用 Monaco 编辑器 -- .
    关闭 Dva 数据流 -- Configuration, Dependencies, and Model Files for Dva
    关闭 MFSU -- .
    切换为 Monorepo 项目 -- .
    切换 React 为 18 -- .
    切换 Antd 为 5 -- .

除此之外,我们还有非常多小而美的 DX 改进。

自动 https

Umi 4 的 https dev server 的实现基于 mkcert,启动过程中会基于 hosts 自动生成对应的 key 和 cert。开发者除了安装前置的 mkcert,其他无需关心。

浏览器里的构建进度条。

如果首次构建没有完成就在浏览器里打开,你会看到一个构建进度条,支持 webpack 多实例,支持 MFSU,完成初始构建后会自动跳转到项目页。

Terminal 中的日志

有些开发者会更希望在命令行里看到项目里通过 console 输出的日志,比如我。因为命令行日志不会随着刷新而失效,大家可能都经历过一些一闪而过的页面,想截屏都难;同时命令行日志还可以做物理存储,导出后可以方便他人排查。此功能复刻自 github.com/patak-dev/v…

import { terminal } from 'umi';
terminal.log(`Some info from the app`);

然后就可以在命令行中看到日志,

umi.js 产物调试

不知大家是否会有这样的需求,开发项目时发现一些比较复杂的问题时,需要调整构建产物的代码。而 Umi 基于 webpack-dev-server,在 dev 阶段所有文件都存于内存中,没有物理文件的形式,并不方便直接修改后验证效果。如果大家用 Umi 4,可以把 umi.js 等产物文件保存到项目根目录,然后可以直接修改即生效。

项目级插件:plugin.ts

为进一步降低项目中使用插件的门槛,Umi 4 中约定项目根目录下的 plugin.ts 为插件,开发者可在此直接调用插件 API,无需注册,支持 TypeScript。有了这个文件,我们可以在项目级做很多事。比如,

import { IApi } from 'umi';
export default (api: IApi) => {
  // 比如修改 HTML
  api.modifyHTML($ => {
    return $;
  });
  // 比如在入口的 umi.ts 中添加代码
  api.addEntryCodeAhead(() => [`console.log('entry code ahead')`]);
  api.addEntryCode(() => [`console.log('entry code')`]);
  // 比如在构建完成时做额外的事
  api.onBuildComplete((opts) => {});
  // 比如在启动阶段做额外的事
  api.onStart((opts) => {});
  // 比如校验每个 JavaScript/TypeScript 代码
  api.onCheckCode((args) => {});
  // 比如动态修改路由
  api.modifyRoutes((routes) => {});
}

deadCode 检测

项目中通常会有未使用的文件或导出,Umi 4 中通过配置 deadCode: {} 即可在 build 阶段做检测。如有发现,会有类似信息抛出。

Warning: There are 3 unused files:
 1. /mock/a.ts
 2. /mock/b.ts
 3. /pages/index.module.less
 Please be careful if you want to remove them (¬º-°)¬.

参考链接:

[umi仓库]: “https://github.com/umijs/umi

Apifox和Leancloud

Apifox和Leancloud

我们为什么选择Apifox和Leancloud的组合呢

Apifox也是一款可以模拟接口的一款工具,当然也不只可以模拟接口

Leancloud选择是当我们去开发一些小的应用的时候,我们降低开发成本的时候,我们就可以去选择Leancloud去当我们的平台进行数据存储,还是比较方便的。

一、Apifox下载

访问官网进行下载

有付费和免费的,根据需求下载

我们可以去新建我们的项目和接口

下面会讲如何设置和填写参数以便于和Leancloud去交互请求

二、Leancloud注册登陆

我们访问官网注册登陆

然后来到我们的控制台创建我们的应用

我们可以对我们每个应用分开管理

我们每个应用都有自己的信息

大家测试的时候可以使用自己的应用信息配置

三、配置Apifox

当我们应用和Apifox环境准备完毕,那么我们就可以根据文档来配置了

文档->REST API->数据存储

1.新建接口

在配置我们的接口的前提是要选好我们接口运行的环境

让我们先配置一下环境

我们可以将我们的服务改成我们Leancloud平台的开放服务地址(后续我们可以改成我们的域名)

根据文档来配置

Header->批量编辑->冒号模式(然后将负责的配置修改成下面这种格式就可以了)

下一步我们来携带参数body

修改状态码并添加响应示例

保存运行

出error了,修复它(给body添加示例值就可以了)

2.发起请求

我们发送(响应成功)

四、Leancloud

当我们查看Leancloud应用的时候,发现我们多了一张表和数据,表示我们的请求已经成功

我们还可以根据文档来修改,查找,删除我们的数据表,代码中的配置也是一样的,这样我们就可以利用LeanCloud来搭建我们一个小应用的数据表了。

Vue-eventBus

Vue-eventBus

父子组件通信与兄弟组件通信

vue组件非常常见的有父子组件通信,兄弟组件通信。

父子组件通信:方法有很多,比如:父组件通过 props 向下传数据给子组件,子组件通过 $emit 告诉父组件。
兄弟组件通信:如果两个页面没有任何引入和被引入关系,需要额外的组件来通信,如:事件总线、Vuex。

一、事件总线是什么

EventBus 又称为事件总线。在Vue中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件。

EventBus若使用不慎,就会造成难以维护的“灾难”,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。 

优点

解决了多层组件之间繁琐的事件传播。
使用原理十分简单,代码量少

缺点

大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。
如果业务有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus在项目中的关系。通常在vue页面销毁时,同时移除EventBus事件监听。

由于是都使用一个Vue实例,所以容易出现重复触发的情景:两个页面都定义了同一个事件名,并且没有用$off销毁(常出现在路由切换时)。

二、使用

1.创建事件

首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它。

方法1、非全局事件组件

新建EventBus.js

import Vue from 'vue'
export const EventBus = new Vue()

法2、全局事件组件

在项目中的 main.js 初始化 EventBus。在main.js添加如下一行:

Vue.prototype.$EventBus = new Vue()

示例:

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

Vue.prototype.$EventBus = new Vue();

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

组件发送事件:this.$EventBus.$emit(…)

组件接收事件:this.$EventBus.$on(…)

移除事件

一般在销毁组件(也就是离开组件)时移除事件。

beforeDestroy(){
  EventBus.$off("eventName")
}

EventBus.$off() //移除EventBus所有事件监听器

EventBus.$off(‘eventName’) //移除’eventName’事件所有监听器

EventBus.$off(‘eventName’, callback) //只移除这个回调的监听器。

2.事件运用

本文父组件:CompA.vue,子组件1:ChildOne.vue,子组件2:ChildTwo.vue。子组件1发送事件给父组件和子组件2。

router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import CompA from "@/components/CompA";

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/compA',
      name: 'compA',
      component: CompA,
    }
  ],
})

1.创建EventBus

新建EventBus.js

import Vue from 'vue'
export const EventBus = new Vue()

2.发送事件

components/ChildOne.vue

<template>
  <div class="childOne">
    <h1>childOne</h1>
    <button @click="sendEvent2CompA">发送事件给父组件(CompA)</button>
    <button @click="sendEvent2ChildTwo">发送事件给兄弟组件(ChildTwo)</button>
  </div>
</template>

<script>
import {EventBus} from "./EventBus"

export default {
  data() {
    return {
      count1: 0,
      count2: 0,
    }
  },
  methods:{
    sendEvent2CompA() {
      this.count1++;
      EventBus.$emit("compA", "compA事件触发次数:" + this.count1)
    },
    sendEvent2ChildTwo() {
      this.count2++;
      EventBus.$emit("childTwo", "childTwo事件触发次数:" + this.count2)
    }
  }
}
</script>

<style scoped>
</style>

3.接收事件

父组件:components/CompA.vue

<template>
  <div class="compA">
    <h1>compA</h1>
    compA收到的事件内容:{{msg}}<hr>
    <child-one></child-one><hr>
    <child-two></child-two>

  </div>
</template>

<script>
import {EventBus} from "./EventBus"
import ChildOne from "@/components/ChildOne";
import ChildTwo from "@/components/ChildTwo";

export default {
  components: {ChildOne, ChildTwo},
  data() {
    return {
      msg: "",
    }
  },
  mounted() {
    EventBus.$on("compA", (payload1)=> {
      this.msg = payload1;
    })
  }
}
</script>

<style scoped>

</style>

子组件2:components/ChildTwo.vue

<template>
  <div class="childTwo">
    <h1>childTwo</h1>
    childTwo收到的事件内容:{{msg}}
  </div>
</template>

<script>
import {EventBus} from "./EventBus"

export default {
  data() {
    return {
      msg: "",
    }
  },
  mounted() {
    EventBus.$on("childTwo", (payload1)=> {
      this.msg = payload1;
    })
  }
}
</script>

<style scoped>

</style>

事物都有两面性,没有好坏之分,且全在于使用者,好钢用在刀刃上,不滥用即可。有其他更好的方式则优先使用。

uniapp生命周期

uniapp生命周期

一、应用生命周期

uni-app 支持如下应用生命周期函数:

这几个函数主要还是在App.vue这个文件进行定义,注意:

应用生命周期仅可在App.vue中监听,在其它页面监听无效。
onlaunch里进行页面跳转

onPageNotFound 页面实际上已经打开了(比如通过分享卡片、小程序码)且发现页面不存在,才会触发,api 跳转不存在的页面不会触发(如 uni.navigateTo)

二、页面生命周期

uni-app 支持如下页面生命周期函数:

函数名 说明 平台差异说明 最低版本
onInit 监听页面初始化,其参数同 onLoad 参数,为上个页面传递的数据,参数类型为 Object(用于页面传参),触发时机早于 onLoad 百度小程序 3.1.0+
onLoad 监听页面加载,其参数为上个页面传递的数据,参数类型为 Object(用于页面传参),参考示例
onShow 监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面
onReady 监听页面初次渲染完成。注意如果渲染速度快,会在页面进入动画完成前触发
onHide 监听页面隐藏
onUnload 监听页面卸载
onResize 监听窗口尺寸变化 App、微信小程序、快手小程序
onPullDownRefresh 监听用户下拉动作,一般用于下拉刷新,参考示例
onReachBottom 页面滚动到底部的事件(不是scroll-view滚到底),常用于下拉下一页数据。具体见下方注意事项
onTabItemTap 点击 tab 时触发,参数为Object,具体见下方注意事项 微信小程序、QQ小程序、支付宝小程序、百度小程序、H5、App、快手小程序、京东小程序
onShareAppMessage 用户点击右上角分享 微信小程序、QQ小程序、支付宝小程序、字节小程序、飞书小程序、快手小程序、京东小程序
onPageScroll 监听页面滚动,参数为Object nvue暂不支持
onNavigationBarButtonTap 监听原生标题栏按钮点击事件,参数为Object App、H5
onBackPress 监听页面返回,返回 event = {from:backbutton、 navigateBack} ,backbutton 表示来源是左上角返回按钮或 android 返回键;navigateBack表示来源是 uni.navigateBack ;详细说明及使用:onBackPress 详解。支付宝小程序只有真机能触发,只能监听非navigateBack引起的返回,不可阻止默认行为。 app、H5、支付宝小程序
onNavigationBarSearchInputChanged 监听原生标题栏搜索输入框输入内容变化事件 App、H5 1.6.0
onNavigationBarSearchInputConfirmed 监听原生标题栏搜索输入框搜索事件,用户点击软键盘上的“搜索”按钮时触发。 App、H5 1.6.0
onNavigationBarSearchInputClicked 监听原生标题栏搜索输入框点击事件(pages.json 中的 searchInput 配置 disabled 为 true 时才会触发) App、H5 1.6.0
onShareTimeline 监听用户点击右上角转发到朋友圈 微信小程序 2.8.1+
onAddToFavorites 监听用户点击右上角收藏 微信小程序、QQ小程序 2.8.1+

onInit使用注意

  • 仅百度小程序基础库 3.260 以上支持 onInit 生命周期
  • 其他版本或平台可以同时使用 onLoad 生命周期进行兼容,注意避免重复执行相同逻辑
  • 不依赖页面传参的逻辑可以直接使用 created 生命周期替代

onReachBottom使用注意 可在pages.json里定义具体页面底部的触发距离onReachBottomDistance,比如设为50,那么滚动页面到距离底部50px时,就会触发onReachBottom事件。

如使用scroll-view导致页面没有滚动,则触底事件不会被触发。scroll-view滚动到底部的事件请参考scroll-view的文档

onPageScroll (监听滚动、滚动监听、滚动事件)参数说明:

属性 类型 说明
scrollTop Number 页面在垂直方向已滚动的距离(单位px)

注意

  • onPageScroll里不要写交互复杂的js,比如频繁修改页面。因为这个生命周期是在渲染层触发的,在非h5端,js是在逻辑层执行的,两层之间通信是有损耗的。如果在滚动过程中,频发触发两层之间的数据交换,可能会造成卡顿。
  • 如果想实现滚动时标题栏透明渐变,在App和H5下,可在pages.json中配置titleNView下的type为transparent,参考
  • 如果需要滚动吸顶固定某些元素,推荐使用css的粘性布局,参考插件市场。插件市场也有其他js实现的吸顶插件,但性能不佳,需要时可自行搜索。
  • 在App、微信小程序、H5中,也可以使用wxs监听滚动,参考;在app-nvue中,可以使用bindingx监听滚动,参考
  • onBackPress上不可使用async,会导致无法阻止默认返回
onPageScroll : function(e) { //nvue暂不支持滚动监听,可用bindingx代替
    console.log("滚动距离为:" + e.scrollTop);
},

onTabItemTap 返回的json对象说明:

属性 类型 说明
index Number 被点击tabItem的序号,从0开始
pagePath String 被点击tabItem的页面路径
text String 被点击tabItem的按钮文字

注意

  • onTabItemTap常用于点击当前tabitem,滚动或刷新当前页面。如果是点击不同的tabitem,一定会触发页面切换。
  • 如果想在App端实现点击某个tabitem不跳转页面,不能使用onTabItemTap,可以使用plus.nativeObj.view放一个区块盖住原先的tabitem,并拦截点击事件。
  • 支付宝小程序平台onTabItemTap表现为点击非当前tabitem后触发,因此不能用于实现点击返回顶部这种操作
onTabItemTap : function(e) {
    console.log(e);
    // e的返回格式为json对象: {"index":0,"text":"首页","pagePath":"pages/index/index"}
},

onNavigationBarButtonTap 参数说明:

属性 类型 说明
index Number 原生标题栏按钮数组的下标
onNavigationBarButtonTap : function (e) {
    console.log(e);
    // e的返回格式为json对象:{"text":"测试","index":0}
}

onBackPress 回调参数对象说明:

属性 类型 说明
from String 触发返回行为的来源:’backbutton’——左上角导航栏按钮及安卓返回键;’navigateBack’——uni.navigateBack() 方法。支付宝小程序端不支持返回此字段
export default {
    data() {
        return {};
    },
    onBackPress(options) {
        console.log('from:' + options.from)
    }
}

注意

  • nvue 页面weex编译模式支持的生命周期同weex,具体参考:weex生命周期介绍
  • 支付宝小程序真机可以监听到非navigateBack引发的返回事件(使用小程序开发工具时不会触发onBackPress),不可以阻止默认返回行为

三、组件生命周期

uni-app 组件支持的生命周期,与vue标准组件的生命周期相同。这里没有页面级的onLoad等生命周期:

函数名 说明 平台差异说明 最低版本
beforeCreate 在实例初始化之前被调用。详见
created 在实例创建完成后被立即调用。详见
beforeMount 在挂载开始之前被调用。详见
mounted 挂载到实例上去之后调用。详见 注意:此处并不能确定子组件被全部挂载,如果需要子组件完全挂载之后在执行操作可以使用$nextTickVue官方文档
beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前。详见 仅H5平台支持
updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。详见 仅H5平台支持
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。详见
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。详见

参考链接:

[uniapp]: “https://zh.uniapp.dcloud.io/tutorial/page.html#lifecycle

  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信