前端知识点整理(三)不定时更新~

目录

一、移动端跨平台开发方案

Hybrid App

React Native

Weex

Flutter 

PWA (Progressive Web App)

小程序

Cordova

html5

组件和模块的区别

组件化

模块化

前端代码规范

前端工程化理解

网站性能监测与优化策略

1.网络传输性能优化

 页面加载时,大致可以分为以下几个步骤:

1.1.浏览器缓存

1.2.资源打包压缩

1.3.图片资源优化

1.4.网络传输性能检测工具——Page Speed

1.5.使用CDN

2.页面渲染性能优化

2.1.浏览器渲染过程(Webkit)

浏览器渲染的流程如下:

2.2.DOM渲染层与GPU硬件加速

2.3.重排与重绘

常见引起重排属性和方法

浏览器的渲染队列:

强制刷新队列:

重排优化建议

1. 分离读写操作

2. 样式集中改变

3. 缓存布局信息

4. 离线改变dom

5. position属性为absolute或fixed

6. 优化动画

7 . visibility: hidden 减小重绘的压力

8.压缩DOM的深度

9.图片在渲染前指定大小

3.JS阻塞性能

js方面的性能优化小建议

4.【拓展】负载均衡

4.2.pm2实现Node.js“多进程”

4.3.nginx搭建反向代理

RESTful架构

资源(Resources)

表现层(Representation)

状态转化(State Transfer)

综述

误区


一、移动端跨平台开发方案

Hybrid App

我们可以把纯前端技术开发的 Web 网页称为 Web App纯原生技术开发的应用称为 Native App,它们各有优缺点:纯 Native 的迭代太慢,不能动态更新,且不能跨平台,而纯 Web 则有很多功能无法实现,虽然其动画效果体验差强人意,但跟原生比还是有些差距。

Hybrid App(混合应用)是指介于这两者之间的 App,兼具“Native App 良好用户交互体验的优势”和“Web App 跨平台开发的优势”。

Hybrid 的技术本质是在 WebView 的基础上,与原生客户端建立 JS Bridge 桥接,以达到 JS 调用 Native API Native 执行 JS 方法的目的。

React Native

Weex

Flutter 

PWA (Progressive Web App)

 参考:https://juejin.im/post/5b076e3af265da0dce48fe95

小程序

小程序开发本质上还是前端 HTML + CSS + JS 那一套逻辑,它基于 WebView 和微信自己定义的一套 JS/WXML/WXSS/JSON 来开发和渲染页面。微信官方文档里提到,小程序运行在三端:iOS、Android 和用于调试的开发者工具,三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:

  • 在 iOS 上,小程序的 JavaScript 代码是运行在 JavaScriptCore 中,是由 WKWebView 来渲染的,环境有 iOS 8+;
  • 在 Android 上,小程序的 JavaScript 代码是通过 X5 JSCore 来解析,是由 X5 基于 Mobile Chrome 53/57 内核来渲染的;
  • 在 开发工具上, 小程序的 JavaScript 代码是运行在 nwjs 中,是由 Chrome Webview 来渲染的。

更多细节请查阅微信小程序官网:

支付宝后来也上线了自己的小程序平台:

因为微信或支付宝都可以在 Android 和 iOS 上运行,所以某种意义上,我们也可以把小程序理解为是一种跨平台开发

Cordova

html5

HTML5 是定义 HTML 标准的最新的版本。 该术语通过两个不同的概念来表现:

  • 它是一个新版本的HTML语言,具有新的元素,属性和行为,
  • 它有更大的技术集,允许构建更多样化和更强大的网站和应用程序。这个集合有时称为HTML5和它的朋友们,不过大多数时候仅缩写为一个词 HTML5

根据其功能分为几个组:

  • 语义:能够让你更恰当地描述你的内容是什么。
  • 连通性:能够让你和服务器之间通过创新的新技术方法进行通信。
  • 离线 & 存储:能够让网页在客户端本地存储数据以及更高效地离线运行。
  • 多媒体:使 video 和 audio 成为了在所有 Web 中的一等公民。
  • 2D/3D 绘图 & 效果:提供了一个更加分化范围的呈现选择。
  • 性能 & 集成:提供了非常显著的性能优化和更有效的计算机硬件使用。
  • 设备访问 Device Access:能够处理各种输入和输出设备。
  • 样式设计: 让作者们来创作更加复杂的主题吧!

参考:https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/HTML5

组件和模块的区别

组件化

含义:就是"基础库"或者“基础组件",意思是把代码重复的部分提炼出一个个组件供给功能使用。

使用:Dialog,各种自定义的UI控件、能在项目或者不同项目重复应用的代码等等。

目的:复用,解耦。

依赖:组件之间低依赖,比较独立。

架构定位:纵向分层(位于架构底层,被其他层所依赖)。

总结:组件相当于,把一些能在项目里或者不同类型项目中可复用的代码进行工具性的封装。

模块化

含义:就是"业务框架"或者“业务模块",也可以理解为“框架”,意思是把功能进行划分,将同一类型的代码整合在一起,所以模块的功能相对复杂,但都同属于一个业务。

