夏洛克的救赎

夏洛克的救赎 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 www.finit.top 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

夏洛克的救赎 赞了文章 · 2020-11-17

RxJS Observable - 一个奇特的函数

前言

RxJS 的 Observable 有点难理解,其实 RxJS 相关的概念都有点难理解。毕竟 RxJS 引入了响应式编程这种新的模式,会不习惯是正常的。不过总得去理解嘛,而认识新的事物时,如果能够参照一个合适的已知事物比对着,会比较容易理解吧。对于 Observable,类比 JS 中的函数,还是比较好的。

开始

封装

先来看一个普通函数调用的例子:

function foo() {
  console.log('process...')
}

foo()
// 输出:
// process...

很简单,函数 foo() 封装了一段逻辑(这里只是向控制台输出),然后通过调用函数,函数执行内部的逻辑。

再来看 RxJS Observable 的一个例子:

var foo = Rx.Observable.create(() => {
  console.log('process...')
})

foo.subscribe()
// 输出:
// process...

上例中,通过 Rx.Observable.create() 来创建 Observable 对象,将同样将一段代码逻辑封装到 Observable 对象 foo 中,然后通过 foo.subscribe() 来执行封装的代码逻辑。

对于普通函数和 Observable 对象,封装的代码逻辑在每次调用时都会重新执行一次。从这一点来看,Observable 能够和普通函数一样实现封装代码进行复用。

返回值

函数调用后可以有返回值:

function foo() {
  console.log('process...')
  return 42
}

console.log(foo())
// 输出:
// process...
// 42

Observable 执行后也会产生值,不过和函数直接返回的方式不同,要通过回调函数方式获取:

var foo = Rx.Observable.create((observer) => {
  console.log('process...')
  observer.next(42)
})

foo.subscribe(value => console.log(value))
// 输出:
// process...
// 42

Observable 对象内部是通过 observer.next(42) 这种方式返回值,而调用方则通过回调函数来接收返回的数据。形式上比普通函数直接返回值啰嗦一些。

从调用方的角度来看,两个过程分别是:

  • 普通函数:调用 > 执行逻辑 > 返回数据
  • Observable:订阅(subscribe) > 执行逻辑 > 返回数据

从获取返回值方式来看,调用函数是一种直接获取数据的模式,从函数那里“拿”(pull)数据;而 Observable 订阅后,是要由 Observable 通过间接调用回调函数的方式,将数据“推”(push)给调用方。

这里 pull 和 push 的重要区别在于,push 模式下,Observable 可以决定什么时候返回值,以及返回几个值(即调用回调函数的次数)。

var foo = Rx.Observable.create((observer) => {
  console.log('process...')
  observer.next(1)
  setTimeout(() => observer.next(2), 1000)
})

console.log('before')
foo.subscribe(value => console.log(value))
console.log('after')
// 输出:
// before
// process...
// 1
// after
// 2

上面例子中,Observable 返回了两个值,第1个值同步返回,第2个值则是过了1秒后异步返回。

也就是说,从返回值来说,Observable 相比普通函数区别在于:

  • 可以返回多个值
  • 可以异步返回值

异常处理

函数执行可能出现异常情况,例如:

function foo() {
  console.log('process...')
  throw new Error('BUG!')
}

我们可以捕获到异常状态进行处理:

try {
   foo()
} catch(e) {
  console.log('error: ' + e)
}

对于 Observable,也有错误处理的机制:

var foo = Rx.Observable.create((observer) => {
  console.log('process...')
  observer.error(new Error('BUG!'))
})

foo.subscribe(
  value => console.log(value),
  e => console.log('error: ' + e)
)

Observable 的 subscribe() 方法支持传入额外的回调函数,用于处理异常情况。和函数执行类似,出现错误之后,Observable 就不再继续返回数据了。

subscribe() 方法还支持另一种形式传入回调函数:

foo.subscribe({
  next(value) { console.log(value) },
  error(e) { console.log('error: ' + e) }
})

而这种形式下,传入的对象和 Observable 内部执行函数中的 observer 参数在形式上就比较一致了。

中止执行

Observable 内部的逻辑可以异步多个返回值,甚至返回无数个值:

var foo = Rx.Observable.create((observer) => {
  let i = 0
  setInterval(() => observer.next(i++), 1000)
})

foo.subscribe(i => console.log(i))
// 输出:
// 0
// 1
// 2
// ...

上面例子中,Observable 对象每隔 1 秒会返回一个值给调用方。即使调用方不再需要数据,仍旧会继续通过回调函数向调用推送数据。

RxJS 提供了中止 Observable 执行的机制:

var foo = Rx.Observable.create((observer) => {
  console.log('start')
  let i = 0
  let timer = setInterval(() => observer.next(i++), 1000)
  return () => {
    clearInterval(timer)
    console.log('end')
  }
})

var subscription = foo.subscribe(i => console.log(i))
setTimeout(() => subscription.unsubscribe(), 2500)
// 输出:
// start
// 0
// 1
// 2
// end

subscribe() 方法返回一个订阅对象(subscription),该对象上的 unsubscribe() 方法用于取消订阅,也就是中止 Observable 内部逻辑的执行,停止返回新的数据。

对于具体的 Observable 对象是如何中止执行,则要由 Observable 在执行后返回一个用于中止执行的函数,像上面例子中的这种方式。

Observable 执行结束后,会触发观察者的 complete 回调,所以可以这样:

foo.subscribe({
  next(value) { console.log(value) },
  complete() { console.log('completed') }
})

Observable 的观察者共有上面三种回调:

  • next:获得数据
  • error:处理异常
  • complete:执行结束

其中 next 可以被多次调用,error 和 complete 最多只有一个被调用一次(任意一个被调用后不再触发其他回调)。

数据转换

对于函数返回值,有时候我们要转换后再使用,例如:

function foo() {
  return 1
}

console.log(f00() * 2)
// 输出:
// 2

对于 Observable 返回的值,也会有类似的情况,不过通常采用下面的方式:

var foo = Rx.Observable.create((observer) => {
  let i = 0
  setInterval(() => observer.next(i++), 1000)
})

foo.map(i => i * 2).subscribe(i => console.log(i))
// 输出:
// 0
// 2
// 4
// ...

其实 foo.map() 返回了新的 Observable 对象,上面代码等价于:

var foo2 = foo.map(i => i * 2)
foo2.subscribe(i => console.log(i))

Observable 对象 foo2 被订阅时执行的内部逻辑可以简单视为:

function subscribe(observer) {
  let mapFn = v => v * 2
  foo.subscribe(v => {
    observer.next(mapFn(v))
  })
}

将这种对数据的处理和数组进行比较看看:

var array = [0, 1, 2, 3, 4, 5]
array.map(i => i * 2).forEach(i => console.log(i))

