js框架读书笔记和思考(一)

在最前



I dreamed a dream in time gone by,
When hope was high.
And life worth living,
I dreamed that love would never die.

我相信程序员都会有一个框架的梦, 这个梦无论出于怎样原因最终搁浅也好,放弃也好, 半途而废也罢。但是便如歌词里面的那句: I dreamed a dream in time gone by.此刻迷梦正甜,起码要些许努力。这是一堆读书笔记里面的第一篇。to:《Javascript框架设计·司徒正美》

我曾错过那些刀耕火种的年代,错过了亲手去抚摸体会那些浏览器兼容的痛苦的年代,但是,这也是最好的年代, 入行时候有jQuery带路跳过那些坑逼兼容, 想学MVC时候有架构清晰的backbone,想做工程化时候有gulp,想玩模块化的时候webpack横空出世,想数据驱动UI时候angularJS风靡,想组件化时候vue、react生态渐丰。

但是还是错过了很多的感觉,所以有了这些文章,司徒正美的这本书早些时候买的时候没有过重视,因为老是觉得不够清晰,那时候痴迷于猫头鹰那本动物书,大抵有种迷信的感觉。回过头来看看,内容其实真没有辜负书名。

章节目录

  • 种子模块
  • 模块加载系统
  • 语言模块
  • 浏览器探嗅与特征侦测
  • 类工厂
  • 选择器引擎
  • 节点模块
  • 数据缓存系统
  • 样式模块
  • 属性模块
  • 事件系统
  • 异步处理
  • 数据交互模块
  • 动画引擎
  • 插件化
  • MVVM

种子模块

命名空间

《Javascript框架设计》中提到了很多库,Prototype,mootools,Base2,YUI,dojo,Mochikit,jQuery等,不过大抵由于年代久远,个人听过的仅限Prototype,mootools,YUI和dojo——并且一个都没有用过(当然,不算jQuery)。

选择

命名空间的选择大致上存在两种选择:

  • 一种是直接扩展(污染或者覆写)全局变量或者方法,
  • 而另外一种是使用定义一个全局变量(类型多是对象),以它为根逐步拓展为整个框架或者库。

文中提到一个之前没有考虑过的说法是jQuery另辟蹊径,使用了函数作为命名空间,这点就突然让我有点惊讶,因为突然想起基本从业以来似乎读过源码的框架比如jQuery,backbone似乎都是构造函数作为命名空间。

所以赶紧去看了看YUI的命名空间——记忆里YUI是YUI()开头的。
下面代码来自YUI的 Quick Start

1
2
3
4
5
6
7
<script>
// Create a YUI sandbox on your page.
YUI().use('node', 'event', function (Y) {
// The Node and Event modules are loaded and ready to use.
// Your code goes here!
});
</script>

——很显然YUI3已经和jQuery一个套路是函数了。
不甘心又跑去看YUI2的代码了。打开核心的js文件 YAHOO.js,头部的注释似乎像我说了很多事:

1
2
3
4
5
6
7
8
/**
* The YAHOO object is the single global object used by YUI Library. It
* contains utility function for setting up namespaces, inheritance, and
* logging. YAHOO.util, YAHOO.widget, and YAHOO.example are namespaces
* created automatically for and used by the library.
* @module yahoo
* @title YAHOO Global
*/

看着YUI2文档上的那句:

YUI 2 has been deprecated as of 2011. This site acts as an archive for files and documentation.


它似乎在向我说那些错过的岁月。

——所以说司徒正美到底没说错,YUI的命名空间确实是个对象,唯一错的,是这个这个时间。

冲突处理noConflict

大抵是jQuery教会了我设计框架时候得有一个函数叫做noConflict。
跑去jquery代码里面扒出来这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,
// Map over the $ in case of overwrite
_$ = window.$;
jQuery.noConflict = function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
};

很显然,noConflict作用有三种:

  • deep空着只清除$保留jQuery
  • deep传true清除jQuery和$
  • 返回jQuery构造函数以便用户赋值给变量

思路说出来其实就容易理解得多了。

对象扩展

对象扩展个人用过最常见的是$.extend();
如书上所说,最简单的extend代码是这样的:

1
2
3
4
5
6
function extend(dest,source){
for(var property in source){
dest[property] = source[property]
}
return dest
}

但是文中也谈到了for…in遍历原型的兼容问题。这里也记录一下相关for…in遍历原型的原因。
for…in会遍历所有可枚举的属性(除非该属性名是一个 Symbol)。
|->可枚举属性是指那些内部 “可枚举([[Enumerable]])” 标志设置为true的属性
|-->对于通过直接的赋值和属性初始化的属性,该标识值默认为即为true,对于通过Object.defineProperty等定义的属性,该标识值默认为false。

