Giter Club home page Giter Club logo

chungguo's Introduction

Hi there

My nickname is chungguo

📫   How to reach me: @chungguo_me

👨‍💻   Read more about my posts at chungguo.me

chungguo's People

Contributors

chungguo avatar dependabot[bot] avatar github-actions[bot] avatar imgbot[bot] avatar

Watchers

 avatar

chungguo's Issues

前端工程师职业生涯中的三个核心问题

自 2016 年正式参加工作,至今已四年有余。和普通的大多数人一样,无论在何处任职,当前正在解决什么问题,似乎都曾陷入一种循环当中:

开始 -> 熟练 -> 厌倦 -> 再开始

从一个公司或者一个项目开始,经过日积月累的经验积累,逐渐对项目得心应手,然后进阶成了一名「专业的熟练工」。长此以往,日复一日,往日手到擒来的事情因缺乏最初的挑战和新鲜感而逐渐让人激情减退,从而开始慢慢进入了厌倦期。长期无法提振的心情会促使自己想着换一家公司或项目重新开始。殊不知,又再一次看到了曾经的自己。于是开始思考,问题出现在了哪里?

在和前辈、大佬的多次沟通后,终有一日,恍然大悟:在无尽的需求之外,真正能够让自己进阶提升的是解决框架、性能及工具三大核心问题。

说到框 /kuàng/(有且只有这一个读音) 架,也许大部分同学第一反应是要求自己做一个像 VueExpress 一样的大而全的优秀开源框架。长久以来,我也是一直如此理解。其实不然,所谓框架其实就是一件事物的总体结构。以建筑为例上,钢筋混凝土加空间布局就是其框架,使用何种型号的水泥砖瓦以及加入何种比例的钢筋,决定着大楼框架搭建的是否牢固稳定,从而决定楼层能盖的有多高。而其内部的空间设计是否合理,决定着住户今后的移动轨迹及装修方案,从而决定其居住体验。相似的,在日常项目中,如果有幸能够成为项目的第一批开发者,如果能在最开始时,为整个项目抽象出一个合理的模型,考虑好稳定性、扩展性、通用性问题,那么其实你就已经做了一个好的框架。更简单的而言 ,框架是你面对某一问题时的实现方案及代码结构。所以,不必畏惧框架这个概念,也许曾经你已经设计实现了一些框架。

再说性能,想必无需多言,每一位工程师都会有意识去优化代码实现,希望能够在性能上有所突破,从而提升用户体验,节约成本。而面对不同问题与不同实现,性能优化方案又各有不同,此处便不一而足了。但不得不多说的是,性能优化是最容易给出量化指标的方向,需要我们深入理解「性能」,已经如何正确衡量。比如首屏时间,performance.timing 中的 FSTFCP 往往不能正确反应真实信息。在结合自身业务特性后,给出符合当前需求的统计方案就显得尤为重要。同时,无论在做任何优化动作之前,都应当对当前系统的性能数据有定量的数据采集。优化前后的数据对比是最有说服力的证据。

最后,所谓工具其实是效率的提升。一个项目或业务中,往往存在大量或重复或机械性的事务,如果能够细心观察,能够通过工具的手段将同类事务加以解决,那会是对人力资源成本的大幅缩减,而当今社会,人力资源又是最贵的资源,所以,能够在效率上有所突破,一定会是工作成果中的一大亮点。换个角度而言,工具研发是投入产出比最高的工作投入,一次工作投入会在将来无限次重复享受其成果,何乐而不为。

以上,便是工作以来的核心经验总结,希望对你有所帮助,也希望自己或大家在找不到方向时能够从框架、性能、工具三个方向多一些思考。

CSS 变换背后的线性变换原理

CSStransform 属性允许开发者无需借助 SVGCanvasJavascript 等方式,仅仅通过 CSS 便能以尽可能简单的方式实现对指定标签元素的旋转(rotate)、缩放(scale)、倾斜(skew)、平移(translate)等操作,大大丰富了前端页面布局及动画效果的可能性。但更深层次而言,在这些直观易懂的属性背后却是更为高阶的 matrix 属性发挥着作用。如果能够正确理解 matrix 属性及其背后的线性变换原理,那么再回头看 matrix 属性时,便会有拨开云雾见青天的快感。

transform

1. 我们为什么需要理解 matrix 属性

1.1 形式简洁

在我们的实际使用场景中,对目标对象的变换往往不是单一的,即一次变换通常是旋转、缩放、平移等变换的多次组合。例如,在一次 2D 平面的变换中,先水平和垂直方向各平移 100px,然后水平方向缩放 2 倍,垂直方向缩放 4 倍,那么常用的实现方式如下所示:

div {
  transform: translate(100px, 100px) scale(2, 4)
}

使用以上这种表示方法,固然十分清晰的描述了整个变换过程,但当我们的变换需要完成十次八次乃至更多次类似的基础变换才能完成时,如此书写便会略显冗长。同时,变换的书写顺序也至关重要,相同的两次变换,相反的顺序,结果也有可能千差万别。此时,如果使用 matrix 属性描述相同的变换组合,形式上便会显得更加简洁。当然,这是以牺牲可读性为代价。

div {
  transform: matrix(2, 0, 0, 4, 100, 100); 
}

如上所示,如果简单从形式上看 matrix 属性隐藏了变换的细节,更加强调了最终的变换结果。关于表述形式上的差异,我们可以拿 css 中字体颜色的设置做类比。同样是将字体颜色设置为白色,我们经常使用的有十六进制表示法和 RGB 表示法,两者的最终结果是一致,但 RGB 表示法更加 Human Friendly,可以很清晰的看出 RGB 分量值。相对的,十六进制表示法则更加 Machine Friendlymatrix 属性便是 transform 诸多属性中 「Machine Friendly」 的那一个。

/* 十六进制表示法 */
h1 {
  color: #FFFFFF;
}
/* RGB表示法 */
h2 {
  color: rgb(255, 255, 255);
}

1.2 JavaScript 解析计算

目前,在所有支持 CSS transform 属性的的浏览器中,当我们使用 window.getComputedStyle 方法获取元素所有最终应用的 CSS 属性值时,细心的你肯定早已发现,所有的 translaterotateskew 都被解析成了 matrix 属性。例如,xscroll 模拟滚动实现方案的原理便是使用了 transformtranslate 属性,浏览器会将其转换成等价的 matrix 属性。如果不理解 matrix 属性,那么获取从 computedStyle 中获取当前滚动值就会比较困难。
translate 最终会被转化为 matrix