使用:按照项目功能需求划分成不同类型的业务框架(例如:注册、登录、外卖、直播.....)

目的:隔离/封装 (高内聚)。

依赖:模块之间有依赖的关系,可通过路由器进行模块之间的耦合问题。

架构定位:横向分块(位于架构业务框架层)。

总结:模块相应于业务逻辑模块,把同一类型项目里的功能逻辑进行进行需求性的封装。

参考:https://www.jianshu.com/p/cac0beae8876

前端代码规范

参考:https://guide.aotu.io/docs/html/code.html

CommonJs与ES6的模块加载机制

目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。

ES6模块

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface) 

浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

<script type="module" src="./foo.js"></script>

上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

对于外部的模块脚本(上例是foo.js),有几点需要注意。

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  • 同一个模块如果加载多次,将只执行一次。

 ES6 模块与 CommonJS 模块的差异

讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。

它们有两个重大差异。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

下面重点解释第一个差异。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

不同的脚本加载相同模块,得到的都是同一个实例。

Node.js 加载

Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"

如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module

{
   "type": "module"
}

一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。

# 解释成 ES6 模块
$ node my-app.js

如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。

参考:https://es6.ruanyifeng.com/#docs/module-loader

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

前端工程化理解

前端工程化本质上就是将前端开发流程,标准化、规范化、工具化、自动化、简单化。其主要目的为了提高效率和降低成本,即提高开发过程中的开发效率,减少不必要的重复工作时间。提高编码、测试、维护阶段的生产效率。让前端开发自成体系

参考:https://juejin.im/post/5e999cecf265da47cd357a24

网站性能监测与优化策略

从性能优化的三大方面工作逐步展开介绍,其中包括网络传输性能、页面渲染性能以及JS阻塞性能,系统性地带着读者们体验性能优化的实践流程。

1.网络传输性能优化

Resource Timing API提供了有关每个资产接收时间的详细信息。请求生命周期的主要阶段是:

  • 重新导向
    • 立即开始startTime
    • 如果发生重定向,则也将redirectStart开始。
    • 如果在此阶段结束时发生重定向,redirectEnd则将采取重定向。
  • 应用缓存(观察网页加载顺序,第一个都是document文件先加载,之后查看document里需要哪些文件,如果缓存有则不再重复请求,所以是否可将“应用缓存”放在“处理响应文件”那一步)
    • 如果应用缓存满足了请求,fetchStart则将花费一些时间。
  • 域名解析
    • domainLookupStart 时间是在DNS请求开始时开始的。
    • domainLookupEnd 时间是在DNS请求结束时开始的。
  • TCP协议
    • connectStart 最初连接服务器时采用。
    • 如果使用TLS或SSL,secureConnectionStart则将在握手开始以确保连接安全时开始。
    • connectEnd 与服务器的连接完成后进行。
  • 请求
    • requestStart 对资源的请求已发送到服务器后,将采用此命令。
  • 响应
    • responseStart 是服务器最初响应请求的时间。
    • responseEnd 是请求结束并检索数据的时间。
  • 处理响应文件
    • domLoading 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
    • domInteractive 表示浏览器已经解析好dom树了,当前网页DOM结构结束解析、开始加载内嵌资源
    • domContentLoadedEventStart 开始触发DomContentLoaded事件,DOM 解析完成后,网页内资源加载开始的时间
    • domContentLoadedEnd DomContentLoaded事件结束,DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)
    • domComplete DOM 树解析渲染完成,且资源也准备就绪的时间(包括图像、脚本文件、CSS 文件等),Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
    • loadEventStart load 事件发送给文档,也即 load 回调函数开始执行的时间
    • loadEventEnd load 事件的回调函数执行完毕的时间

参考:http://www.alloyteam.com/2015/09/explore-performance/

这是navigation timing监测指标图,从图中我们可以看出,浏览器在得到用户请求之后,经历了下面这些阶段:重定向→拉取缓存→DNS查询→建立TCP链接→发起请求→接收响应→处理HTML元素→元素加载完成。

例如发送一个请求,会经过如下:

排队

排队的请求表明:

  • 该请求被渲染引擎推迟了,因为它被认为比关键资源(例如脚本/样式)的优先级低。这通常发生在图像上。
  • 该请求被暂停以等待将要释放的不可用的TCP套接字。
  • 由于浏览器在HTTP 1上每个源仅允许六个TCP连接,因此该请求被暂停。
  • 花在制作磁盘缓存条目上的时间(通常非常快。)

 堵转/堵转

请求等待发送之前花费的时间。它可能正在等待队列中描述的任何原因。此外,此时间包括在代理协商中花费的任何时间。

 代理协商

与代理服务器连接进行协商所花费的时间。

DNS查询

执行DNS查找所花费的时间。页面上的每个新域都需要完整的往返来进行DNS查找。

 初始连接/连接

建立连接所花费的时间,包括TCP握手/重试和协商SSL。

 SSL协议

完成SSL握手所花费的时间。

 请求发送/发送

发出网络请求所花费的时间。通常为一毫秒的时间。

等待中(TTFB)