是不是有点像?

除了 map() 方法,Observable 还提供了多种转换方法,如 filter() 用于过滤数据,find() 值返回第一个满足条件的数据,reduce() 对数据进行累积处理,在执行结束后返回最终的数据。这些方法和数组方法功能是类似的,只不过是对异步返回的数据进行处理。还有一些转换方法更加强大,例如可以 debounceTime() 可以在时间维度上对数据进行拦截等等。

Observable 的转换方法,本质不过是创建了一个新的 Observable,新的 Observable 基于一定的逻辑对原 Observable 的返回值进行转换处理,然后再推送给观察者。

总结

Observable 就是一个奇怪的函数,它有和函数类似的东西,例如封装了一段逻辑,每次调用时都会重新执行逻辑,执行有返回数据等;也有更特殊的特性,例如数据是推送(push)的方式返回给调用方法,返回值可以是异步,可以返回多个值等。

不过将 Observable 视作特殊函数,至少对于理解 Observable 上是比较有帮助的。

Observable 也被视为 data stream(数据流),这是从 Observable 可以返回多个值的角度来看的,而数据转换则是基于当前数据流创建新的数据流,例如:

observable.map(x => 10 * x)

不过上图看到的只是数据,而将 Observable 视为特殊函数时,不应该忘了其内部逻辑,不然数据是怎么产生的呢。

查看原文

赞 4 收藏 4 评论 0

夏洛克的救赎 发布了文章 · 2019-03-12

翻译书籍《计算机科学与数学》

一、概述

曾几何时,利用Google搜索某问题的时候,意外地接触到了一个网站:https://www.gitbook.com/。一个在线编辑书籍、文章的文章,具体描述可以去其网站观看。该网站旧版地址:legacy.gitbook.com.

刚工作不到一年的时候,接触到公司的商业项目,逐步意识到编程说难不难,说不难也难。之前看文章说学计算机绕不开的两项技能:英语和数学,在此期间深刻体会到了其重要性。尤其是数学,我发现一般开发只需要中学数学知识就够了,尤其是高中数学,当年只是为了高考,不知有何用,现在真要感谢高数的数学老师。当然搞人工智能只有高中数学是不够的,我想从事人工智能行业的朋友应该对大学数学的作用有更深刻的认识。

编程中两项核心能力——抽象和逻辑能力,都可以通过扎实的数学训练得到加强。为什么说编程的核心能力也是难点所在是抽象和逻辑能力呢?数据结构与算法是大部分程序员头痛的地方,数据结构即抽象,是对现实世界的人和物的抽象表示;算法即逻辑。还有同样令人头疼的设计模式不也是因为太抽象了吗? 还有一旦涉及到软件系统设计,这也是抽象。

二、下面通过例子体会,高中数学在计算机的应用。

比如,编程语言的循环和递归,不就是数学归纳法的体现吗?

再如几个常见数学概念在计算机和软件开发中的体现,

1.函数

数学函数三要素:定义域、对应法则、值域。

对应于编程语言中的函数:形式参数、函数主体(逻辑、计算规则)、返回值。

2.命题

(1)命题的真假对应分支语句的真与假

分支语句判断条件有无遗漏,从以下两点分析:

a.条件有没有遗漏

分支语句范围要完整,才不会有遗漏,导致逻辑错误。另外还要注意else if语句是排他的。

举例,else if 语句:

if(x > 60){......}
else if(x > 40){......}
else if(x > 20){......}

b.条件有没有重复

三、结语

铺垫了那么长,就是想强调数学的趣味性和重要性。因此,本人就特意查找到了专门讲解有关计算机科学的数学的课程,准备好好学习,并翻译其教材,即精读。翻译工具即Gitbook。

初步成果展示链接:https://finit-xu.gitbook.io/m...。也可以点击阅读原文查看课程详情。

查看原文

赞 0 收藏 0 评论 0

夏洛克的救赎 赞了文章 · 2019-01-22

协议简史:如何学习网络协议?

大学时,学到网络协议的7层模型时,老师教了大家一个顺口溜:物数网传会表应。并说这是重点,年年必考,5分的题目摆在这里,你们爱背不背。
考试的时候,果然遇到这个问题,搜索枯肠,只能想到这7个字的第一个字,因为这5分,差点挂科。
后来工作面试,面试官也是很喜欢七层模型,三次握手之类的问题,但是遇到这些问题时,总是觉得很心虚。
有时候也会想,面试官考这些协议方面的东西有什么用呢?能加工资吗?
说实在的,作为一个前端开发,即使你对协议一窍不通,也不影响你用使用React,或者Vue等框架。但是如果你对底层通信协议有个差不多的认识,你将有能力解决更多的问题。

1. 协议分层

四层网络协议模型中,应用层以下一般都是交给操作系统来处理。应用层对于四层模型来说,仅仅是冰山一角。海面下巨复杂的三层协议,都被操作系统给隐藏起来了,一般我们在页面上发起一个ajax请求,看见了network面板多了一个http请求,至于底层是如何实现的,我们并不关心。

clipboard.png

  • 应⽤层负责处理特定的应⽤程序细节。
  • 运输层运输层主要为两台主机上的应⽤程序提供端到端的通信。
  • 网络层处理理分组在⽹网络中的活动,例例如分组的选路
  • 链路层处理理与电缆(或其他任何传输媒介)的物理理接⼝口细节

常见网络协议

clipboard.png

  • 应用层的协议,例如SIP, 即可以使用TCP协议,也可以使用UDP协议
  • 别太把七层的OSI模型太当回事,因为OSI模型只是模型,基本上已经停用。重要的理解4成网络模型。
  • 有些应用层的网络协议比较复杂,可能会涉及到多个底层的协议。可以参考下面的WebRTC所使用的底层协议栈。

clipboard.png

下面重点讲一下运输层和网络层

1.1. 运输层的两兄弟

运输层有两个比较重要的协议。tcp和udp。

大哥tcp是比较严谨认真、温柔体贴、慢热内向的协议,发出去的消息,总是一个一个认真检查,等待对方回复和确认,如果一段时间内,对方没有回复确认消息,还会再次发送消息,如果对方回复说你发的太快了,tcp还会体贴的把发送消息的速度降低。

弟弟udp则比较可爱呆萌、调皮好动、不负责任的协议。哥哥tcp所具有的特点,弟弟udp一个也没有。但是有的人说不清哪里好 但就是谁都替代不了,udp没有tcp那些复杂的校验和重传等复杂的步骤,所以它发送消息非常快,而且并不保证对方一定收到。如果对方收不到消息,那么udp就会呆萌的看着你,笑着对你说:我已经尽力了。一般语音而视频数据都是用udp协议传输的,因为音频或者视频卡了一下并不影响整体的质量,而对实时性的要求会更高。

1.2. 运输层和网络层的区别

  • 运输层关注的是端到端层面,及End1到End2,忽略中间的任何点。
  • 网络层关注两点之间的层面,即hop1如何到hop2,hop2如何到hop3
  • 网络层并不保证消息可靠性,可靠性上层的传输层负责。TCP采用超时重传,分组确认的机制,保证消息不会丢失。

clipboard.png

从下图tcp, udp, ip协议中,可以发现

  • 传输层的tcp和udp都是有源端口和目的端口,但是没有ip字段
  • 源ip和目的ip只在ip数据报中
  • 理解各个协议,关键在于理解报文的各个字段的含义

clipboard.png

1.3. ip和端口号的真正含义

上个章节讲到运输层和网络层的区别,其中端口号被封装在运输层,ip被封装到网络层

那么端口号和ip地址到底有什么区别呢?

  • ip用来用来标记主机的位置
  • 端口号用来标记该数据应该被目标主机上的哪个应用程序去处理。端口号占用16位,2的16次方等于65536,所以你明白了为什么端口号的范围从0到65535了吧。

clipboard.png

1.4. 数据在协议栈的流动 封装与分用

  • 当发送消息时,数据在向下传递时,经过不同层次的协议处理,打上各种头部信息
  • 当接受消息时,数据在向上传递,通过不同的头部信息字段,才知道要交给上层的那个模块来处理。比如一个ip包,如果没有头部信息,那么这个消息究竟是交给tcp协议来处理,还是udp来处理,就不得而知了

clipboard.png

2. 深入阅读,好书推荐

上面讲的都是很基础的知识,具体细数据报各个字段的含义,还是需要看书的。纸上得来终觉浅,绝知此事要抓包。边看书边学习抓包。要学会使用wireshark工具,能够熟练使用netstat去发现tcp链接的相关问题。

  • 《http权威指南》有人说这本书太厚,偷偷告诉你,其实这本书并厚,因为这本书的后面的30%部分都是附录,这本书的精华是前50%的部分
  • 《图解http》《图解tcp/ip》这两本图解的书,知识点讲的都是比较通俗易懂的,适合入门
  • 《tcp/ip 详解 卷1》这本书,让你知其然,更知其所以然
  • 《tcp/ip 基础》、《tcp/ip 路由技术》这两本书,会让你从不同角度思考协议
  • 《精通wireshark》、《wireshark网络分析实战》如果你看了很多书,却从来没有试过网络抓包,那你只是懂纸上谈兵罢了。你永远无法理解tcp三次握手的怦然心动,与四次分手的刻骨铭心。
  • 《网络是怎样连接的》非常好看,引人入胜的科普书籍,作者户根勤从软件到硬件,方方面面造诣都很深。
  • 《tcp ip 入门经典》

扫码订阅我的微信公众号:洞香春天。每天一篇技术短文,让知识不再高冷。

图片描述

查看原文

赞 190 收藏 142 评论 1

夏洛克的救赎 收藏了文章 · 2018-11-16

如何优雅的选择字体(font-family)

大家都知道,在不同操作系统、不同游览器里面默认显示的字体是不一样的,并且相同字体在不同操作系统里面渲染的效果也不尽相同,那么如何设置字体显示效果会比较好呢?下面我们逐步的分析一下:

一、首先我们看看各平台的默认字体情况

1、Window下:
  • 宋体(SimSun):Win下大部分游览器的默认字体,宋体在小字号下(如12px、14px)的显示效果还可以接受,但是字号一大就非常糟糕了,所以使用的时候要注意。

  • 微软雅黑("Microsoft Yahei"):从 Vista 开始,微软提供了这款新的字体,一款无衬线的黑体类字体,并且拥有 RegularBold 两种粗细的字重,显著提高了字体的显示效果。现在这款字体已经成为Windows游览器中最值得使用的中文字体。从Win8开始,微软雅黑又加入了 Light 这款更细的字重,对于喜欢细字体的设计、开发人员又多了一个新的选择。

  • Arial:Win平台上默认的无衬线西文字体(为什么要说英文字体后面会解释),有多种变体,显示效果一般。

  • Tahoma:十分常见的无衬线字体,被采用为Windows 2000、Windows XP、Windows Server 2003及Sega游戏主机Dreamcast等系统的预设字型,显示效果比Arial要好。

  • Verdana:无衬线字体,优点在于它在小字上仍结构清晰端整、阅读辨识容易。

  • 其他:Windows 下默认字体列表:微软官网维基百科Office字体

  • 结论:微软雅黑为Win平台上最值得选择的中文字体,但非游览器默认,需要设置;西文字体的选择以ArialTahoma等无衬线字体为主。

2、Mac OS下:
  • 华文黑体(STHeiti)、华文细黑(STXihei):属于同一字体家族系列,OS X 10.6 之前的简体中文系统界面默认字体,也是目前Chrome游览器下的默认字体,有 RegularBold 两个字重,显示效果可以接受,华文细黑也曾是我最喜欢的字体之一。

  • 黑体-简(Heiti SC):从 10.6 开始,黑体-简代替华文黑体用作简体中文系统界面默认字体,苹果生态最常用的字体之一,包括iPhone、iPad等设备用的也是这款字体,显示效果不错,但是喇叭口设计遭人诟病。

  • 冬青黑体( Hiragino Sans GB ):听说又叫苹果丽黑,日文字体Hiragino KakuGothic的简体中文版,简体中文有 常规体粗体 两种,冬青黑体是一款清新的专业印刷字体,小字号时足够清晰,拥有很多人的追捧。

  • Times New Roman:Mac平台Safari下默认的字体,是最常见且广为人知的西文衬线字体之一,众多网页浏览器和文字处理软件都是用它作为默认字体。

  • Helvetica、Helvetica Neue:一种被广泛使用的传奇般的西文字体(这货还有专门的记录片呢),在微软使用山寨货的Arial时,乔布斯却花费重金获得了Helvetica苹果系统上的使用权,因此该字体也一直伴随着苹果用户,是苹果生态中最常用的西文字体。Helvetica NeueHelvetica的改善版本,且增加了更多不同粗细与宽度的字形,共拥有51种字体版本,极大的满足了日常的使用。

  • 苹方(PingFang SC):在Mac OS X EL Capitan上,苹果为中国用户打造了一款全新中文字体--苹方,去掉了为人诟病的喇叭口,整体造型看上去更加简洁,字族共六枚字体:极细体、纤细体、细体、常规体、中黑体、中粗体

  • San Francisco:同样是Mac OS X EL Capitan上最新发布的西文字体,感觉和Helvetica看上去差别不大,目前已经应用在Mac OS 10.11+、iOS 9.0+、watch OS等最新系统上。

  • 其他:Mac下默认字体列表:苹果官网维基百科

  • 结论:目前苹方San Francisco为苹果推出的最新字体,显示效果也最为优雅,但只有最新系统才能支持,而黑体-简Helvetica可以获得更多系统版本支持,显示效果也相差无几,可以接受。

3、Android系统:
  • Droid Sans、Droid Sans FallbackDroid Sans为安卓系统中默认的西文字体,是一款人文主义无衬线字体,而Droid Sans Fallback则是包含汉字、日文假名、韩文的文字扩展支持。

  • 结论:Droid Sans为默认的字体,并结合了中英文,无需单独设置。

4、iOS系统:
  • iOS系统的字体和Mac OS系统的字体相同,保证了Mac上的字体效果,iOS设备就没有太大问题。

5、Linux:
  • 文泉驿点阵宋体:类似宋体的衬线字体,一般不推荐使用。

  • 文泉驿微米黑:几乎是 Linux 社区现有的最佳简体中文字体。

二、选择字体需要注意的问题

1、字体的中英文写法:

我们在操作系统中常常看到宋体微软雅黑这样的字体名称,但实际上这只是字体的显示名称,而不是字体文件的名称,一般字体文件都是用英文命名的,如SimSunMicrosoft Yahei。在大多数情况下直接使用显示名称也能正确的显示,但是有一些用户的特殊设置会导致中文声明无效。
因此,保守的做法是使用字体的字体名称(英文)或者两者兼写。如下示例:

font-family: STXihei, "Microsoft YaHei";
font-family: STXihei, "华文细黑", "Microsoft YaHei", "微软雅黑";
2、声明英文字体:

绝大部分中文字体里都包含英文字母和数字,不进行英文字体声明是没有问题的,但是大多数中文字体中的英文和数字的部分都不是特别漂亮,所以建议也对英文字体进行声明。
由于英文字体中大多不包含中文,我们可以先进行英文字体的声明,这样不会影响到中文字体的选择,因此优先使用最优秀的英文字体,中文字体声明则紧随其次。如下示例:

font-family: Arial, "Microsoft YaHei";
3、照顾不同的操作系统:
  • 英文、数字部分:在默认的操作系统中,Mac和Win都会带有Arial, Verdana, Tahoma等几个预装字体,从显示效果来看,Tahoma要比Arial更加清晰一些,因此字体设置Tahoma最好放到前面,当找不到Tahoma时再使用Arial;而在Mac中,还拥有一款更加漂亮的Helvetica字体,所以为了照顾Mac用户有更好的体验,应该更优先设置Helvetica字体;Android系统下默认的无衬线字体就可以接受,因此无需单独设置。最后,英文、数字字体的最佳写法如下:

font-family: Helvetica, Tahoma, Arial;
  • 中文部分:在Win下,微软雅黑为大部分人最常使用的中文字体,由于很多人安装Office的缘故,Mac电脑中也会出现微软雅黑字体,因此把显示效果不错的微软雅黑加入到字体列表是个不错的选择;同样,为了保证Mac中更为优雅字体苹方(PingFang SC)黑体-简(Heiti SC)冬青黑体( Hiragino Sans GB )的优先显示,需要把这些字体放到中文字体列表的最前面;同时为了照顾到Linux操作系统的体验,还需要添加文泉驿微米黑字体。最后,中文字体部分最佳写法如下:

font-family: "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei";

中英文整合写法:

font-family: Helvetica, Tahoma, Arial, "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei";
font-family: Helvetica, Tahoma, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei";
4、注意向下兼容

如果还需要考虑旧版本操作系统用户的话,不得不加上一些旧版操作系统存在的字体:Mac中的华文黑体冬青黑体,Win中的黑体等。同样按照显示效果排列在列表后面,写法如下:

font-family: Helvetica, Tahoma, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", STXihei, "Microsoft YaHei", SimHei, "WenQuanYi Micro Hei";

加入了 STXihei(华文细黑) SimHei(黑体)

5、补充字体族名称

字体族大体上分为两类:sans-serif(无衬线体)serif(衬线体),当所有的字体都找不到时,我们可以使用字体族名称作为操作系统最后选择字体的方向。一般非衬线字体在显示器中的显示效果会比较好,因此我们需要在最后添加 sans-serif,写法如下:

font-family: Helvetica, Tahoma, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

三、我们看一下大公司的常见写法(2016.07查看)

1、小米
font: 14px/1.5 "Helvetica Neue",Helvetica,Arial,"Microsoft Yahei","Hiragino Sans GB","Heiti SC","WenQuanYi Micro Hei",sans-serif;

小米公司优先使用Helvetica Neue这款字体以保证最新版本Mac用户的最佳体验,选择了Arial作为Win下默认英文字体及Mac的替代英文字体;中文字体方面首选了微软雅黑,然后选择了冬青黑体黑体-简作为Mac上的替代方案;最后使用文泉驿微米黑兼顾了Linux系统。

2、淘宝

鉴于淘宝网改版频率较频繁,这里截图保存了一下,点此查看

font: 12px/1.5 tahoma,arial,'Hiragino Sans GB','\5b8b\4f53',sans-serif;

其实从图中明显看出淘宝网的导航及内容有着大量的衬线字体,鉴于淘宝网站大部分字号比较小,显示效果也还可以接受。代码中可以看出淘宝使用了TahomaArial作为首选英文字体,中文字体首选了冬青黑体,由于Win下没有预装该款字体,所以显示出了后面的宋体(5b8b4f53为汉字宋体用 unicode 表示的写法,不用SimSun是因为 Firefox 的某些版本和 Opera 不支持 SimSun的写法)

3、简书
font-family: "lucida grande", "lucida sans unicode", lucida, helvetica, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

自认为简书的阅读体验很棒,我们看看简书所用的字体,简书优先选择了lucida家族的系列字体作为英文字体,该系列字体在Mac和Win上都是预装的,并且有着不俗的表现;中文字体方面将冬青黑体作为最优先使用的字体,同样考虑了Linux系统。

各大公司的字体设置大同小异,这里不再一一举例查看,有兴趣的可以自己多多查看。

四、其他的一些注意点

1、字体何时需要添加引号

当字体具体某个取值中若有一种样式名称包含空格,则需要用双引号或单引号表示,如:

font-family: "Microsoft YaHei", "Arial Narrow", sans-serif;

如果书写中文字体名称为了保证兼容性也会添加引号,如:

font-family: "黑体-简", "微软雅黑", "文泉驿微米黑";
2、引用外部字体

大多数的中文字体由于版权原因不能随意使用,这里推荐一个免版权而且漂亮的中文字体思源黑体,该字体为Adobe与Google推出的一款开源字体, 有七种字体粗细(ExtraLight、Light、Normal、Regular、Medium、Bold 和 Heavy),完全支持日文、韩文、繁体中文和简体中文,字形优美,依稀记得小米公司好像有使用过。
鉴于中文字体的体积比较大(一般字库全一点的中文字体动辄几Mb),所以较少人会使用外部字体,如果真的需要引入,也可以考虑通过工具根据页面文字的使用多少单独生成中文字体,以减小文件大小。