1.3 矩阵变换实现图形变换

如果说从上述两个角度解释为什么我们需要理解看似复杂的 matrix 属性是站在更加上层的应用层角度做出的解释。那么从更根本的层面上而言,是因为计算机对图形图像的处理过程,需要用到各种各样的几何变换,而这些变换基本都是由平移、缩放、旋转等基本变换组合而来(其实,这也解释了为什么 css transform 提供了 translaterotatescaleskew 等属性,因为这些属性恰恰正是这些基础变换的语法糖)。对于一个指定空间中的指定图形图像,为了便于精确描述,通常会将其放入一个坐标系统中,这样图形图像上的每一个点都有一个唯一的坐标。对图形图像的变换实际就是图形图像上的每一个点进行坐标变换,如果使用传统的解析几何描述这些坐标变换,计算会变得非常复杂,显然不符合「大道至简」自然法则。矩阵的出现则在形式上更加简洁优美,同时将各种变换通过矩阵乘法运算进行了语言表达的统一,具体而言,二维平面内 css transform 基础变换均可以统一为如下所示的三行三列的矩阵乘法:

rotate、skew、scale、translate 变换的 matrix 表示法,将所有变换统一为了矩阵的乘法运算

从上列各式,我们引入了矩阵的简单概念及形式,但因矩阵本质上描述的是向量空间(线性空间)中的线性变换。所以,了解矩阵之前,首先需要了解什么是向量、向量空间以及什么是线性变换。

2. 向量与向量空间

向量是一个同时具有大小和方向,且满足平行四边形法则的几何对象,我们高中物理力学及解析几何中称之为矢量。更形象而言,从物理学的角度来看,我们习惯于使用一个有方向的箭头表示,且物理中的向量是可以随意移动的,只要向量的大小和方向不变,无论如何移动,均为同一向量,如图:

向量表示

而站在数字化的计算机角度,向量则表示的是一有序的列表,类似数组。因数学是高度抽象的,所以从数学的角度出发,只要满足一定的规则,向量可以是一个具体的函数,也可以是经抽象后更一般的向量概念,使用向量符号,如 $\vec v$

而所谓空间,我们最熟悉的莫过于每天生活的这个三维空间(牛顿绝对时空观)。在这个空间中,有草木虫鱼,有人及人类活动,有位置坐标,以及看不见摸不着的空间中每个个体之间的联系,且这个三维空间中的一切在指定参考系下,都按照一定的自然规律运转。所以类似的,对于向量空间,也称线性空间,我们可以这样简单的类比理解:向量空间中的任何一个对象,在数学上通过选取基(参考系)和坐标的办法,都可以表达为向量的形式,而可加性和数乘性是这个空间的「自然规律」,线性变换便是发生在向量空间中符合「自然规律」的「运动」。

3. 线性变换

线性空间(向量空间)中对象的「运动」被称作线性变换。换言之,线性空间中的任意一点的运动都可以用线性变换来完成,而点汇聚成线,线展开成面,面旋转成体。从而,我们可以说线性变换描述了线性空间中对象的变换,进而实现图形图像的变换操作。

3.1 线性

「线性」直观印象可能就是一条直线,对于如何描述直线,我们初中就学过——一次函数,不失一般性,形如:$y = kx + b$。其中$k$,$b$是常数且 $k \neq 0$ 。但是,从代数层面上而言,线性要满足以下两条性质。

  1. 可加性:$f(x_{1} + x_{2}) = f(x_{1}) + f(x_{2})$,即自变量单独作用的结果与自变量共同作用的结果相同。
  2. 数乘性:$f(ax) = af(x)$,其中$a$为常数。即因变量与自变量等比例变化。

可加性和数乘性是线性变换的基础,因为矩阵加法、矩阵数乘运算基本构成了线性变换的全部,所以理解可加性和数乘性至关重要。

如果同时满足可加性与数乘性(比例性),我们就认为是线性的。所以从代数层面上看$ y= kx + b$并不是严格的线性函数,因为如果
$$ f(x) = kx + b $$
则:
$$f(x_{1} + x_{2}) = k(x_{1} + x_{2}) + b $$
$$f(x_{1}) + f(x_{2}) = k(x_{1} + x_{2}) + 2b $$

显然,当$b \neq 0$时,$f(x_1 + x_2) \neq f(x_1) + f(x_2) $,不满足可加性。再看数乘性:

$$ f(ax) = akx + b $$ $$ af(x) = a(kx + b) = akx + ab $$

显然,当$ a \neq 1$ 时,不满足数乘性。故而,正比例函数 $ y= kx $ 才是最简单的线性函数。

4. 变换

变换(transformation)指的是空间中从一个点/元素到另一个点/元素的运动,但它不是微积分中的连续性的运动,而是瞬间发生的变化。以我们熟知的函数做类比,函数接收一个输入,得到一个输出。类似的,变换亦如此。之所以没有使用「函数」的概念,也许正是想让我们抛开代数式的理解方式,以运动的眼光看变换。

变换

至此,一言以蔽之,向量是线性空间的基本元素,所有满足可加性与数乘性的向量集合构成了向量空间,我们用向量坐标表示矩阵,通过矩阵运算描述线性变换。

5. 矩阵

从数学定义而言,矩阵是一系列复数或者实数的集合,最早是由方程组的系数和常数项构造而来。

$$ \begin{cases} x+y+z = 6 \\ 0x + 2y+5z = -4 \\ 2x + 5y - z = 27 \end{cases} \Rightarrow \begin{bmatrix} 1 & 1 & 1 \\ 0 & 2 & 5 \\ 2 & 5 & -1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \end{bmatrix} = \begin{bmatrix} 6 \\ -4 \\ 27 \end{bmatrix}$$