extend方法实现基础原理就是上面最简陋版本的加强版——允许合并、是否覆写同名属性&深拷贝。

数组化

数组化函数存在的意义是存在很多类数组对象,它们很像是数组,但是却不能使用数组相对完善的api,这个是一个很大的遗憾。
常见的有:

  • function内部的arguments
  • document.forms
  • from.elements
  • document.links
  • slect.options
  • document.getElementByName
  • docuemnt.getElementByTagName
  • childNodes
  • children

最常见的转换方式是Array.prototype.slice.apply(obj)。不过书中提及这里存在的旧版IE的兼容问题是HTMLCollection、NodeList不是Object的子类所以就没法用Array.prototype.slice.apply(obj)了。

作者举了很多例子展示诸多框架在这里的实现。核心的思想其实就一个: 创建类数组等长度的数组,递减类数组的length并赋值类数组值到真数组上,最后返回这个真实的数组。
根据这个思路我自己写个最简单的:

1
2
3
4
5
6
7
function makeArray(array){
var length = array.length,arr = new Array(length);
while (length--) {
arr[length] = array[length];
}
return arr;
}

类型的判定

Javascript有7种数据类型:string,number,boolean,undefined,null,symbol,object;其中前面6中是简单类型,object是复杂类型。
简单类型是可以通过typeof来判断的。
书上说了很多typeof在低版本浏览器下的bug存在,但是个人认为这里需要注意暂时是两个:

1
2
typeof null === 'object'
typeof Symbol === 'function'

然后是isNaN存在文中所说到的,字符串和ojbect它也会返回true。不过,在我使用的Chrome 55.0.2883.75 (64-bit)上,isNaN已经修复传入字符串返回true的问题。

isXXX系列函数有太多奇技淫巧的感觉。个人觉得有必要了解熟记常用的,但是如果如果可以背下常见的isXXX函数其实会使代码更加健壮。
但是出于简单便于记忆来说,type函数是最简单可靠的——如果它不遇到旧浏览器各种历史bug的话。

1
2
3
function type(obj){
return Object.prototype.toString.call(obj).slice(8,-1).toLowerCase()
}

domReady

domready,就这一个地址吧,domReady,简直就是一个代码的hack集合…

模块加载系统

模块加载系统是一个成体系的库所必须要具备的。目前来说市面上常见的模块规范有AMD,CMD,CommonJS,UMD。其中,CMD是seaJs的模块规范,其作者玉伯也说是时候给他一个树墓碑了,而UMD其实就是AMD+CommonJS+Golbal全局的兼容方案。所以就目前来说,如果不是打算新建立一个自己的规范,那么只需要考虑AMD和CommonJS就够了。

AMD

AMD是异步加载模块的意思,最出名的实现就是requireJS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 模块开发
/**
* id 模块id
* deps 依赖模块数组
* callback 模块定义它包含若干参数,和deps里面的顺序一致
*/
define(id, deps, callback)

// 模块引用
/**
* deps 依赖数组
* callback 业务逻辑 它包含若干参数,依次对应deps中的依赖
*/
require(deps, callback)

上面那个只有用法也不太好理解,下面是在网上找到一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
define('myModule',
['foo', 'bar'],
// 模块定义函数
// 依赖项(foo 和 bar)被映射为函数的参数
function ( foo, bar ) {
// 返回一个定义了模块导出接口的值
// (也就是我们想要导出后进行调用的功能)
// 在这里创建模块
var myModule = {
doStuff:function(){
console.log('Yay! Stuff');
}
}
return myModule;
});
// 假设 'foo' 和 'bar' 是两个外部模块
// 在本例中,这两个模块被加载后的 'exports' 被当做两个参数传递到了回调函数中
// 所以可以像这样来访问他们
require(['foo', 'bar'], function ( foo, bar ) {
// 这里写其余的代码
foo.doSomething();
});

根据这里的初步使用和逻辑,我们尝试对AMD加载器做一些实现上的考虑。

1. 模块加载器的目标是js,如果扩展一下还可以扩展到css,当异步获取资源时候必须考虑到js和css很有可能是它站资源,所以不能用ajax(这是书中观点,不过个人窃以为如果约定只能载入自己的资源或者兼容ajax和script标签也挺好)。
2. 以js为例,实现过程中应该使用动态插入script标签的办法来处理相关js资源
3. src要求获取资源的路径,所以需要一个获取实际路径的方法。