五、最后,推荐写法

由于每个人的审美不一样,钟爱的字体也会有所有不同,这里给出我个人的常用写法:

font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

另外推荐两个github上的关于中文字体和排版的项目:

--参考资料

查看原文

夏洛克的救赎 发布了文章 · 2018-11-14

如何跨操作系统共享文件?你还在用U盘傻瓜式地拷贝文件吗?

一般而言为了系统安全,都会专门新增一个共享账户,但是为了方便阐述,不再说明如何创建系统账户。
本文探讨macOS、Linux、Windows三种操作系统两两之间的文件共享方式,根据数学中的排列组合知识可知,总共有六种两两组合方式。

一、应用场景

一般来说,都是为了同一局域网内文件传输的便利性,而且局域网文件传输速度很快。自己可能有多台电脑或者需要与他人共享文件都可以采用这种方式。还有就是需要远程调试代码,比如在Windows下IDE做开发,在远程Linux系统上调试。

注意:共享账户设置的密码不是安全存储的。

二、windows系统之间共享文件

1.添加网路位置,如下图:
在这里插入图片描述
2.根据示例提示,输入正确的网站共享位置,如下图:
在这里插入图片描述
3.输入需要访问的主机/网站的账户和密码。

三、MacBook与windows 共享文件

1.Windows连接MacBook
(1)MacBook共享文件设置
在MacBook系统偏好设置中选择文件共享,点击“”选项“”,指定共享用户并选择SMB方式共享,如下图所示:
在这里插入图片描述
(2)Windows连接MacBook
windows+R -> 输入: "\mac的ip地址" ,如 \192.168.1.1,MacBook的IP地址可以在刚刚文件共享窗口看到,类似于“smb://ip地址”;输入刚刚指定的共享账户的账户名称和密码。
2. MacBook连接Windows
(1)在Windows系统下右击需要共享的文件夹,选择高级共享,如图:
在这里插入图片描述
指定共享用户,如图:
在这里插入图片描述
(2)在MacBook上打开Finder,在菜单栏中选择前往-->连接服务器,服务器地址输入“smb://主机名称或者主机IP地址”;输入刚刚指定的共享账户的账户名称和密码。

四、Linux与window系统之间共享文件

1.Windows访问Linux共享的文件
使用Samba软件:在Linux系统上安装该软件并进行相应配置即可:

yum install samba


安装完成后,修改配置文件,打开:/etc/samba/smb.conf,在文件末尾加上如下配置:

  [shareUser]
  comment = shareUser
  path = / 
  create mask = 0755
  writeable = yes 
  browseable = yes
  valid users = root

添加共享用户:smbpasswd -a shareUser
重启服务:`service smb restart
`
访问Linux系统:win+R->\\192.168.1.110。
完成共享。
2. Linux访问Windows共享的文件
设置Windows共享文件内容的步骤如前面所述,不再赘述,接着在Linux系统上挂载windows共享目录,举例:

   mount -t cifs -o username="share" //192.168.1.1/sourcecode 
    /root/sourcecode

接下来可以像访问本地文件一样正常访问window共享文件。

五、Linux系统之间共享文件

一台Linux作为服务端,另一台作为客户端。

1.服务端

(1)查看服务端系统是否已安装NFS

rpm -qa | grep rpc
rpm -qa | grep nfs

没有的话,就安装

yum -y install nfs* rpcbind

(2)指定共享文件

共享文件,比如共享/home/sharedFolders目录下的文件、编写要共享的机器名

vi /etc/exports

内容填写如下:

/home/sharedFolders 172.31.93.1(ro)

/home/share/是要共享的文件夹 后面接的是ip+网段,括号里面的表示客户机对该目录下的文件的操作权限,r表示可读,w表示可写,x表示可执行。

(3)重启NFS服务

特别注意:因为在6.0的系统里,portmap已经改名了。在Redhat或CentOS5中可以使用 service portmap start启动服务,然后在启动nfs服务,实现挂载。6里面可是试试 service rpcbind start启动

service rpcbind start
service nfs start

可以查看NFS服务端服务状态

service rpcbind status
service nfs status

设置NFS服务端nfs服务自启动,避免每次都要启动共享文件服务

chkconfig --list rpcbind
chkconfig --level 35 rpcbind on


chkconfig --list nfs
chkconfig --level 35 nfs on

(4)查看NFS服务端的共享状态与查看客户端连接信息
首先:

exportfs
showmount -e 

注:要客户端连接之后才有。
然后:

exportfs -rv
service nfs restart

(5)挂载共享目录
在客户端上面挂载NFS服务器中的共享目录 ,格式:mount NFS服务器ip:共享目录 本地目录

mount 172.31.93.0:/home/sharedFolders /mnt/

六、MacBook系统与Linux之间的文件共享

采用Samba协议,全称Server Message Block,即信息服务块。适用于类Linux/Unix系统,可以在局域网上共享文件和打印机。针对不同操作系统,具体操作细节有所不同,原理一致,可以参考前面的“ 五、Linux系统之间共享文件”。

七、MacBook系统之间的文件共享

与Windows系统和MacBook系统之间共享文件的方式相同。可以参考前面的“ 三、MacBook与windows 共享文件”。

八、原理

Windows系统提供一种远程文件系统机制,NAS协议的一种——CIFS协议。

Linux系统呢,同样有另外一种NAS协议——NFS协议来实现远程访问。那么这两种NAS协议能否互通呢?答案是否定的。虽然二者不能互通,但是,在Linux系统上面已经有了CIFS协议的服务端和客户端的实现,这样,无论是Linux给Windows共享还是Windows给Linux共享都可以借助这些已有的实现来做到了。

另一方面,Windows系统天然就是一个CIFS的服务端和客户端,既然Windows系统可以给Windows系统共享目录,那么Linux系统能否访问这些共享呢?答案是肯定的,由于有强大的VFS支撑,Linux支持挂载和访问各种文件系统。 mount工具支持挂在CIFS甚至NTFS的文件系统。如果是Windows本机上面的Linux虚拟机,那么可以直接通过虚拟机管理软件,如Vmware直接共享本地的磁盘分区给Linux系统,Linux系统根据Windows的磁盘分区的文件系统类型挂载即可。

前面MacBook用到的SMB网络文件协议是CIFS父集。
MacBook系统还可以选择 AFP作为文件共享协议,即苹果文件协议,是苹果公司的专有协议,AFP 3.0 之后完全依赖TCP/IP创建通信。AFP在Mac OS9 是文件服务的主要协议。