等待初始响应所花费的时间,也称为第一个字节的时间。除了等待服务器传递响应所花费的时间之外,该时间还捕获了到服务器的往返延迟。

 内容下载/下载

接收响应数据所花费的时间。

参考:https://developers.google.cn/web/tools/chrome-devtools/network/understanding-resource-timing

 页面加载时,大致可以分为以下几个步骤:

  1.   开始解析HTML文档结构
  2.   加载外部样式表及JavaScript脚本
  3.   解析执行JavaScript脚本
  4.   DOM树渲染完成
  5.   加载未完成的外部资源(如 图片)
  6.        页面加载成功

1.1.浏览器缓存

我们都知道,浏览器在向服务器发起请求前,会先查询本地是否有相同的文件,如果有,就会直接拉取本地缓存,这和我们在后台部属的Redis、Memcache类似,都是起到了中间缓冲的作用,我们先看看浏览器处理缓存的策略:

浏览器默认的缓存是放在内存内的,但我们知道,内存里的缓存会因为进程的结束或者说浏览器的关闭而被清除,而存在硬盘里的缓存才能够被长期保留下去。很多时候,我们在network面板中各请求的size项里,会看到两种不同的状态:from memory cachefrom disk cache,前者指缓存来自内存,后者指缓存来自硬盘。而控制缓存存放位置的,不是别人,就是我们在服务器上设置的Etag字段。在浏览器接收到服务器响应后,会检测响应头部(Header),如果有Etag字段,那么浏览器就会将本次缓存写入硬盘中。

之所以拉取缓存会出现200、304两种不同的状态码,取决于浏览器是否有向服务器发起验证请求。 只有向服务器发起验证请求并确认缓存未被更新,才会返回304状态码。

【!!!特别注意!!!】在我们配置缓存时一定要切记,浏览器在处理用户请求时,如果命中强缓存,浏览器会直接拉取本地缓存,不会与服务器发生任何通信,也就是说,如果我们在服务器端更新了文件,并不会被浏览器得知,就无法替换失效的缓存。所以我们在构建阶段,需要为我们的静态资源添加md5 hash后缀,避免资源更新而引起的前后端文件无法同步的问题。

1.2.资源打包压缩

我们之前所作的浏览器缓存工作,只有在用户第二次访问我们的页面才能起到效果,如果要在用户首次打开页面就实现优良的性能,必须对资源进行优化。我们常将网络性能优化措施归结为三大方面:减少请求数、减小请求资源体积、提升网络传输速率。现在,让我们逐个击破:

结合前端工程化思想,我们在对上线文件进行自动化打包编译时,通常都需要打包工具的协助,这里我推荐webpack,我通常都使用Gulp和Grunt来编译node

在对webpack进行上线配置时,我们要特别注意以下几点:

①JS压缩:(这点应该算是耳熟能详了,就不多介绍了)

②HTML压缩

③提取公共资源

④提取css并压缩

⑤使用webpack3的新特性:ModuleConcatenationPlugin

⑥在服务器上开启Gzip传输压缩,它能将我们的文本类文件体积压缩至原先的四分之一,效果立竿见影

如果你在网站请求的响应头里看到这样的字段,那么就说明咱们的Gzip压缩配置成功啦:

 

【!!!特别注意!!!】不要对图片文件进行Gzip压缩!不要对图片文件进行Gzip压缩!不要对图片文件进行Gzip压缩!我只会告诉你效果适得其反,至于具体原因,还得考虑服务器压缩过程中的CPU占用还有压缩率等指标,对图片进行压缩不但会占用后台大量资源,压缩效果其实并不可观,可以说是“弊大于利”,所以请在gzip_types 把图片的相关项去掉。针对图片的相关处理,我们接下来会更加具体地介绍。

1.3.图片资源优化

刚刚我们介绍了资源打包压缩,只是停留在了代码层面,而在我们实际开发中,真正占用了大量网络传输资源的,并不是这些文件,而是图片,如果你对图片进行了优化工作,你能立刻看见明显的效果。

1.3.1.不要在HTML里缩放图像

需要用多大的图片时,就在服务器上准备好多大的图片,尽量固定图片尺寸。

1.3.2.使用雪碧图(CSS Sprite)

这里给大家推荐一个自动化生成雪碧图的工具:www.toptal.com/developers/… 

1.3.3.使用字体图标(iconfont)

阿里矢量图标库(网址:www.iconfont.cn/ ),图片能做的很多事情,矢量图都能作,而且它只是往HTML里插入字符和CSS样式而已,和图片请求比起来,在网络传输资源的占用上它们完全不在一个数量级,如果你的项目里有大量的小图标,就用矢量图吧。

1.3.4.使用WebP

WebP格式,是谷歌公司开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器带宽资源和数据空间。Facebook、Ebay等知名网站已经开始测试并使用WebP格式。

我们可以使用官网提供的Linux命令行工具对项目中的图片进行WebP编码,也可以使用我们的线上服务,这里我推荐叉拍云(网址:www.upyun.com/webp )。但是在实际的上线工作中,我们还是得编写Shell脚本使用命令行工具进行批量编码,不过测试阶段我们用线上服务就足够了,方便快捷。

