写代码如盗墓笔记,如何逃出生天?

2021-02-05 09:21:06 +08:00
 muchan92

为什么说写代码如盗墓笔记呢,是因为写代码确实有一些特点和盗墓笔记很像,比如坑比较多,也都处于暗处,没有在明面上,在进入之前是无法知晓整个结构的,只有亲身深入其中,详细探索完每一个地方之后才能知晓。

但是这个探索过程是非常艰难的,因为一切都是未知的,也没有任何导航可用,只能两眼抹黑的往前瞎拱,自然免不了各种迷路,各种碰壁,各种掉坑。

这种艰难的状况经历了多了之后,我们就应该思考了,是什么原因导致了这种状况?那么又有没有方法可以解决这种状况吗?

分析代码的特点

我们重新审视分析下程序代码的特点,只有了解了这些特点,才能探究出原因。
程序代码主要是由数据变量和逻辑控制流构成的,其中逻辑控制流占据了绝大部分。而逻辑控制流包含了许多分支,就像本来是一条道路,却分叉出了许多支路,选择一条支路,走着走着又继续分叉出更多的支路出来,密密麻麻,乱乱糟糟的,这就导致了这些道路非常难走。并且更令人抓狂的是,这些道路没有导航,没有地图,没有路标,非得抹黑走到路的尽头才能知道尽头到底是什么。
数据变量基本上有两个职能,一个是用来标记逻辑控制流的运行状态;另一个就是用来组成数据结构。而实际上离散的数据变量也算是数据结构的一种,只是不同于经典数据结构,不太明显而已,容易让人误认为其仅是一个离散的变量,而忽略其影响并左右逻辑控制流的结构性的意义。

所以整体看来就是,数据变量和逻辑控制流是互相关联互相影响的。

而从分析过程中也可以知道,程序代码的特点就是分支散碎,藏于尽头,难以获知。导致这一特点的原因也是逻辑控制流的复杂性、散碎、藏于尽头。也即,程序代码的这些不好的特点都是由逻辑控制流体现的。

那么逻辑控制流能避免这些缺点吗?答案是不能。因为这是由于逻辑控制流的根本复杂性和本质复杂性所决定的。逻辑控制流说白了就是平常说的过程,所以也是无法跳过的,因为你不能跳过过程而直接获得结果。
所以从这里看来,程序代码的这些缺点也依然存在,无法避免。

什么是数据流向?

不过有一点非常值得思考,逻辑控制流的目的是什么?是进行数据转换,也就是进行数据结构的转换,也就是描述并控制数据的流向。
这也是程序的本质:从输入数据结构,来获取输出数据结构。
所以,写逻辑控制流的意义不是纯粹为了写逻辑控制流,而是为了进行数据结构的转换,描述并控制数据的流向。因此,我们可以获得一个非常重要的有用信息,只要能够理清数据的流向,就能显著降低阅读代码的难度。 我们阅读代码的目的其实也就是为了弄清楚数据的流向。

那么我们就把重心转移到理清数据的流向上来,分析下如何才能更好的理清数据的流向。
因为逻辑控制流的本质复杂性,所以我们无法通过逻辑控制流来清晰的理清数据的流向,这也正是我们一直以来读写代码所采用的方式,事实证明,这不是好的方式,数据流向散落在逻辑控制流的各个地方,根本没有好的办法可以串联起来,形成一个有导航意义的地图。

前文说过,程序代码是由数据变量和逻辑控制流构成的,也提到过,数据变量和逻辑控制流是互相关联互相影响的,并且数据流向自然也离不开数据变量。既然没有好的办法通过逻辑控制流来理清数据流向,那么就需要思考下在数据变量上是否有好的办法。

数据变量是什么?前文解释过,数据变量就是数据结构。
请着重注意 结构 这个概念,结构表示了层级和关联,而关联就是流向,我们正是想理清流向。所以在数据结构身上,我们看到了理清数据流向的希望。

在开始分析如何通过数据结构来理清数据流向之前,我觉得有必要,也非常有必要重新强化对结构的认知。因为有太多的小伙伴们对结构没有足够的重视,不以为然,莫不关心,没有认知到结构的重要性以及不可或缺性以及根本本质性。

结构是“神”的语言