微信公众号:技术很有趣

在这里插入图片描述


查看原文

赞 1 收藏 0 评论 0

夏洛克的救赎 赞了回答 · 2018-10-31

解决js如何实现链式调用?

链式调用的根本在于上一个函数的返回值有下一个要调用的方法,依次类推。

比较常见做法是自己写个类来封装数据,然后提供一堆处理这个数据的方法,每个(或大部分)方法都返回 this,这样就能链式调用了。

你要做的事情 jQuery 或者 Zepto 都已经封装得很好了,直接用吧。有兴趣就去研究下它们的源码。

关注 5 回答 5

夏洛克的救赎 赞了文章 · 2018-10-31

web前端攻击技术与防范——XSS、CSRF、网络劫持、控制台注入、钓鱼

web攻击技术

我了解到的web安全方面的一些攻击有:
   XSS攻击
   CSRF攻击
   网络劫持攻击
   控制台注入代码
   钓鱼

XSS攻击(cross-site script)

1、XSS攻击形式:

主要是通过html标签注入,篡改网页,插入恶意的脚本,前端可能没有经过严格的校验直接就进到数据库,数据库又通过前端程序又回显到浏览器

例如一个留言板:
如果内容是
    hello!<script type="type/javascript data-original="恶意网址"></script>
 这样会通过前端代码来执行js脚本,如果这个恶意网址通过cookie获得了用户的私密信息,那么用户的信息就被盗了

2、攻击的目的:

攻击者可通过这种方式拿到用户的一些信息,例如cookie 获取敏感信息,甚至自己建网站,做一些非法的操作等;或者,拿到数据后以用户的身份进行勒索,发一下不好的信息等。

3、攻击防御