1.4.网络传输性能检测工具——Page Speed

谷歌监测网络传输性能的插件——PageSpeed Insights for Chrome

Page Speed最人性化的地方,便是它会对测试网站的性能瓶颈提出完整的建议,我们可以根据它的提示进行优化工作。这里我的网站已经优化到最好指标了(•́⌄•́๑)૭✧,Page Speed Score表示你的性能测试得分,100/100表示已经没有需要优化的地方。

1.5.使用CDN

再好的性能优化实例,也必须在CDN的支撑下才能到达极致。

当然,凭着我们单个人的资金实力(除非你是王思聪)是必定搭建不起来CDN的,不过我们可以使用各大企业提供的服务,诸如腾讯云等,配置也十分简单,这里就请大家自行去推敲啦。

其实我们的CDN域名一般是和我们的网站主域名不同的,大家可以看看淘宝、腾讯的官方网站,看看他们存放静态资源的CDN域名,都是和主域名不一样的。为什么要这么做?主要有两个原因:[内容摘选自:bbs.aliyun.com/simple/t116… ]

①便于CDN业务独立,能够独立配置缓存。为了降低web压力,CDN系统会遵循Cache-Control和Expires HTTP头标准对改请求返回的内容进行缓存,便于后面的请求不在回源,起到加速功能。而传统CDN(Web与CDN共用域名)的方式,需要对不同类型的文件设置相应的Cache规则或者遵循后端的HTTP头,但这样难以发挥CDN的最大优势,因为动态请求回源的概率非常之大,如果访客与源站的线路并不慢,通过CDN的请求未必快于直接请求源站的。 大型网站为了提升web性能到极致,通常缓存头设置比较大,像谷歌JS设置一年缓存,百度首页logo设置十年缓存,如果将静态元素抽取出来,就可以很方便的对所有静态元素部署规则,而不用考虑动态请求。减少规则的条数可以提升CDN的效率。

②抛开无用cookie,减小带宽占用。我们都知道HTTP协议每次发送请求都会自动带上该域名及父级域名下的cookie,但对于CSS,JS还有图片资源,这些cookie是没用的,反而会浪费访客带宽和服务器入带宽。而我们的主站,为了保持会话或者做其他缓存,都会存放着大量的cookie,所以如果将CDN与主站域名分离,就能解决这一问题。

不过这样一来,新的问题就出现了:CDN域名与主站域名不同,DNS解析CDN域名还需要花费额外的时间,增加网络延迟。不过这难不住我们伟大的程序员前辈,DNS Prefetch闪亮登场。

如果大家翻看大型网站的HTML源代码,都会在头部发现这样的link链接:(这里以淘宝首页为例)

这就是DNS Prefetch。DNS Prefetch是一种DNS预解析技术,当我们浏览网页时,浏览器会在加载网页时对网页中的域名进行预解析并缓存,这样在浏览器加载网页中的链接时,就无需进行DNS解析,减少用户的等待时间,提高用户体验。DNS Prefetch现已被主流浏览器支持,大多数浏览器针对DNS解析都进行了优化,典型的一次DNS解析会耗费20~120ms,减少DNS解析时间和次数是个很好的优化措施。

大公司的静态资源优化方案,基本上要实现这么几个东西:

  1. 配置超长时间的本地缓存 —— 节省带宽,提高性能(强制浏览器使用本地缓存(cache-control/expires),不要和服务器通信。)
  2. 采用内容摘要作为缓存更新依据 —— 精确的缓存控制
  3. 静态资源CDN部署 —— 优化网络请求(静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上)
  4. 更资源发布路径实现非覆盖式发布 —— 平滑升级

参考:https://www.zhihu.com/question/20790576/answer/32602154

2.页面渲染性能优化

2.1.浏览器渲染过程(Webkit)

 

浏览器渲染的流程如下:

  1. HTML被HTML解析器解析成DOM 树
  2. css则被css解析器解析成CSSOM 树
  3. 结合DOM树和CSSOM树,生成一棵渲染树(Render Tree)
  4. 生成布局(flow),即将所有渲染树的所有节点进行平面合成
  5. 将布局绘制(paint)在屏幕上

第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染

网页生成的时候,至少会渲染一次

在用户访问的过程中,还会不断重新渲染

重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)。

注意:

  • css加载不会阻塞DOM树的解析

  • css加载会阻塞DOM树的渲染,由于Render Tree是依赖于DOM Tree和CSSOM Tree的,所以他必须等待到CSSOM Tree构建完成,也就是CSS资源加载完成(或者CSS资源加载失败)后,才能开始渲染。因此,CSS加载是会阻塞Dom的渲染的。

  • css加载会阻塞后面js语句的执行

 link和@import的区别

结论

就结论而言,强烈建议使用link标签,慎用@import方式。
这样可以避免考虑@import的语法规则和注意事项,避免产生资源文件下载顺序混乱和http请求过多的烦恼。

区别

1.从属关系区别
@import是 CSS 提供的语法规则,只有导入样式表的作用;link是HTML提供的标签,不仅可以加载 CSS 文件,还可以定义 RSS、rel 连接属性等。