既然有了$x, y, z$的出现,那么从代数未知数$x, y, z$到三维几何空间中某一个点的坐标$(x, y, z)$的联想便显得非常自然而然。在线性代数中,所有的向量必须从坐标原点出发,那么向量的终点坐标$(x, y)$便唯一确定了该向量,为了与坐标表示做区分,向量使用$ \begin{bmatrix} x \\ y \end{bmatrix} $表示。那么从矩阵的角度看上述方程组,就相当于在问:空间中的一向量$ \begin{bmatrix} x \\ y \\ z \end{bmatrix}$ 经矩阵$\begin{bmatrix} 1 & 1 & 1 \\ 0 & 2 & 5 \\ 2 & 5 & -1 \\ \end{bmatrix}$变换后,转换为了向量$ \begin{bmatrix} 6 \\ -4 \\ 27 \\ \end{bmatrix}$,求该向量。先不论该问题到底如何求解,但由此,我们至少说明了矩阵是如何描述向量空间中的线性变换——矩阵乘法。又如,对于空间中向量$\begin{bmatrix} x \\ y \\ z \end{bmatrix}$先进行$A$变换,再进行$B$变换,我们可以表示为

transform

只不过形式上,变换的顺序是从右到左的,与我们常见的函数形式做类似,则形如$g(f(x))$,我们会先计算$f(x)$,然后才是$g(t)$,变换$A$则相当于函数$f$,变换$B$则相当于函数$g$。如此从形式上做对比,便更利于理解。当我们完成从代数到几何的思维转换之后,接下来我们就需要在几何中确定一组单位向量(基向量)以便于表示和理解矩阵计算。众所周知,我们定义沿着坐标轴方向,长度为单位长度的向量为该方向的单位向量(基向量),如$x$轴的基向量为$\overrightarrow{OA}$,记作$\overrightarrow{i}$,$y$轴的基向量为$\overrightarrow{OB}$,记作$\overrightarrow{j}$。那么在确定一组基向量后,由该组基向量所构成的线性空间内的所有向量均可用该组基向量的线性组合表示。由于变换是线性的,所以我们只需要知道该组基向量经线性变换后被变换到了哪里,就可以知道任何一个向量经变换后的坐标表示。

基向量及其组合而来的其他向量

那么举例而言$\overrightarrow{OC}$的坐标计算如下:

$$ \overrightarrow{OC} = 2 \overrightarrow{OA} + 2 \overrightarrow{OB} = 2 \begin{bmatrix}1 \\ 0 \end{bmatrix} + 2 \begin{bmatrix} 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 2 \times 1 + 2 \times 0 \\ 2 \times 0 + 2 \times 1 \end{bmatrix} = \begin{bmatrix} 2 \\ 2 \end{bmatrix} = 2 \begin{bmatrix} 1 \\ 1 \end{bmatrix} $$

由上式可知,向量$\overrightarrow{OC}$可以看作是$\overrightarrow{oa}$在$X$和$Y$轴方向放大两倍之后的结果,也就是保持坐标原点位置及坐标不动,将$X$轴和$Y$轴放大为原来的 2 倍,此时与正好相等。放大后,原来的基向量$\overrightarrow{i}$与$ \overrightarrow{j} $坐标分别变为了$\begin{bmatrix} 2 \\ 0 \end{bmatrix}$和$\begin{bmatrix} 0 \\ 2 \end{bmatrix}$,这个变换过程可以用矩阵表示为$\overrightarrow{OC} = \begin{bmatrix} 2 \\ 2 \end{bmatrix} = \begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix} \begin{bmatrix} 1 \\ 1 \end{bmatrix}$,即向量$\overrightarrow{oa} = \begin{bmatrix} 1 \\ 1 \end{bmatrix}$,经过矩阵$\begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix}$变换由原坐标$\begin{bmatrix} 1 \\ 1 \end{bmatrix}$变成了$\begin{bmatrix} 2 \\ 2 \end{bmatrix}$,变换矩阵$\begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix}$的第一列正好为变换后$\overrightarrow{i}$的坐标,第二列则为$\overrightarrow{j}$的坐标。我们追踪基向量的变化过程,$i: \begin{bmatrix} 1 \\ 0 \end{bmatrix} => \begin{bmatrix} 2 \\ 0 \end{bmatrix}$,$j: \begin{bmatrix} 0 \\ 1 \end{bmatrix} => \begin{bmatrix} 0 \\ 2 \end{bmatrix}$,所以此处,由变换后$\overrightarrow{i}$和$\overrightarrow{j}$向量坐标组成的矩阵$\begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix}$表示放大 2 倍的缩放变换矩阵。
$$ \overrightarrow{OC} = \begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix} \begin{bmatrix} 1 \\ 1 \end{bmatrix} = 1 \begin{bmatrix} 2 \\ 0 \end{bmatrix} + 1 \begin{bmatrix} 0 \\ 2 \end{bmatrix} = \begin{bmatrix} 1 \times 2 + 1 \times 0 \\ 1 \times 0 + 1 \times 2 \end{bmatrix} = \begin{bmatrix}2 \\ 2 \end{bmatrix} $$ (该计算过程非常重要,是可加性与数乘性的体现,也是其他复杂计算的基础)

6. CSS 中基础变换的 matrix 表示推导

至此,我们回过头来看,上文图 3 中的基础变换为何可以与复杂的 matrix 对应。但在此之前,首先要解释一下,为何在二维坐标中矩阵变换会需要三维坐标表示。其实,这里主要引入了齐次坐标的概念,也就是用 N+1 维来代表 N 维坐标。还记得上一节缩放矩阵的例子吗?在缩放时,我们并没有改变原点的位置,因为线性变换要求变换前后坐标原点不能发生变化,平移变化移动了原点,所以不能称之为线性变化。把现有的二维空间升纬到三维空间去看,而多出的那一维度对于二维空间而言并用不到,所以并不会有任何影响,升维之后便可以在高维度通过线性变换完成低维度的仿射变换。

仿射变换

另外,引入齐次坐标也可以统一计算形式,因为平移变化在表示上为矩阵加法,如将矩阵$\overrightarrow{OC} = \begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix}$向右平移一个单位长度,那么其计算方法为:$\overrightarrow{OC'} = \begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix} + \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} = \begin{bmatrix} 3 & 0 \\ 0 & 3 \end{bmatrix}$。但缩放、旋转却为矩阵乘法,如将$\overrightarrow{OC} = \begin{bmatrix} 2 \\ 2 \end{bmatrix}$放大 0.5 倍,其计算方法为 $\overrightarrow{OC''} =
\begin{bmatrix} 0.5 & 0 \\ 0 & 0.5 \end{bmatrix} \begin{bmatrix} 2 \\ 2 \end{bmatrix} = \begin{bmatrix} 1 \\ 1 \end{bmatrix} $ 这样在一次平移与缩放同时存在的复杂变换中,就会存在矩阵加法与乘法的同时存在,因加法与乘法的计算规则不同,势必会增添计算的复杂性。齐次坐标的引入,便是为了解决该问题,将矩阵加法统一成了矩阵乘法,描述和计算上更加简洁,是一种数学之美的体现。下面,我们来看图 3 中各式的推导过程:

旋转变换

对于旋转变换,如果进行逆时针旋转$\theta$,那么基向量的变化过程,$i: \begin{bmatrix} 1 \\ 0 \end{bmatrix} => \begin{bmatrix} cos(\theta) \\ sin(\theta) \end{bmatrix}$,$j: \begin{bmatrix} 0 \\ 1 \end{bmatrix} => \begin{bmatrix} -sin(\theta) \\ cos(\theta) \end{bmatrix}$,故而对于二维平面内任意向量,其旋转矩阵用笛卡尔坐标系表示则为:$\begin{bmatrix} cos(\theta) & -sin(\theta) \\ sin(\theta) & cos(\theta)\end{bmatrix} $,引入齐次坐标后,则表示为$\begin{bmatrix} cos(\theta) & -sin(\theta) & 0 \\ sin(\theta) & cos(\theta) & 0 \\0 & 0 & 1 \end{bmatrix}$(引入$Z$轴基向量$\begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix}$,下同),所以对向量$\begin{bmatrix} x \\ y \\ 1\end{bmatrix}$进行旋转,矩阵可表示为$rotate(\theta) = \begin{bmatrix} cos(\theta) & -sin(\theta) & 0 \\ sin(\theta) & cos(\theta) & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1\end{bmatrix}$

对于缩放变换 (scale) 则很好理解,如果单纯对$\overrightarrow{i}$进行 $m$ 倍缩放,则的变换为$i: \begin{bmatrix} 1 \\ 0 \end{bmatrix} => \begin{bmatrix} m \\ 0 \end{bmatrix}$,同理,当单纯对$\overrightarrow{j}$进行 $n$ 倍缩放,则的变换为$j: \begin{bmatrix} 0 \\ 1 \end{bmatrix} => \begin{bmatrix} 0 \\ n \end{bmatrix}$,所以,缩放变换矩阵用笛卡尔坐标系表示则为:$\begin{bmatrix} m & 0 \\ 0 & n \end{bmatrix}$,引入齐次坐标后,则表示为$\begin{bmatrix} m & 0 & 0 \\ 0 & n & 0 \\ 0 & 0 & 1 \end{bmatrix}$。所以对向量$ \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $进行缩放,矩阵可表示为:

$$scale(m, n) = \begin{bmatrix} m & 0 & 0 \\ 0 & n & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}$$

斜切变换

对于斜切变换,参照示意图,我们同样不难推导出斜切变换的矩阵表示:

$$skew(\alpha, \beta) = \begin{bmatrix} 1 & tan(\alpha) & 0 \\ tan(\beta) & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}$$

最后,对于二维空间的平移变换,我们通过仿射变换实现,参照图 8,变换过程中$\overrightarrow{i}$,$\overrightarrow{j}$基向量并未发生改变,仅仅是$Z$轴基向量发生了改变,因此平移变换的矩阵表示为:

$$translate(m, n) = \begin{bmatrix} 1 & 0 & m \\ 0 & 1 & n \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}$$

至此,希望当下次看到 matrix 属性时,对你而言,其中的数字并不仅仅是一堆数字,而代表着一种具体变换

7. 线性化的其他应用场景

目前火热的人脸识别技术便是通过线性化实现的。人可以很容易区分两张面容图是不是同一个人,而计算机只能将所有问题数字化,实现方案便是将人脸线性化。如果将人脸中较为重要的鼻子、眼睛、嘴巴等采样点数据通过某种算法数字化,并用坐标表示,那么当给出一张新的面容时,按照相同的方法算出当前面容图的坐标,通过结果坐标是否落在目标平面或平面附近,就可以判断是不是同一个人。


参考资料

  1. The CSS3 matrix() Transform for the Mathematically Challenged
  2. CSS Transforms Module Level 1
  3. 3Blue1Brown

被忽略的关键信息

在前端领域内,框架及工具库的百花齐放,不仅提高了我们的日常工作效率,同时也为后来者提供了优秀的学习素材。通过优秀开源项目源码的阅读与学习,我们能够从中吸收最佳实践、框架设计**、奇技淫巧等知识,并为我所用,助我升职加薪。

通常而言,我们不会在对一个框架或库一无所知的情况下,就直接一头栽到庞杂的源码中去,毕竟干嚼馒头并不甜。相反,源码阅读一定要建立在自己已经对目标项目有所认知,最好能够熟练使用的基础上。通过日常使用,发现其中的某些功能或实现非常值得学习,再开始源码的阅读之旅,往往能够事半功倍。

而对于前端项目而言,阅读完 README.md 之后,拿到源码,需要阅读的第一个文件往往是该项目的描述文件 ——package.json 文件。在其中,我们能够找到项目的入口文件( main )是什么,依赖( dependencies )及开发依赖( devDependencies )有哪些,以及有哪些可供使用的( scripts )脚本。知道了这些,就相当于参观景区时,找到了景区的入口。从入口开始,按图索骥就能够完整的浏览完整个景区。但在浏览的过程中,我们只能看到眼前的景观,无法对景区的整体分布及行进路径了然于胸,故而容易迷路及重复走同一条路,效率不高。

阅读源码也一样,main 指定了项目入口,从入口文件开始,按照文件索引关系,也能完成源码的阅读。按照此方法阅读,我们能看清更多的实现细节,但也更容易陷入实现细节之中,拖慢阅读进度。同时,因缺少对项目整体架构的认识,脱离项目整体去看细节,往往会造成理解上的困扰,当这种困扰积少成多时,阅读积极性则会受到致命打击。这种阅读方法,只适合那些对源码已经有所了解同学使用。

这个时候,我们就需要在浏览景区之前,找到景区的浏览全景地图。那怎么才能在开始阅读源码前,找到项目的全景图呢?我之前的做法是在网上搜索别人的阅读笔记,久而久之发现这种做法效率较低:

  • 一是信息检索筛选耗费时间精力
  • 二是要核对他人的阅读笔记所基于的项目版本要与自己要阅读的版本一致
  • 三是,网络信息质量与准确性无法保证
  • 四是,他人消化后的二手信息总没有自己理解后的内容来的深刻

最近,在阅读 Vue 源码的过程中偶然间发现,之前一直当作隐藏/配置文件而被忽略的 .github 文件夹里却藏着「官方的项目全景图」。一般而言,开源共建项目都会包含该目录,目录下包含但不限于如下内容:

文件名 功能描述
ISSUE_TEMPLATE.md Issues 模版
PULL_REQUEST_TEMPLATE.md Pull Requests 模版
CONTRIBUTING.md 代码贡献指引
... ...

这些文件,望文生义,向开发者描述了如果向当前项目提 issue、发起 PR,以及如何贡献代码。因此,我们主要关注 CONTRIBUTING.md 文件,该文件向开发者描述了如何启动、调试项目。Vue 同时也向开发者介绍了 项目结构

这便是我们要找寻的「项目全景图」,它比任何其他个人的阅读笔记更加准确、清晰、具有关键的概括性。从而帮助我们在阅读的过程中,准确排除干扰信息,不被细节羁绊,紧抓主线。

除此之外,如果项目中存在 examplestest 目录,其中一般存放简短的示例文件,或者单一功能的测试文件,也建议在进入正式的源码阅读之前,能够快速浏览下该目录的内容,不失为阅读前的一项不错的热身运动。

我的虚拟云主机

2014-10-30 我在阿里云注册购买了第一个属于自己的域名,借助七牛云的存储服务,搭建起了第一版个人博客主页。初版的个人主页帮助我拿到了阿里巴巴的实习生 offer。巧的是,一位同组的实习生同学恰巧是一名主题开发爱好者,Github 上的项目也收获了很多的 star,而我使用的也恰巧是他的作品。

2015-07-29 同样是在阿里云,下单了第一台虚拟云主机,开始正式了解并接触 Linux 服务器。

一晃近五年过去了,如今云厂商已从阿里云更换到了腾讯云(因为老东家有福利,省钱🤦‍♂️),域名也换成了如今的 chungguo.me。记得网络上有人曾问,如果有一台自己的服务器,可以用来做什么?目前,我的答案便是下面这张图1

composition

当然,拥有一台个人主机后能做的远远不止这些。就当前个人需求而言,主要将其用作网络代理以满足家庭环境下日常工作生活需要使用(机器位于香港,无网络限制)。所以,在配置方面选择了最基本的单核CPU1GB内存的算力,将资金集中用于提升带宽。

同时,考虑到主机有 50G 的硬盘空间,便将无太多浏览量的个人主页放置在主机上,同时借助 Githubactions 功能,实现了 CI/CD 过程,使得本地提交更新后能够自动触发构建及部署工作2

物理架构
Github Runners

其次,工作之余也使用 Node 接入了公众号服务。这样,通过公众号入口,也可以完成一些服务自己及少部分人群的自定义功能。

最后,考虑经常拿这台机器做试验性操作,可能会导致服务、软件、配置等管理混乱及未来可能的机器迁移发生。便将所有在用的基础服务纳进了一个初始化脚本当中。以后在需要更换机器或重装系统时,便可以一条命令快速部署。

这样,在每月不到 22 RMB 的成本3下我得到了:

  • 完整的网络环境
  • 个人品牌影响力的主页
  • 个性化的公众号服务
  • 不受限制的内容平台
  • 熟悉掌握 Linux 系统及服务搭建

等等....

当然,借助繁荣的社区力量,你也可以部署更多个性化服务以方便自己及他人

Footnotes

  1. 2021 年 03 月,博客生成器已由 Hexo 换成了 Next.js

  2. 2021 年 05 月添加

  3. 成本低是因为在参加官网折扣活动的基础上,叠加使用了诸多优惠券,购买 48 个月的总价由原价 8122.48元 变成了 1034.41元。

拥抱变化

眼见他起高楼,眼见他宴宾客,眼见他楼塌了

——《桃花扇》

「我从远方来到陌生的地方,放眼望去一路春光不再平凡」。很快,在还没有意识到的青春,又独自走过了一个自我磨练的年头,一个留下深深烙印的年头、一个以「变化」为主基调的年头。

既然是回忆过去,而往往,回忆具有主观选择性,又参杂浓重的主观意识,所以现在看到的过去就会多少有些那么的不真实。

2015 年 02 月 10 日,为什么会记得这个日子?这一天,警察叔叔告诉我「这里没有你说话的份,你说的那些学校里的东西,实际中根本没有」。是的,后面的事情也证实他说的没错。这正如,我们总会不痛不痒感同身受着别人的感受,但真只有亲身体验后才认识的那么真切。「我们是小老百姓」,在落后的小地区生活,就还得接受那些他们眼中「合理」地处事方式。所以,我改变了对于现实的认知。

学校里的生活自然是相对安逸缓慢的,有及时行乐、无所事事者,也不乏锐意进取之人。差距,他就是这么大,这取决于你看不看得清前方,知不知道自己在做什么,最终又为了什么。同时,学校以及那些领导的档次也决定着,在你前进的方向上,我们「亲爱的母校」扮演的角色是垫脚石,还是绊脚石?有时,真搞不懂**固化迂腐,与当今各行各业飞速变革的社会严重脱节的「教育者」们,是怎么做好引导万千学子以「优雅的姿势」踏入社会这份高尚工作的。所以,我坚信再猛烈的暴风雨也阻挡不了翱翔的海燕,即使代价有时会是惨重的,因为时时刻刻知道自己在做什么显得弥足珍贵。可有太多人走着走着就迷路了。

2015 年 07月 07 日,11 时 13 分,杭州。从此时此地,我开始了至今将近半年的实习之旅。去年稍晚的这个时候,那时刚大三的我们已经感受到了就业的压力,那感觉就像:

我怎敢倒下,我身后空无一人

那种无助与迷茫难以言表。是的,我仅有的只是自己。时至今日,我不知如何去感谢遥远路途中诸如Franklittlewords师兄morgan等等给予莫大帮助的人。也正因为如此,我会尽己所能的帮助所有需要帮助的人,他们就像曾经的自己。

在阿里的那段短暂的时间里,我结实了优秀的小伙伴,更是为自己遇到一位优秀的曾手把手教自己的师兄而庆幸,师兄给了我这一年中最宝贵的收获。但也不得不「拥抱变化」,带着从「前人」那里快速学到的东西,满满不舍的离开。所以,我认识到只有自己快速变化届时才能客观接纳变化

正如开题引言,世事变化无常。入职一个月后,现处的部门又经历了一次不大不小的组织架构调整,又是一次变化。

而我本为前端,但很感激这么一次机会能够深入接触后台的开发中来,这又何尝不是一次变化呢?

工作本就是生活的一部分,一个人难免也会孤单,毕竟情感要有所依托,庆幸自己能够适时恰当调整,并能够慢慢放下不太可能的一些人一些事,也许「那些年,我们两个没有缘」。

我们还是能够很简单的看到,一切「变化」都是对于自身软实力的硬性要求,所以呢,2016 仍需不断自我提升,像我们的出身一样,弯腰埋头耕种于眼下的一小块土地,再当我们抬起头来往回看时,每一寸土地都埋下了收获的种子。相信吧,刘妈也说,所有的努力都会有回报

而其他的,自然都会水到渠成。

一图看懂 JavaScript 执行上下文

根据 ECMAScript 2015 语言规范的定义,执行上下文是抽象概念上的代码执行环境,ECMAScript 实现(通常来说就是浏览器,更具体而言就是 V8 引擎)用来记录代码运行时的值。一言以蔽之,JavaScript 无论何时都是在执行上下文中运行,且代码运行时,最多只能存在一个执行上下文。

首先,我们以 Chrome 浏览器为例,看下 V8 是如何执行 JavaScript 代码的。如下图所示,V8 引擎在生成字节码之后,会生成该段代码对应的执行上下文。

image

JavaScript 中,我们常见的通常有 3 种执行上下文,分别是:

  • 全局执行上下文

    我们知道,JavaScript 在执行的时候,默认会有一个基础的全局环境,里面存放着全局对象,在浏览器中有我们熟悉 window 对象,在 Node.js 中便是 global 对象。除此默认的全局对象之外,任何我们声明的全局变量,或者任何不在函数内的代码均是定义或运行在该全局执行上下文中。且在一段代码中,有且只有一个全局执行上下文,直至整个程序退出。

  • 函数执行上下文

    JavaScript 中,函数是一等「公民」。每一个函数都有其自己的执行上下文,每次函数在调用时,随即新的函数执行上下文也会被创建,并在函数执行结束后被销毁。

  • eval 函数执行上下文

    eval 函数被调用时,在其中运行的代码也会有一个专属的执行上下文。但是因为 eval 因安全性和性能问题并不被推荐使用,所以下文将不会对其进行介绍(主要是没有找到相关资料,但既然都属于执行上下文,其定义与其他执行上下文应无较大差异)。

如图所示,每一个执行上下文均包含 3 个内部的状态组件:

  • Code Evaluation State

    顾名思义,该组件表示任何和该执行上下文执行、暂停、恢复代码运行相关的状态

  • Function

    如果这个执行环境正在执行函数对象的代码,那么这个组件的值就是该函数对象。如果上下文执行脚本或模块的代码,则该值为 null

  • Realm

    所有的 ECMAScript 代码在执行前必须关联一个 Realm。可以简单理解为,一个 Realm 表示着该段代码能够访问哪些资源,如全局对象和全局环境。

除此之外,执行上下文还会包含两个可选的状态组件:词法环境(LexicalEnvironment) 和 变量环境(VariableEnvironment)。词法环境由三部分组成:

image

  • Environment Record

    我们使用 varletconstfunction 定义的变量及函数便存储在该环境记录中。只不过,根据执行上下文的不同,分为对象环境记录和声明环境记录两种。在全局执行上下文中便是对象环境记录,其中记录着全局对象、全局变量、全局函数。在函数执行上下文中存储中变量和函数声明,同时函数的参数对象 argument 也存储在其中。

  • Outer

    outer 记录着作用域查找的外部对象,当当前作用域找不到变量时,便会从 outer 指向的外部对象查找,同理类似,直至最外层作用域。outer 所指向的外部对象在代码定义时确定,而非像 this 一样在运行时确定。下面这个例子中,foo 函数在执行时,其函数执行上下文中 outer 所指向的外部对象是 foo 函数定义时所在的全局对象。

    var str = 'global'
    
    function foo () {
      console.log(str)
    }
    
    function bar () {
      var str = 'bar'
      foo()
    }
    bar() // global
  • This binding

    在全局执行上下文中,this 绑定的便是全局对象。而在函数执行上下文中,this 值绑定根据调用方式的不同而不同,所以说 this 的值是动态绑定的。

说完 LexicalEnvironment,再说 VariableEnvironment。 根据 ECMAScript 的标准定义,本质上VariableEnvironmentLexicalEnvironment 都是 LexicalEnvironment,并且他们拥有相同的初始值。这也是图中除了 Environment Record 处稍有不同外,其他均相等的原因。

The LexicalEnvironment and VariableEnvironment components of an execution context are always Lexical Environments. When an execution context is created its LexicalEnvironment and VariableEnvironment components initially have the same value.
ecma-262

我们知道,ES5 中是没有块级作用域概念的,在 ES6 中才引入了块级作用域,同时也多了 letconst 的块级作用域变量声明方式。ECMAScript 便是通过词法环境和变量环境中的 Environment Record 环境记录做到同时兼容 varletconst 声明的。ES6 中函数及 letconst声明的函数或变量会被保存在词法环境的环境记录中,而 var 声明的函数或变量便会保存在变量环境的环境记录中,当需要寻值时,先从词法环境的环境记录中查找,再去变量环境的环境记录中查找。

如上,熟悉一张执行上下文组成图,便能够清晰搞懂执行上下文相关的概念、用途。


参考资料

  1. understanding-execution-context-and-execution-stack-in-javascript

  2. ECMA-262

React FiberNode 在业务中的具体应用

《前端工程师职业生涯中的三个核心问题》一文中,曾提到过「工具」的重要性。今天,就以去年开发的一个实际应用到工作中的浏览器插件为例,简单阐述下在这一方面的具体实践。

1. 背景

在工作场景中,报表是一种常见的业务形态。老板需要通过报表了解公司的经营状态,财务需要通过报表掌握收支明细,产品运营需要通过报表了解用户数据。从抽象层面上来看,种种报表均是数据与展示形式组合的产物。其中,