宏观方面:
需要初步理清 require 和 define 到底是怎么里应外合的。如果这样说不够明晰那么先理出以下 API 思路上的东西再深入

  1. 首先使用define写一个模块,如上文的那个简单的myModule模块依赖了foo、bar,然后是模块的主体函数(函数是最重要的场景因此这里姑且认为一定是函数)。
  2. 然后是require, 它接受deps数组,然后执行调用。它是 AMD 加载器的核心——模块的载入的各种细节都是在这里处理,载入成功后的操作也是在这里执行。
  3. 那么现在可以思考: 当 require 载入了 js 模块文件(define包裹的),并开始执行define函数(这个函数不管怎样一定是挂在 window 下的全局函数),内部会怎样处理这些参数呢?

    反复看require 和 define 的API 设计,是否可以看到它们惊人的相像?是的,其实它们就是这样设计起来的: require是核心,它处理了90%的模块加载细节和一堆乱糟糟的东西——总之,你只需要知道它能把依赖处理好并执行你的后续代码即可。而define则是助攻,它接受的参数核心里面包括 deps依赖和模块主体,实际上它基本上最核心的只是把自己接受的参数根据情况略作修改传给了 require 而已——干活的还是 require。一句话说明define和require的关系: define其实是个光杆司令,require是那个唯一的小兵, 活儿来了之后define略作批示然后活儿还是派require去干去了。(当然,如果不使用匿名模块,而是定义了模块id的话,实际上 require 也调用到了 define)

微观方面:

  1. 模块加载器的目标是js,如果扩展一下还可以扩展到css,当异步获取资源时候必须考虑到js和css很有可能是它站资源,所以不能用ajax(这是书中观点,不过个人窃以为如果约定只能载入自己的资源或者兼容ajax和script标签也挺好)。
  2. 以js为例,实现过程中应该使用动态插入script标签的办法来处理相关js资源
  3. src要求获取资源的路径,所以需要一个获取实际路径的方法。

等等这些,暂时就按书中思路来吧。

实现一个加载器的技术细节

获取basePath

1
2
3
4
5
6
7
8
9
10
11
12
function getBasePath(){
var nodes = document.getElementsByTagName("script")
if(window.VBArray){
for(var i = 0,node;node=nodes[i++];){
if(node.readyState === 'interactive'){ break }
}
}else{
node = nodes[nodes.length - 1]
}
var src = document.querySelector ? node.src : node.getAttribute("src", 4)
return src.replace(/[?#].*/, "").slice(0,src.lastIndexOf('/') + 1)
}

主要思路:

  • 加载器应该是最后一个script标签所在的文件,所以获取nodes[nodes.length - 1]
  • 如果是现代浏览器返回node.src,否则使用node.getAttribute(“src”, 4)获取src

这段代码主要是兼容上不太好理解。诸如VBArray、readyState、getAttribute第二个参数个人从没用过。。。这里记录一下相关知识点

  1. VBArray用来判断IE可以理解
  2. readyState:微软私有属性可以查看资源加载情况(document.image,xhr,script等有这个属性)。

    它有五种取值:
    uninitialized 默认状态
    loading 下载开始
    loaded 下载完成
    interactive 下载完成但尚不可用
    complete 所有数据已经准备好

  3. getAttribute在ie里面有些棘手,IE7以及之前的版本版本接受两个参数。下面是第二个参数:

    • 0,默认值,特性名称大小写不敏感,并根据需要转化特性值(例如把href转成完整的URL)。
    • 1,特性名称大小写敏感。
    • 2,返回特性值的字符串值,即不做任何转换。
    • 4,返回完整的URL对象,仅针对返回URL的特性,如href、background等。

require函数细节

首先需要明确require作用: 它的作用最直接点说,是载入模块并执行指定回调
看书之前个人想的require在使用用法上的主要是3块:

  • 获取每个模块的src路径
  • 插入script标签并监控加载状况+缓存js+使用闭包将每个js隔离
  • 执行callback回调
    不过看完相关总结,发现自己漏掉了重要的一环就是require需要分析依赖文件队列,然后判断是不是要script引入。进一步说,require函数会分析整个模块的依赖树。

现在构建一下require函数的实现需求,无非4点:

获得文件路径 -> 分析依赖&判断是否载入 -> 载入 -> 执行回调

当然,如果真的去实现一个可用的加载器不是一件简单的事情。依赖树的处理,js的加载,以及各种牵一发动全身的东西,关于实践我将会在后面补上。但是从整体上思考全局,这就是require的要点了。

define函数细节

如果说require是调用模块用的,那么define就是响应这个调用的「内鬼」。
关于这个函数的调用个人从书籍上看了好一会细节,不过所获不多。个人考虑到的是这个函数和对应的require其实是钥匙与锁的关系,大抵只有深入去了解require函数的细节后,才会对define的细节有深入的理解。
这里等待补充作业&细节。

看了下 define 的源码,理清了它和 require 的关系,在宏观分析上做了分析,所以这里就主要说细节上的东西。