首先前端要对用户输入的信息进行过滤,可以用正则,通过替换标签的方式进行转码或解码
例如<> 空格 & '' ""等替换成html编码

  htmlEncodeByRegExp:function (str){  
       var s = "";
       if(str.length == 0) return "";
       s = str.replace(/&/g,"&amp;");
       s = s.replace(/</g,"&lt;");
       s = s.replace(/>/g,"&gt;");
       s = s.replace(/ /g,"&nbsp;");
      s = s.replace(/\'/g,"&#39;");
      s = s.replace(/\"/g,"&quot;");
      return s;  
 },
    
其次在java后端还要进行安全防御,具体可以看一下这个http://blog.csdn.net/qq_34120041/article/details/76890092

CSRF攻击(cross site request forgery,跨站请求伪造)

CSRF也是一种网络攻击方式,比起xss攻击,是另外一种更具危险性的攻击方式,xss是站点用户进行攻击,而csrf是通过伪装成站点用户进行攻击,而且防范的资源也少,难以防范

csrf攻击形式:攻击者盗用用户的身份信息,并以用户的名义进行发送恶意的请求等,例如发邮件,盗取账号等非法手段

例如:你登录网站,并在本地种下了cookie
     如果在没退出该网站的时候 不小心访问了恶意网站,而且这个网站需要你发一些请求等
     此时,你是携带cookie进行访问的,那么你的重在cookie里的信息就会被恶意网站捕捉到,那么你的信息就被盗用,导致一些不法分子做一些事情
     

攻击防御:
1、验证HTTP Referer字段

在HTTP头中有Referer字段,他记录该HTTP请求的来源地址,如果跳转的网站与来源地址相符,那就是合法的,如果不符则可能是csrf攻击,拒绝该请求

2、在请求地址中添加token并验证

这种的话在请求的时候加一个token,值可以是随机产生的一段数字,
token是存入数据库之后,后台返给客户端的,如果客户端再次登录的时候,
后台发现token没有,或者通过查询数据库不正确,那么就拒绝该请求

如果想防止一个账号避免在不同的机器上登录,那么我们就可以通过token来判断,
如果a机器登录后,我们就将用户的token从数据库清除,从新生成,
那么另外一台b机器在执行操作的时候,token就失效了,只能重新登录,这样就可以防止两台机器登同一账号

3、在HTTP头中自定义属性并验证

如果说通过每次请求的时候都得加token那么各个接口都得加很麻烦,
那么我们通过http的请求头来设置token
例如:
    $.ajax({
        url: '/v1/api',
        dataType: 'json',
        data: param,
        type:'post',
        headers: {'Accept':'application/json','Authorization':tokenValue}
        success:function(res){
            console.log(res)
        }
    })

网络劫持攻击

网络劫持攻击这种攻击主要是通过一些代理服务器,或者wifi等有中间件的网络请求,进行劫持,不法分子通过这种方式获取到用户的信息,那么我们该怎么防御呢?
最好是采用https进行加密,这种通过请求网络地址攻击的我们可以通过对http进行加密,来防范,这样不法分子即使或得到,也无法解密

控制台注入代码

这种就是不法分子通过各种提示诱骗用户在控制台做一些操作,从而获取用户信息,那么我们最好在控制台对用户进行友好的提示,必要轻易相信这种提示灯。

钓鱼

钓鱼!一个传统的攻击方式,也是通过人性的弱点来诱骗用户登录一些不法网站,我们要科学上网,不要被钓...

如果各位有什么好的前端安全方法,欢迎评论、私信、互相学习....

查看原文

赞 24 收藏 62 评论 1

夏洛克的救赎 回答了问题 · 2018-10-23

想系统地学习浏览器渲染基础,有什么书籍或者网站推荐?

关注 2 回答 1

夏洛克的救赎 赞了回答 · 2018-10-16

解决vuex中存储的数据在页面刷新之后都是失去,我想让vuex中的数据在刷新之后不会丢失怎么办。

我是在router.js中对其进行监听,如果vuex中的值为空,则从sessionStorage中重新赋值

// 以token为例,页面刷新时,重新赋值
if (sessionStorage.getItem("token")) {
store.commit("set_token", sessionStorage.getItem("token"));
}

关注 26 回答 16

夏洛克的救赎 收藏了文章 · 2018-09-21

浏览器渲染页面过程与页面优化

由一道面试题引发的思考:

从用户输入浏览器输入url到页面最后呈现 有哪些过程?
一道很常规的题目,考的是基本网络原理,和浏览器加载css,js过程。

答案大致如下:

  1. 用户输入URL地址

  2. 浏览器解析URL解析出主机名

  3. 浏览器将主机名转换成服务器ip地址(浏览器先查找本地DNS缓存列表 没有的话 再向浏览器默认的DNS服务器发送查询请求 同时缓存)

  4. 浏览器将端口号从URL中解析出来

  5. 浏览器建立一条与目标Web服务器的TCP连接(三次握手)

  6. 浏览器向服务器发送一条HTTP请求报文

  7. 服务器向浏览器返回一条HTTP响应报文

  8. 关闭连接 浏览器解析文档

  9. 如果文档中有资源 重复6 7 8 动作 直至资源全部加载完毕

以上答案基本简述了一个网页基本的响应过程背后的原理。
但这也只是一部分,浏览器获取数据的部分,至于浏览器拿到数据之后,怎么渲染页面的,一直没太关注。
所以抽出时间研究下浏览器渲染页面的过程。
通过研究,了解一些基本常识的原理:

  1. 为什么要将js放到页脚部分

  2. 引入样式的几种方式的权重

  3. css属性书写顺序建议

  4. 何种类型的DOM操作是耗费性能的

浏览器渲染主要流程

不同的浏览器内核不同,所以渲染过程不太一样。

clipboard.png

WebKit 主流程

clipboard.png

Mozilla 的 Gecko 呈现引擎主流程

由上面两张图可以看出,虽然主流浏览器渲染过程叫法有区别,但是主要流程还是相同的。
Gecko 将视觉格式化元素组成的树称为“框架树”。每个元素都是一个框架。WebKit 使用的术语是“呈现树”,它由“呈现对象”组成。对于元素的放置,WebKit 使用的术语是“布局”,而 Gecko 称之为“重排”。对于连接 DOM 节点和可视化信息从而创建呈现树的过程,WebKit 使用的术语是“附加”。

所以可以分析出基本过程:

  1. HTML解析出DOM Tree

  2. CSS解析出Style Rules

  3. 将二者关联生成Render Tree

  4. Layout 根据Render Tree计算每个节点的信息

  5. Painting 根据计算好的信息绘制整个页面

HTML解析

HTML Parser的任务是将HTML标记解析成DOM Tree
这个解析可以参考React解析DOM的过程,
但是这里面有很多别的规则和操作,比如容错机制,识别</br><br>等等。
感兴趣的可以参考 《How Browser Work》中文翻译
举个例子:一段HTML

<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>

经过解析之后的DOM Tree差不多就是

clipboard.png

将文本的HTML文档,提炼出关键信息,嵌套层级的树形结构,便于计算拓展。这就是HTML Parser的作用。

CSS解析

CSS Parser将CSS解析成Style Rules,Style Rules也叫CSSOM(CSS Object Model)。
StyleRules也是一个树形结构,根据CSS文件整理出来的类似DOM Tree的树形结构:

clipboard.png

于HTML Parser相似,CSS Parser作用就是将很多个CSS文件中的样式合并解析出具有树形结构Style Rules。

脚本处理

浏览器解析文档,当遇到<script>标签的时候,会立即解析脚本,停止解析文档(因为JS可能会改动DOM和CSS,所以继续解析会造成浪费)。
如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。现在可以在script标签上增加属性 defer或者async
脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM Tree和Style Rules上。

呈现树(Render Tree)

Render Tree的构建其实就是DOM Tree和CSSOM Attach的过程。
呈现器是和 DOM 元素相对应的,但并非一一对应。Render Tree实际上就是一个计算好样式,与HTML对应的(包括哪些显示,那些不显示)的Tree。

在 WebKit 中,解析样式和创建呈现器的过程称为“附加”。每个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。

clipboard.png

样式计算

样式计算是个很复杂的问题。DOM中的一个元素可以对应样式表中的多个元素。样式表包括了所有样式:浏览器默认样式表,自定义样式表,inline样式元素,HTML可视化属性如:width=100。后者将转化以匹配CSS样式。

WebKit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系,并且:

  1. 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是“:hover”状态,而另一个不是)

  2. 任何元素都没有 ID

  3. 标记名称应匹配

  4. 类属性应匹配

  5. 映射属性的集合必须是完全相同的

  6. 链接状态必须匹配

  7. 焦点状态必须匹配

  8. 任何元素都不应受属性选择器的影响,这里所说的“影响”是指在选择器中的任何位置有任何使用了属性选择器的选择器匹配

  9. 元素中不能有任何 inline 样式属性

  10. 不能使用任何同级选择器。WebCore 在遇到任何同级选择器时,只会引发一个全局开关,并停用整个文档的样式共享(如果存在)。这包括 + 选择器以及 :first-child 和 :last-child 等选择器。

为了简化样式计算,Firefox 还采用了另外两种树:规则树和样式上下文树。WebKit 也有样式对象,但它们不是保存在类似样式上下文树这样的树结构中,只是由 DOM 节点指向此类对象的相关样式。

clipboard.png

样式上下文包含端值。要计算出这些值,应按照正确顺序应用所有的匹配规则,并将其从逻辑值转化为具体的值。
例如,如果逻辑值是屏幕大小的百分比,则需要换算成绝对的单位。规则树的点子真的很巧妙,它使得节点之间可以共享这些值,以避免重复计算,还可以节约空间。
所有匹配的规则都存储在树中。路径中的底层节点拥有较高的优先级。规则树包含了所有已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为所有的节点进行计算,而是只有当某个节点样式需要进行计算时,才会向规则树添加计算的路径。

举个例子 我们有段HTML代码:

<html>
  <body>
    <div class="err" id="div1">
      <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
      </p>
    </div>
    <div class="err" id="div2">another error</div>
  </body>
</html>

对应CSS规则如下:

1. .div {margin:5px;color:black}
2. .err {color:red}
3. .big {margin-top:3px}
4. div span {margin-bottom:4px}
5. #div1 {color:blue}
6. #div2 {color:green}

则CSS形成的规则树如下图所示(节点的标记方式为“节点名 : 指向的规则序号”)

clipboard.png

假设我们解析 HTML 时遇到了第二个 <div> 标记,我们需要为此节点创建样式上下文,并填充其样式结构。
经过规则匹配,我们发现该 <div> 的匹配规则是第 1、2 和 6 条。这意味着规则树中已有一条路径可供我们的元素使用,我们只需要再为其添加一个节点以匹配第 6 条规则(规则树中的 F 节点)。
我们将创建样式上下文并将其放入上下文树中。新的样式上下文将指向规则树中的 F 节点。

现在我们需要填充样式结构。首先要填充的是 margin 结构。由于最后的规则节点 (F) 并没有添加到 margin 结构,我们需要上溯规则树,直至找到在先前节点插入中计算过的缓存结构,然后使用该结构。我们会在指定 margin 规则的最上层节点(即 B 节点)上找到该结构。

我们已经有了 color 结构的定义,因此不能使用缓存的结构。由于 color 有一个属性,我们无需上溯规则树以填充其他属性。我们将计算端值(将字符串转化为 RGB 等)并在此节点上缓存经过计算的结构。

第二个 <span> 元素处理起来更加简单。我们将匹配规则,最终发现它和之前的 span 一样指向规则 G。由于我们找到了指向同一节点的同级,就可以共享整个样式上下文了,只需指向之前 span 的上下文即可。

对于包含了继承自父代的规则的结构,缓存是在上下文树中进行的(事实上 color 属性是继承的,但是 Firefox 将其视为 reset 属性,并缓存到规则树上)
所以生成的上下文树如下:

clipboard.png

以正确的层叠顺序应用规则

样式对象具有与每个可视化属性一一对应的属性(均为 CSS 属性但更为通用)。如果某个属性未由任何匹配规则所定义,那么部分属性就可由父代元素样式对象继承。其他属性具有默认值。
如果定义不止一个,就会出现问题,需要通过层叠顺序来解决。

一些例子:

 *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

利用上面的方法,基本可以快速确定不同选择器的优先级。

布局Layout

创建渲染树后,下一步就是布局(Layout),或者叫回流(reflow,relayout),这个过程就是通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置,而有些时候我们会在文档布局完成后对DOM进行修改,这时候可能需要重新进行布局,也可称其为回流,本质上还是一个布局的过程,每一个渲染对象都有一个布局或者回流方法,实现其布局或回流。

对渲染树的布局可以分为全局和局部的,全局即对整个渲染树进行重新布局,如当我们改变了窗口尺寸或方向或者是修改了根元素的尺寸或者字体大小等;而局部布局可以是对渲染树的某部分或某一个渲染对象进行重新布局。

大多数web应用对DOM的操作都是比较频繁,这意味着经常需要对DOM进行布局和回流,而如果仅仅是一些小改变,就触发整个渲染树的回流,这显然是不好的,为了避免这种情况,浏览器使用了脏位系统,只有一个渲染对象改变了或者某渲染对象及其子渲染对象脏位值为”dirty”时,说明需要回流。

表示需要布局的脏位值有两种:

  • “dirty”–自身改变,需要回流

  • “children are dirty”–子节点改变,需要回流

布局是一个从上到下,从外到内进行的递归过程,从根渲染对象,即对应着HTML文档根元素,然后下一级渲染对象,如对应着元素,如此层层递归,依次计算每一个渲染对象的几何信息(位置和尺寸)。

每一个渲染对象的布局流程基本如:

  • 1.计算此渲染对象的宽度(width);

  • 2.遍历此渲染对象的所有子级,依次:

    • 2.1设置子级渲染对象的坐标

    • 2.2判断是否需要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height)

  • 3.设置此渲染对象的高度:根据子渲染对象的累积高,margin和padding的高度设置其高度;

  • 4.设置此渲染对象脏位值为false。

