宇宙万物彼此之间息息相关,彼此互动形成了各种关系。这些关系都具有链的形态。
程序执行时,依赖查找,函数执行,都是基于链进行的。
探讨程序中的链,从链的角度来看程序方面的问题,也许能够找到一些解决编程领域问题的新思路。
概念上的链
A原因导致B结果,而B作为原因导致了C结果,ABC因果关系串联形成了因果链。
数学公式推导中,前一个推导结果作为下一个推导条件,就把多个推导链接起来,最终完成一个完整的推导。
生产流水线上,各种制程环环相扣形成了链,生产物料走完这条链,就变成了产品。
可以说万事万物运动发展的一种表象就是不断的链接。
在这些链上传递的一方面是动力,比如力的传导,起到推动作用;一方面是信息,在链接的环节间彼此相互可以知道的信息。
动力可以看作是能量,而信息可以看成是抽象了的物质,两者相辅相成。
没有动力,链传递不下去。没有信息,链也不知道为什么要传递。
就逻辑推导来说,动力就是我们大脑产生的心流,信息就是推导中的各种论点,条件,结论等,链则是以推导语句的形式存在。
计算机程序是固化了的思维,是人设计的各种逻辑链条的集合,计算机可以把这个逻辑链条运行起来。
计算机的运算,在形式上都在模拟或实现这种链的传递。
计算机程序中的链
微观的来说,浏览器中运行的JS就存在很多链:作用域链,原型链,依赖链,调用链等。
作用域链:
JS是一门脚本执行语言。Script脚本包含了JS的代码,JS引擎会装载这个脚本并且逐行解释执行。
JS代码需要运行在一个环境当中,这个环境称为执行上下文(execution context),执行上下文可以认为是一个对象。
执行上下文中定义的变量,都会保存在与之关联的变量对象(variable object)中。当执行上下文中的所有代码执行完毕后,该执行上下文将销毁。
执行上下文分为全局执行上下文和函数执行上下文。Script脚本的执行上下文就是全局上下文,而函数执行时会创建自己的执行上下文。
JS的作用域是变量或函数定义的地方。比如函数内定义了一个变量,那么这个变量在这个函数内部可以被访问到。
执行上下文创建的时候,一方面会关联变量对象,一方面还会创建一个作用域链(Scope chain)。作用域链可以认为是一个数组,里面放着一系列的变量对象。
某个执行上下文的作用域链的第一项保存的是该执行上下文自己的变量对象,第二项则是父级执行上下文的变量对象,第n项如此类推,直到全局执行上下文。
某个执行上下文中,要访问某一个变量的时候,会从该执行上下文的作用域链的第一项开始找,直到找到全局执行上下文。如果找到了返回,如果没有找到那么就抛出ReferenceError。
可以看到变量的查找,是在一个链中去进行的。
原型链:
JS也是一门面向对象语言。对象之间继承的机制是原型继承,这些继承形成的链成为原型链。
当访问对象的某个属性是,该对象实例本身没有该属性,则在其指向的原型上去找,如果还找不到,则在原型的原型上去找。如果找到了返回,找不到就抛出ReferenceError。
基于原型链的继承,需要使用一些阻隔的手段解决污染的问题,这里不探讨。
依赖链:
函数需要访问的变量或数据,都是其依赖。A函数访问B函数,B函数访问C函数,这种依赖关系形成了依赖链。
不同函数之间,虽然没有直接的相互调用关系,却依赖了相同的数据,那么他们彼此之间依然有链的关系。
调用链:
调用链是我们讨论的重点。程序运行的本质就是不停的调用,那么底层就是不断的计算。不管软件的功能看上去多复杂,都可以归结于是调用。
从这点上来说,我们已经找到了几乎可以解决任何问题的关键:设计/实现函数,然后调用,直到完成你的目标。
我们看一下JS代码是如何运行起来的。
JS代码在一个单线程中去执行。JS引擎维护了一个内存模型。内存分为两部分,栈和堆。
栈有许多种,一种用来保存变量的,一种用来保存代码执行状态的,称之为调用栈(Call stack)。
调用栈里保存的是帧(frame)。当一个函数被执行的时候,其句柄会被压入栈,当执行完毕之后,会出栈。
比如a调用b,那么a会入栈,然后b入栈。b执行完毕,b出栈,然后a出栈。
当某个函数运行时报错的时候,控制台会打印出调用栈的详细信息。
入栈出栈就是了JS运行时的微观状态。JS引擎或者说计算机,逐栈帧执行代码,也意味着,JS一个时刻只能干一个事情。
而在浏览器中, 网页运行需要解决用户交互,图形渲染,网络请求等等任务,而JS一个时刻只能干一个事情。
如果干一个事情的时候需要等待,那么会阻塞其他的事情,比如,一个网络请求很慢,浏览器的界面则不能交互。
为了解决这个问题,浏览器中实现了一个基于事件循环(Event Loop)的并发模型(Concurrency Model)。
比如发起网络请求,JS引擎发调用浏览器API送请求以后,就继续执行下面的代码,并不会等待请求结果。
浏览器拿到网络请求的结果后,会创建一个Task,放在一个任务队列(Task queue)里,
等调用栈执行完毕,栈空了,没事情干的时候,就到这个任务队列里取任务执行。
具体的即使把Task关联的Callback放入执行栈中执行。
每次调用栈执行完毕,就去任务队列里取。如此循环往复。
由于异步任务不会阻塞调用栈的执行,所以Nodejs充分利用了这个特点,实现了非阻塞 I/O 操作的机制,能够满足很高的处理性能。
事件循环是浏览器或Nodejs的实现的并发模型,现在我们了解了JS执行时许的特点。
代码运行就是不停的调用。a函数调用b,b函数调用c,彼此的调用形成了链,就叫做调用链。
调用链(调用链本身也是一条依赖链)和依赖链,实际上代表了软件的结构。软件结构的优劣,也能在调用链和依赖链上找到影子。
后面我们将好好的分析一下调用链。
宏观的来说,软件与运行环境存在相互调用关系,而软件内部不同部件相互调用,也形成了链。
通常解决一个问题,我们会把问题分解成子问题,然后逐个求解进而解决整个问题。这些子问题之间有逻辑链。
设计系统时,我们会划分多个子系统,子系统各司其职,彼此之间相互调用,进而实现了整个系统。
子系统,由多个模块组成,而模块又可以由更小的模块组成,直到分解为函数或对象。
不同层次和粒度的模块,或代码块之间,相互调用,进而完成了系统功能。所以系统的功能的运转,本质上还是调用链的运行。
或者说设计系统的功能,本质上就是设计调用链,不同层次和粒度的模块之间的调用链。
面向对象程序设计的主要思路就是识别和定义对象以及它们之间的交互。这些交互本质就是调用。
(持续完善中,仅供交流,不喜勿喷~o~)