2.加载顺序区别
加载页面时,link标签引入的 CSS 被同时加载;@import引入的 CSS 将在页面加载完毕后被加载。

3.兼容性区别
@import是 CSS2.1 才有的语法,故只可在 IE5+ 才能识别;link标签作为 HTML 元素,不存在兼容性问题。

4.DOM可控性区别
可以通过 JS 操作 DOM ,插入link标签来改变样式;由于 DOM 方法是基于文档的,无法使用@import的方式插入样式。

5.权重区别(该项有争议,下文将详解)
link引入的样式权重大于@import引入的样式。

css优化建议:

1. 内联首屏关键CSS(Critical CSS)

Github上有一个项目Critical CSS4,可以将属于首屏的关键样式提取出来,大家可以看一下该项目,结合自己的构建工具进行使用。当然为了保证正确,大家最好再亲自确认下提取出的内容是否有缺失。

2. 文件压缩

相信大家都早已习惯对CSS进行压缩,现在的构建工具,如webpack、gulp/grunt、rollup等也都支持CSS压缩功能。压缩后的文件能够明显减小,可以大大降低了浏览器的加载时间。

3. 去除无用的css

虽然文件压缩能够降低文件大小。但CSS文件压缩通常只会去除无用的空格,这样就限制了CSS文件的压缩比例。那是否还有其他手段来精简CSS呢?答案显然是肯定的,如果压缩后的文件仍然超出了预期的大小,我们可以试着找到并删除代码中无用的CSS

一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。

当然,如果手动删除这些无用CSS是很低效的。我们可以借助Uncss7库来进行。Uncss可以用来移除样式表中的无用CSS,并且支持多文件和JavaScript注入的CSS。

4. 有选择地使用选择器

大多数朋友应该都知道CSS选择器的匹配是从右向左进行的,这一策略导致了不同种类的选择器之间的性能也存在差异。相比于#markdown-content-h3,显然使用#markdown .content h3时,浏览器生成渲染树(render-tree)所要花费的时间更多。

我们在使用选择器时,只需要记住以下几点,其他的可以全凭喜好。

  1. 保持简单,不要使用嵌套过多过于复杂的选择器。
  2. 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。
  3. 不要使用类选择器和ID选择器修饰元素标签,如h3#markdown-content,这样多此一举,还会降低效率。
  4. 不要为了追求速度而放弃可读性与可维护性。

5. 减少使用昂贵的属性

在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。

6. 不要使用@import

参考: https://juejin.im/post/5b6133a351882519d346853f

 

 

浏览器的解释器,是包括在渲染引擎内的,我们常说的Chrome(现在使用的是Blink引擎)和Safari使用的Webkit引擎,Firefox使用的Gecko引擎,指的就是渲染引擎。而在渲染引擎内,还包括着我们的HTML解释器(渲染时用于构造DOM树)、CSS解释器(渲染时用于合成CSS规则)还有我们的JS解释器。不过后来,由于JS的使用越来越重要,工作越来越繁杂,所以JS解释器也渐渐独立出来,成为了单独的JS引擎,就像众所周知的V8引擎,我们经常接触的Node.js也是用的它。

2.2.DOM渲染层与GPU硬件加速

页面的真实样子就是这样,是由多个DOM元素渲染层(Layers)组成的,实际上一个页面在构建完Render Tree之后,是经历了这样的流程才最终呈现在我们面前的:

①浏览器会先获取DOM树并依据样式将其分割成多个独立的渲染层

②CPU将每个层绘制进绘图中

③将位图作为纹理上传至GPU(显卡)绘制

④GPU将所有的渲染层缓存(如果下次上传的渲染层没有发生变化,GPU就不需要对其进行重绘)并复合多个渲染层最终形成我们的图像

从上面的步骤我们可以知道,布局是由CPU处理的,而绘制则是由GPU完成的。

把那些一直发生大量重排重绘的元素提取出来,单独触发一个渲染层,那样这个元素不就不会“连累”其他元素一块重绘了对吧。

那么问题来了,什么情况下会触发渲染层呢?大家只要记住:

Video元素、WebGL、Canvas、CSS3 3D、CSS滤镜、z-index大于某个相邻节点的元素都会触发新的Layer,其实我们最常用的方法,就是给某个元素加上下面的样式:

transform: translateZ(0);
backface-visibility: hidden;

这样就可以触发渲染层啦 。

我们把容易触发重排重绘的元素单独触发渲染层,让它与那些“静态”元素隔离,让GPU分担更多的渲染工作,我们通常把这样的措施成为硬件加速,或者是GPU加速。大家之前肯定听过这个说法,现在完全清楚它的原理了吧。

2.3.重排与重绘

①重排(reflow):渲染层内的元素布局发生修改,都会导致页面重新排列,比如窗口的尺寸发生变化、删除或添加DOM元素,修改了影响元素盒子大小的CSS属性(诸如:width、height、padding)。

②重绘(repaint):绘制,即渲染上色,所有对元素的视觉表现属性的修改,都会引发重绘。