绘制(Painting)

在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。

CSS2 规范定义了绘制流程的顺序。绘制的顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块呈现器的堆栈顺序如下:

  1. 背景颜色

  2. 背景图片

  3. 边框

  4. 子代

  5. 轮廓

这里还要说两个概念,一个是Reflow,另一个是Repaint。这两个不是一回事。
Repaint ——屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变。
Reflow 元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,需要重新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,在reflow过程中,可能会增加一些frame,比如一个文本字符串必需被包装起来。

Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。在一些高性能的电脑上也许还没什么,但是如果reflow发生在手机上,那么这个过程是非常痛苦和耗电的。 所以,下面这些动作有很大可能会是成本比较高的。

  • 当你增加、删除、修改DOM结点时,会导致Reflow或Repaint

  • 当你移动DOM的位置,或是搞个动画的时候。

  • 当你修改CSS样式的时候。

  • 当你Resize窗口的时候(移动端没有这个问题),或是滚动的时候。

  • 当你修改网页的默认字体时。

  • 注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。

基本上来说,reflow有如下的几个原因:

  • Initial。网页初始化的时候。

  • Incremental。一些Javascript在操作DOM Tree时。

  • Resize。其些元件的尺寸变了。

  • StyleChange。如果CSS的属性发生变化了。

  • Dirty。几个Incremental的reflow发生在同一个frame的子树上。

看几个例子:

$('body').css('color', 'red'); // repaint
$('body').css('margin', '2px'); // reflow, repaint

var bstyle = document.body.style; // cache

bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; //  再一次的 reflow 和 repaint

bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint

bstyle.fontSize = "2em"; // reflow, repaint

// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));

当然,我们的浏览器是聪明的,它不会像上面那样,你每改一次样式,它就reflow或repaint一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行reflow。

但是有些时候,我们的脚本会阻止浏览器这么干,比如:如果我们请求下面的一些DOM值:

offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
IE中的 getComputedStyle(), 或 currentStyle

因为,如果我们的程序需要这些值,那么浏览器需要返回最新的值,而这样一样会flush出去一些样式的改变,从而造成频繁的reflow/repaint。

Chrome调试工具查看页面渲染顺序

页面的渲染详细过程可以通过chrome开发者工具中的timeline查看

clipboard.png

  1. 发起请求;

  2. 解析HTML;

  3. 解析样式;

  4. 执行JavaScript;

  5. 布局;

  6. 绘制

页面渲染优化

浏览器对上文介绍的关键渲染路径进行了很多优化,针对每一次变化产生尽量少的操作,还有优化判断重新绘制或布局的方式等等。
在改变文档根元素的字体颜色等视觉性信息时,会触发整个文档的重绘,而改变某元素的字体颜色则只触发特定元素的重绘;改变元素的位置信息会同时触发此元素(可能还包括其兄弟元素或子级元素)的布局和重绘。某些重大改变,如更改文档根元素的字体尺寸,则会触发整个文档的重新布局和重绘,据此及上文所述,推荐以下优化和实践:

  1. HTML文档结构层次尽量少,最好不深于六层;

  2. 脚本尽量后放,放在前即可;

  3. 少量首屏样式内联放在标签内;

  4. 样式结构层次尽量简单;

  5. 在脚本中尽量减少DOM操作,尽量缓存访问DOM的样式信息,避免过度触发回流;

  6. 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;

  7. 动画尽量使用在绝对定位或固定定位的元素上;

  8. 隐藏在屏幕外,或在页面滚动时,尽量停止动画;

  9. 尽量缓存DOM查找,查找器尽量简洁;

  10. 涉及多域名的网站,可以开启域名预解析

总结

浏览器渲染是个很繁琐的过程,其中每一步都有对应的算法。
了解渲染过程原理可以有针对的性能优化,而且也可以懂得一些基本的要求和规范的原理。
最后文章中间很多语句都是直接复制的原文,自己的语言概况还是不及原文精彩。

参考链接

  1. 《How Browser Work》

  2. 浏览器的工作原理:新式网络浏览器幕后揭秘

  3. 浏览器渲染原理

  4. 浅析前端页面渲染机制

  5. 浏览器 渲染,绘制流程及性能优化

  6. 优化CSS重排重绘与浏览器性能

查看原文

认证与成就

  • 获得 4 次点赞
  • 获得 17 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 15 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-09-20
个人主页被 623 人浏览