概念 解释
数据 顾名思义,也即后端返回的结构化数据
展示形式 则需要根据数据的不同、使用报表的目的不同而差异化的表现为常见的表格、饼图、折线图、柱状图等形式

也即:

$$报表 = 动态数据(后端) + 图表多样展示(前端)$$

如下图:
报表示例文件

从实现的角度来看,客户端将用户在界面设置的筛选条件,格式化成特定查询语句后,交由后端做具体的数据处理,在数据返回到客户端后,客户端再将结构化数据按照展示需要进行特定格式化,从而展示在用户面前。

data-flow

可见,在客户端侧,技术模型并不复杂。客户端所面临的复杂度来源于:

  • 如何将通用业务流程模型抽象化
  • 报表种类繁多,导致页面数量庞大
  • 前后端交互传输的数据多、格式各异、数据处理流程个性化

一旦数量上去之后,比如一百多张报表,且随着时间的流逝与人员的更迭,当前维护人员往往并不清楚前后端数据交换协议的细节以及格式化规则,这对于前端页面的后期维护成本而言,可以说是陡然而升。
排查因传参或取值类型的错误,花费了大量人力成本。因此,如果有一个工具,能够一眼看到前端发送给后端的数据、以及页面渲染时又是取的什么值,在一定程度上便能够降低问题排查的成本。

2. 目标

明确了当前面临的问题,下一步则需要确定一个具体的目标。对于上述阐述的小问题,目标是非常明确的:能够提供一个工具或平台,帮助开发人员快速查看当前报表的具体请求参数和渲染取值

3. 方案

首先最容易想到的便是侵入式地给页面组件添加特定标识或按照某种约定命名,然后外部再去读取相应的信息。
但此种做法侵入性太强,对本就 BUG 频出的业务页面及脆弱的开发人员而言,都无疑是雪上加霜。因此,无代码侵入,变成了方案的首要前提。

使用 React 的同学一定熟悉 React Devtools,在 Component 面板,我们能够清晰的看到页面的组件层级及每个组件对应的 props。这便是灵感的来源,心想,在 DOMReact Compoent之间一定存在着某种连接,才能够做到 DOMComponent 以及 ComponentDOM 的映射。如果能够获取到 Component,也就获取到了有关组件的所有信息。且是代码无侵入的。

react-devtools

随后,在 React 的源码中看到使用随机字符串生成key的操作。打开控制台,果然,我们找到了这种映射关系

fiber

如上图所示,每一个React DOM元素都包含有以__react(视ReactDOM版本不同而不同)开头的属性,其中存储着当前DOMVirtualDOM也即FiberNode信息。根据FiberNode的定义,其中memoizedProps中便存储着渲染当前元素所使用的props

有了这层映射关系,在外层,就不需要关心业务以及React的处理过程,只关注最后生成的DOM结果,因为结果的对错,是我们最关心的问题。然后,剩下的事情就是处理具体的业务场景,顺水推舟了。

比如:

  • 最终实现无侵入,还是通过浏览器扩展实现,但浏览器扩展与页面不在一个执行环境中,无法读取到DOM属性,此时我们选择通过扩展,向页面注入JS脚本,通过事件机制传递数据
  • React 不同版本稍微有些许差异的处理
  • 在获取表格头时,表头是嵌套分组的,需要处理表头嵌套问题
  • 执行时机问题,每次数据变化时需要重新获取React Component的最新实例,以获取最新的值。在页面中,引起数据变化的是请求,因此,在扩展中监听网络请求,从而做到数据的同步

最终效果如下:
example

此处只阐述了,具体思路,有了这一方面的思路与具体实践后,该思路也运用在了目前正在主导的自动化测试中。后续有时间再一一道来。

如何理解 CSS 中的 float 属性

在最初的一段时间里,要说前端给人最直接的感受,心想要莫过于所见即所得了。我们前端所做的一切都那么直接裸露的呈现在我们面前。而几乎所有的呈现均需要CSS加以修饰,更需要CSS给予其布局。在布局当中,最让人捉摸不透的当属float属性。有人当说,float难以控制,我们就不用它,换一种方式呗。但是无论对于初学者,还是老手,float有其方便性和不可替代性。再者,换个角度来说,我们应该面对问题,而不是逃避问题。尤其是对于我们这种初学者,深入搞懂一个问题比学更多浅显的问题更有意义。所以,本文尝试解释一下什么是float以及如何解决float带来的问题。

一、两种观念

1.「望文生义」

我们在认识新事物时,往往是从该事物的名称获得对于它的第一感受。私以为第一认识准确了,这对于后面继续理解会有有极大的帮助。那我们首先从词典来看看什么叫做float

词典结果:

float [英][fləʊt] [美][floʊt]

vt.& vi.(使)浮动; (使)漂浮; 自由浮动;

vi.游荡;

vt.提出,提请考虑; (股票)上市;

n.彩车; 漂浮物; 浮板; 浮有冰淇淋的饮料;

也许这时,有人该说我们是来说float属性的,你拿个词典结果出来,是让我们备战四六级吗?或者,有种我裤子都脱了,你让我看这个的愤怒。先别急,请问,你听说过RegExp吗?也许你该说,不就是正则表达式吗,地球人都知道!那好,这时,如果你让一个从没接触过这方面内容的人阐述一下,他觉的正则表达式应该是什么,以及干什么用的。我想,你问的极大多数人会完全说不出与实际正则相关的内容。因为这个名字太晦涩了有没有?或者请你回想一下,你第一次听到这个名字的时候,自己对于正则的第一认识,也许会有所感受。但反过来,请问RegExp的全称是什么?对,学过正则的你肯定知道是Regular Expression,那请你自己翻译一下这个短语什么意思。在没有了解正则,第一次见到这个的时候,你也许会说**「规则表达式」**。这就对了,不要以为和现在的惯用叫法不同。直接翻译过来的才更直接的说明RegExp是干嘛的,正则就是表示一种规则的表达式。这就是正则的本质。带着这种意识去学习正则,我想也许会轻松一些。

以上扯了这么多,就想说明一点——要先从最简单直接的一面建立对于新事物的正确认知。哦,对了,说float之前,请根据词典结果中加粗的部分感受一下float吧。

2.初衷

下面从另外一个方面说明认识事物的又一重要方法——初衷

请问,为什么会有float属性?他是用来干什么的?还记得刚开始提到的一句话吗——float有其方便性和不可替代性。对,说的是不可替代性,那什么情况非得用float不可呢?答案是文字环绕效果,就像张鑫旭指出的那样,类似于word当中的图文混排效果。是的,float仅仅是为了让文字能够环绕着图片排列,严谨点说:一个图像浮动时,允许其他内容(如文本)围绕该图像,形象表示如下图。

image

image

到这,我们还没有正式的认识float属性,只是在帮大家初步了解float,在脑海里有一个基本的概念。再重复一遍,还请牢记一个事实:一个元素浮动时,其他内容会环绕该元素。

二、初识float

下面正式来看float属性

float

值: left | right | none | inherit

应用于:所有元素

继承性:无

计算值:根据指定确定

浮动的普通应用,大家可以尝试看下实际效果,这里需要强调,并且值得注意的地方已经有前人帮我们总结,不过原文是英文的,为了方便大家阅读,我已尝试翻译了一下。

阅读中文请点:【翻译】CSS float理论:你应该知道的那些事

阅读原文请点:CSS Float Theory: Things You Should Know

这里再次把其中重要的地方并且平时不会太注意的地方拿出来:

You should always set a width on floated items (except if applied directly to an image – which has implicit width). If no width is set, the results can be unpredictable.

你应该始终为一个浮动元素指定宽度(除非被直接应用一个图片上,因为图片有隐式的宽度) ,如果不设置宽度,那么结果是不可预知的。

浮动元素的外边距不会合并。

无论浮动元素在浮动之前本身是什么,一旦设置浮动,该元素就会生成一个块级框。

《CSS权威指南》指出的浮动元素摆放的九条规则 (P292-P297)

浮动元素会延伸,从而包含其所有后代浮动元素。

行内框与一个元素重叠时,其边框、背景和内容都在该浮动元素「之上」显示。

块框与一个浮动元素重叠时,其边框,背景在该浮动元素「之下」显示,而内容在浮动元素「之上」显示。

三、再识float

我非常赞同的一个观点是:

In order to really understand float theory you have to understand what a line box means in CSS. Unfortunately, that in turn requires you to understand what is meant by an inline box. […] An inline box is generated by those elements that aren’t block-level, such as EM. […] A line box is an imaginary rectangle that contains all the inline boxes that make up a line in the containing block-level element. It is (at least) as tall as its tallest line box.

为了真正的理解浮动原理,你必须理解CSS中什么是行框。不幸的是,反过来,你必须理解什么是内联元素[…]。一个内联元素是由那些不是块级元素的元素生成的,比如em,[…]一个行框是一个包含所有的组成一行包含块级元素的内联盒子组成的虚构的矩形,它至少和他最高的行内框一样高。

这也是本文重点想说的,因为理解了什么是行框(line box),是什么行内框(inline box)才能更好的理解因为float造成的比如高度坍塌问题。请看下图
image

框模型,或者盒子模型是CSS的经典模型。CSS假定每一个元素都会生成一个或多个矩形框,成为元素框,这是我们所熟知的。一个元素框的中心有一个内容区,周围有可选的内边距,边框或外边距。CSS布局实际上就是盒子盒子之间的摆放问题,也就是排版问题。在布局中,可以讲所有的元素(包括文字)全部看做是一个个矩形盒子,然后,将他们依次排列起来,这就是布局

再看上图,我们把ABC看做是一个单词,DEFGH看做是另外一个单词,他们都由字母组成。其中,每一个字母都是一个em框,或者称之为字符框。两个单词之间由空格隔开,那么,ABC、空格、DEFGH这三部分就是三个匿名文本,他们共同构成内容区,如灰色背景所示。上下半间距加在一起叫做行间距,其是font-sizeline-height值之差,将行间距均等的分布在内容区上下形成半间距。如图可见,内容区加上行间距就构成了行内框。将在该行出现的所有行内框的最高点和最低点都包含进来的框就是行框了。其中,替换元素和非替换元素会分别形成行内框,而行框把他们之中最高点作为行框的上边界,最低点作为行框的下边界。

image

图1每一个细线框就是一个行内框,图二每一个细线框就是一个行框

到这里我们再回头想想前面CSS Float理论:你应该知道的那些事中提到的这么多,我们应该记得打事情。当我们给一个元素添加float属性时,他会从正常流中删除,其余内容会无视该float元素,而该float元素也会变成块级元素,向左或向右飘到其包含块的边界,如果还有其他的float元素,他们会一个接着一个的飘起来,按照《CSS权威指南》指出的浮动元素摆放的九条规则排列。同时,现在你也可以回头看看词典对于float的解释中加粗的词语。

你可以在这里尝试更改outerwidth属性

那好,现在我们就可以简单的两句话解释清楚为什么当元素浮动之后,父级会出现高度坍塌的问题了。还记得,行框的高度怎么决定的吗?如果行框内除了浮动元素没有其他元素的时候,当元素浮动之后,他会从普通流中删除,此时,父元素中什么都没有了,高度自然就消失了。但如果,还有其他元素,行框的高度就是现在剩余的所有行内框最高点到行内框最低点的高度。

四、再见float

现在我们清楚的认识了浮动,就可以其造成的问题开始说拜拜的了。不过,在挥手之前还有最后一个问题没有解决——闭合浮动。经典的闭合浮动的方法有以下集中,实际运用时,我们只取其中最优的方案。

不过,在使用这些方法之前,我会默认大家都了解什么叫做hasLayoutBFC

1.使用clear属性

clear:left | right | both | none;

这种方法大家最熟悉,不多说。

2.添加空标签

<div style=”clear:both”></div>

或者

<br clear="both"> //大多数浏览器的默认行为都是为生成行内框,所以clear不能应用于br,除非改变其display的值。

3.设置父元素

  overflow: hidden; //触发BFC
  *zoom: 1; //为了IE兼容性

4.让父元素也浮动起来

6.父元素设置display:table

7.使用:after 伪元素

.clearFloat:after {
  display:block;
  content:'任意字符';
  clear:both;
  line-height:0;
  visibility:hidden;
}

这种方法,也是大家推荐的比较好的方法。


参考资料

  1. 那些年我们一起清除过的浮动

  2. CSS float浮动的深入研究、详解及拓展(一)

  3. CSS float浮动的深入研究、详解及拓展(二)

《 JavaScript 高级程序设计》读书笔记

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.