我们眼前的一切,周围的一切,手机、电脑、衣食住行、山川大海、以及你我,都是由结构构成的,更确切的讲,这一切就是结构。我们第一次认知这个世界就是从结构开始,第一次睁眼看到父母,第一次拆解玩具,第一次感受花草。人类学习结构,利用结构,组成各种各样的新的结构、新的工具来帮助我们生活。人类研究周围的各种物体,以及日月星辰、宇宙天体,其实就是在探索它们的结构,和结构之间的关联。只有知晓了它们的结构与之间的关联,我们才能组装更高级的结构来生产工具,才能进一步知晓周围万物的原理和世界的运行规律。

我们都知道我们身处的宏观世界是三维的立体世界,这个世界中的任何物体都有其形状,有其结构,形状就是结构,没有结构将无法存在于这个世界中。即使加入看不见摸不着的时间维度组成四维的时空世界,结构依然存在于任何一个维度之中。我们都知道万有引力,其中引力并不是虚无飘渺的能量,而是质量在时空中引起的时空扭曲,正是因为时空可以被扭曲,所以任何一个维度都是有结构的,人类一直在探索其中的结构和关联。

在微观世界中,物质是由分子构成的,人类对分子的结构进行探索发现了原子,对原子的结构进行探索发现了原子核,继续不断对结构进行探索发现了夸克。反过来讲就是,各种更小的粒子结构通过 关联 组合成较大的粒子结构,层层组合最终构成了宏观世界。化学的本质是物理的粒子运动,粒子运动其实是结构之间的关联,人类的情感、想法等都是细胞间的化学信号,而化学信号又是物理的粒子运动,本质又是结构之间的关联,所以我们人类的一切行为,生物的一切行为都是结构之间的关联。

所以,结构是“神”的语言,用结构和结构之间的关联来描述整个世界。

分析如何理清数据流向

数据结构之于程序代码也同样如此,其是程序的根本,数据结构与之间的关联描述了整个程序世界,逻辑控制流依存于数据结构,若离开其,则程序将崩塌不复存在。

现在我们开始分析如何通过数据结构来理清数据流向。
由前文可知,数据的流向是由逻辑控制流来控制的,但是逻辑控制流本身就是复杂的,现在我们换个说法,数据的流向是由逻辑控制流来驱动的,逻辑控制流只是通过一些条件、规则来驱动数据进行流动,其流向并不是在写逻辑控制流的过程中而突然打通的,而是在写逻辑控制流之前,在数据结构定义的时候,就已经开始设计构思其流向了。只是几乎我们每一个程序员都忽略了这一点,我们错误的以为数据流向是存在于逻辑控制流之中的。

这种误解是如何产生的?

首先,程序的原作者在定义数据变量也即数据结构的时候,就已经在脑中设计构思其流向了,但他并没有在该数据结构上做说明来描述数据的流向。然后,他认为数据的流向应该用逻辑控制流来描述就可以,就够用了,所以就零零散散的用代码在逻辑控制流中进行描述。这就导致了别人不得不完整阅读逻辑控制流中的散碎的、反人类的代码来弄清楚数据的流向,原作者留给后来人的仅有这一堆蜿蜒曲折,支离破碎的代码,而清晰的导航地图只留在了原作者的脑海中,别人无法获取。只有神和原作者知道。时间久一点后,就只有神知道了。

强化数据结构的表达

阅读到此,我们已经非常确定数据的流向是隐含于数据结构之上的,只是数据结构太沉默,缺乏表达。而逻辑控制流又太活跃,喧宾夺主。

所以,我们要强化数据结构的表达,在数据结构上描述数据的流向,也就是结构之间的关联。

事实上,当我们把几个独立的数据结构,编排组织在一起成为一个较大的新的结构时,这种关联就已经建立了,只是我们并未对其进行更多的、更详细的描述,关联隐晦其中。我们只是把它当作了数据的存储,然后通过逻辑控制流再对其进行读读写写。

寻找解决方案

有两种方案可以更好的描述结构之间的关联。

注释型方案

一种是注释型方案,在数据结构上通过注解来解释并描述结构之间数据的流动,可以描述流动的方向性,然后通过预处理,静态编译来校验结构之间关联的正确性。我们需要实现一个静态的编译检查工具。