不论是重排还是重绘,都会阻塞浏览器。要提高网页性能,就要降低重排和重绘的频率和成本,近可能少地触发重新渲染。正如我们在2.3中提到的,重排是由CPU处理的,而重绘是由GPU处理的,CPU的处理效率远不及GPU,并且重排一定会引发重绘,而重绘不一定会引发重排。所以在性能优化工作中,我们更应当着重减少重排的发生。

常见引起重排属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子:

  1. 添加或者删除可见的DOM元素;
  2. 元素尺寸改变——边距、填充、边框、宽度和高度
  3. 内容变化,比如用户在input框中输入文字
  4. 浏览器窗口尺寸改变——resize事件发生时
  5. 计算 offsetWidth 和 offsetHeight 属性
  6. 设置 style 属性的值

浏览器的渲染队列:

思考以下代码将会触发几次渲染?

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

根据我们上文的定义,这段代码理论上会触发4次重排+重绘,因为每一次都改变了元素的几何属性,实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制

当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。

强制刷新队列:

div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);

这段代码会触发4次重排+重绘,因为在console中你请求的这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。

因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘

强制刷新队列的style样式请求

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop, scrollLeft, scrollWidth, scrollHeight
  3. clientTop, clientLeft, clientWidth, clientHeight
  4. getComputedStyle(), 或者 IE的 currentStyle

我们在开发中,应该谨慎的使用这些style请求,注意上下文关系,避免一行代码一个重排,这对性能是个巨大的消耗

重排优化建议

就像上文提到的我们要尽可能的减少重排次数、重排范围,这样说很泛,下面是一些行之有效的建议,大家可以参考一下。

1. 分离读写操作

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

还是上面触发4次重排+重绘的代码,这次只触发了一次重排:

在第一个console的时候,浏览器把之前上面四个写操作的渲染队列都给清空了。剩下的console,因为渲染队列本来就是空的,所以并没有触发重排,仅仅拿值而已。最最最客观的解决方案,就是不用JS去操作元素样式,这也是我最推荐的。

2. 样式集中改变

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

虽然现在大部分浏览器有渲染队列优化,不排除有些浏览器以及老版本的浏览器效率仍然低下:

建议通过改变class或者csstext属性集中改变样式

// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";
// good 
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";

3. 缓存布局信息

// bad 强制刷新 触发两次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';

// good 缓存布局信息 相当于读写分离
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';

4. 离线改变dom

  • 隐藏要操作的dom

    在要操作dom之前,通过display隐藏dom,当操作完成之后,才将元素的display属性为可见,因为不可见的元素不会触发重排和重绘。

    dom.display = 'none'
    // 修改dom样式
    dom.display = 'block'
    
  • 通过使用DocumentFragment创建一个dom碎片,在它上面批量操作dom,操作完成之后,再添加到文档中,这样只会触发一次重排。

  • 复制节点,在副本上工作,然后替换它!

5. position属性为absolute或fixed

position属性为absolute或fixed的元素,重排开销比较小,不用考虑它对其他元素的影响

6. 优化动画

  • 可以把动画效果应用到position属性为absolute或fixed的元素上,这样对其他元素影响较小

    动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:

    比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。

  • 启用GPPU加速

    此部分来自优化CSS重排重绘与浏览器性能

    GPU(图像加速器):

    GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。

    GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。

    (这项策略需要慎用,得着重考量以牺牲GPU占用率为代价能否换来可期的性能优化,毕竟页面中存在太多的渲染层对于GPU而言也是一种不必要的压力,通常情况下,我们会对动画元素采取硬件加速。)

    /*
     * 根据上面的结论
     * 将 2d transform 换成 3d
     * 就可以强制开启 GPU 加速
     * 提高动画性能
     */
    div {
      transform: translate3d(10px, 10px, 0);
    }

7 . visibility: hidden 减小重绘的压力

将没用的元素设为不可见:visibility: hidden,这样可以减小重绘的压力,必要的时候再将元素显示。

8.压缩DOM的深度

一个渲染层内不要有过深的子元素,少用DOM完成页面样式,多使用伪元素或者box-shadow取代。

9.图片在渲染前指定大小

因为img元素是内联元素,所以在加载图片后会改变宽高,严重的情况会导致整个页面重排,所以最好在渲染前就指定其大小,或者让其脱离文档流。

参考:https://juejin.im/post/5c15f797f265da61141c7f86

3.JS阻塞性能

脚本带来的问题就是他会阻塞页面的平行下载,还会提高进程的CPU占用率。更有甚者,现在node.js已经在前端开发中普及,稍有不慎,我们引发了内存泄漏,或者在代码中误写了死循环,会直接造成我们的服务器崩溃。

在编程的过程中,如果我们使用了闭包后未将相关资源加以释放,或者引用了外链后未将其置空(比如给某DOM元素绑定了事件回调,后来却remove了该元素),都会造成内存泄漏的情况发生,进而大量占用用户的CPU,造成卡顿或死机。我们可以使用chrome提供的JavaScript Profile版块,开启方式同Layers等版块

其实浏览器强大的内存回收机制在大多数时候避免了这一情况的发生,即便用户发生了死机,他只要结束相关进程(或关闭浏览器)就可以解决这一问题,但我们要知道,同样的情况还会发生在我们的服务器端,也就是我们的node中,严重的情况,会直接造成我们的服务器宕机,网站崩溃。所以更多时候,我们都使用JavaScript Profile版块来对我们的node服务进行压力测试,搭配node-inspector 插件,我们能更有效地检测JS执行时各函数的CPU占用率,针对性地进行优化。

(PS:所以没修炼到一定水平,千万别在服务端使用闭包,一个是真没啥用,我们会有更多优良的解决办法,二是真的很容易内存泄漏,造成的后果是你无法预期的)

js方面的性能优化小建议

①不要使用 with() 语句

和函数类似 ,with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度,由于额外的作用域链的查找,在with语句中执行的代码肯定会比外面执行的代码要慢,在能不使用with语句的时候尽量不要使用with语句。

②避免全局查找

在一个函数中会用到全局对象存储为局部变量来减少全局查找,因为访问局部变量的速度要比访问全局变量的速度更快些

function search() {
            //当我要使用当前页面地址和主机域名
            alert(window.location.href + window.location.host);
        }
        //最好的方式是如下这样  先用一个简单变量保存起来
        function search() {
            var location = window.location;
            alert(location.href + location.host);
        }

③通过模板元素clone,替代createElement

如果文档中存在现成的样板节点,应该是用cloneNode()方法,因为使用createElement()方法之后,你需要设置多次元素的属性,使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多元素,应该先准备一个样板节点。

④在循环时将控制条件和控制变量合并起来

提到性能,在循环中需要避免的工作一直是个热门话题,因为循环会被重复执行很多次。所以如果有性能优化的需求,先对循环开刀有可能会获得最明显的性能提升。

一种优化循环的方法是在定义循环的时候,将控制条件和控制变量合并起来,下面是一个没有将他们合并起来的例子:

for ( var x = 0; x < 10; x++ ) {
};

当我们要添加什么东西到这个循环之前,我们发现有几个操作在每次迭代都会出现。JavaScript引擎需要:

#1:检查 x 是否存在
#2:检查 x 是否小于 0 <span style="color: #888888;">(这里可能有笔误)</span>
#3:使 x 增加 1

然而如果你只是迭代元素中的一些元素,那么你可以使用while循环进行轮转来替代上面这种操作:

var x = 9;
do { } while( x-- );

⑤注意NodeList

最小化访问NodeList的次数可以极大的改进脚本的性能

 var images = document.getElementsByTagName('img');
        for (var i = 0, len = images.length; i < len; i++) {

        }

编写JavaScript的时候一定要知道何时返回NodeList对象,这样可以最小化对它们的访问

  1. 进行了对getElementsByTagName()的调用
  2. 获取了元素的childNodes属性
  3. 获取了元素的attributes属性
  4. 访问了特殊的集合,如document.forms、document.images等等

要了解了当使用NodeList对象时,合理使用会极大的提升代码执行速度

⑥避免与null进行比较

由于JavaScript是弱类型的,所以它不会做任何的自动类型检查,所以如果看到与null进行比较的代码,尝试使用以下技术替换:

  1. 如果值应为一个引用类型,使用instanceof操作符检查其构造函数
  2. 如果值应为一个基本类型,作用typeof检查其类型
  3. 如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上
  4. Object.prototype.toString方法检测对象类型

可以通过 toString() 来获取每个对象的类型。为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg。 

这里写图片描述

var toString = Object.prototype.toString;

toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(Math); // [object Math]
toString.call('hahah'); // [object String]

//Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]

⑦循环引用

⑧释放javascript对象

在rich应用中,随着实例化对象数量的增加,内存消耗会越来越大。所以应当及时释放对对象的引用,让GC能够回收这些内存控件。

对象:obj = null

对象属性:delete obj.myproperty

数组item:使用数组的splice方法释放数组中不用的item

⑨switch语句相对if较快

通过将case语句按照最可能到最不可能的顺序进行组织

 扩展:你不知道的Node.js性能优化Redis【入门】就这一篇!Redis的优势和特点

前端性能优化 24 条建议(2020)

4.【拓展】负载均衡

我们都知道node的核心是事件驱动,通过Event Loop去异步处理用户请求,相比于传统的后端服务,它们都是将用户的每个请求分配一个进程进行处理,推荐大家去看这样一篇博文:mp.weixin.qq.com/s?__biz=MzA… 。特别生动地讲解了事件驱动的运行机制,通俗易懂。事件驱动的最大优势是什么?就是在高并发IO时,不会造成堵塞,对于直播类网站,这点是至关重要的,我们有成功的先例——快手,快手强大的IO高并发究其本质一定能追溯到node。

其实现在的企业级网站,都会搭建一层node作为中间层。大概的网站框架如图所示:

 

 

4.2.pm2实现Node.js“多进程”

