mirror of
				https://github.com/cheetahlou/CategoryResourceRepost.git
				synced 2025-11-04 08:13:45 +08:00 
			
		
		
		
	mod
This commit is contained in:
		
							
								
								
									
										152
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐1 | 作为一名程序员,数学到底要多好?.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐1 | 作为一名程序员,数学到底要多好?.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,152 @@
 | 
			
		||||
<audio id="audio" title="加餐1 | 作为一名程序员,数学到底要多好?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/49/09/49632471f56176bc7f4851367c728709.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是月影。
 | 
			
		||||
 | 
			
		||||
刚刚学完了可视化的数学篇,今天咱们放松一下,以我的个人经历来聊一聊,数学对我们程序员的重要性。
 | 
			
		||||
 | 
			
		||||
作为奇舞团团长和从事前端15年以上的“老人”,我为团队面试过许多同学,也和许多同学聊过前端或者程序员的职业发展方向。一般来说,我面试的时候会要求面试者有一定的数学基础,在聊关于职业发展的话题时,我也会强调数学对于程序员成长的重要性。甚至,在可视化这门课里面,我也认为学习可视化的第一步是学好图形学相关的数学知识。
 | 
			
		||||
 | 
			
		||||
不过,行业里也有些不同的声音,有些人觉得除了部分特殊的领域,大部分普通的编程领域不太需要数学知识。比如说,在我们平时的Web开发项目中,不论是前端还是后端,似乎更多地是和产品与业务逻辑打交道,比较少或几乎没有用到数学知识。甚至有些人认为,程序员根本用不着学好数学,特别是在前端这样偏UI层的领域,数学更是没有用武之地。
 | 
			
		||||
 | 
			
		||||
当然,以上这些认为数学不重要的想法,我都可以理解,曾经我自己也没有意识到数学和编程有什么必然的联系。而且,我当年在学校学习的时候,数学也学得很马虎,基础也不是那么好。不过后来,我个人的一段经历,让我很早就意识到数学对编程的重要性,而这个认知,对我后来的职业发展有着非常重要的影响。所以,我想在这里和你分享一些我个人成长中的经历和收获,希望能对你有些帮助。
 | 
			
		||||
 | 
			
		||||
## 实习面试的两个问题
 | 
			
		||||
 | 
			
		||||
2003年,因为朋友的推荐,我获得了微软亚洲研究院(MSRA)访问学生的面试机会,当时的面试官是浙江大学的刘利刚博士,他也是我后来的实习导师。那时,他正在MSRA做访问学者。
 | 
			
		||||
 | 
			
		||||
在这之前,我没有任何面试经验,在学校里面,我的学习成绩也一般,只是对编程比较感兴趣,自己做过一些小项目。我不知道会被面试什么问题,所以也没特意准备。见到了利刚博士之后,他并没有问我任何有关编程的问题,而是问了我两个数学问题,这两个问题让我至今仍记忆犹新。
 | 
			
		||||
 | 
			
		||||
第一个问题是这样的:
 | 
			
		||||
 | 
			
		||||
> 
 | 
			
		||||
已知ABC是三个不同的数字,且能使以下等式成立,求A、B、C分别是多少。
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/f5/3f/f57c583ec3134c974fyy55a08125c23f.jpeg" alt="">
 | 
			
		||||
 | 
			
		||||
求这道题的答案并不是很难,但是花多久的时间能得出答案却是一个问题。我当时回答出这个问题,大概只用了**不到10秒钟**。你可以先试着解一下,看看你能在10秒内给出这个问题的答案吗?
 | 
			
		||||
 | 
			
		||||
其实,利刚博士出这道题,主要在考察我的**数感**。啥是数感呢?这不是指一个人具备了多么高深的数学知识,而是指他对数字的一种直觉以及洞察力。
 | 
			
		||||
 | 
			
		||||
我在解决这个问题的时候,完全是脱口而出答案,我甚至都没有意识到自己是怎么得出来的。但是,当我一下说出答案之后,再回想为什么才反应过来,这道题其实是有规律的。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/84/2f/8432ccabd92ec9yy30bf1c820121252f.jpeg" alt="">
 | 
			
		||||
 | 
			
		||||
你仔细看中间这一列,应该能一下子得出A的值是9。然后再看第一列,就能得到C的值是4,最后B的值自然就是5了。所以答案就是A = 9、B = 5、C = 4。
 | 
			
		||||
 | 
			
		||||
那面试为啥要考察数感呢?利刚博士是这么给我解释的:数感好表示学习和理解能力强,因为访问学生要做的工作内容就是图形学的基础研究,一些知识肯定是要现学的,这需要有比较强的学习能力和理解力,所以作为数学基础的数感就很重要了。正是通过这个问题,我认识到了数感的重要性。
 | 
			
		||||
 | 
			
		||||
好了,接下来我们接着来看第二个问题。
 | 
			
		||||
 | 
			
		||||
> 
 | 
			
		||||
给你一个天平和一个物体,让你设计一些砝码,无论这个物体的重量是在1~100克之间的任何一个整数克数,都能用这些砝码称量出来,并且砝码的数量要尽可能少,你最少需要几个砝码呢?
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
乍一看,这个问题似乎与计算机和编程完全不沾边,但实际上这个问题涉及基础的**数的进制原理**。为什么说这个天平称重涉及数的进制原理呢?
 | 
			
		||||
 | 
			
		||||
因为我们知道,天平一般来说标准的用法是左边托盘放物体,右边托盘放砝码。那如果我们要称重的物体在1~100克之间,还要设计尽可能少的砝码,最优解肯定是称某个克数的砝码组合是唯一的,这样最省砝码。
 | 
			
		||||
 | 
			
		||||
怎么理解呢?我举个例子。
 | 
			
		||||
 | 
			
		||||
假如说,现在我们有3个砝码,分别为A砝码1克、B砝码2克、C砝码也是2克,显然这3个砝码可以称1~5克的物体。如果物体是1克的话,那么用A砝码就行;如果物体是2克的话,有两种方法,用B砝码,或者用C砝码;如果物体是3克的话,也有两种方法,用A+B或A+C砝码;如果物体是4克的话,用B+C砝码,如果物体是5克的话,用A+B+C砝码。
 | 
			
		||||
 | 
			
		||||
但是我们看到,在物体是2克和3克的时候,分别有两种砝码组合对应的称量方法。如果我们把一种砝码组合作为一种编码,再把一种物体克数作为一个状态的话,那么重复的编码就只表示同一种状态,这就属于浪费。显然更好的解决办法,是用最少的编码组合表示尽可能多的状态。甚至我们应该做到一种编码唯一对应一种状态,这样才是最优的。
 | 
			
		||||
 | 
			
		||||
所以呢,我们应该把C砝码改为4克,这样一来,3个砝码就可以称出1~7克的物体,而且没有任何两种编码表示同一种状态,这就是最优的。我把具体的称量方法总结出一张表,列在了下面。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/0b/b5/0b5fd71ddfafdf0ec00b9c59cc2930b5.jpeg" alt="">
 | 
			
		||||
 | 
			
		||||
看到这里,聪明的同学应该已经知道这一题的答案了。实际上,我们将砝码被使用记为1,将砝码不被使用记为0,那这个问题就等价于:用多少位二进制数可以表示不大于100的正整数?因此,答案自然是7位,也就是说砝码需要7个,重量分别是1克、2克、4克、8克、16克、32克和64克。
 | 
			
		||||
 | 
			
		||||
所以你看,这个问题表面上是天平问题,实际上牵扯到数的进制表示,或者说是编码,这显然是一个计算机问题。这其实也是利刚博士问我这个问题的真正目的,它同时考查了我关于数学模型的抽象能力,以及对计算机基础知识的理解程度。
 | 
			
		||||
 | 
			
		||||
顺便再说一下,这个问题如果允许将砝码放在天平左侧托盘中,那么有一个技巧可以让用到的砝码数量更少,你能想到该怎么做吗?如果你想到了,可以在留言里分享你的答案。
 | 
			
		||||
 | 
			
		||||
## 我的图形学实习经历
 | 
			
		||||
 | 
			
		||||
回答出这两个问题之后,我通过了面试,来到微软亚洲研究院实习。我的课题是图形学基础研究,恰好是和三角剖分有关,具体来说是简单多边形的相容三角剖分。
 | 
			
		||||
 | 
			
		||||
什么是简单多边形的相容三角剖分呢?简单来说,就是将两个简单多边形剖分成同样数量的三角形,同时还需要保证每个三角形的顶点能够一一对应。所谓一一对应,就是给两个多边形的顶点进行编号之后,它们中每一对三角形顶点的编号都相同。
 | 
			
		||||
 | 
			
		||||
如果两个简单多边形的边数相同,在不允许添加内部点的情况下,并不总能构成相容三角剖分。比如下图中的两个六边形,左边三条虚线构成三角形,而右边的三条虚线却相交于一个点,所以对这两个图形来说,如果我们不添加内部点,就不存在相容三角剖分了。
 | 
			
		||||
 | 
			
		||||