依然还是在逻辑控制流中对数据的流向进行控制,只是额外增加了在数据结构定义时的说明,弊端是程序员可以偷懒不进行注解,这样就又退化回了原始的蛮荒。更主要的弊端是,还是采取了以逻辑控制流为主的编码方式,也就保留了前文说过的逻辑控制流的所有缺点,虽然通过注解有了数据流向的地图,但是辅助作用有限,阅读并梳理代码还是有较大难度,治标不治本。

数据结构化编程

另一种方案是,以数据结构为主,以逻辑控制流为辅,也就是面向数据结构编程(数据结构化编程),而非面向过程。

注意,这不是为了提出新概念,也不是为了喊口号,而是为了改变以往的过程式思维方式,转到结构化思维方式上来。意识形态决定思维方式,思维方式决定实际行为。

这就是为何一直要强调结构的重要性来建立意识形态,从而塑造结构化思维,从而决定实际的代码编写。这个初始的思维转变可能不太习惯,请耐心些,因为我们已经越过了逻辑控制流的层层迷雾,开始直接面对程序的核心“数据结构”,握住了程序的命脉。

结构化思维 & 结构的规则

改变根深蒂固的看法

在结构化思维方式中,我们首先要改变把数据结构仅仅当作数据存储这一根深蒂固的看法。

或许你会认为这违背了以往的准则,离经叛道,数据结构就应该也仅应该当作逻辑控制的存储区域,这样才有简单性。貌似没错,但实际上整个程序代码变简单了吗?用事实说话就是并没有!唯一保持“简单”的只有数据结构,能不简单吗,就是声明定义了一个变量而已,当然简单了。但为此付出的代价是无比高昂的,仅为了保持变量声明的简单性,而不得不在逻辑控制流中来控制其流向,操纵其行为,通过各种令人绝望的骚操作来让其运动起来,引发绝望的复杂性。

这就好比,太阳和地球纯粹就是简单的球体而已,是固定静止的,没有地球绕着太阳旋转,而如果要让地球绕着太阳旋转的话,则只能由上帝额外施加一个 神力 来让地球绕着太阳旋转。如果我们的宇宙真是这样的话,我们永远无法弄清楚宇宙天体的运行规律,绝不可能!这直接丢弃了万有引力,而是通过神力来模拟引力的效果,而神力却是我们永远无法触及的,除非我们是神。更恐怖的是,一旦引入这个神力,就会破坏原来世界的平衡(蝴蝶效应),而为了维持平衡,则会引入更多的神力,从而陷入无尽的循环。一个充满神力的世界,是极致复杂的,永远不可能弄清楚。

而我们却习惯于在程序代码中充当神的角色,用各种神力来实现目的,并艰难的维护着平衡,以至于自己也难以看懂。摘去神力的面具,这其实是幽灵之力,魔鬼之力,地狱之力,想当神的我们,却实际成为了撒旦。

改变了看法之后,数据结构就不应该是一个静静的数据存储了,数据结构本身一定是有自己的规则。 就像太阳和地球一样,其自身就具有引力的规则,可以自我解释清楚,而不是借助外部的神力。宇宙的规则“物理”的简单之处也就是所有的规则一定是存在于其物质本身的,通过对物质本身的结构和规则进行探索,就能掌握万物的奥秘。

改变主观视角

所以我们要在数据结构上定义规则,用其本身的规则来描述结构之间的关联。