我们都知道node的优劣,这里分享一份链接,找了挺久写的还算详细:www.zhihu.com/question/19… 。其实很多都是老套路,那些说node不行的都是指着node是单进程这一个软肋开撕,告诉你,我们有解决方案了——pm2。这是它的官网:pm2.keymetrics.io/ 。它是一款node.js进程管理器,具体的功能,就是能在你的计算机里的每一个内核都启动一个node.js服务,也就是说如果你的电脑或者服务器是多核处理器(现在也少见单核了吧),它就能启动多个node.js服务,并且它能够自动控制负载均衡,会自动将用户的请求分发至压力小的服务进程上处理。听起来这东西简直就是神器啊!而且它的功能远远不止这些,这里我就不作过多介绍了,大家知道我们在上线的时候需要用到它就行了,安装的方法也很简单,直接用npm下到全局就可以了$ npm i pm2 -g具体的使用方法还有相关特性可以参照官网。

下面是pm2启动后的效果图:

 

参考: https://www.cnblogs.com/chyingp/p/pm2-documentation.html 

4.3.nginx搭建反向代理

在开始搭建工作之前,首先得知道什么是反向代理。可能大家对这个名词比较陌生,先上一张图:

 

所谓代理就是我们通常所说的中介,网站的反向代理就是指那台介于用户和我们真实服务器之间的服务器(说的我都拗口了),它的作用便是能够将用户的请求分配到压力较小的服务器上,其机制是轮询。听完这句话是不是感觉很耳熟,没错,在我介绍pm2的时候也说过同样的话,反向代理起到的作用同pm2一样也是实现负载均衡,你现在应该也明白了两者之间的差异,反向代理是对服务器实现负载均衡,而pm2是对进程实现负载均衡。大家如果想深入了解反向代理的相关知识,我推荐知乎的一个贴子:www.zhihu.com/question/24…

参考:https://juejin.im/post/5b0b7d74518825158e173a0c#heading-18

 

 

 

 

RESTful架构

即Representational State Transfer的缩写。我对这个词组的翻译是"表现层状态转化"。

要理解RESTful架构,最好的方法就是去理解Representational State Transfer这个词组到底是什么意思,它的每一个词代表了什么涵义。如果你把这个名称搞懂了,也就不难体会REST是一种什么样的设计。

资源(Resources)

REST的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是"资源"(Resources)的"表现层"。

所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。

所谓"上网",就是与互联网上一系列的"资源"互动,调用它的URI。

表现层(Representation)

"资源"是一种信息实体,它可以有多种外在表现形式我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。

比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。

URI只代表资源的实体的位置,不代表它的形式。严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而URI应该只代表"资源"的位置。它的具体表现形式,应该在HTTP请求的头信息中用AcceptContent-Type字段指定,这两个字段才是对"表现层"的描述。

状态转化(State Transfer)

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。

互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。

客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

综述

综合上面的解释,我们总结一下什么是RESTful架构:

  (1)每一个URI代表一种资源;

  (2)客户端和服务器之间,传递这种资源的所需的外在表现形式类型;

  (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。

误区

RESTful架构有一些典型的设计误区。

最常见的一种设计错误,就是URI包含动词。因为"资源"表示一种实体,所以应该是名词,URI不应该有动词,动词应该放在HTTP协议中。

举例来说,某个URI是/posts/show/1,其中show是动词,这个URI就设计错了,正确的写法应该是/posts/1,然后用GET方法表示show。

如果某些动作是HTTP动词表示不了的,你就应该把动作做成一种资源。比如网上汇款,从账户1向账户2汇款500元,错误的URI是:

  POST /accounts/1/transfer/500/to/2

正确的写法是把动词transfer改成名词transaction,资源不能是动词,但是可以是一种服务:

  POST /transaction HTTP/1.1
  Host: 127.0.0.1
  
  from=1&to=2&amount=500.00

另一个设计误区,就是在URI中加入版本号

  http://www.example.com/app/1.0/foo

  http://www.example.com/app/1.1/foo

  http://www.example.com/app/2.0/foo

因为不同的版本,可以理解成同一种资源的不同表现形式,所以应该采用同一个URI。版本号可以在HTTP请求头信息的Accept字段中进行区分(参见Versioning REST Services):

  Accept: vnd.example-com.foo+json; version=1.0

  Accept: vnd.example-com.foo+json; version=1.1

  Accept: vnd.example-com.foo+json; version=2.0

参考:http://www.ruanyifeng.com/blog/2011/09/restful.html

css优先级

下面列表中,选择器类型的优先级是递增的:

  1. 类型选择器(例如,h1)和伪元素(例如,::before
  2. 类选择器 (例如,.example),属性选择器(例如,[type="radio"])和伪类(例如,:hover
  3. ID 选择器(例如,#example)。
  4. 给元素添加的内联样式 (例如,style="font-weight:bold") 总会覆盖外部样式表的任何样式 ,因此可看作是具有最高的优先级。

通配选择符(universal selector)(*关系选择符(combinators)(+>~' '||)和 否定伪类(negation pseudo-class)(:not())对优先级没有影响。(但是,在 :not() 内部声明的选择器会影响优先级)。

您可以访问 https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_and_inheritance#Specificity_2  或者 https://specifishity.com 来了解更多关于优先级的详细信息。

 

 

 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值