[<img src="https://static001.geekbang.org/resource/image/3a/fb/3aceba1a5f298aee4a187209e8a10afb.jpg" alt="">](https://www.sciencedirect.com/science/article/pii/0925772193900285)
 | 
			
		||||
 | 
			
		||||
如果我们允许在图形内部添加点进行三角剖分,就可以得到相容三角剖分了,剖分后的效果如下图:
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/b5/5a/b596d3964bd48084e9c00f5b1d93c65a.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
而我当时的工作主要是研究如何对多边形进行快速相容三角剖分。之所以研究相容三角剖分,是因为通过相容三角剖分可以生成拓扑结构相同的三角网格,而拓扑结构相同的三角网格是实现物体变形特效的基础。比如说,我们可以用相容三角剖分实现人物的“变脸”特效等等。
 | 
			
		||||
 | 
			
		||||
[<img src="https://static001.geekbang.org/resource/image/9d/70/9da642f984cdc8c90a9845f34873cc70.jpg" alt="">](https://mappingignorance.org/2018/02/21/triangulations-face-morphing/)
 | 
			
		||||
 | 
			
		||||
大体上,我的研究就是围绕相容三角剖分涉及的算法,因为它们都比较复杂,这里我就不多说了。接下来,我主要介绍一下我的工作中遇到的问题。因为涉及图形学的基础内容,自然会有一些基础的图形学计算,比如计算点到直线和线段的距离,计算边的切线和法线,判断线段的关系,绘制圆锥曲线等等。
 | 
			
		||||
 | 
			
		||||
## 我实习的第一个任务
 | 
			
		||||
 | 
			
		||||
在我刚开始实习的时候,第一个任务就是要计算点到直线和线段的距离。
 | 
			
		||||
 | 
			
		||||
不过,一开始我在求点到直线的距离的时候,是先写出直线的两点式代数方程,然后求点与直线的垂线方程,接着将两个方程联立求交点,最后再求出点与交点的距离。
 | 
			
		||||
 | 
			
		||||
这么做当然是可以求出结果来的,但是计算过程可以说是相当繁琐了,而且它还有缺陷。缺陷究竟是什么呢?我们知道,用直线方程求垂线的时候,要用到点斜式,但是点斜式的斜率,在直线垂直于X轴时,会有斜率计算出来是无穷大的问题。这种特殊情况还需要特殊处理,就更增加了我计算的复杂度。
 | 
			
		||||
 | 
			
		||||
所以,当时我花了一天时间才把“求点到直线和线段距离的问题”用代数方程解决。但是,第二天给利刚博士交差的时候,就被他批评了一顿,他问我为什么要用代数方程去做这个问题,如果用向量来做,根本就是分分钟的事儿。
 | 
			
		||||
 | 
			
		||||
如果你认真学了数学篇的课程,应该也已经知道,用向量解决这个问题的确非常简单。因为向量叉积的几何意义就是平行四边形的面积,在用向量叉积除以底边就是高,也就是点到向量所在直线的距离了。而我当时并没有想起可以用向量来解决这类问题,所以才走了弯路。
 | 
			
		||||
 | 
			
		||||
经过这次教训,我深刻意识到**选择正确数学工具,能够把看似非常复杂的问题转化为简简单单问题,从而顺利解决**。这也是为什么数学对于程序员来说非常重要。这个教训也是我在MSRA实习中最重要的收获。
 | 
			
		||||
 | 
			
		||||
## 两次数学实践
 | 
			
		||||
 | 
			
		||||
大约4个多月后,我就结束了MSRA的实习,回到了学校。毕业后,我去了一家深圳的软件公司,真正地成为了一名程序员。后来更是在机缘巧合下接触到了前端,也一直成长到今天。
 | 
			
		||||
 | 
			
		||||
在成长的过程中,我始终牢记:用数学思想和意识去解决工作中的问题。你别说,还真被我遇到了两个事儿。接下来,我就和你说说我印象最深刻的这两个案例。
 | 
			
		||||
 | 
			
		||||
第一个案例是我在08年到百度时遇到的。当时,某个产品中有一个绘制椭圆的需求,负责开发的工程师是使用椭圆的代数方程来计算的。因为代数方程涉及开平方的问题,所以开发人员还要根据象限来判断正负号,这会非常麻烦。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/ea/54/eafdc7f9f5798c7041c2c5623b23d054.jpeg" alt="" title="椭圆代数方程涉及开根号">
 | 
			
		||||
 | 
			
		||||
现在你学习了数学篇的课程,应该知道用椭圆的参数方程来解决,根本不会涉及开根号和象限判断问题,操作起来也会简单很多。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/95/7a/95832de155b7bb82f9ed6682fc6de17a.jpeg" alt="" title="椭圆的参数方程">
 | 
			
		||||
 | 
			
		||||
另一个案例,是我在360搜索的一个运营活动中遇到的。当时需要我们实现一个Canvas2D的图片特效,效果如下:
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/96/60/965a77b94f7f9608145e3a0041a88f60.gif" alt="">
 | 
			
		||||
 | 
			
		||||
具体要求是在一张静态图片上,选择若干个运动区域,比如示意图中的猫的耳朵、眼睛、鼻子区域,然后对区域做这样逆时针旋转的运动,最终将它变为动图。
 | 
			
		||||
 | 
			
		||||
这其实是一个Canvas2d的特效。最初的时候,我们的工程师是使用代数方法来解决的,具体的方法如下:
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/e4/1e/e420d7c88ab0f83bab3b198ff2afa81e.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
代数方法虽然能解决问题,但是会有4个缺陷:
 | 
			
		||||
 | 
			
		||||
1. 求角需要计算反正切,性能差;
 | 
			
		||||
1. 计算中需要开平方,要判断符号;
 | 
			
		||||
1. 求反正切有无穷大问题,需要特殊处理;
 | 
			
		||||
1. 有重复的计算量,进一步消耗性能。
 | 
			
		||||
 | 
			
		||||
不过,在我采用了向量法进行优化之后,这个动图的性能提升了数倍。具体算法如下图:
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/3f/13/3f6aff92c3e441f6fe689726a9d02f13.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
这两个案例其实共同说明一个道理:**学会选择合适的数学工具,才能用最优的方式轻松解决问题。**
 | 
			
		||||
 | 
			
		||||
## 小结
 | 
			
		||||
 | 
			
		||||
以上就是我和数学相关的个人经历了。总的来说,我收获到最重要的经验就是,要学会选择合适的数学工具来解决问题。当你选对工具之后,那些看似复杂的问题,可能会变得无比简单,从而被迅速解决。所以我们要重视数学基础的积累,锻炼自己的数感,拓宽知识面,以数学思维来思考计算机问题。
 | 
			
		||||
 | 
			
		||||
当然,我也不是说,程序员必须要有多么高深的数学知识。你看我们专栏中需要的数学知识,基本上就是一些高中数学知识和一部分基础的线性代数知识。但是数感、数学思维锻炼还是很重要的,这些锻炼越多,程序员的逻辑能力、抽象能力也会得到提高。更重要的是,通过锻炼我们能形成用数学思维思考问题的习惯,这样才能迅速找到最合适解决某类问题的数学工具,从而提升我们的技术能力。
 | 
			
		||||
 | 
			
		||||
## 小试牛刀
 | 
			
		||||
 | 
			
		||||
最后我再留两道思考题,来锻炼一下你的数感和数学思维能力。
 | 
			
		||||
 | 
			
		||||
1. 我们知道简单多边形和复杂多边形区别是,是否有非相邻的边相交。那为了判断一个多边形是不是简单多边形,我们可以实现一个函数,来判断两个线段是否相交。你能实现这个函数吗?
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/90/4c/90d19b7614fd4a83c6eeb8224d4f8d4c.jpg" alt="" title="左侧ab、cd线段相交,右侧ab、cd线段不相交">
 | 
			
		||||
 | 
			
		||||
1. 假如你手里有5个硬币,已知随机抛一次之后,有的硬币正面朝上,有的硬币反面朝上。请问:随机抛一次,有3个或3个以上硬币正面朝上的概率是多少?
 | 
			
		||||
 | 
			
		||||
欢迎你在留言说说数学对你的影响,也欢迎你和我分享关于数学方面的疑惑,我们一起探讨。
 | 
			
		||||
							
								
								
									
										164
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐2 | SpriteJS:我是如何设计一个可视化图形渲染引擎的?.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐2 | SpriteJS:我是如何设计一个可视化图形渲染引擎的?.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
<audio id="audio" title="加餐2 | SpriteJS:我是如何设计一个可视化图形渲染引擎的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/f6/83eec3973a79e8c60fe7f667a119d1f6.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是月影。
 | 
			
		||||
 | 
			
		||||
今天,我们来聊一个相对轻松的话题,它不会有太多的代码,也不会有什么必须要掌握的理论知识。不过这个话题对你理解可视化,了解渲染引擎也是有帮助的。因为我今天要聊的话题是SpriteJS,这个我亲自设计和实现的图形渲染引擎的版本迭代和演进。
 | 
			
		||||
 | 
			
		||||
SpriteJS是从2017年下半年开始设计的,到今天已经快三年了,它的大版本也从1.0升级到了3.0。那么它为什么会被设计出来?它有什么特点?1.0、2.0、3.0版本之间有什么区别,未来会不会有4.0甚至5.0?别着急,听我一一道来。
 | 
			
		||||
 | 
			
		||||
## SpriteJS v1.x (2017年~2018年)
 | 
			
		||||
 | 
			
		||||
我们把时间调回到2017年下半年,当时我还在360奇舞团。奇舞团是360技术中台的前端团队,主要负责Web开发,包括PC端和移动端的产品的前端开发,比较少涉及可视化的内容。不过,虽然团队以支持传统Web开发为主,但是也支持过一部分可视化项目,比如一些toB系统的后台图表展现。那个时候,我们团队正要开始尝试探索可视化的方向。
 | 
			
		||||
 | 
			
		||||
如果你读过专栏的预习篇,你应该知道,要实现可视化图表,我们用图表库或者数据驱动框架都能够实现,前者使用起来简单,而后者更加灵活。当时,奇舞团的小伙伴更多是使用数据驱动框架[D3.js](https://d3js.org/)来实现可视化图表的。
 | 
			
		||||
 | 
			
		||||
对D3.js来说,[D3-selection](https://github.com/d3/d3-selection)是其核心子模块之一,它可以用来操作DOM树,返回选中的DOM元素集合。这个操作非常有用,因为它让我们可以像使用jQuery那样,快速遍历DOM元素,并且它通过data映射将数据与DOM元素对应起来。这样,我们用很简单的代码就能实现想要的可视化效果了。
 | 
			
		||||
 | 
			
		||||
比如,我们通过 `d3.select('body').selectAll('div').dataset(data).enter().append('div')`,把对应的div元素根据数据的数量添加到页面上的body元素下,然后,我们直接通过.style来操作对应添加的div元素,修改它的样式,就能轻松绘制出一个简单的柱状图效果了。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
    const dataset = [125, 121, 127, 193, 309];
 | 
			
		||||
    const colors = ['#fe645b', '#feb050', '#c2af87', '#81b848', '#55abf8'];
 | 
			
		||||
 | 
			
		||||
    const chart = d3.select('body')
 | 
			
		||||
      .selectAll('div')
 | 
			
		||||
      .data(dataset)
 | 
			
		||||
      .enter()
 | 
			
		||||
      .append('div')
 | 
			
		||||
      .style('left', '450px')
 | 
			
		||||
      .style('top', (d, i) => {
 | 
			
		||||
        return `${200 + i * 45}px`;
 | 
			
		||||
      })
 | 
			
		||||
      .style('width', d => `${d}px`)
 | 
			
		||||
      .style('height', '40px')
 | 
			
		||||
      .style('background', (d, i) => colors[i]);
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/07/37/07f31da9e193e658c7ed5c528733b437.jpeg" alt="">
 | 
			
		||||
 | 
			
		||||
这是一个非常快速且方便的绘图方式,但它也有局限性。D3-selection只能操作具有DOM结构的图形系统,也就是HTML和SVG。而对于Canvas和WebGL,我们就没有办法像上面一样,直接遍历元素并且将数据和元素结构对应起来。
 | 
			
		||||
 | 
			
		||||
正因为D3-selection操作DOM使用起来特别方便,所以常见的D3例子都是用HTML或者SVG来写的,很少使用Canvas和WebGL,即便后两者的性能要大大优于HTML和SVG。因此,当时实现SpriteJS 1.0的初衷非常简单,那就是我希望让团队的同学既能使用熟悉的D3.js来支持可视化图表的展现,又可以使用Canvas来代替默认的SVG进行渲染,从而达到更好的性能。
 | 
			
		||||
 | 
			
		||||
所以,**SpriteJS 1.0实现了整个DOM底层的API,我们可以像操作浏览器原生的DOM一样来操作SpriteJS元素,而我们最终渲染出的图形是调用底层Canvas的API绘制到画布上的**。这样一来,SpriteJS和HTML或者SVG,就都可以用D3-selection来操作了,在使用上它们没有特别大的差别,但SpriteJS的最终渲染还是通过Canvas绘制的,性能相比其他两种有了较大的提升。
 | 
			
		||||
 | 
			
		||||
比如说,我用D3.js配合SpriteJS实现的柱状图代码,与使用HTML绘制的代码区别不大,但是由于是绘制在Canvas上,性能会提升很多。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
    const {Scene, Sprite} = spritejs;
 | 
			
		||||
    const container = document.getElementById('container');
 | 
			
		||||
    const scene = new Scene({
 | 
			
		||||
      container,
 | 
			
		||||
      width: 800,
 | 
			
		||||
      height: 800,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const dataset = [125, 121, 127, 193, 309];
 | 
			
		||||
    const colors = ['#fe645b', '#feb050', '#c2af87', '#81b848', '#55abf8'];
 | 
			
		||||
 | 
			
		||||
    const fglayer = scene.layer('fglayer');
 | 
			
		||||
    const chart = d3.select(fglayer)
 | 
			
		||||
      .selectAll('sprite')
 | 
			
		||||
      .data(dataset)
 | 
			
		||||
      .enter()
 | 
			
		||||
      .append('sprite')
 | 
			
		||||
      .attr('x', 450)
 | 
			
		||||
      .attr('y', (d, i) => {
 | 
			
		||||
        return 200 + i * 45;
 | 
			
		||||
      })
 | 
			
		||||
      .attr('width', d => d)
 | 
			
		||||
      .attr('height', 40)
 | 
			
		||||
      .attr('bgcolor', (d, i) => colors[i]);
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
除了解决API的问题,以及让D3-selection可以使用之外,为了让使用方式尽可能接近于原生的DOM,我还让SpriteJS 1.0 实现了这4个特性,分别是标准的DOM元素盒模型、标准的DOM事件、Web Animation API (动画)以及缓存策略。
 | 
			
		||||
 | 
			
		||||
盒模型、DOM事件和 Web Animation API ,我想你作为前端工程师肯定都知道,所以我多说一下缓存策略。还记得在性能篇里我们说过,要提升Canvas的渲染性能,就要尽量减少绘图指令的数量和执行时间,比较有效的方式是,我们可以将绘制的图形用离屏Canvas缓存下来。这样,在下次绘制的时候,我们就可以将缓存未失效的元素从缓存中用drawImage的方式直接绘制出来,而不用重新执行绘制元素的绘图指令,也就大大提升了性能。
 | 
			
		||||
 | 
			
		||||
因此,**在SpriteJS 1.0中,我实现了一套自动的缓存策略,它会根据代码运行判断是否对一个元素启用缓存,如果是,就尽可能地启用缓存,让渲染性能达到比较好的水平**。
 | 
			
		||||
 | 
			
		||||
SpriteJS 1.0实现的这些特性,基本上满足了我们当时的需要,让我们团队可以用D3.js配合SpriteJS来实现各种可视化图表项目需求,而且使用上非常接近于操作原生的DOM,非常容易上手。
 | 
			
		||||
 | 
			
		||||
## SpriteJS v2.x (2018年~2019年)
 | 
			
		||||
 | 
			
		||||
到了2018年底,我开始思考SpriteJS的下一个版本。当时我们解决了在PC和移动Web上绘制可视化图表的诉求,不过外部的使用者和我们自己,在一些使用场景中,逐渐开始有一些跨平台的需求,比如在服务端渲染,或者在小程序中渲染。
 | 
			
		||||
 | 
			
		||||
因此,我开始重构代码,将绘图系统分层设计,实现了渲染的适配层。在适配层中,所有的绘图能力都由Canvas底层API提供,与浏览器DOM和其他的API无关。这样,SpriteJS就能够运行在任何提供了Canvas运行时环境的系统中,而不一定是浏览器。
 | 
			
		||||
 | 
			
		||||
重构后的代码能够通过[node-canvas](https://github.com/Automattic/node-canvas)运行在Node.js环境中,所以我们就能够使用服务端渲染来实现一些特殊的可视化项目。比如,我们曾经有一个项目要处理大量的历史数据,大概有几十万到上百万条记录,如果在前端分别绘制它们,性能一定会有问题。所以,我们将它们通过服务端绘制并缓存好之后,以图像的方式发送给前端,这样就大大提升了性能。此外,我们还通过在适配层上提供不同的封装,让SpriteJS 2.0支持了小程序环境,也能够运行在微信小程序中。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/d8/de/d89d6595133c63993a7cd178212ecfde.jpeg" alt="">
 | 
			
		||||
 | 
			
		||||
上图是SpriteJS 2.0的主体架构,它的底层由一些通用模块组成,Sprite-core是适配层,SpriteJS是支持浏览器和Node.js的运行时,Sprite-wxapp是小程序运行时,Sprite-extend-*是一些外部扩展。我们通过外部扩展实现了粒子系统和物理引擎,以及对主流响应式框架的支持,让SpriteJS 2.0可以直接支持[vue](http://vue.spritejs.org/)和[react](http://react.spritejs.org/)。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/b7/bd/b7703aa427cfbc75576a17e092d1eebd.gif" alt="" title="SpriteJS 2.0通过扩展实现物理引擎">
 | 
			
		||||
 | 
			
		||||
除此以外,SpriteJS 2.0还支持了文字排版和布局系统。其中,文字排版支持了多行文本自动换行,实现了几乎所有CSS3支持的文字排版属性,布局系统则支持了完整的弹性布局(Flex layout)。这两个特性被很多用户喜爱。
 | 
			
		||||
 | 
			
		||||
可以说,我们对SpriteJS 2.0做了加法,让它在1.0的基础上增加了许多强大且有用的特性。到了2019年底,我又开始思考实现SpriteJS 3.0。这次我打算对特性做一些取舍,将许多特性从SpriteJS 3.0中去掉,甚至包括深受使用者喜爱的文字排版和布局系统。这又是为什么呢?
 | 
			
		||||
 | 
			
		||||
这是因为SpriteJS 2.0虽好,但是它也有一些明显的缺点:
 | 
			
		||||
 | 
			
		||||
1. 只支持Canvas2D,尽管有缓存策略,性能仍然不足;
 | 
			
		||||
1. 多平台适配采用不同的分支,维护起来比较麻烦;
 | 
			
		||||
1. 支持了许多非核心功能,如文字排版、布局,使得JavaScript文件太大;
 | 
			
		||||
1. 不支持3D绘图。
 | 
			
		||||
 | 
			
		||||
## SpriteJS v3.x (2019年~2020年)
 | 
			
		||||
 | 
			
		||||
在SpriteJS 3.0中,我舍弃了非核心功能,将SpriteJS定位为纯粹的图形渲染引擎, 核心目标是追求极致的性能。
 | 
			
		||||
 | 
			
		||||
在适配层上,SpriteJS 3.0完全舍弃了2.0设计里面较重的sprite-core,采用了更轻量级的图形库[mesh.js](https://github.com/mesh-js/mesh.js)作为2D适配层,mesh.js以gl-renderer作为webgl渲染底层库,结合Canvas2D的polyfill做到了优雅降级。当运行环境支持WebGL2.0时,SpriteJS 3.0默认采用WebGL2.0渲染,否则降级为WebGL1.0,如果也不支持WebGL1.0,再最终降级为Canvas2D。
 | 
			
		||||
 | 
			
		||||
在3D适配层方面,SpriteJS 3.0采用了OGL库。这样一来,SpriteJS 3.0就完全支持WebGL渲染,能够绘制2D和3D图形了。
 | 
			
		||||
 | 
			
		||||
SpriteJS 3.0继承了SpriteJS 2.0的跨平台性,但是不再需要使用分支来适配多平台,而是采用了更轻量级的polyfill设计,同时支持服务端渲染、Web浏览器渲染和微信小程序渲染,理论上讲还可以移植到其他支持WebGL或Canvas2D的运行环境中去。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/bd/f5/bdba6a3a2466a882abeyybaeb7f7f6f5.jpeg" alt="" title="SpriteJS 3.0 结构">
 | 
			
		||||
 | 
			
		||||
与SpriteJS 1.0和SpriteJS 2.0采用缓存机制优化性能不同,SpriteJS 3.0默认采用WebGL渲染,因此使用了批量渲染的优化策略,我们在性能篇中讲过这种策略,在绘制大量几何图形时,它能够显著提升WebGL渲染的性能。
 | 
			
		||||
 | 
			
		||||
由于发挥了GPU并行计算的能力,在大批量图形绘制的性能上,SpriteJS 3.0的性能大约是SpriteJS 2.0的100倍。此外,SpriteJS 3.0支持了多线程渲染,可避免UI阻塞,从而进一步提升性能。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/8a/6b/8abb93673a58afe174349c310268f36b.gif" alt="" title="SpriteJS 3.0 绘制5万个地理信息点,60fps帧率">
 | 
			
		||||
 | 
			
		||||
总之,SpriteJS 3.0 随着性能的优化,已经成为一个纯粹的可视化渲染引擎了,但在我看来它仍然有些问题:
 | 
			
		||||
 | 
			
		||||
1. 性能优化得不够极致,数据压缩和批量渲染没有做到最好;
 | 
			
		||||
1. JS的矩阵运算还是不够快,计算性能有提升空间;
 | 
			
		||||
1. 因为考虑到兼容性的问题,所以我采用了Canvas2D的降级,这让JavaScript包仍然有些大;
 | 
			
		||||
1. 3D能力不够强,与ThreeJS等主流3D引擎仍有差距。
 | 
			
		||||
 | 
			
		||||
## SpriteJS的未来版本(2020年~2021年)
 | 
			
		||||
 | 
			
		||||
今年下半年,我开始设计SpriteJS 4.0。这一次,我打算把它打造成一个更纯粹的图形系统,让它可以做到真正跨平台,完全不依赖于Web浏览器。
 | 
			
		||||
 | 
			
		||||
下面是SpriteJS 4.0的结构图,它的底层将采用OpenGL ES和Skia来渲染3D和2D图形,中间层使用JavaScript Core和JS Bindings技术,将底层Api通过JavaScript导出,然后在上层适配层实现 WebGL、WebGPU和Canvas2D的API,最上层实现SpriteJS的API。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/50/42/508df6e988a9b1cbef17595f441b7642.jpg" alt="" title="SpriteJS 4.0 体系结构">
 | 
			
		||||
 | 
			
		||||
根据这个设计,SpriteJS 4.0将对浏览器完全没有依赖,同时依然可以通过Web Assembly方式运行在浏览器上。这样SpriteJS 4.0会成为真正跨平台的图形系统,可以以非常小的包集成到其他系统和原生App中,并且达到原生应用的性能。
 | 
			
		||||
 | 
			
		||||
在这一版,我还会全面优化SpriteJS的内存管理、矩阵运算和多线程机制,力求渲染性能再上一个台阶,最终能够完全超越现在市面上的任何主流的图形系统。
 | 
			
		||||
 | 
			
		||||
## 要点总结
 | 
			
		||||
 | 
			
		||||
在SpriteJS 1.0中,我们追求的是和DOM一致的API,能够使用D3.js结合SpriteJS来绘制可视化图表到Canvas,从而提升性能。到了SpriteJS 2.0,我们追求跨平台能力和一些强大的功能扩展,比如文字排版和布局系统。而到了SpriteJS 3.0,我们决定回归到渲染引擎本质,追求极致的性能发挥GPU的能力,并支持3D渲染。再到今年的SpriteJS 4.0,我打算把它打造成更纯粹的图形系统,让它的渲染能力和性能最终能够超越目前市面上的主流图形系统。
 | 
			
		||||
 | 
			
		||||
总的来说,在SpriteJS 1.0到4.0的设计发展过程中,包含了我对整个图形系统架构的思考和取舍。我希望通过我今天的分享,能够帮助你理解图形系统和渲染引擎的设计,也期待在你设计其他系统和平台的时候,它们能给你启发。
 | 
			
		||||
 | 
			
		||||
## 课后思考
 | 
			
		||||
 | 
			
		||||
最后,请你试着回想你曾经接触过的可视化项目,如果用SpriteJS来实现它们会不会有更好的效果呢?欢迎把你的思考和答案写在留言区,我们一起讨论。
 | 
			
		||||
 | 
			
		||||
看了我给SpriteJS未来版本定下的目标,你有没有心动呢?SpriteJS是一个开源项目,如果你学完这门课,也想参与进SpriteJS的开发,那我非常欢迎你成为一名SpriteJS开发者,为我们提交PR、贡献代码。
 | 
			
		||||
 | 
			
		||||
好了,今天的内容就到这里,我们下节课见!
 | 
			
		||||
 | 
			
		||||
## 推荐阅读
 | 
			
		||||
 | 
			
		||||
1. [D3.js](https://d3js.org)
 | 
			
		||||
1. [SpriteJS](https://spritejs.org)
 | 
			
		||||
1. [Mesh.js](https://github.com/mesh-js/mesh.js)
 | 
			
		||||
							
								
								
									
										59
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐3 | 轻松一刻:我想和你聊聊前端的未来.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐3 | 轻松一刻:我想和你聊聊前端的未来.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
<audio id="audio" title="加餐3 | 轻松一刻:我想和你聊聊前端的未来" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/04/47/045bfb932c7d13d12c871ea3c6f91647.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是月影。今天咱们来聊一个轻松点的话题。
 | 
			
		||||
 | 
			
		||||
我做前端工程师,也有15年了。常常听到有前端开发“抱怨”,“别更新了,学不动了”,也会有人经常问我,Deno、TypeScript 等新轮子层出不穷,未来前端重点方向在哪?还有,在大前端浪潮下,前端开发该如何持续学习、成长?所以今天,我想和你围绕这些话题来聊一聊。
 | 
			
		||||
 | 
			
		||||
## 别更新了,学不动了?
 | 
			
		||||
 | 
			
		||||
我曾经听一位前端技术专家说过,“前端十八个月难度翻一番”,这句话真的说出了前端领域更新换代之快背后的前端开发血泪史。也因此,“别更新了,学不动了”这句话成为了不少前端开发玩梗的口头禅。
 | 
			
		||||
 | 
			
		||||
但是对我来说,技术发展得越多、越快我就越兴奋。我非常喜欢研究技术,尝试新东西,不怕学习,也更没有学不动这种感觉。我一直觉得,如果一个行业的新东西层出不穷,说明这个行业一直在高速发展,这本身对于从业者来说是一个非常好的事情,因为这说明这个行业中有更多的机会和成长空间。
 | 
			
		||||
 | 
			
		||||
不过,一些前端开发对技术更新的担忧,我也能够理解。我的建议是,如果你**不盲目**地去追求所谓的“时髦”技术,不去刻意担心自己是否落伍,而是去多观察这个行业,找到技术发展内在的规律和脉络,那么你就知道该怎么前进,不会有任何恐慌了。
 | 
			
		||||
 | 
			
		||||
在任何一个领域或方向,知识体系都可以分为基础知识和领域知识,而领域知识又可以分为通用领域知识和专用领域知识。它们之间的变化是不一样的,基础知识的变化最慢,其次是通用领域知识,然后是专用领域知识。
 | 
			
		||||
 | 
			
		||||
用可视化这个方向来举例,基础知识是数学和图形学知识,比如向量、矩阵运算、三角剖分这些知识属于基础知识,它们基本上不会随着时间发生很大变化。而JavaScript、WebGL 这种属于通用领域知识,它们会改变,也会慢慢发展,比如从 WebGL1.0 发展到 WebGL2.0,从 ES2019 发展到 ES2020,但不会变化、发展得那么快。而类似 ThreeJS、BabylonJS、SpriteJS和D3.js这些属于专用领域知识,很有可能一个大版本升级就会有很大的变化。
 | 
			
		||||
 | 
			
		||||
学习这些知识,也有不同的方法。一般来说,如果是基础知识,随便什么时候我们都可以学,而且越早学习越好。基础知识就像是你的内功,学好它们,融会贯通之后,学习其他的知识都是事半功倍的。如果是通用的领域知识,一旦你下决心从事这个领域,也是能够尽早学习它比较好,不过由于这些知识是领域相关的,如果能一边学习,一边通过实践来打磨就会掌握得更快。专用领域知识,不一定要很早去学,有一个技巧是,当你用到的时候再去学习它们。如果你没有用到,你可以知道有这门技术,能做什么就行了,不用花大量时间和精力去钻研它们。
 | 
			
		||||
 | 
			
		||||
如果你觉得技术更新太快,学不过来,很可能就是被这些专用领域知识给“迷惑”了。比如,我听人说前端工程化里的代码打包很重要,于是今天学习了 webpack,明天又去学习 rollup。可实际上这种专用领域知识,我们只需要知道它们能做什么,在用到的时候再去详细学习就好了。
 | 
			
		||||
 | 
			
		||||
## 如何看待 Deno、TS 和未来的前端重点方向?
 | 
			
		||||
 | 
			
		||||
好,解决了第一个问题,我们再来说说Deno、TS和未来前端的重点方向。
 | 
			
		||||
 | 
			
		||||
最近几年流行的编程语言很多都号称是 JavaScript 的替代语言,比如 TypeScript。前端三大框架现在也基本都增加了对 TypeScript 的支持,这背后的本质原因是什么呢?
 | 
			
		||||
 | 
			
		||||
我认为,近几年 JavaScript 的语言标准发展很快,这背后依托的依然是 Web 应用领域的高速发展,JavaScript 是 Web 领域事实上的“原生语言”和技术标准,很多编程语言都是 JavaScript 的衍生语言。TypeScript 就是其中之一,它是一个很优秀的编程语言,其静态类型对一些规模较大的项目提高代码的可维护性很有帮助,因此现在写 TypeScript 的开发者越来越多,三大框架增加对其支持是顺其自然的事。
 | 
			
		||||
 | 
			
		||||
Deno 最近也发布了正式的 1.0 版本。我认为它是一个很好的 Runtime,在 Node.js 之后走了另外一条道路,规避了 Node.js 设计上的不足之处。不过,未来 Deno 不见得会取代 Node,它们两个很有可能会一直共同发展下去。但是 Deno 的设计本身就是建立在对 Node 的思考和改进之上的,所以我们学习它,对理解 Node.js 的精髓也非常有帮助。不过,我也只对 Deno 有简单的了解,也希望之后有机会可以去深入地学习和使用它。
 | 
			
		||||
 | 
			
		||||
因为我自己这两年的主要精力放在可视化领域,主要是可视化渲染方面,所以我觉得可视化是非常值得前端工程师重视的一个领域。随着 Web 技术的发展,视觉特别是 WebGL/GPU 相关的应用场景会越来越丰富,对技术要求也会越来越高。与前端其他大部分技术不同,WebGL 的上手门槛比较高,需要对数学、图形学有比较扎实的基础,而图形学和视觉呈现技术本身的天花板非常高,未来这块一定会有非常大的发展空间。
 | 
			
		||||
 | 
			
		||||
另外,AI 以及 VR/AR 也是未来前端的发展方向。对于 VR/AR,主流浏览器也开始支持 webXR 技术,而且无论 AI 还是 XR 这些领域,其实也和 GPU 息息相关,所以它们和可视化技术也是有关联的。
 | 
			
		||||
 | 
			
		||||
除此之外,还有一些跨端技术,从 RN 到 Flutter,经过了很多的发展,但还不是很成熟,而跨端本来就有很多应用场景,未来依然有很大的成长空间。PC 端的 Electron 也不容忽视,作为跨平台应用开发,它是一个非常好用的工具。
 | 
			
		||||
 | 
			
		||||
最后是一些非常新的技术,比如Web Assembly、JS Binding,它们是一些跨界交叉领域发展来的前沿的技术,同样也值得我们持续关注。
 | 
			
		||||
 | 
			
		||||
## 给前端开发的一些真诚建议
 | 
			
		||||
 | 
			
		||||
最后,我想从前端工程师以及技术管理的角度,总结一些我自己的经验分享给你。
 | 
			
		||||
 | 
			
		||||
首先,你要确定自己是不是真正喜欢和热爱前端开发这个职业。当然我相信,大多数同学成为前端工程师,是因为内心真正喜欢这个职业。但是,之前我也听到有些同学说,因为觉得在程序员中前端比较“简单”,或者觉得自己数学或算法基础不好,做前端对这些要求不高,再或者就是觉得前端工程师算是份体面的职业,所以才选择它,其实内心并没不是真的热爱这个职业。
 | 
			
		||||
 | 
			
		||||
如果你仅仅把它当作一份谋生工作的话,那么你可能在这个职业道路上也走不了多远,肯定也无法达到很高的高度。所以我建议你反思一下,自己是否真的适合前端开发这个职业。
 | 
			
		||||
 | 
			
		||||
如果你确实热爱这个职业,正在考虑长远发展,我建议你最好选择一个好的平台,一个技术氛围好的团队,一份节奏合适的工作。我说的节奏合适指的是忙闲交替,既不会长时间特别忙,也不会持续特别闲。在这样的节奏下,项目积累再加上自己的学习沉淀,你就可以快速地成长了,而且技术氛围好的团队,也可以加快你学习沉淀的速度。
 | 
			
		||||
 | 
			
		||||
想在专业上达到一定的高度,因为每个人的情况不一样,所以我们要根据自己的情况来规划。不过总还是能找到一些共通点的,我觉得有一点很合适:找到并突破前端领域的“边界”。这个边界可以是某些有深度领域的技术前沿,也可以是某个交叉领域,如与服务端的交界,与移动客户端的交界等等。如果我们能在这些边界上做出突破,就肯定可以步入前端专家的行列了。
 | 
			
		||||
 | 
			
		||||
其实前端专家除了需要技术能力以外,还需要有意识地打造自己的个人影响力,锻炼自己的领导力,要让自己心态开放、眼界开阔,不排斥新技术,拥抱开源,多参与社区。
 | 
			
		||||
 | 
			
		||||
总之一句话,想要在职业之路上达一个比较高的高度,软实力和硬实力我们要两手抓。
 | 
			
		||||
 | 
			
		||||
今天说了这么多,其实希望如果你真的下定决心在前端这条路上钻研下去,一定别忘了,方向和努力缺一不可。最后,希望正在看这节课的你,可以成为未来优秀的前端专家,我们一起让前端行业变得更好。
 | 
			
		||||
 | 
			
		||||
今天的分享有没有解决你的疑惑呢?快把这节课分享给你的朋友吧!今天的内容就到这里了,我们下节课见!
 | 
			
		||||
							
								
								
									
										272
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐4 | 一篇文章,带你快速理解函数式编程.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐4 | 一篇文章,带你快速理解函数式编程.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,272 @@
 | 
			
		||||
<audio id="audio" title="加餐4 | 一篇文章,带你快速理解函数式编程" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/28/dedb0fcdceff5526512e9996fb510228.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是月影。今天,我们来讨论函数式编程。
 | 
			
		||||
 | 
			
		||||
我看到很多同学留言说,课程中给出的代码例子有的地方看不明白。我把同学们看不懂的地方汇总了一下,发现大部分都是我使用函数式编程来写的。比如,我在第7讲说过的 parametric 高阶函数,第12讲说过的 traverse 的设计,还有第15讲中使用的 toPolar/fromPolar 和改进版的 parametric 设计,以及数据篇中的数据处理和D3.js的使用。
 | 
			
		||||
 | 
			
		||||
如果你还不习惯函数式编程思想,并且也觉得这些代码不容易理解,想知道为什么一定要这么设计,那这节课,你一定要好好听,我会和你系统地说说过程抽象和函数式编程这个话题。
 | 
			
		||||
 | 
			
		||||
## 两种编程范式:命令式与声明式
 | 
			
		||||
 | 
			
		||||
首先,我先来说说什么是编程范式。编程范式有两种,分别是命令式(Imperative)和声明式(Declarative),命令式强调做的步骤也就是怎么做,而声明式强调做什么本身,以及做的结果。因此,编程语言也可以分成命令式和声明式两种类型,如果再细分的话,命令式又可以分成过程式和面向对象,而声明式则可以分成逻辑式和函数式。下面这张图列出了编程语言的分类和每个类型下经典的编程语言。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/37/ae/3797e89ec55b48662c0e2ca58aa792ae.jpeg" alt="">
 | 
			
		||||
 | 
			
		||||
你注意看,这张图里并没有JavaScript。实际上像JavaScript这样的现代脚本语言,通常具有混合范式,也就是说JavaScript同时拥有命令式和声明式的特征。因此开发者可以同时用JavaScript写出命令式与声明式风格的代码。举个例子,我们要遍历一个数组,将每一个元素的数值翻倍,我们可以分别用命令式和声明式来实现。
 | 
			
		||||
 | 
			
		||||
首先,是命令式的实现代码:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
let list = [1, 2, 3, 4];
 | 
			
		||||
 | 
			
		||||
let map1 = [];
 | 
			
		||||
for(let i = 0; i < list.length; i++){
 | 
			
		||||
  map1.push(list[i] * 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后是声明式的实现代码:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
let list = [1, 2, 3, 4];
 | 
			
		||||
const double = x => x * 2;
 | 
			
		||||
list.map(double);
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
从上面的代码我们可以看到,虽然两段代码的目的相同,但是具体的实现手段差别很大。其中命令式强调怎么做,使用的是for循环来遍历,而声明式强调做什么,用到了double算子。
 | 
			
		||||
 | 
			
		||||
## 函数式与纯函数
 | 
			
		||||
 | 
			
		||||
既然编程风格有命令式和声明式,为什么我们在一些设计中更多会选择声明式风格的函数式编程,它究竟有什么好处呢?通过和前面的代码对比,我们看到似乎声明式(函数式)代码写起来更加简洁。是的,大部分情况下,函数式编程的代码更加简洁。但除了能减少代码量之外,函数式还有什么具体的好处呢?这个就要从纯函数说起了。
 | 
			
		||||
 | 
			
		||||
我们知道,函数是对过程的封装,但函数的实现本身可能依赖外部环境,或者有副作用(Side-effect)。所谓函数的副作用,是指函数执行本身对外部环境的改变。我们把不依赖外部环境和没有副作用的函数叫做纯函数,依赖外部环境或有副作用的函数叫做非纯函数。
 | 
			
		||||
 | 
			
		||||
这里,我们先来看一组例子:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
function add(x, y) {
 | 
			
		||||
  return x + y;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEl(id) {
 | 
			
		||||
  return document.getElementById(id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
funciton join(arr1, arr2) {
 | 
			
		||||
  arr1.push(...arr2);
 | 
			
		||||
  return arr1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
在上面的代码中,add是一个纯函数,它的返回结果只依赖于输入的参数,与调用的次数、次序、时机等等均无关。而getEl是一个非纯函数,它的返回值除了依赖于参数id,还和外部环境(文档的DOM结构)有关。另外,join也是一个非纯函数,它的副作用是会改变输入参数对象本身的内容,所以它的调用次数、次序和时机不同,我们得到的结果也不同。
 | 
			
		||||
 | 
			
		||||
## 纯函数的优点
 | 
			
		||||
 | 
			
		||||
现在我们知道了纯函数与非纯函数的区别,但我们又为什么要人为地把函数划分为纯函数和非纯函数呢?这是因为纯函数与非纯函数相比,有三个非常大的优点,分别是易于测试(上下文无关)、可并行计算(时序无关)、有良好的Bug自限性。下面,我一一来解释一下。
 | 
			
		||||
 | 
			
		||||
首先纯函数易于测试,在用单元测试框架的时候,因为纯函数不需要依赖外部环境,所以我们直接写一个简单的测试case就可以了。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
//test with pure functions
 | 
			
		||||
test(t => {
 | 
			
		||||
  dosth...
 | 
			
		||||
  
 | 
			
		||||
  done!
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
而非纯函数因为比较依赖外部环境,在测试的时候我们还需要构建外部环境。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
//test with impure functions
 | 
			
		||||
 | 
			
		||||
//always need hooks
 | 
			
		||||
test.before(t => {
 | 
			
		||||
  //setup environments
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.after('cleanup', t => {
 | 
			
		||||
  //clean
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test(t => {
 | 
			
		||||
  dosth...
 | 
			
		||||
  
 | 
			
		||||
  done!
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
其次,纯函数可以并行计算。在浏览器中,我们可以利用Worker来并行执行多个纯函数,在Node.js中,我们也可以用Cluster来实现同样的并行执行,而使用WebGL的时候,纯函数有时候还可以转换为Shader代码,利用GPU的特性来进行计算。
 | 
			
		||||
 | 
			
		||||
最后,纯函数有良好的Bug自限性。这是什么意思呢?因为纯函数不会依赖和改变外部环境,所以它产生的Bug不会扩散到系统的其他部分。而非纯函数,尤其是有副作用的非纯函数,在产生Bug后,因为Bug可能意外改变了外部环境,所以问题会扩散到系统其他部分。这样在调试的时候,就算发现了Bug,你可能也找不到真正导致Bug的原因,这就给系统的维护和Bug追踪带来困难。
 | 
			
		||||
 | 
			
		||||
总而言之,我们设计系统的时候,要尽可能多设计纯函数,少设计非纯函数,这样能够有效提升系统的可测试性、性能优化空间以及系统的可维护性。
 | 
			
		||||
 | 
			
		||||
## 函数式编程范式与纯函数
 | 
			
		||||
 | 
			
		||||
那么问题来了,我们该如何让系统的纯函数尽可能多,非纯函数尽可能少呢?答案是用函数式编程范式。我们还是通过一个例子来理解。
 | 
			
		||||
 | 
			
		||||
我们要实现一个模块,用它来操作DOM中列表元素,改变元素的文字颜色,具体的实现代码如下:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
function setColor(el, color){
 | 
			
		||||
  el.style.color = color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setColors(els, color){
 | 
			
		||||
  els.forEach(el => setColor(el, color));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
这个模块中有两个方法,其中setColor是操作一个DOM元素,改变它的文字颜色,而setColors则是批量操作若干个DOM元素,改变所有元素的颜色。
 | 
			
		||||
 | 
			
		||||
尽管这两个方法都非常简单,但它们都改变了外部环境(DOM)所以它们是两个非纯函数。因此,我们在做系统测试的时候,两个方法都需要构建外部环境来实现测试。
 | 
			
		||||
 | 
			
		||||
如果想让系统测试更简单,我们是不是可以采用函数式编程思想,把非纯函数的个数减少一个呢?当然可以,我们可以实现一个batch函数来优化。batch函数接受的参数是一个函数f,就会返回一个新的函数。在这个过程中,我们要遵循的调用规则是,如果这个参数有length属性,我们就以数组来遍历这个参数,用每一个元素迭代f,否则直接用当前调用参数来调用f就可以了。
 | 
			
		||||
 | 
			
		||||
具体的实现代码如下:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
function batch(fn){
 | 
			
		||||
  return function(target, ...args){
 | 
			
		||||
    if(target.length >= 0){
 | 
			
		||||
      return Array.from(target).map(item => fn.apply(this, [item, ...args]));
 | 
			
		||||
    }else{
 | 
			
		||||
      return fn.apply(this, [target, ...args]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
因为batch函数的参数和返回值都是函数,所以它有一个专属的名字,**高阶函数(High Order Function)**。高阶函数虽然看上去复杂,但它实际上就是一个纯函数。它的执行结果只依赖于参数(传入的函数),与外部环境无关。
 | 
			
		||||
 | 
			
		||||
我们可以测试一下这个batch 函数的正确性,方法十分简单只要用下面这个Case就行了。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
test(t => {
 | 
			
		||||
  let add = (x, y) => x + y;
 | 
			
		||||
  let listAdd = batch(add);
 | 
			
		||||
  
 | 
			
		||||
  t.deepEqual(listAdd([1,2,3], 1), [2,3,4]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
有了batch函数之后,我们的模块就可以减少为一个非纯函数。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
function setColor(el, color){
 | 
			
		||||
  el.style.color = color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let setColors = batch(setColor);
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
这里我们用 batch 来实现 setColors,只要 batch 实现正确,setColors 的行为就可以保证是正确的。
 | 
			
		||||
 | 
			
		||||
## 高阶函数与函数装饰器
 | 
			
		||||
 | 
			
		||||
刚才我说,batch是一个高阶函数。所谓高阶函数,是指输入参数是函数,或者返回值是函数的函数。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/63/57/63d5941b67608e33cb71ebca2352c557.jpeg" alt="">
 | 
			
		||||
 | 
			
		||||
如果输入参数和返回值都是函数,这样的高阶函数又叫做**函数装饰器(Function Decorators)**。当一个高阶函数是用来修饰函数本身的,它就是函数装饰器。也就是说,它是在原始函数上增加了某些带有辅助功能的函数。
 | 
			
		||||
 | 
			
		||||
这么说你可能不太理解,我们再来看一个例子。
 | 
			
		||||
 | 
			
		||||
假设,我们的代码库要进行大版本升级,在未来最新的版本中我们想要废弃掉某些API,由于很多业务中使用了老版本的库,不可能一次升级完,因此我们需要做一个平缓过渡。具体来说就是在当前这个版本中,先不取消这些旧的API,而是给它们增加一个提示信息,告诉调用它们的用户,这些API将会在下一次升级中被废弃。
 | 
			
		||||
 | 
			
		||||
如果我们手工修改要废弃的API代码,这会是一件非常繁琐的事情。而且,我们很容易遗漏或者弄错些什么,从而产生不可预料的Bug。
 | 
			
		||||
 | 
			
		||||
所以,一个比较聪明的办法是,我们实现一个通用的函数装饰器。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
function deprecate(fn, oldApi, newApi) {
 | 
			
		||||
  const message = `The ${oldApi} is deprecated.
 | 
			
		||||
Please use the ${newApi} instead.`;
 | 
			
		||||
 | 
			
		||||
  return function(...args) {
 | 
			
		||||
    console.warn(message);
 | 
			
		||||
    return fn.apply(this, args);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
然后,在模块导出API的时候,对需要废弃的方法统一应用这个装饰器。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
// deprecation.js
 | 
			
		||||
// 引入要废弃的 API
 | 
			
		||||
import {foo, bar} from './foo';
 | 
			
		||||
...
 | 
			
		||||
// 用高阶函数修饰
 | 
			
		||||
const _foo = deprecate(foo, 'foo', 'newFoo');
 | 
			
		||||
const _bar = deprecate(bar, 'bar', 'newBar');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 重新导出修饰过的API
 | 
			
		||||
export {
 | 
			
		||||
  foo: _foo,
 | 
			
		||||
  bar: _bar,
 | 
			
		||||
  ...
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
这样,我们就利用函数装饰器,无侵入地修改了模块的API,将要废弃的模块用deprecate包装之后再输出,就实现了我们想要的效果。这里,我们实现的deprecate就是一个纯函数,它的维护和使用都非常简单。
 | 
			
		||||
 | 
			
		||||
## 过程抽象
 | 
			
		||||
 | 
			
		||||
理解了前面的例子之后,咱们再回过头来,说说课程中的函数式编程。我们直接来看第7节课里parametric函数的实现。
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
function parametric(xFunc, yFunc) {
 | 
			
		||||
  return function (start, end, seg = 100, ...args) {
 | 
			
		||||
    const points = [];
 | 
			
		||||
    for(let i = 0; i <= seg; i++) {
 | 
			
		||||
      const p = i / seg;
 | 
			
		||||
      const t = start * (1 - p) + end * p;
 | 
			
		||||
      const x = xFunc(t, ...args); // 计算参数方程组的x
 | 
			
		||||
      const y = yFunc(t, ...args);  // 计算参数方程组的y
 | 
			
		||||
      points.push([x, y]);
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      draw: draw.bind(null, points),
 | 
			
		||||
      points,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
如上面代码所示,parametric是一个高阶函数,它比上面的函数装饰器更加复杂一点的是,它的输入是两个函数xFunc和yFunc,输出也是一个函数,返回的这个函数实际上是一个**过程**,这个过程是对x、y的参数方程根据变量t的值进行采样。
 | 
			
		||||
 | 
			
		||||
所以,实际上parametric函数封装的是一个过程,这种封装过程的思路,叫做**过程抽象。<strong>前面的函数装饰器,还有batch方法,实际上也是过程抽象。对应的一般程序设计中我们不是封装过程,而是封装数据,所以叫做**数据抽象</strong>。
 | 
			
		||||
 | 
			
		||||
过程抽象是函数式编程的基础,函数式编程对待函数就像对待数据一样,都会进行封装和抽象,这样能够设计出非常通用的功能模块。
 | 
			
		||||
 | 
			
		||||
## 要点总结
 | 
			
		||||
 | 
			
		||||
函数式编程的内容非常多,这一节课,我只是借助了这些基础的概念和代码,把你带进了函数式编程的大门。
 | 
			
		||||
 | 
			
		||||
首先,我们了解了两种不同的编程范式,分别是命令式和声明式。其中,函数式属于声明式,而过程式和面向对象则属于命令式。JavaScript语言是同时具有命令式和声明式特征的编程语言。
 | 
			
		||||
 | 
			
		||||
然后,我们知道函数式有一个非常大的优点,就是能够减少非纯函数的数量,这也是我们设计系统时要遵循的原则。因为相比于非纯函数,纯函数具有更好的可测试性、执行效率和可维护性。
 | 
			
		||||
 | 
			
		||||
最后,我们还学会了使用高阶函数和函数装饰器来设计纯函数,实现通用的功能。这种思路是对过程封装,所以叫做过程抽象,它是函数式编程的基础。
 | 
			
		||||
 | 
			
		||||
## 小试牛刀
 | 
			
		||||
 | 
			
		||||
1. 如果你了解react,你会发现react-hooks其实上就是纯函数设计。你可以思考一下,如果引入了它,能给你的系统带来什么好处?
 | 
			
		||||
1. 我们在前端业务中,也会用到一些常用的函数装饰器,比如,节流throttle和防抖debounce,你能说说它们的使用场景吗?如果让你实现这两个函数装饰器,你又会怎么做呢?
 | 
			
		||||
 | 
			
		||||
函数式编程的思想你都理解了吗?那不妨也把这节课分享给你的朋友吧。今天的内容就到这里了,我们下节课见!
 | 
			
		||||
							
								
								
									
										82
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐5|周爱民:我想和你分享些学习的道理.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								极客时间专栏/跟月影学可视化/特别放送/加餐5|周爱民:我想和你分享些学习的道理.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
 | 
			
		||||
你好,我是月影。
 | 
			
		||||
 | 
			
		||||
专栏更新过半啦,我也收到了一些基础薄弱的用户反馈,说这门课难学。今天,我特意邀请了爱民老师,来和我们分享他的学习方法、心得。你可能会好奇,为什么今天没有音频。我希望你能静下心来,花上十几分钟,跟随着爱民老师一起思考学习的道理。
 | 
			
		||||
 | 
			
		||||
> 
 | 
			
		||||
<p>昔闻人言:天下之事,最轻易者莫过于求学,最恒难者,莫过于问道。予深以为然。<br>
 | 
			
		||||
 <br>
 | 
			
		||||
——引言</p>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
你好,我是周爱民。今天出现在这个课堂上,并不是因为我对月影兄所讲的“可视化”有着怎样的经验,也并没有什么应用技能或理论思想能和你交流。我在这里,想要与你讲的是“如何学习”。即便鼓了很大的勇气才领下这个题目,也不是因为我很善于学习。我在学习这件事上,是一个只会用笨功夫的人,没有什么技巧能跟你讲。
 | 
			
		||||
 | 
			
		||||
那我究竟为什么要来讲这个话题,又有什么可以跟你讲的呢?
 | 
			
		||||
 | 
			
		||||
这就要从同学们的感受说起了。“很难”,这大概是许多同学对月影这门课的直接感受。而这个感受,与引言中所谓“天下最轻易者”实在是天差地别。而我历来认为学习不同于问道:问道恒难,而学习甚易。这便是我想跟你聊一聊的道理。尤其重要的是,只要把这个道理听明白了,我想你学习起来也就轻松了。
 | 
			
		||||
 | 
			
		||||
正如我前面说过的,这不是学习的技巧,而是学习的道理。
 | 
			
		||||
 | 
			
		||||
## 为什么你会觉得难
 | 
			
		||||
 | 
			
		||||
很多人知道,我不是计算机专业出身的。在我决定自学计算机时,也并没有老师领路,因此,我需要自己解决的第一个问题便是:自修的话,先选什么课程。当时我是拿了计算机专业几年的课程表来看,然后把它们之间的关系理顺,最后决定先花一个学期单单只看一门《数据结构》。之所以这么选,是因为其它课程好像都要用到它,看起来也都与它有关。至于这其中的原因,我当时却是不明白的,因为没有人教,也没有人可以问。
 | 
			
		||||
 | 
			
		||||
那个时候,这门课用的是Pascal,我没有语言基础,学校的电脑上也没有这个语言,所以我基本是手写代码,脑袋编译。至于一段代码运行的正确性,对不起,没有上机测试,所以我也不知道。就这样忽忽的学了一个学期,我去考了试,得了70多分。
 | 
			
		||||
 | 
			
		||||
于是,我的《数据结构》的底子就这样扎下了。自那以后,我在计算机语言、应用这方面,再没遇到比当时更难的困境。这其中的道理很简单,**只要扎实基础,循序渐进,几乎所有语言与代码方面的问题,我们都可以一点点啃下去**。 直到如今,如果有人去看我写的那本《程序原本》,就能看得出这个硬底子还在。
 | 
			
		||||
 | 
			
		||||
当然,一定会有同学说:我的《数据结构》成绩比你还好,但一样觉得很难啊!
 | 
			
		||||
 | 
			
		||||
这是正常的。上面我说的这个道理,其实只说了一层。要知道我从业二十多年来,像《数据结构》这样一门一门、一本一本啃下来的课,也并不止一个。如今看起来学什么都容易,只不过是积腋成裘的结果罢了。在这个过程中,但凡我觉得有“非常之疑难”者,无不是中间少啃了那么一本两本“《数据结构》”。也就是说,**那些称得上“疑难不懂”的,其实都只是缺了几门知识,少花了几次笨功夫而已**。所以要学好月影这门课,且先不说他如何讲,你只要笃定一个笨想法就可以了:你看到不懂的名词,必然是少学了几门功课;看到不懂的话语,必然是少推断了几个逻辑,等等如此。
 | 
			
		||||
 | 
			
		||||
有着这些种种不懂,也就当然有种种能懂的知识。你把这些坑坑洞洞都补起来,自然也就懂了。只要你愿意像在学校里一样地去做笨功夫,这些缺失的知识又怎么算得上难呢?
 | 
			
		||||
 | 
			
		||||
## 为什么你会听不明白
 | 
			
		||||
 | 
			
		||||
不过很多人所面对的,倒不见得是“难”,而是所谓“疑”。疑难二字,其实并不是同一个东西:你攀山越岭时,所感觉到的便是“难”,只要努力,一座座地翻过去,总还是可以到达的。而你隔岸观花时,那种似是而非才是“疑”。
 | 
			
		||||
 | 
			
		||||
之所以有“疑”,并不是目力不济,而是心志不济。
 | 
			
		||||
 | 
			
		||||
在《[程序原本](https://github.com/aimingoo/my-ebooks)》这本书的前言致谢中,我感谢了我的小学数学老师,因此便有同学问我其中的缘故。我说:那本书中,所用到的数学知识,止于小学足矣。然而这本书却是我在极客时间开设《[JavaScript核心原理解析](https://time.geekbang.org/column/intro/100039701)》一课的基础,所以如今反观这本书、这门课程,其实在数学知识上都是“止于小学足矣”。
 | 
			
		||||
 | 
			
		||||
所以我很想问,都这样了,为什么还有人觉得“难”?又或者问,既然已是“如此这般地不难”了,那么你所“疑”的,又是什么呢?
 | 
			
		||||
 | 
			
		||||
但凡我们受过高等教育的,又或者写过几年程序的,都不妨把自己看成是“有知识、有经验”的人。这很正常,我们本来就是靠着这些知识与经验吃饭的,这都是看家底儿的东西。然而,这些东西“正确”吗?**我们之所“疑”的,往往就是所“见到”的与我们所“知道”的之间的矛盾**。比如说在《JavaScript核心原理解析》中,第一讲便是说“引用”这个概念的,然而ECMAScript中的所谓“引用”,与一般语言中的“引用”是似是而非的。所以,从这门课一开始,就有无数同学深陷于这一个概念之中,用既有的知识来解释它:有解释为指针的,有解释为结构的,有解释为类型的,……不一而足。
 | 
			
		||||
 | 
			
		||||
**为什么有“疑”?其实这是缘于我们对“已知”的不疑。**当这种不疑与面前的真相矛盾时,我们就怀疑了、拒绝了、不可接受了。学习中怕的不是看不懂,而是明明表面什么都看得懂,心底里却什么都不接受。这才可怕,这才畏怯,这才寸厘不进。
 | 
			
		||||
 | 
			
		||||
这也是很多课程“学不明白”的根由:你从一开始,就放不下那些你认为“对”的东西;既然你认为已看到的是对的,那么眼前所学的,又是什么呢?
 | 
			
		||||
 | 
			
		||||
所以**要解决学习中的“疑”,反倒是放开态度,认为自己什么也不懂,心志坚定地跟着眼前这位老师的步子一点点看,一遍遍练**。这个过程,**并不是要怀疑自己,而是要敢于舍弃自己**。相信他人既然可以为师,必有所长,必不吝于教,无有成见地去学习,才能学有所得,进而更新自己。
 | 
			
		||||
 | 
			
		||||
在《[大道至简](https://book.douban.com/subject/11874745/)》中说过“问道于盲”的故事:只要你愿意闭上眼睛,你也可以向盲人问道。睁开眼问,这止于眼前的所谓“真相”,便是你的疑难了。
 | 
			
		||||
 | 
			
		||||
## 为什么你觉得什么也没学会
 | 
			
		||||
 | 
			
		||||
其实月影这门课还有一个副标题,是“系统掌握图形学与可视化核心原理”,这与《JavaScript核心原理解析》有一个关键词的重合。并且,事实上在我那门课程的开篇中,我还专门提到这门课的要点是“体系性(系统)”,这算得上另一处巧合。
 | 
			
		||||
 | 
			
		||||
不过,“核心原理”+“体系性”却并不见得是“你觉得难”的根源。说到这里,恐怕又有不少人要跳起来反对了:都讲“核心原理”了,还不难啊?!
 | 
			
		||||
 | 
			
		||||
其实核心原理通常来说真的不难。例如密码学,核心原理就是映射变换;图形学,核心原理就是点(位置/坐标)等等。同样的,“体系性”也并不难,例如月影的课程,体系性就摆在那里了,你去看看课程目录就知道了。所以无论是体系,还是核心原理,都不是这些课程让你觉得难的真正原因。
 | 
			
		||||
 | 
			
		||||
这让我想起了《大道至简》这本书最后有一个“问智”的桥段。起首的一问是“何谓愚”,书中说的是:不知道事情的起始,也不知道它的终点,就是所谓的“愚”了。而在我看来,一门课程要是学不好、学不懂、学不通,起步上的错处,就在你对于要学会的东西“不知其始、非知所终”。
 | 
			
		||||
 | 
			
		||||
不管是什么课程,你得先知道老师“想讲什么”,这就是结果。如果一个课程你知道了结果,并且又假设结果是对的,那么学习它的方法就是按照老师所讲的内容,一步步推演过去就好了。这个过程是确定的,结果也是确定的,你所需的无非是推演中所缺的几门功课,以及一点耐心而已。所谓前人、所谓先行者,其实无非就是比你多走了一遍路途而已,所以只要我们放下自我的姿态,亦步亦趋地跟着老师讲的往前走,那么这个结果总是能看到的。
 | 
			
		||||
 | 
			
		||||
学习之“易”也就在于此:总有一个确定的过程,以及一个确定的结果。但是,这往往还不够。因为如果老师只教你这个结果,那也只是最低一等的“师”。无论如何,真正的“良师”,会帮助你解决“不知起始”的问题。——于此,你真正还需要知道的是老师“为什么要教”。一旦你知道了“教的目的”,也就知道了复核自己“学的结果”的方法,这就是相互印证了。如果学习不知道起始、不知道目的、不知道印证,那么怎么学都是茫茫然。
 | 
			
		||||
 | 
			
		||||
而核心原理和体系性的“易”也就在于此:核心原理总是一个简单的结论,你所学的,只是这个结论得到的过程;体系性总是一种明确的结构(组合),你所学的,只是将这个结构组织起来的方法。所以最终来说,你就是要得到一个核心推论的过程,以及了解它所涉及的那些领域之间的关系而已。
 | 
			
		||||
 | 
			
		||||
多数情况下,就结果来说,你甚至可以从课程目录,以及每章的小节中直接读到它们。那么不管它与你既有的知识是否有冲突,将这门课程粗略地看过去,拿了这样的结果(核心过程与结构关系)就可以了。
 | 
			
		||||
 | 
			
		||||
**不过,学习这样的课程,不要在“结果”上花功夫,从过程上去找、去看方法,才是正途。**
 | 
			
		||||
 | 
			
		||||
## 道理我都懂
 | 
			
		||||
 | 
			
		||||
正如引言中所说,学习这件事情,其实真的不是很难。至于是不是“天下最轻易者”,我可不敢跟古人去论争。所以在今天的分享里,我也只是想把一些似难实简的地方跟你说说:**其一,你可能只是缺了点知识,要补课;其二,你可能只是宥于所知,请开放思想;其三,你可能只是埋头学习,而忘了要学会的是什么。**
 | 
			
		||||
 | 
			
		||||
是的,我知道已经有人开始在说:“道理我都懂……”。
 | 
			
		||||
 | 
			
		||||
这种腹诽,实在是这么些年来我最怕见到的。因为持这种态度者,大多都是不愿下苦功夫的人。所以我希望读到这里,心底下还叹着“道理我都懂”的朋友,能再回到第一小节,从苦功夫做起。要知道**古往今来之有成就者,不过将学习持之以恒而已,哪里有什么技巧**。
 | 
			
		||||
 | 
			
		||||
不过如果要舍了学习不谈,只想拿个现成的东西去用,那么作为程序员,最好的地方是在GitHub,而不是在极客时间。我想,这个看起来众所周知的答案,便是我对那些急于掩卷而去的人,最后的劝慰了。
 | 
			
		||||
							
								
								
									
										51
									
								
								极客时间专栏/跟月影学可视化/特别放送/用户故事 | 非前端开发,我为什么要学可视化?.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								极客时间专栏/跟月影学可视化/特别放送/用户故事 | 非前端开发,我为什么要学可视化?.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
<audio id="audio" title="用户故事 | 非前端开发,我为什么要学可视化?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/43/cf/434cdddf4849aa57cdcfb41fd2a500cf.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是月影。专栏已经更新过半啦,首先我想感谢仍然坚持认真学习的你,很高兴见证你的成长。在这个过程中,我的课程对你有哪些帮助呢?你可以写在留言区分享给我。
 | 
			
		||||
 | 
			
		||||
今天,我想和你分享一篇罗同学的故事。罗同学每堂课都会非常认真地完成课后作业。在这里,我想对他说,很高兴能够看到你分享自己的学习故事,谢谢你能喜欢这个专栏。你是一名Android开发工程师,实际上我也写过Android App,所以我们也算是半个同行。做技术的同学能够喜爱折腾技术是特别好的特质,尤其是在折腾技术的同时还能重视基础的积累,这就更难得了。这样的同学往往能成长到很高的高度,希望我的专栏能够让你在专业之路上走得更远。
 | 
			
		||||
 | 
			
		||||
好了,下面,我们一起来看看他的收获吧。
 | 
			
		||||
 | 
			
		||||
你好,我是罗乾林,是一名有着5年工作经验的Android应用开发工程师,坐标上海,很高兴能跟你分享我与可视化的故事。
 | 
			
		||||
 | 
			
		||||
讲起来还挺惭愧的,我的本职工作并不是前端开发,甚至我连JavaScript的语法都不怎么熟,但这并不影响我学习可视化专栏,反而除了图形学的知识以外,在阅读老师代码的过程中,我还学到了JavaScript的相关语法知识。因此,我希望结合我的经历,给同样非前端,正在学或者想学这门课的你一些帮助。
 | 
			
		||||
 | 
			
		||||
## 我为什么会学可视化?
 | 
			
		||||
 | 
			
		||||
首先,我想先说说,我为什么要学可视化,原因主要有两方面。
 | 
			
		||||
 | 
			
		||||
一方面,我的工作是开发Android应用,在需要绘图的时候,我通常会使用内部提供的Canvas, 它底层使用的是Skia图形引擎,提供的绘图接口跟浏览器上的Canvas有些差别。这让我对原生的Canvas产生了好奇。
 | 
			
		||||
 | 
			
		||||
而且,平时我就是一个比较喜欢折腾其他技术的人,比如MFC绘图的DC、QT的QPainter 、JavaFx的Canvas。这些环境都提供了一些类似的绘制基本图形的接口。但是,一直以来我只是在调用这些接口,并没有认真思考过这些绘图系统的核心是什么,对它们的理解也都不深入。
 | 
			
		||||
 | 
			
		||||
这就导致,我在绘制一个效果的时候,从一个提供了接口的绘图环境中,换到另一个没有提供接口的环境中就会不知所措。
 | 
			
		||||
 | 
			
		||||
另一方面,因为项目的原因我接触到了OpenGL ES绘图,而OpenGL ES只支持几种图元的绘制,很多图形的绘制都要自己实现。这里,我想先来简单说说这个项目。
 | 
			
		||||
 | 
			
		||||
这个项目简单来说就是开发一个自定义播放器,播放器运行在Android设备上。播放器从服务端获取布局文件,根据布局文件将界面划分为多个大小不等的区域,分别用来展示视频、图片、文本。其中,播放视频我们采用的方案是,将视频解码为一帧一帧的RGB数据并缩放到相应大小送入Canvas 显示。
 | 
			
		||||
 | 
			
		||||
在开发过程中我发现,对一些稍大分辨率的视频文件,播放器播放起来非常卡顿。通过在网上查阅资料了解到,我可以将视频解码出YUV格式的数据直接送入GPU中展示,以此来提升播放性能。就在这个过程中,我接触到了OpenGL ES。
 | 
			
		||||
 | 
			
		||||
刚接触一项新技术的时候,我们应该都是倾向于先写个“Hello World”找找感觉。于是,我就跟着网上的教程写了一个绘制三角形的demo。写完后感觉整个人都不好了,因为绘制一个简单的三角形,我就写了快90行代码,中间涉及的vertexbuffer 和indexbuffer更是看得人一头雾水,而且OpenGL ES 提供的图元就只有点、线、三角形。这个时候,我就在想那要绘制一个圆该咋办呢?使用OpenGL ES完全没有我之前使用绘图系统提供的绘图接口丰富、易用。这更加深了我在可视化方面的困惑。
 | 
			
		||||
 | 
			
		||||
## 我与《跟月影学习可视化》专栏
 | 
			
		||||
 | 
			
		||||
因为我一直有用极客时间来学习,刚好在这个时候,我看到平台推出了“跟月影学可视化”这门课程,看了看介绍,发现就是在讲解图形学的知识,并且,OpenGL ES和专栏中讲的WebGL几乎一样,这正是我想要的,所以我就立马买了下来。
 | 
			
		||||
 | 
			
		||||
开始学习专栏之后我发现,我前面遇到的这些问题,都是因为缺少图形学的基础知识。就像月影老师在[第5讲](https://time.geekbang.org/column/article/255584)所说的“如果我们手中只有解决具体问题的工具,没有统一的方法论,那我们也无法一劳永逸地解决问题的根本”。
 | 
			
		||||
 | 
			
		||||
比如说,我在初中的时候,就学了点构成线、线构成面、面构成体,这些最基本的数学原理却一直不能在实践中进行应用。课程中的数学基础篇让我重新认识了数学的强大,不局限于某一图形系统提供的API,图形学的基础知识才是根本。扩展到其他的计算机知识都是这样,现在新技术层出不穷,让人眼花缭乱,而其最本质的知识都是不变的。
 | 
			
		||||
 | 
			
		||||
而且,随着学习的深入,我把之前有想过但并没有深入思考过的问题都发掘了出来,并且也都有了答案。比如说,判断点是否在区域内。我平时在绘图的时候,大部分是判断点是否在一些规则图形内部,即使碰到了一些不规则图形,也是使用现成的API判断,如Android 提供了Path、Region,浏览器Canvas2D的isPointInPath等。
 | 
			
		||||
 | 
			
		||||
接着,[第8讲](https://time.geekbang.org/column/article/258101)让我了解到,图形学领域有三角剖分这样成熟的技术来解决这类问题。第14讲和第19讲,又解开了我困惑已久的,如何使用着色器实现几何造型和动画的问题。
 | 
			
		||||
 | 
			
		||||
在平时的开发过程中,我也会简单使用仿射变换来做图形操作,适当地使用坐标变换,使我们在绘制一些图案时更加简单。而专栏中讲解的生成重复图案、分形图案及随机效果,更加让我体会到了数学知识的强大(离开学校后才意识到数学很有用)。
 | 
			
		||||
 | 
			
		||||
像学习其他软件编程的时候,我们在初期并不会觉得数学知识很重要,即使是做了几年的软件开发也未必会觉得数学知识在编程中帮助很大。而在图形学中很快就能让人意识到数学很重要,特别是线性代数,我们会经常使用矩阵变换、曲线方程、向量运算等数学原理,甚至还要用到物理学的知识,如实现动画涉及的缓动函数,实现自由落体等模拟现实世界的图形绘制。这也激发了我学习的兴趣,让我觉得这样的技术才是有门槛的。
 | 
			
		||||
 | 
			
		||||
## 关于学习专栏的一点建议
 | 
			
		||||
 | 
			
		||||
最后,我想给正在学习专栏的你一点建议,在绘图过程中有很多细节需要我们特别注意,如果只是听听老师的讲解,没有自己动手去尝试,那有很多细节就会留意不到,也就不会引起我们的重视。这门课程对我来说,学习起来确实不是很轻松,我相信也有很多人像我一样,即使我们现在学习起来有点困难,可只要我们把代码拉下来运行一下,调整下参数,再观察图形是否像我们预料的那样,也是能够加速对原理的理解,所以我的建议就是一定要多动手实践。
 | 
			
		||||
 | 
			
		||||
对底层技术的学习能让我们在技术方面更加地自由、自信,也是我们快速掌握新技术的内功。我认为计算机技术的核心是:操作系统、数据结构和算法、编译原理、计算机网络、计算机图形学。这也是我一直学习的方向。
 | 
			
		||||
		Reference in New Issue
	
	Block a user