在结构化思维方式中,我们思考的角度应该立足于数据结构上。 这就产生一个非常有意思的思考流程:

  1. 首先告诫自己,这是一个从输入数据转化为输出数据的运动规则,一定抽象出两类字段,即输入字段和输出字段。
  2. 分析需求,找出提供的物料和期望的结果,在结构上定义一个或多个输入字段,和一个或多个输出字段。
  3. 因为要定义规则,规则的最大特点就是可以被动的自动执行(物理规则也是如此;其不同于函数,函数是主动执行的),所以要找到执行的时机。
  4. 考虑到函数执行的时机是当所有入参都传入时,所以很自然的,规则的执行时机,应该是规则所依赖的输入字段都准备好时。
  5. 这里获知一个很有趣的性质,就是规则的执行是不依赖入参的顺序的,也即无需操心字段的赋值顺序,程序员只需要负责把字段塞满就好,如果有规则该执行,那么就会自动执行。
  6. 由此,定义规则就变得简单了,主要分为三部分:
    • 所需要的条件(输入数据),即声明需要哪些输入字段
    • 规则细节(数据转化的过程),即实现数据转化的过程
    • 产生预期输出结果(输出数据),即标明会有哪些输出字段(暂时简化,写此文时,在评估确定较好的通用标明方案)
  7. 发现结构化的一个重要性质,即数据流向是清晰的,易于梳理的,这对可维护性很重要。(达到理清数据流向的目标
  8. 发现结构化的另一个重要性质,即数据结构的复用性很高,因为每个结构都是自洽的。
  9. 对于复杂的需求,可以构建树形数据结构,由子结构的输出结果作为父结构所需要的条件,执行规则,产生属于父结构的输出结果。最终通过这种简单的方式,在最外层生成最终的预期输出结果,交付给用户。
  10. 最终,重新认知到,程序是由简单易懂的数据结构遵循规则运行后,产生预期输出结果(输出字段),而不应该是进入黑盒函数后经过玄学操作返回结果。这是因为,前者的结果是可以预期的,规则是恒定的,可以扁平展开的;而后者的结果是难以预期的,也难以进入梳理运行流程,复杂令人绝望。

结构化的基本性质和特性

可以发现结构化的三大基本性质:易于读写、流向清晰、高度复用
和高级特性:安全、并发。字段会对数据进行完备校验(不仅校验类型,也校验数据值是否符合预期规则),每个字段都是安全的,那么整个程序就会是安全的。由于字段赋值是不要求顺序的,因而可以有更好的并发性。

定义结构 & 定义规则

实际编程需要做的只有 2 、6 两步,这也正是结构化思维后数据结构的两个根本构成:字段和规则。在以往只有字段的情况下,新添加了规则这一个概念,整个程序世界变得完全不同了,开始变得清晰明朗了,一直萦绕的迷雾消失不见了,如透明般展示于我们的眼前。就像有了货币规则之后,人类的整个文明世界都变得不同了,社会飞速进步。

显而易见的,在走完整个思考流程后,最终我们对程序有了简单易懂的全新认知,获益是巨大的。整个程序如树形结构一般清晰的展示于我们眼前,对于我们阅读来讲,基本上只需要关心字段的输入和输出,而不必探究详细的实现,因为字段是如此的清晰易读。每一个子结构都可以使用同样的方式,扁平展开,阅读复杂度是恒定平缓的,而这在过程式编程中是无法想象的,是不可能的,其复杂度如蜗牛一般是螺旋状的,总能把人搞晕,代码量越多,复杂度就越恐怖。而现在我们终于找到了结构化编程,将复杂度变得平缓,有一种打破枷锁的解放感。

耳听为虚,眼见为实,接下来通过一个示例来体会下结构化编程的简单易懂。(此为演示代码,现在已经有 TypeScript 版可用: https://rainforesters.github.io/rainforest-js

// 定义结构

// 定义跳跃的信号结构
typedef JumpSignal {
  jump bool
  event Event
  stage Stage
}

// 定义规则函数

// 这个规则应该解读为:将用户的操作转化为跳跃信号
funcdef JumpSignal (
  // 声明待观察的字段(可以有多个)
  // 一旦所有字段都准备好(被赋值),则触发规则函数
  event
) => {
  if (self.event.type == "tap") {
    self.jump = true // 为 jump 字段赋值,表示 jump 字段已经准备好
  }
}

funcdef JumpSignal (
  stage
) => {
  self.stage.addEventListener("tap", (e) => {
    self.event = e
  })
}


typedef Avatar {
  ...
}

typedef Player {
  avatar Avatar
  jumpSignal JumpSignal
}

// 这个规则应该解读为:当跳跃信号发生时,将角色变成跳跃状态
funcdef Player (
  jumpSignal {
    // 可以直接观察子结构的字段,而无需关心 jumpSignal 是否为空,
    // 因为,遵循结构化编程的思维,这里只需要声明待观察的目标字段,
    // 也就是,我们只期望 jump 准备好,
    // 这也就间接表明了 jumpSignal 肯定会准备好。
    // 这种简单直白的条件声明,是以往过程式编程所无法实现的。
    jump
  }
) => {
  if (self.jumpSignal.jump) {
    self.avatar.state = "jump"
  }
}


func main() {
  // 初始化结构实例
  // avatar jumpSignal 会自动初始化
  const player = typeinit(Player)
  player.jumpSignal.stage = stage // 将舞台赋值给跳跃信号
}

核心是以数据结构为主,在其上定义规则,充分利用规则的被动自动执行。
整个程序的结构现在是完全可视的了,子结构都层次分明,职责明确,可以快速的弄懂结构的意图,梳理整个程序的架构,理清数据的流向。我们能避免像以往过程式编程那样掉入逻辑漩涡,无法自拔,可以松快的读写代码。

结构化编程的新写法

以往的过程式编程的写法是,需要通过逻辑控制流,一步一步小心翼翼的线性编写,同时还要考虑最令人烦恼的分支判断等,需要费很大的力气来编写。

现在,编程方式开始有变化了,通过结构化编程,我们获得了三个恒定的写法:

得益于结构的高复用性,所以,最常用的写法就是为字段赋值,这个活很好干。不用引入逻辑控制流,不用操心赋值顺序,只要需要被赋值,那就赋值好了,只要需要准备的都准备好了,系统会聪明的遵循规则自动运行。这就像是在设置配置文件,简单直白,通俗易懂。

一定要有新认知、新思维

可能依然有些同学会觉得上述示例晦涩难懂,发现不了结构化编程的好处,说白了就是一时还没有转过弯来,依然用过程式的思维来阅读上述代码,旧思维太固执,太根深蒂固。

正如前面花大力气强调的,一定要认知到结构的重要性,一定要改变数据结构是数据存储的固执看法,一定要秉承结构化的思维,以数据结构为主,着眼于结构本身去理解规则。 意识形态决定思维方式,思维方式决定实际的阅读和编写行为。

总之,要扭转过程式思维到结构化思维上来,想通了,就明朗了。

结构化编程的好处

使用结构化编程,我们能获得一些前所未有的令人欣喜的好处,如我们可以直接从结构化的三大基本性质(易于读写、流向清晰、高度复用)中获益,也可以从更高级的安全和并发特性中获益。还有就是得益于结构化编程的新写法,我们可以实现更多工具在静态检查阶段或运行阶段,通过可视化的方案来一览整个程序的数据流向,可以知道哪些字段是输入数据;哪些字段是输出数据;哪些字段是被依赖的,又间接依赖哪些字段;哪些字段已经准备好;哪些字段尚未准备好,等等等等。 这在以往的过程式编程中是难以实现的。

还有一个不得不说的性质:高度复用
同样得益于结构化编程的新写法,我们统一了结构引用定义的方式,和为字段赋值的方式。
这就表示,你自己定义的或者他人定义的结构,都能够复用、通用。这也表示了,如果大家开放自己定义的结构给所有人使用,那么随着积累、沉淀,我们写的代码将会越来越少。
以往,我们受限于组件的不可检索性,只能通过一个项目的文字简介来获知是否对自己适用,而现在得益于结构的完整自描述性(自洽),可以归纳出输入字段的类型和输出字段的类型,构建索引,从而可以提供终端的用户检索。
同时,由于数据结构代表数据本身,可以方便进行序列化,这就表示,可以抹平不同编程语言之间的差异,可以方便的跨语言执行规则,也易于分布式执行规则。

另一个不得不说的特性:安全
毫不客气的讲,绝大多数程序员最容易忽视的就是安全,除了讨厌写注释之外,就是讨厌写安全校验,不喜欢穿衣服就出门。还有一个长久困扰程序员的麻烦是 Debug,除去明显的逻辑错误,大多数 Debug 的过程其实是把异常数据校正的过程,但是追踪问题根源却要耗费九牛二虎之力。
现在,受益于结构的自洽,我们也可以让类型自洽,对类型进行自我描述,不但要校验输入数据的类型,同时也校验数据的值是否合规,是否符合预期。也即类型是可以有明确语义的,可信的,可以预期的,是安全的。
比如期望是偶数,那么就定义一个偶数类型,然后作为数据结构的字段类型,那么对任何偶数类型字段的赋值都会自动进行校验,校验的目的就是要确保符合预期,保持确定性。
每个字段都是符合预期的,那么整个结构就是符合预期的,从而整个程序也是符合预期的,是安全的。整个程序的状态在任何时刻都是确定的,任何时刻都是安全的。

清晰的数据流向也提高了 可维护性
现在,整个程序的流向地图不是隐藏于原作者的脑海了,在编写程序的时候就已经开始绘制出来了,这对于后续的维护者来说是天大的福音。
同时,得益于上面所说的安全,让结构保持在确定的、可以预期的状态,这也能减少 Bug 的产生,降低 Debug 的难度。

结构化思维带来的编程写法也更有助于 多人协作
多人协作的开发方式,提高了开发速度,但也增加了整个程序的不确定性和沟通成本。每个人的思维逻辑都不同,所以代码实现也五花八门,对接接口也千奇百怪。再好的开发规范,也只能约束代码的编写行为,却没办法规范统一人们的逻辑实现。不同的对接风格,增加了沟通成本,也无法保证确定性。
遵循结构化思维,能够将开发人员从以往的主观逻辑实现,拉回到客观的结构规则上来,消除主观差异性,回归到结构本质的输入和输出,令其自洽,易于大家共识。为字段赋值的写法,也简单直白的统一了对接风格,降低沟通成本。并且得益于安全特性,能够更好的消除多人协作带来的不确定性,以更低成本,保证整个程序的安全稳定。

应用场景

结构化编程的方式,除了常规的前端、后端,对于运维等也是大有裨益,就像写配置文件。
由于结构的自洽性,其相当于是物理概念的物质,所以理论上只要结构自身的规则准确,那么可以更容易的对任何场景进行模拟,比如对互相干涉性较强的星系模拟、气候模拟、神经模拟等也非常有潜力。

总结

如前所述,此文的意义在于,打破根深蒂固的陈旧过程式编程观念和思维,找到结构化编程的新观念、新思维,站在更好的视角,拥有更广阔的视野,重塑对程序的认知,建立新的意识形态,发挥优势,提高生产力

现在,让我们的思维解放吧!

3653 次点击
所在节点    程序员
48 条回复
xxfboy
2021-02-05 09:37:37 +08:00
nb
NexTooo
2021-02-05 09:43:32 +08:00
翻了半天没看到营销链接,怅然若失
是我层次太浅了没看出来吗……
clown007
2021-02-05 09:45:36 +08:00
我直接来看评论了...
Justin13
2021-02-05 09:51:29 +08:00
有限状态机?
thrinity
2021-02-05 09:55:19 +08:00
太深奥了
lskjdfgl
2021-02-05 10:01:39 +08:00
@NexTooo 可能楼主忘记留公众号或二维码了🐶
vaporSpace
2021-02-05 10:02:20 +08:00
这比喻有意思
zion03
2021-02-05 10:06:13 +08:00
TL;DR
jheroy
2021-02-05 10:08:15 +08:00
难道不是一直这样写代码的么。过程化编程还是上古时代用 goto 的时候吧。
reed2020
2021-02-05 10:09:20 +08:00
快,放链接!
muchan92
2021-02-05 10:14:36 +08:00
@jheroy 数据结构化编程,不是 `逻辑` 结构化编程
chinaliuhan
2021-02-05 10:19:36 +08:00
学习了
jheroy
2021-02-05 10:19:45 +08:00
@muchan92 就是以数据为中心嘛,我觉得你说的这种更像是函数式编程,而不是结构化编程,毕竟函数式编程就是“倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。”
VoidChen
2021-02-05 10:20:18 +08:00
盗墓就盗墓,笔记是笔记
xingshu1990
2021-02-05 10:22:04 +08:00
@NexTooo #2
@lskjdfgl #6
推广的就是那个 github 项目
jheroy
2021-02-05 10:23:36 +08:00
而且你说的这几个有点:高度复用,安全,可维护性,多人协作,基本上和函数式编程一模一样。代表语言就是 erlang, 连思维模式要转换一下这个特点都是一样的。
ming168
2021-02-05 10:26:54 +08:00
nb
jheroy
2021-02-05 10:30:04 +08:00
还有你说的“以数据结构为主,以逻辑控制流为辅”,这叫λ演算,“是一套从数学逻辑中发展,以变量绑定和替换的规则,来研究函数如何抽象化定义、函数如何被应用以及递归的形式系统”,这是函数式编程的基础。
vindurriel
2021-02-05 10:41:18 +08:00
数据驱动 之前看 d3.js 的代码感觉非常简洁有力
ianshow15
2021-02-05 10:44:26 +08:00
我以为是说写代码跟去上坟一样呢

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/751452

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX