现在JS里有async/await
了,处理异步代码几乎不再有什么争议,但还是会有人有疑问,为什么不把所有函数都定义成async
的,然后所有函数调用都写成await
的,这样最终不就可以省略掉所有的async/await
关键字了吗(默认隐式async/await
)?这样不就达成了“天下无异步”的太平盛世了吗?
只要稍微动点脑筋就不会有这种想法。
我们都知道目前的环境下JS它还是一门单线程的语言,然后通过Event Loop来实现异步IO。虽然也有fibjs这种“异类”,会稍微打破一些认知。基于这个前提,我们就有一些共识,比如:
那么有如下代码
1 | function A() { |
如果单看这两个函数,如果A
、B
、foo
、bar
都没有副作用,那么会觉得这两个函数的效果没什么差别,在这种情况下“默认async/await
”似乎是可行的。
但如果有共享资源和竞争,事情就会变得完全不一样。
1 | var shared = 0 |
如果A
和B
用到了共享资源,对于A
而言,因为是完全同步执行的,那么整个A
的代码会在一个event里执行,它是“线程安全”的(这里加引号的是因为这个不是严格的线程安全的概念,只是表示个意思),只要通过静态代码分析的手段就可以得知foo
和bar
对shared
有没有副作用,那么这个A
函数的执行结果就是可预知的。
但B
则完全不一样,因为await
关键字会出让执行权,也就是foo
、bar
、return
不在一个event里执行,那么在这三行代码的“行缝儿”之间就有无数的可能性,这些缝隙里塞进去一万个event也不得而知。这种情况下对foo
和bar
执行静态分析(去判断他们对shared
有没有副作用)是没什么卵用的,因为shared
被修改的可能性有无数种,比如触发了一个事件导致别的listener修改了shared
。也就是说B
函数的执行结果是不能通过静态分析而预知的,它不再是纯函数了(废话)。
这就是async/await
的重要性了,它绝对不是一个简单的语言设计的品味问题,不全局省略async/await
是因为它明确的告诉写代码的人这个地方会发生什么事情。开发者只要看到它,马上就会对这里的共享资源多提一个心眼,会以完全不一样的眼光去看待B
函数。
而对于严肃地写代码、写严肃的代码而言,“知道一行代码会发生什么”这件事有多重要我想不需要再多强调了。
]]>书接上回,竟然已经 10 个月过去了,真是羞于见人(并没有羞,脸皮太厚,咬我啊)。
本期节目将会介绍在 JS 中如何高效率的处理二进制文件,这里的“高效率”不仅限于性能方面,还包括我们的编程体验。
在我们日常的编程当中,直接和二进制打交道的机会其实并不多,因为编程语言已经给我们准备好了数据类型,比如在 JS 里我们有Number/String/Boolean
等等这些基础数据类型、Array/Object
等复杂数据类型或数据结果,已经够帮我们解决 99% 的日常需求。真正要处理二进制的时候,最常见的场景,比方说要搞一个图片上传、文件上传,也无外乎是用 Blob/File/Buffer
这一类的封装,把二进制的数据当作一个整体来处理,很少很少会需要对它的内容进行处理。
但在这篇文章里缩说的“二进制处理/操作”都是指对二进制文件或数据的内容进行具体的操作,比方说,如果您想裸写一个 WebSocket 协议实现,或者写一个 Protobuf 编解码,那么就免不了要和二进制打交道了。下文中具体涉及到的技术细节和 API 怎么用这类的东西可以看上面的课前阅读。如果那篇文章看完了,那么这篇文章在技术点方面几乎不会有无法理解的地方,只是一些实现的思路和技巧而已。
所以这一节先对 JS 里的二进制操作,也就是上面那篇文章里的主要内容做一个梗概:
ArrayBuffer
来做字节数组的作用,指向一片内存,承载一份数据,但一般很少直接用它来对内容进行操作。TypedArray
来作为ArrayBuffer
的“视图”(View),处理多字节整数、浮点数的读写。同一个ArrayBuffer
上可以随便建立TypedArray
,它们的读写都会落到同一片内存上。DataView
来实现非内存对齐情况下的多字节数据读写,以及指定字节序的处理。XMLHttpRequest/File/Blob/剪贴板
等方式拿到二进制数据。首先是对资源文件的处理,仙剑的资源文件是用一种叫 yj_1 的算法(应该是他们自己弄的,基本上就是哈夫曼,具体不在这里讨论)压缩的二进制文件,它拥有一个文件头和被压缩的载荷,载荷分为若干个块(Block),每个块也有自己的头和载荷。所以要使用资源文件先要对它进行解压。
有了解压以后的资源文件,它通常是多个同类型文件封装在一个压缩包里的,每个类型的文件又有各自的文件头和载荷。
到了具体的载荷,比如位图、地图的 Tiles 描述、Tile 图元素、Sprite、调色盘、道具条目、角色条目、敌人条目、脚本条目、存档文件……所有资源,都有各自的二进制格式,需要一一处理。
SDLPAL 是用 C 写的,它使用了一种非常常用的技巧,来实现快捷的二进制操作。
我们知道在 C 里面一个结构体占多少内存是可以算出来的,因为它声明的时候就已经决定了它的内存布局,比方说
1 | struct Vector3F { |
那么编译的时候就能知道,它的结构是 3 个double
顺次排列,每个double
是 8 字节,因此它会占 24 字节的内存,如果把它当作字节数组写进文件,它也就会占 24 字节的磁盘空间。
那么反过来,如果要通过fread
函数把这个文件读进内存里,也要准备 24 字节的内存空间,fread
接受的缓冲区参数类型是void *
,这是因为 C 里缺少byte
类型的锅,初始化的时候我们可以用unsigned char
来代替,准备一个长为 24 的unsigned char
数组,把它强制转换成void *
,传给fread
,就可以顺利的读入内存了。
读进来以后,我们再把unsigned char *ptr
强制转换为Vector3F *
。技巧时间到,一个Vector3F *v
指向我们刚才那片内存的时候,我们对它的成员的读写,自然就会被编译器翻译成偏移量,落在正确的内存偏移位置上。如果访问v->x
,就落在ptr[0]
上,如果访问v->y
就会落在ptr[8]
上,v->z
则是ptr[16]
。
这样一来,只要一个结构体的内存布局明确,就可以把结构体指针当作一个“View”,把它“贴”在一片内存上,就能用比较对象化的方式来访问这片内存,而不需要再去记忆它的具体内存布局和偏移量。如果要把它写入文件的时候,只要把结构体指针强制转换为void *
然后传给fwrite
(并且传入正确的长度)就可以把它存上了,根本不需要序列化和反序列化。
这样做还有一个非常好的应用场景就是,当我们要处理一个文件头,或者协议的包头/帧头这类东西的时候,我们可以直接对照着它的二进制布局,声明一个结构体,通过结构体的字段来访问内存就显然比通过偏移量来访问要方便一百倍。
比如仙剑里的 yj_1 文件头定义如下
那么对应的内存布局和结构体声明就是
前文中我们提到,用DataView
可以精确读写任意偏移量的二进制多字节数据,并且能指定字节序,于是它非常适合用来读写这种有固定内存布局的二进制数据,比如我们的Vector3F
在 JS 里就可以这么用
1 | const vRaw = new ArrayBuffer(24) // 申请内存 |
当我们需要把这个Vector3F
写进文件里的时候直接把vRaw
写进去就行了,读出来的时候也一样原封不动。
但是这么用只解决了内存“View”的建立的问题,用的时候还是要记每一个字段的偏移量,这不还是坑爹吗?
在 h5pal 里,我利用 JS 中的Object.defineProperty
来实现了一个使用体验与 C 中结构体非常近似的方案。这里还是用Vector3F
来举例子
1 | function Vector3F(buffer) { |
这样就可以通过如下代码来使用
1 | const ptr = ..... // 管它从哪来的 24 字节内存 |
就实现了一个“空壳”类,对它的字段操作都会落在背后指定的一片内存上,具体的过程如图
当然上面那一堆defineProperty
如果手写的话是很难受的,h5pal 中为了方便定义各种结构体,自己搞了一个库,一个类似 DSL 的东西。中间为了迁移到这个新的方式,经历了一次惨绝人寰的重构。后来我把相关代码重新实现了一遍,单独弄了一个项目 liuji-jim/c-struct。不过是给 nodejs 用的,那时候 nodejs 貌似还没有全面推行ArrayBuffer
和TypedArray
相关的东西,不然其实可以一套代码前后端都能跑。
当然这样做也有缺点,因为类成员是运行时添加的,造成它们无法被编辑器的基于类声明的自动补全所识别,解决方法要么是改成代码生成,要么是再借助Annotation
这类的新语言特性,不过那些自然都是后话了。
在 h5pal 里随处都要和二进制打交道,因为它所有资源文件——从地图到 Sprite 到道具再到脚本等等一切——都是二进制存储的,而非序列化存储。当然,如果我做一次预处理,把它们全都弄成 JSON,那用的时候不是方便得多?是的,但那样就不够“原汁原味”了,背离了 h5pal 的初衷。
另外,在仙剑中有一个巨大的全局变量Global
,是整个游戏的状态,它本身的定义是一个巨大的结构体,存档和读档的实现就是用上面那种方式,直接把Global
所指向的内存写进文件/从文件读出来,可以说是非常简单粗暴直接有效。于是也造成不同版本的仙剑存档互相使用的时候很容易有问题,甚至根本没法用。这也是序列化相对于二进制的优势之一,在新旧版本之间的兼容会稍微容易一些。
那么,本期节目就到这里了,散~
]]>最近终于折腾了一下 NAS,开个坑记录一下(流水账)。
软件篇-概述及媒体中心。
首先远程桌面肯定是要开的,Windows 自带的远程桌面客户端还是几百年前那个,上 Windows Store 下一个新版的,好用得多。Mac 版的话,中国区 Mac App Store 是没有的,祖传美服账号掏出来。
路由器配置一下,家庭路由器一般都会开 DHCP,但我给服务器的 MAC 地址绑了一个固定 IP。把目前需要的端口转发到服务器,偷懒的我直接开了 DMZ。配置一下动态 DNS,我的域名是扔给 DNSPOD 解析的,华硕路由器本身不支持 DNSPOD 的 DDNS,但刷了梅林固件后有个插件可以直接用。实际用下来这个插件不是很完美,有时候可能宽带掉线重拨了,它没有及时更新,而是需要我进到后台去把这个插件配置点一下“确定”,大概就是让它重新执行一次吧。如果在远程的话,大概就得重启路由器了。当然也可以试试别的自带插件的 DDNS 提供商,里面不乏免费的,大不了域名难看点呗。
Windows Admin Center 安装了一下,看起来功能是非常强大的,不过目前没怎么通过它来配置系统。
官方已经在客户端里去掉了添加远程下载机的入口,估计是为了推它的下载宝,所以迅雷远程就不用想了。另外,前文已经说过,远程迅雷的离线加速功能点了无卵用,所以实用性非常查。
在会员的加持下,迅雷依然是国内最强大的下载工具,以我的北京联通自称 200Mbps 带宽,打满的时候竟然可以达到 30MB/s 的下载速度,非常牛逼,使我为了正常上网不得不把迅雷限速到 20MB/s。应该说有了迅雷,我装 Windows 已经不亏。
另外用迅雷记得用完不关可以,但是可以把任务删掉,把下载文件挪走,不然它会占很多上传带宽(谁叫咱是吸血鬼呢)。
现在已经不具备同步功能,这个产品的定位也逐渐从同步盘转型到互联网资源集散中心。但它的离线下载功能还是比较可以的,而且目前也有远程推送下载功能。这样如果追剧更新了,并且有百度云资源,那么在公司就可以先把字幕组的资源转存到自己的百度云,然后远程推送下载,这样下班回到家直接就能看了。当然这只是一个简单场景,毕竟现在网速这么快,下一集非高清的电视剧也就几分钟的事情,不至于这么大动干戈。
另外百度云的客户端在远程推送任务那一栏竟然看不到下载速度,也是很废柴。当然这只是个体验问题并不是功能问题。
百度云的上传比迅雷更恐怖,而且设置了上传速度限制并无卵用,经常把我的 20Mbps 的上传带宽打满,使我不得不下完东西就赶紧把文件挪走。
Aria2 是一个无 UI 的多线程下载引擎,它支持 HTTP/FTP/BT 等多种协议(然而并不支持eMule)。
它其实是绿色软件,网上找个启动脚本,改改配置文件就好。按照网上的经验,去 github 上找了一个 tracker 列表给添加上,不过这玩意我用的少,大多数时候还是在用迅雷,目前还没看出来加了 tracker 以后速度有没有提升,先就这样吧。
在 Windows 上配置 HTTPS 一直没成功,看了下 GitHub 上的 issue 好像别人也有这个问题,读取证书文件的时候出错。最后我使用了曲线救国的办法,我用 nginx 做一个反向代理,因为 nginx 配 HTTPS 是很容易的,而且也自带 WebSocket 支持,于是让 Aria2 可以支持 WebSocket over HTTPS 方式来连接了,当然用 IIS 做反向代理,对 HTTPS 和 WebSocket 支持应该也是没问题的。
AriaNg 是一个 Aria2 的 UI,其实根本不用安装,它是个 WebApp,只需要用官方 DEMO 改一下配置就可以用了(配置是存 localStorage 里的)。当然如果想在自己服务器上部署一个也很简单,由于是纯静态的,只需要弄一个 Web Server 就可以了。甚至可以用 Electron 直接打包一个当作客户端来用。
uTorrent 自带 WebUI,不过我不玩 PT,这玩意我暂时没搞。
电驴的官方客户端我不太喜欢(呃?),所以如果一个资源同时被迅雷禁了、被百度云禁了,而且它还没有 BT 或 megnet 那是比较懵逼的……
上面也说到,其实以现在的生态环境以及网速而言,远程下载并不是特别重要的,即使在外面突然起意,有多想回家看某个片子,回到家再下载 1080p 时间也不是不能接受(要看 4K 就另说了),毕竟这点时间也可以去先洗个澡啊、切个水果啊、买点零食啊啥的混过去,还能获得更好的观影体验是吧……
以纯屯片儿或者网盘屯片儿的思路,都是自己整理下载的文件,但媒体中心的概念出现以后,围绕它的片儿管理思路就变了,屯片儿不再是屯那么简单,媒体中心还能帮助同步存下封面、剧情介绍、导演、演员介绍、甚至宣传片,对于美剧还能有每一集的副标题和分集的剧情梗概。比如《西部世界》在正片里没有给出副标题,而国内字幕组为了逃避百度云的关键词封禁,很多时候文件名也只是 s02e08 这样的狂野模式,并没有把副标题写在文件名里,粉丝自然可以去各种歪果网站上查,不然的话很多观众甚至都不知道《西部世界》是有副标题的。
当然纯屯片儿也不是不可以,Android 手机上弄一个 ES 文件浏览器,添加一下网络共享目录,就可以浏览 NAS 上的片儿了,但这样自然少了一些感觉。有了媒体中心,面对的就不再是一堆冷冰冰的文件了,不管新片儿还是老片儿,看片的冲动又更大了。我就兴冲冲的把《硅谷》跟《权力的游戏》每一季都下了下来,然后才发现每一季的封面竟然都是不一样的。
PLEX 和 Emby 大概是现在最主流的两个媒体中心服务了。这俩都是服务端和 WebUI 免费,客户端付费。它们的客户端付费是按月付费,也可以一次性买断,买断的价格都是 119 美元,不贵,但也谈不上便宜。但 Emby 限制 15 台设备,家庭用肯定也够了,只是不知道换手机啊啥的会不会坑;PLEX 的设备数限制没查到。
Kodi —— 原名 XMBC —— HTPC 玩家更了解的应该是它,与 PLEX/Emby 这种中心化的流媒体服务相对,它是一个客户端的媒体中心,通过扫描硬盘——当然也可以是局域网(甚至互联网?)里的共享存储空间——来建立一个媒体库。这样的缺点自然是在每一台设备上都需要安装配置一遍 Kodi,虽然网上有办法通过自建 MySQL 来把 Kodi 的媒体库进行中心化存储,但是大概看了一下,还是觉得挺麻烦的。
Kodi 插件很丰富,举个例子,它的字幕搜索功能可以通过插件支持 zimuzu、zimuku、163sub 等国内字幕网站,而且可以不仅可以用文件名搜索,还能自定义关键字。而 PLEX/Emby 则只能通过媒体名称自动搜索,并且字幕来源也只能是 OpenSubtitles.org,这玩意搜电影的字幕还凑合,搜美剧日剧就别想了,当然是不可能跟强大的国内字幕组相比。
PLEX/Emby 则明显更像云端时代的产物,中心化的媒体库建设,云端转码可以充分利用 NAS 机器的性能让菜鸡客户端设备也能播放,貌似我的投影仪没法解 4K H265 10bit 的片源,所以回头有机会尝试一下通过 Emby 在服务端实时转码成 1080p 播放的体验(我现在没有这么高规格的片源)。甚至它们都提供了真正云端播放功能,可以在外面通过互联网播放家里的媒体中心的内容(然而我并不可能这么干)。
设想一个家庭常见的播放设备,无外乎台式机、俩人的笔记本 + 手机(然而我没有对象)、iPad、电视机、智能投影仪、HTPC 等,设备越多,中心化的媒体库就越有优势,而这也是家用 NAS 的职责所在。
但它们的客户端要钱,而我前期不想投资这么多(至少等我 2T 硬盘塞满再说啊)。
那么,在我心中现阶段的方案自然是:通过 PLEX/Emby 建立中心化资料库,在播放设备上则是用 Kodi 结合他们对应的插件。
安装,两者都很简单,下载后一键启动,进入 WebUI 跟随向导配置一下媒体库的路径,扫描一次媒体就操练起来了。
资料库建立并不复杂,主要让我不习惯的一点是,过去我自己整理电影、电视剧的时候,一般是按“欧美、日韩、大陆、港台”这种思路去组织的。而 PLEX、Emby 这样的媒体中心,它们是按照命名规则去整理的,然后通过搜刮片子的其他信息如类型、地区、导演、演员等来建立索引后提供分类与查询的体验。但文件方面它只支持全都放一块,建议是每个电影一个目录,按它的规则命名,里面还可以放一下字幕啊、海报啊、预告片啊啥的,甚至同一电影不同版本。于是如果按照原本的从目录结果的思路去管理媒体库的话,是无法配合他们的扫描建库机制的。
不过经过几天的适应我觉得这样也还行,只是如果有朝一日又不用媒体中心了,回到目录结构去,就会是一片完全没有整理过的样子,非常混乱,非常闹心了=.=
建库的时候匹配信息有一个比较麻烦的点,是文件名/目录名里进来加进官方英文名,因为 PLEX/Emby 使用的数据库是 IMDB、TheMovieDB、TheTVDB,用汉语搜索效果很差。这对于欧美电影电视剧问题不大,对于中文、日韩电影麻烦点,得去网上找一下它们的官方名称是啥,比如我现在手上有个日剧叫《贷款买下了男朋友》,用 TheTVDB 是直接搜不到的,后来我自己去豆瓣上找到了它的日文名称《彼氏をローンで買いました》才搜刮到信息。
Emby 也不是非要强制用命名规则来自动搜刮信息,可以自己右键媒体属性来手工指定关键词搜索,自己去那几个数据库里搜索以后在这里填写 ID 也可以。
日剧似乎不太爱用独立外挂字幕的形式,但美剧还挺多的,比如我下了《权利的游戏》1-7 季的 720p 版本,然后配上我喜欢的衣柜字幕组的字幕,借助 Total Commander 强大的文件批量重命名,整理起来不麻烦。然后看着媒体库一刷新就有了每一季的封面、每一集的副标题、截图和剧情梗概(剧透啊混蛋),还是小爽的。
两者的服务端和 WebUI 都是免费的,WebUI 的体验总的来说差不多,由于用的时间还比较短,暂时来说我也没有明显感觉谁更好或者不好。
PLEX 的 Android 手机客户端只让播一分钟,而且它的 Kodi 插件整合度非常低,只能看作是从 Kodi 里面打开了一个功能不全的 PLEX 客户端,甚至连播放界面都是独立的,自然也不能享受到 Kodi 其他插件的功能。
Emby 的 Android 手机客户端似乎可以随便用,除了一些高级功能(比如下载到手机、通过互联网观看等,通常也用不着)手上没有 iPad 所以没有尝试。然后在智能投影仪上装了 Emby 的 Android TV 客户端,可以试用两星期,用的时候会不停的提示你建议购买,而且在播放我某个 1080p 片源的时候它竟然是服务端解码,明明可以客户端解码更合适啊混蛋,完全没弄明白为什么要这么干。
Emby 的 Kodi 插件有两个,其中一个是连 PLEX 那种都不如的叫 EmbyCon,很废柴。另一个官方更推荐的就比较厉害了,它能够相当好的把 Emby 的媒体库融合进 Kodi 里,用的时候可以说是几乎感觉不到插件的存在,就是启动的时候会自动拉一下 Emby 服务器上的媒体库跟本地 merge 一下。目前我使用了这个方案,PLEX 暂时打入冷宫。
PLEX 和 Emby 都自带外网访问功能,其实就是通过 UPnP 把内网端口映射一下,然后在外网用的时候通过官方账号来做一个中介,告知一下客户端现在服务端的 IP 和端口,原理跟群晖的 QuickConnect 应该是一样的,这是一种典型的内网穿透/打洞方法。
但事实上有公网 IP 的话(赞一个北京联通),配合 DDNS 根本不需要这么麻烦,直接在路由器上端口转发,尝试访问,显示 Forbidden,进去 Emby 的配置一通胡找,打开外网访问,就可以了,并且选一下证书就自带 HTTPS。家里 10-20 Mbps 的上传速度(speedtest.net 测出),实测可以服务端转码播 1080p/10Mbps 的视频不卡,到 20Mbps 就偶尔会卡顿了——当然这只是证明了一下能力,实际上我并没有人在外面远程看家里的片儿的需求。
PC 上其实不需要再弄 Emby 客户端了,因为直接用 WebUI 就可以了,而且 Emby 的 Windows 客户端其实也不过是 Electron 套壳而已。当然 Kodi 其实也有 Windows 版,但用在 PC 上实际体验怪怪的,感觉还不如直接用 WebUI。
我现在用的是 Emby 的手机客户端,反正看个片是没说要收费。为什么手机上不用 Kodi?主要还是 Kodi 的多端统一界面在手机上用起来有点遭罪,而我也不想折腾 Kodi 皮肤……
这个就比较折腾了,我的投影仪是极米 H1s,安卓系统,因为众所周知的原因,自然是没有 Google Play,而老外的项目很烦躁的就是他们一般不提供 apk 下载。那么需要曲线救国,而在极米这种高度定制且又资料匮乏的相对小众的系统上,想装 Google Play 简直难,于是需要用第三方应用市场,而国内的那些渠道,由于众所周知的原因,它们一般都偏向于预装与运营,歪国 App 少得要死,所以需要找一个歪国的第三方应用市场,我用的是 Aptoide。
那么怎么装它呢?由于极米系统自带的文件管理器里面是只能看见视频、音频、图片文件的,即使把 apk 文件放在 U 盘里面,也显示不出来。所以先要装一个“ES 文件浏览器”。通过系统自带的应用市场,或者手机端“无屏助手”装上“ES 文件浏览器”(如果极米的应用市场里没有的话,就线装个“当贝市场”或者“沙发管家”这类的其他应用市场再通过它们解决)。ES 文件浏览器自然是啥文件都能看见的,然后想个办法——比如 U 盘或者 Samba 共享,把 Aptoide TV 版的 apk 文件弄过去,装到投影仪系统里。
燃鹅,这样是远远不够的,由于众所周知的原因,Aptoide 在国内的互联网环境也是无法正常使用的,其实我的华硕路由器是可以安装“艾斯艾斯”或“微屁恩”插件的,只是我不喜欢全局 fanqiang,而一般都是用相对灵活的方式,所以如法炮制,在投影仪上也安装 fanqiang 软件,然后把 Aptoide 加到 fanqiang 软件的名单里面。
一切准备就绪之后就可以通过 Aptoide TV 来安装各种国内应用市场没有的 App 了。
按照 Emby 官方 Wiki 的指导只需要几步就可以安装配置好 Kodi Emby 插件,这时候已经可以近乎无缝的在 Kodi 里使用 Emby 的媒体库。
GitHub 上找 xbmc-addons-chinese,有一些适合国内环境使用的 Kodi 插件,按照 How to Use 安装配置一下。我装了几个字幕组插件,不过目前没有在用,因为我之前屯的美剧日剧都是整合字幕的普清版本。我准备把权游都换成 720p 蓝光版本,然后手工配好字幕,所以除非很犯懒,不然实时匹配字幕的意义其实并没有想象中那么大来着。
xbmc-addons-chinese 项目里还有一些优酷、爱奇艺、bilibili 等视频源,试了下 bilibili 并没有刷出资源来,大概年久失修用不了了吧。不过这些应用各自都有自己的 TV 版 App,也不稀罕跑 Kodi 里来看了。
Emby 同时是一个 DLNA 媒介,可以在电脑/手机上浏览影片库,然后通过 DLNA 投屏到智能投影仪/电视上去播放。实测它的投屏连接速度比直接用手机优酷投屏反应慢几秒钟。
但其实在家里的话这么做没有太大必要,用遥控器操作直接在投影仪上选要看的片儿也没什么问题,何必多此一举呢,DLNA 还无法外挂字幕。
投屏更大的意义在于,像优酷这类平台,手机端和电视端的 VIP 不互通,电视端的 VIP 要贵一些,如果只买了手机端,有些 VIP 资源可以通过 DLNA 投屏来解决问题,很多时候就不再需要额外的购买电视端的 VIP 了。
Emby 在开始播放一个片子的时候,会有大概 3-5 秒的等待时间,不知道它在做些什么。家里有线设备都是千兆网络,手机和投影仪也是 5G WiFi,试过手机测速是能有 600-700 Mbps 的,局域网里肯定不会有网速问题,只能理解为它要处理一些元数据吧,不过总的来说这不影响体验。
目前我主要的用法是,Android 手机用 Emby 官方客户端,实际播放体验反而一般,主要在它的播放器不支持手机上常见的视频软件屏幕上横划快进/快退功能,也不支持左右半屏上下滑动调节亮度/音量,但貌似它其实还是可以调用其他播放器来播的,暂时还没研究。投影仪用的是 Kodi 配合 Emby 插件,Kodi 其实本身是个 HTPC 软件,UI 就是为大屏设计的,所以用遥控器的体验甚至比在 PC 上用鼠标还要好……
PC 主要就用 WebUI 来进行一下媒体资料库的管理。自从有了智能投影仪,用 PC 看片的时候逐渐减少,于是 PC 上的播放体验反而最不重要,而且真的如果嫌弃播放体验差的话,还能用 PotPlayer 来播啊=.=
本期节目就先到这里,私有云还没弄好,下期可能会过阵子再发了。
]]>最近终于折腾了一下 NAS,开个坑记录一下(流水账)。
硬件及系统篇。
优点是系统整合度高,群晖的 DSM 软件口碑不错,硬件设计比较精密(体积小耗电低)。但价格偏高,4 盘位的群晖,X86 型号 DS418play 的话价格接近 4000,即使 618 期间可以 3600 左右拿下,还是贵啊。
这个多花几句话说说。
优点是自由度高,可以发扬图吧精神淘宝各种电子垃圾,肯折腾和钻研的话价格是很低的,折腾硬件的过程也可以获得一些乐趣(Are you serious??)盘位设计比较极限,有的机箱提供了 4 x 3.5 盘位同时还能在侧板上装一个 2.5 的。
缺点也比较明显,体积方面 Mini-ITX 是极限。但很多淘宝货 4 盘位的 NAS 小机箱对 CPU 散热器要求严苛,大概只有 4-5 厘米的高度空间,这样的散热器嗷嗷贵(像样一点,最便宜的也要一百多,大概 5 厘米高,再薄一点的就两三百了),另外因为 NAS 机箱的设计一般都是主板上方正对硬盘盒,造成即使装进去了下压式的散热器,也没有风道可用,散热效果大概率靠不住。当然也有贵的、稍微大一点点的机箱(比如联力 Q25)可以装塔式散热,能提供 7 盘位,PCI-E 挡板还是双槽位的,然而真的会有那么烧么……用整合 CPU 的板子,选择不多,而且基本上都是赛扬 J 系列,这倒不是不够用,不过就是为了压成本的玩法了。
另外就是 Mini-ITX 的用服务器芯片组(比如 C226、C236)的主板不是没有,妖板自然先看华擎,E3C226D2I 这板子就是,Chiphell 上有人就晒了基于它的装机。但问题也不少,首先它只有 VGA 输出,不能兼职 HTPC 了,其次它自带了一个某某某芯片,用来接管显示和 IMPI 功能,这样就不能用核显(这大概也是它只有 VGA 的原因吧),也就意味着它作为流媒体服务使用的话,服务端是没法利用 Intel 核显的 QuickSync 硬解技术的,所以它其实是比较严格的服务器主板,在家用这种可能需要身兼数职的场合会比较受限。
一般 4 盘位,体积和淘宝货 NAS 机箱差不多,胜在不需要折腾,主板一般是服务器主板,提供 VGA 和 DP 输出,2 到 3 个千兆网口,大概 6 个左右 USB。用 1U 电源或者 FLEX 电源,然后还有一个无卵用的 Slim 光驱位。
我的理解:不折腾自然选成品,但性能是一般,价格也最高。愿意折腾可以选自组,其实也非常受限,如果不追求小体积的话换个 M-ATX 机箱,那么整体成本和装机难度都大幅下降,选择也一下多很多。而服务器准系统相对来说比较平衡,可以自由发挥的空间不大,顶多也就是换个 CPU、扩下内存啥的,硬件方面基本不需要折腾。
所以最终我的选择是服务器准系统。
经过一番不走心的调研,圈定了 HPE ProLiant MicroServer Gen10 和 Acer Altos C100 F3,共同点都是 4 盘位,体积差不多,HP 似乎还稍微小一点点。
HP 机器新一些,性能也更好,用的是 AMD Opteron X3000 系列,据说性能可以对标 6 代酷睿 i3。2 LAN,2 DP 输出,设相对较偏家用,外观比较漂亮。
Acer 机器旧一些,芯片组是 Intel C226,LGA 1150 平台,LGA 1150 接口最高可以上 E3 1285L v3,3 LAN,1 DP 输出,偏服务器一点,外观比较粗犷。
最后选了 Acer 那个,价格便宜一些……查了一下各种电子垃圾旧 DDR3 ECC 内存,太他娘的贵了,8G 竟然要四百多,内存不愧是年度最佳理财产品。还是直接买卖家组好的吧,烈士墙在等待我,淘宝挑了一个北京的卖家,名叫“联想服务器北京xxx”,呵呵呵一听就是专卖电子垃圾的啊。配置选了一个“中配”,i3 4130 + 8G 内存。选这个配置是因为再高一档的 E3 1225 v3 + 8G 感觉目前而言用不上,而且 TDP 高了 20W,以后有需要了再升级 CPU 吧,最后入手价格 2230。
核心 | 主频 | 睿频 | TDP | L3 Cache | ¥ | |
---|---|---|---|---|---|---|
i3 4130 | 2C4T | 3.4 | N/A | 54 | 3M | 430 |
E3 1225 v3 | 4C4T | 3.2 | 3.6 | 84 | 8M | 750 |
E3 1285L v3 | 4C8T | 3.1 | 3.9 | 65 | 8M | 1200 |
先买了个 Sandisk 的 120G SSD 做系统盘(再山寨的牌子实在不敢买,也就便宜二三十块),和一个 WD 2T 红盘。为什么只买 2T 这么小的?因为长线计划是 2T 做私有云,8T 存片儿,但目前片儿少,并不需要 8T 那么大,而且现在 8T 要接近 2000 块钱,这成本吓人的,先把系统搭建起来再说,就不一次性投资那么大了,大不了后面忍受一次数据迁移的苦呗。
考虑到私有云需要数据安全(不损坏)、而存片儿则没那么高要求谁说的!小姐姐丢了怎么办?,其实我理想中的形态是 2T x 2 Raid 1 + 8T,但是不知道这种混合了 Raid 与非 Raid 的方案是否可行。
到手,拆开看看,没啥好看的,里面布局还是比较紧凑的,但其实整体并不小,从 4 盘位的群晖或 QNAP 产品图上看,这货要大一圈,大概是供电和散热方面、还有那个无卵用的光驱位占了不少体积吧,毕竟定位是比较通用的服务器准系统,而且是 LGA 1150 平台的,集成度肯定没有那么高。
这机器 4 x 3.5 的盘位,没有 M.2 接口,于是系统盘要占一个盘位。装的时候我还刻意把 HD 放在了最下面一个盘位,感觉重心低点可能减少工作时共振的噪音(yy的)?但其实如果肯折腾一下,是可以把 2.5 寸 SSD 装在光驱位的,反正现在我盘位也没用满,我折腾那劲干啥啊。
因为是准系统,装机环节就没多少好说的了,记得 BIOS 里开启网络唤醒(WOL)就好,现在的路由器一般也都支持,这部分就先到这里,照片也没拍,从淘宝上盗两张吧。
以盘位为参考大小,可以对比一下 DS418play 看的话体积还是比较大的,考虑以后如果放在电视柜兼职 HTPC 的话体积还真不能说小,估计还是找个角落藏起来比较好。
系统可以选 Linux 和 Windows,为什么不选黑群晖、FreeNAS 这类的专门做 NAS 的系统?首先,既然有 X86 那么肯定是性能取向的,虚拟机和 docker 可以燥起来,于是宿主环境自然要选择全功能系统,不然那些玩意够折腾死,遇到有需要的时候再用虚拟化来解决。
先做一个粗略的功能对比吧
PowerShell 不太会,Linux 上有些过去我自己写的以及从运维那里偷来的脚本可以沿用一下。
Linux 用远程桌面的人应该不多吧……另外 Jump Desktop 的 Mac 客户端好贵好贵。
微软官方的新版远程桌面体验很好,再也不是过去那个老掉牙的了,Mac 版中国区没上架,需要用个外国区的 Apple ID 才能下。
Cockpit 更多是一个 Web 监控,附带一些基础运维功能、一个 Web 终端;Windows Admin Center 没用过,但看介绍的话很强大,连注册表编辑器都有。
NFS 性能应该比 Samba 好一些?Windows 自然也可以用 NFS,不过系统整合程度就没有 Samba 那么高,折腾一点。
Seafile 国产的,Nextcloud 歪国的,都没用过,不知道。
xware 是迅雷官方的一个东西,原本是作为迅雷路由器的固件来发布的,于是自然有人给它做了前端。主流的用法其实是把它跑做一个后台程序,作为一个远程下载的设备。然而迅雷远程这玩意官方已经关掉大多数入口,怕是不知道啥时候就下线了。而且实际用下来(我是 docker 跑的)发现不能离线加速(点了无卵用),感觉略废柴。而且 xware 官方早已不更新,只是服务本身还没下线而已。
Win 客户端虽然又臃肿又慢,但离线加速的功能至少是好的,由于 Win 客户端也已经关闭了添加远程的入口,无法实验远程离线加速是否可用。
bypy 这玩意是基于 PCS API 的一个微型客户端(一堆脚本),它只能访问应用沙盒里的路径,用起来比较别扭。另一个问题是,PCS API 的所有入口都关闭了,只是服务还在,过去申请的 AppKey/AppSecret 依然可以使用。
如果没有自己的 AK/AS 的话只能用项目作者的中转服务器来做 OAuth,虽然不诛心,不过谁知道他会不会把 access token 存下来……当然他也提供了一个本地认证的方式,需要自己有 AK/AS 并且是开通了 PCS API 权限的,现在自然是无法申请到了,还好我有一个祖传的。
没啥区别,Aria2 的 WebUI 选择不少,我用的是 AriaNG。不过 Aria2 不支持 eMule,有时候显得很无能狂怒。
都没用过,到时候再看看。
这两者我一开始是偏向 Linux 的,因为我在 Windows 方面的运维经验几乎为 0。发行版就选 Ubuntu 吧,apt 用起来比较简单,社区热度和其他开源项目对 Ubuntu 的友好程度也还行。
于是,做好 Ubuntu Server 18.04 LTS 的安装 U 盘就操练起来了。
Ubuntu Server 的安装不难,过程中配置一下网卡,网………………卡,等等,卡住了……再试试,又卡住了。思索 10 秒钟,从现象判断是这版本 Linux 不支持其中两张网卡(???),啧啧啧,毕竟 Acer 的官方文档里也没写这机器支持 Ubuntu,要换 SUSE 吗?No way 我就是喜欢 Ubuntu 嘛(扭)。好,至少我现在只用一根网线,那么就先去 BIOS 里屏蔽掉另外两张网卡吧。
系统安装成功。
进系统,终于可以把显示器和键盘还给我的台式机了,剩下的工作就用 SSH 搞定吧。
嗡嗡嗡………………等下这声音从哪来的,我去为什么机箱风扇转速有 19xx RPM?啧啧啧,找了一下资料,装上 sensors-detect 和 fancontrol ,却找不到 PWM 调速风扇。
啊?难道这倒霉系统不支持另外两块网卡也就算了,连给风扇调速都不支持?要不……换 Win 试试?
嗯,反正折腾呗,做个 Windows Server 2016 的安装 U 盘,再次操练起来吧。
安装过程也是很顺利的,开启了远程桌面服务以后,就可以把显示、鼠标键盘还给台式机了。
然后装一个 fanspeed 软件,当场脸青了,还是找不到风扇。
所以……绝了,我大概明白了,这风扇只受 BIOS 控制,目测 BIOS 只给它两档,不开智能调速就是 6100 RPM,那声音跟进了厂房差不多,开了的话常规情况就是 19xx RPM,一是转速不算低,二是它很厚,叶片面积那是相当的大,声音还是不能接受。
于是默默地京东下单买了个 12cm 的低速风扇,1200 RPM 的,50 块钱,好贵,但淘宝解不了急啊,第二天装上以后安静了,只有安静的时候在机器范围大概 1 米以内的样子能听见轻微的声音。
中途有一次重启过后突然进不去远程桌面了,无奈,再次把显示器、鼠标键盘借给服务器,发现开机显示 Warning: Chassis is opened
,呵呵呵,服务器就是矫情,本来想着风扇到货之前先拆掉裸奔,既然这样那就还是都装上吧。
却还是继续报这个,好的,按 F1 忽略或按 F2 进 BIOS,我按,我按,却没反应……
啊???
难道刚才折腾的时候把传感装置弄坏了?拆开机箱,观察了一下,主板上有一个 Case Open
的 2 Pin 接口,连一根线接到机箱旁边有一个继电器(还是微动开关?whatever,反正就是个开关),只要机箱盖子正确的盖上,开关接通,2 Pin 就会接通。
简单嘛,手把那个开关压住就行了,开机,问题依旧。
难道是因为刚才暴力拆装把开关弄坏了?声音很清脆啊,弹簧手感也很正常,没这么娇嫩吧?管它,先把开关拆了,直接镊子短接一下主板上的两根针脚,开机,问题依旧。
难道是我手法不够犀利,想当年电脑电源开关坏了,用镊子短接针脚一下开机,不要太娴熟啊,唉,老了老了。
查下说明书,把 BIOS Reset 一下,开机,问题依旧。
我苦思冥想,等等,难道………………………………
我不服啊,去了一趟公司,把另一块键盘拿回家,顺便把无线鼠标也拿回来了,接上,开机,好……好了…………
倒霉键盘我掰了……算了,不少钱呢,反正在台式机上用着是好的,留着继续用吧。
所以最终是什么原因呢?我也不知道,反正第一时间进 BIOS 里把这个提示给关了。
要不……咱又换回 Linux?算了……闹心,用 Windows,挺好。
本期节目就先到这里,下期开始进入软件篇。这期写的枯燥了一点,主要是过程中也懒得拍照片,不过既然是准系统大概也没什么好晒图的,下期既然是软件那就争取多配点图吧。
]]>拿去,你们儿子。
视频+手机观看:bilibili
设备&&软件:
键盘/音源:YAMAHA MOXF8
摄像:SONY NEX-6 + 24-70/4 ZA
编辑:Adobe Premiere
《千与千寻》插曲。
视频+手机观看:bilibili
设备&&软件:
键盘/音源:YAMAHA MOXF8
摄像:SONY NEX-6 + 24-70/4 ZA
编辑:Adobe Premiere
h5pal AKA 仙剑奇侠传Web版是我造过最大的轮子,也是开过最大的坑。曾经装过的逼,终究是要还,不然我脸被打得啪啪响那还了得。虽然早就没更新(呃,准确的说,是自从开源以后就再也没更新),但对我自己而言心里其实还是想把这个坑给填了——至少是以另一种方式——也就是现在这个新坑——h5pal是怎样练成的。
其实一开始我是想做一个PPT完事儿,但最后发现信息量放进一个PPT里实在是有点太大了,还是老老实实的写文章吧。
挖坑如山倒,填坑如抽丝,今天是第0篇,开坑之作自然也没什么干货(逃,不过重点是先帮助我自己梳理一下这艘万吨巨轮要从何说起。
所以开篇其实只是一个提纲性质,我的打算是按照模块又底层到表层来慢慢介绍h5pal是如何从船坞到下水,当然我在写的过程中也毫无疑问肯定会被自己当年写的代码丑到吐,看我都吐了的份上,你们就别跟着吐槽了是吧……
h5pal的绝大部分代码来自于对SDLPAL的从C到JS的人肉翻译,因此大致也沿用了SDLPAL的架构。在之前的文章中我列了一个完成度的表,这里先照搬一下
模块 | 进度 |
---|---|
资源 | 90% |
读档 | 99% |
存档 | 40% |
Surface | 90% |
位图 | 99% |
Sprite | 99% |
地图 | 90% |
场景 | 90% |
调色盘 | 90% |
文本 | 99% |
脚本(天坑) | 70% |
平常UI | 90% |
战斗UI | 90% |
战斗(天坑) | 70% |
播片 | 90% |
结局 | 95% |
音乐 | 0% |
音效 | 0% |
在h5pal里这些模块和SDLPAL里的作用不一定是完全一样的,比方说SDLPAL的资源模块除了负责加载还负责卸载这样可以节省内存,但在JS中缺乏手工控制内存的方式,而且像我这样不负责任的男人怎么可能节省内存口亨。再比如SDLPAL的输入模块除了支持键盘以外还支持Symbian手机。再比如SDLPAL当中有很多支持仙剑的分支版本(比如什么什么梦幻版)的代码,还用条件编译分开了95版和98版,这些我都大幅度简化了。
这里对h5pal中每个模块做一个概述性的介绍:
除了这些游戏系统本身的模块以外,还有一些基础模块
那么这么多模块我当然不会都说,只会讲一些有含金量的或者有意思的,比如二进制、地图场景、战斗等,二对于脚本这样又臭又长的系统我就不打算写了,可能在写其他模块的时候顺带提几句吧。
那么,坑是挖好了,你看么?嗯,就算你看我也未必真的能填坑,因为我现在心里真的是好虚啊……
]]>JavaScript 2015中引入了Generator Function(相关内容可以参考前作ES6 generator函数与co一瞥与ES6 generator函数与co再一瞥),并且在加入了Symbol.iterator
之后,使得构造拥有自定义迭代器的集合变得相当容易(可以参考前作在JavaScript中实现LINQ——一次“失败”的尝试)。
前几天在群里@徐叔提出了这样一个问题:
1 | function* listen(element) { |
音锤思婷……
我理解,叔叔写listen
的目的是为了把事件源抽象成一个“可以被遍历的集合”。
要理解JS里的迭代器模式,首先必须从GeneratorFunction
和Symbol.iterator
说起。
JS的迭代器模式和C#有些许不同(原谅我经常用C#力的接口来做例子,其实只是因为我觉得它这些接口设计得比较工整良好,而且强类型语言也挺适合做例子),C#中使用两个接口IEnumerable<T>
和IEnumerator<T>
来实现迭代器模式,分别定义为
1 | public interface IEnumerable<T> { |
实现了IEnumerable<T>
的类型可以享受到foreach
语法糖,foreach
展开后就是通过对IEnumerator<T>
不断地MoveNext()
来完成迭代过程,这很好理解。
JS的迭代器模式围绕Symbol.iterator
,任何对象只要实现了Symbol.iterator
就可以享受for-of
语法糖。
在迭代过程方面,C#只用IEnumerator<T>
一个接口同时实现了迭代和取值两个操作,但JS里用了两个接口,这里举个例子
1 | var array = [1, 2, 3, 4, 5] |
可以看到调用Symbol.iterator
所得到的iter
对象只是负责next()
工作,而其不断next
所得到的it
对象则负责value
和done
工作。
也就是说,在不借助yield
的情况下,要实现Symbol.iterator
只需要构造一个满足上述接口的对象就OK了,举个例子
1 | var fakeArray = { |
然后我们尝试一下,能不能用yield *
语法来实现它和Generator
的无缝衔接:
1 | function* gen() { |
耶,成功了,解糖后手工遍历呢?
1 | var iter = gen() |
先说结论,我认为是:仅从上面所讨论的范围来看,不可行。
使用迭代器模式,无外乎是为了能工用for-of
语法(或者解糖以后自己不断next()
)来遍历集合。我们知道迭代器模式是一种典型的“Pull”模型,迭代过程是不断从集合里把东西拉出来,直到什么都拉不出来了(怎么听起来这么膈应)。
事件源是一个异步的东西,只有当事件发生的时候才会有货,但我们并不知道事件什么时候发生,因此当被“拉”的时候,不知道该把什么东西交给迭代器。
这时候有同学要问了,之前我们不是用co通过yield
来处理异步的东西吗,这不是证明yield/generator
是可以处理异步问题的吗?
其实只要看过我之前文章或者对co有了解的同学肯定就会知道,co是对yield/generator
的“误用”,我之所以加引号是因为在Unity的C#里甚至官方就直接用yield
和IEnumerator<T>
来实现了官方的协程API(我就不吐槽了您赶紧把C#版本升级了用async/await
吧),据我了解Python也有这么干的。这说明这个“误用”是一个有据可循的东西。
在co这样的语境下,yield/generator
已经完全不是为了构造自定义集合以及配合for-of
语法糖实现迭代器模式而用的,所以我们费了老鼻子劲实现的Symbol.iterator
到底还有没有卵用?
我要说,如果跳出上面所讨论的范围来看呢,还是有点儿卵用的。
我们先设定一个“目标语法”
1 | function* eventListeningByCoroutine() { |
看到没,用一个while (true)
,死命地从eventSource
里拉东西出来,由于这个拉的过程是不确定(异步)的,我们只好加了yield
。
所以现在模型建立了,我们剩下两个问题,一个是someMagicFunction
如何实现,一个是startCoroutine
如何实现。
如果看过我之前写的ES6 generator函数与co再一瞥,嗯,也可以起一个新名字,叫做《手把手教你实现一个山寨的co),那么应该很快就能写出上面的startCoroutine
函数。
1 | function startCoroutine(generatorFunction) { |
具体过程就不展开分析了,呃,我的意思是大概这样↓
然后更关键的是someMagicFunction
怎么实现
1 | function someMagicFunction() { |
完整演示在这里runjs/yzbro1a1。
嗯,其实我就是劣质地抄了一个js-csp,它是一个CSP(Communicating sequential processes)的实现,相当于Clojure里的core.async
和Go里的chan
。这里的例子也基本就是js-csp的其中一个例子的简化版而已。
在CSP中,事件源被抽象为一个channel
(或者像erlang里好像叫mailbox之类的,很形象),发生事件的时候往里面put
,监听事件这个事情体现为源源不断地(while-true)从里面take
——注意,这个take
是一个“阻塞”操作,体现为它必须冠以yield
。
Observable
(RxJS)对比从上面可以看到,只靠迭代器模式是不能用来抽象异步事件源的(至少吧,以我当前的理解能力,是不能的)。
本质上是因为迭代器模式使用的是“Pull”模型,什么时候发生迭代完全是由迭代者本身什么时候去“拉”数据决定的;而观察者模式是“Push”模型,什么时候发生迭代是由数据源本身决定的,这也使得它非常适合“事件流”、“消息推送”这类的持续、异步数据的迭代,也就是所谓的“Reactive Programming”。
那为什么最后的DEMO就用更类似“Pull”的方式实现了呢?因为startCoroutine
和someMagicFunction
这两者之间实现了消息传递,startCoroutine
接管了yield
和迭代中“什么时候该next()
”的过程,someMagicFunction
向反过来向它发送“你可以继续拉了”的消息(注意:上面的例子中实现为回调函数),这俩一推一拉,好不默契(???
值得注意的一点是不论CSP还是Observable都会存在一个“什么时候push”的问题,在RxJS和js-csp中,体现为它们有一个Scheduler的存在,在RxJS中它决定subscribe
什么时候被发射,在js-csp中它决定taker
什么时候被满足。RxJS内置的Scheduler就有诸如Rx.Scheduler.immediate
, Rx.Scheduler.currentThread
, Rx.Scheduler.default
等好几种,并且对于不同的Observable它根据策略会默认选择不同的Scheduler。
当然最后实现了一个劣质的CSP的DEMO,也算填了一个我两年前学习Go以及第一次看到js-csp的时候就开的坑——是啊,在我脑海里开了坑,但没敢告诉你们,免得你们又吐槽我挖坑不填(逃
]]>补几个2017年2-3月大作简评,《仁王》、《地平线:零之曙光》、《尼尔:自动人形》。
仁王 | 地平线 | 尼尔 | |
---|---|---|---|
画面 | 8 | 10 | 8 |
流畅度 | 10 | 9 | 10 |
剧情叙事 | 8 | 9 | 10 |
场景 | 8 | 10 | 7 |
战斗 | 9 | 9 | 9 |
音乐音效 | 7 | 8 | 10 |
系统 | 10 | 8 | 8 |
容量 | 9 | 9 | 8 |
小游戏 | - | - | - |
细节 | 8 | 8 | 9 |
总评 | 8.6 | 8.9 | 8.8 |
备注:
总的来说,仁王和尼尔各自有一些创新点,也有明显的短板,尤其是尼尔。反过来看地平线就是好莱坞大片,高工业水准保证它起点就很高,但要有创新就显得更难了。
三者都是绝对能值回票价的。
由于我没有NS,所以无法评价《塞尔达:荒野之息》了。由于我个人不感冒,也没有买《女神异闻录5》。不过从目前的风评看,这两者也都是神作级别的,肯定是差不了。
]]>补几个2016年游戏的简评,黑暗之魂3、极限竞速地平线3、看门狗2、最终幻想15、守望先锋,顺便把之前写过的神秘海域4和古墓丽影10也搬过来。
黑魂3 | 地平线3 | 看门狗2 | FF15 | 神秘海域4 | 古墓丽影崛起 | 守望先锋 | |
---|---|---|---|---|---|---|---|
画面 | 9 | 10 | 9 | 9 | 10 | 10 | 9 |
流畅度 | 10 | 8 | 9 | 8 | 9 | 9 | 10 |
剧情叙事 | 8 | - | 8 | 6 | 10 | 8 | - |
场景 | 10 | 10 | 9 | 9 | 10 | 10 | 8 |
战斗 | 10 | - | 7 | 10 | 9 | 9 | 10 |
音乐音效 | 8 | 9 | 8 | 10 | 9 | 9 | 8 |
系统 | 9 | 9 | 9 | 8 | 9 | 9 | 9 |
容量 | 9 | 9 | 8 | 10 | 9 | 10 | 9 |
小游戏 | - | - | 7 | 7 | 8 | 8 | - |
细节 | 9 | 9 | 8 | 9 | 10 | 9 | 9 |
总评 | 9.1 | 9.1 | 8.2 | 8.6 | 9.3 | 9.1 | 9 |
我的年度游戏: 神秘海域4:贼途末路
]]>这篇文章的起因是我在知乎上对JavaScript 函数式编程存在性能问题么?这个问题的回答。其实在这个问题之前挺久我就想做相关的尝试,但懒癌无药医,挖坑如山倒,填坑如抽丝。
废话不多说,走你。
C# 3.0引入了引以为豪的LINQ(Language INtergrated Query),可以用类函数式的方式操作集合(C#中的IEnumerable
在JS中,数组也有类似的filter
、map
、reduce
一类方法,但存在重复遍历问题,利用C#中LINQ的思路,给JS实现一套LINQ是否可行呢?
C#中的LINQ是通过yield
来避免重复遍历的,抽象的说,Where
(对应filter)、Select
(对应map)这类的方法调用的时候,都只会把操作“暂存”起来,直到调用了ToArray
、Aggregate
(对应reduce)之类的方法,才会“驱动”它去进行遍历。
举一个简单的例子
1 | var array = new []{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; |
上面是一个最基本的filter
/map
/reduce
的过程(下文也会继续用这个例子),只有在Aggregate
调用的时候,才会对数组进行遍历,而Where
和Select
只是一些类型为IQueryable<T>
的中间过程。
C#中的LINQ得益于C#的yield
关键字,配合First-Class-Function可以不费吹灰之力地构建IEnumerable<T>
,而C#中的foreach
提供了对IEnumerable<T>
的语法糖,这样就可以很自然的对LINQ的中间结果进行二次加工,而不需要繁琐地手工调用.Next()
。
JS中的原生数组就自带了filter
/map
/reduce
等一系列函数化的集合操作方法,但使用中有一个隐患就是,每次调用它们都会进行一次完整的遍历,这样当用这样连写的风格,就会造成重复遍历
1 | var sum = array.filter(n => n % 2 === 0) |
上面的代码,在filter
和map
被调用的时候,都会遍历一次数组,reduce
的时候再遍历一次,这样总共就被遍历了三次,当集合比较大的时候,这估计不是大家所想见发生的事情。
如果在filter
/map
/reduce
的回调函数里打印一些调试信息,我们会发现调用的次序大概会是这样的
1 | filter |
ES6中有了yield
和Generator Function(不熟悉的可以先回顾一下我几百年前写的这篇和这篇文章),并且,由于Symbol.iterator
和for of
语法的引入,能用生成器构造集合了,并且还能和for of
无缝衔接。
也就是说,ES6已经有了C#那样优雅地实现LINQ的基础设施,我们就来实现一个简单的试试。
首先我们像C#那样实现一个IQueryable
类,并且它通过Symbol.iterator
能够支持被for of
遍历
1 | class Queryable { |
由于我们是“面向接口编程”的,这里我们并不关心new Queryable(xxx)
传入的是一个Array
、一个Generator
还是一个Queryable
,反正它们都可以被for of
遍历。
然后为了方便,在Array.prototype
上挂了一个方法,别嫌脏,娱乐而已。
尝试一下
1 | let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
我们的Queryable
类已经可以享受for of
语法糖的便利了,然后我们就可以基于这个给它愚快地添加各种集合操作方法了
1 | function* _filter(iterable, predicate) { |
这里注意,filter
和map
分别调用了_filter
和_map
方法,它们返回的结果都是Generator
,我们知道一个Generator
只定义了集合如何“被遍历”,而事实上它没有真正发生操作,需要调用next()
或者for of
(也就是next()
的语法糖)来“驱动”它进行遍历。
而reduce
当中调用了for of
,也就是它真正发生了遍历。
赶紧爽一爽
1 | var sum = array.asQueryable() |
如果在filter
/map
/reduce
的回调函数里打印一些调试信息,我们会发现调用的次序大概会是这样的
1 | filter/map/reduce |
只遍历了一遍
有了上面三个方法我们可以顺便构造一下length
和toArray
这类的方法,比如
1 | Queryable.prototype.length = function() { |
当然其实map
/reduce
都是foldl
/foldr
的具象(吃我一发安利,参考我写的使用JavaScript实现“真·函数式编程”,所以上面的那些方法其实都可以写得更“函数式”,但既然这篇文章只是为了实验,就不搞那么多幺蛾子了。
我们用benchmark模块对上述代码进行性能测试,并且引入两个对照组,不多说了,直接看代码吧
1 | function useRawLoop() { |
用长度为100的数组进行测试,结果
1 | RawLoop x 380,068 ops/sec ±1.01% (88 runs sampled) |
可以看出,我们的LINQ性能非常非常的废柴,主要原因:
for of
的优化非常废柴——因为它就是Generator#next()
的语法糖虽然我们通过ES6的一系列新特性给JS实现了lazy的LINQ,避免重复遍历,是实现了,但想象中的性能提高却是化为泡影。
当然,通过不断优化,减少for of
的使用,改为手工.next()
遍历,也许性能还会高一些,但一来我不太相信它会有很明显的变化。二来更重要的是,不用for of
的话,我们就不能实现“无痛”的集合操作代码编写了,既然已经不能“无痛”,那么同样“痛”的方法自然有性能更优的,而且根本不需要Symbol.iterator
、Generator等等这一大堆新特性。
所以这是一个成功的尝试,也是一个失败的尝试。成功之处在于很开心能看到ES6有如此强大的基础设施用于编写优雅代码,发挥创造力。失败之处么,自然是由于现阶段的JS引擎并没有对这些新引入的特性进行值得称道的优化,这也提醒我对于这些新特性——至少是说,需要runtime支持的新特性——不要盲目的追新。
时间来到2017年8月1日中国人民解放军建军90周年,在node.js 8.x上,新的V8对for-of
和Iterator进行了惊人的优化,上面的测试结果变成了:
1 | RawLoop x 422,242 ops/sec ±1.31% (84 runs sampled) |
使用for-of
遍历数组竟然比用for
循环直接遍历还要快一倍这简直不科学,让我怀疑是不是掉进了陷阱!!!而且用LINQ方式也比用Array原生方法迭代更快了,快了将近一倍!
这意味着LINQ以后性能将会可以接受,它可行了!意味着在JS里面终于可以优雅地实现迭代器模式了!哦,这样啊,真是一个激动人心的好消息啊,反正我是懒得去把它写完的。
在NGA上看到这么一个帖子,认为这幅红绿色盲测试图
只看出羊的====================红绿色弱只看出鸡的====================蓝绿色弱看出鸡和羊,但是还是想说是羊的====正常人
我觉得这个是不对的。
我认为,看到鸡的人是红绿色弱或色盲,看到羊是正常的。
不过某一楼所用魔术棒的方法也是不科学的。
正确的办法是在PS里调成LAB颜色模式(这个模式比较接近人眼),禁用A通道就是红绿色盲视角,禁用B通道就是蓝绿色盲视角。
偷懒的办法是RGB模式,禁用R和G通道模拟红绿色盲视角,禁用G和B通道模拟蓝绿色盲视角。
有些版本的PS色彩模式里直接有色盲模式。
可以看出,红绿色盲患者因为分不清红色和绿色,对于红色和绿色会混淆,但是深红之于浅红、深绿之于浅绿,他们是通过亮度(L通道)能区分出来的。
由于原图本身就是靠红色和绿色的分界来作为羊的轮廓的,所以红绿色盲患者的眼中就丢失了羊的大部分轮廓,并且把一部分深红和浅红的界线(羊脖子下方和羊的腹部)、深绿和浅绿的界线(羊背和脖子上方)看成了轮廓,所以才会看成是鸡。
我认为这幅图是不能检测蓝绿色盲的,至于所谓的正常人羊和鸡都能看出来,我觉得是在知道这个图的结果以后自行脑补出来的。
]]>最近心血来潮,想搞一下ASP.NET Core,于是准备把练琴记录仪的服务端迁移到ASP.NET Core MVC上,不过遇到些小问题,记录一下。
官网上可以下载各平台的Binary,macOS很简单直接按它说明的操作步骤做就行了。
中间有个步骤是要求用brew安装openssl,我试了一下brew link
然而并没有什么卵用,于是就按它说的做了。
装完以后就可以使用dotnet
命令了,用dotnet new
可以初始化一个Hello World程序,dotnet restore
用来安装依赖,dotnet run
编译运行。
VS Code中搜索C#,就能找到C#扩展,官方的。安装重启VSC后会自动下载OmniSharp,如果网速不好的话这个过程比较悲剧……
然后就可以用VSC打开刚才建的Hello World了,和VS差不多,按F5启动调试,就可以愚快的断点调试了。
一种网上比较多的方式是用yeoman做脚手架,这个是OK的,但是如果网速不是很快的话千万不要装那个完整的web app示例,因为它会下载一大堆依赖,bower超级坑爹。
ASP.NET Core和ASP.NET在这里有稍许不同,ASP.NET Core可以不需要作为IIS的一个模块来运行了,内建了一个Kestrel模块,就是用.NET Core自己实现的轻量级Web Server,这个也算一种潮流的方法吧。当然它也还是有一个IISIntergration模块,不过我没琢磨。
于是启动网站的方法还是和普通程序一样F5,或者dotnet run
,然后会告诉你启动在多少端口上,就能访问了。
ASP.NET Core里加了一堆依赖注入的功能,就是说可以在程序启动的时候配置好不同级别的依赖注入,比如注入单件、每个Request注入一个新实例等等。配置好了以后再为Controller重载一个构造函数,把你需要的那几个被注入了的类型作为参数,就可以在构造函数里拿到注入的实例了。
然而,我并不是很适应,因为这样的话我想注入多个对象的时候需要把他们挨个写在构造函数里,徒增一大堆参数——也许有更简便的自动注入到属性的办法吧,不过我暂时没这么做,而是只注入了少量几个类,写了一个BaseController
类,其它用得上的类都在这个地方初始化,然后所有的Controller都继承它,就完成集中式的依赖初始化了。
在Controller里return Json(xxx);
可以不需要加JsonRequestBehavior.AllowGet
了,虽然我早就在BaseController
里面封装了一个吧。
默认的JSON序列化格式当中,DateTime
类型的序列化结果不再像之前那样是蛋疼的\Date(xxxxxxx)
了,而是改成了比较友好的yyyy-MM-dd HH:mm:ss
,方便多了。
原本在System.Web.HttpUtility
中的UrlEncode
方法现在改用System.Net.WebUtility
了,您说这不是蛋疼么。
原本的HttpContext.Request
现在有了比较大的变化,现在的类型是Microsoft.AspNetCore.Http.HttpRequest
,原本的.Url
属性没有了,要取当前请求的URL的话需要用Request.Path + Request.QueryString
了。
由于没有了nuget命令行,所以原本用PM-Install
来搞定的事情现在需要先去project.json
里添加一行依赖,然后再dotnet restore
安装依赖。需要去nuget网站上查对应的程序集的版本号是多少,手工指定,相比之下就会稍微麻烦一点点。
主要涉及到引入Microsoft.EntityFrameworkCore
,具体方法网上到处都是,就不说了,貌似默认的那个yeoman脚手架已经有了,不过配置的是Sqlite的。
最坑爹的事情来了,我搞了半个晚上都没搞定,因为…………………………
MySQL官方实现的那个程序集MySql.Data.EntityFrameworkCore
,您注意看,是MySql
,但是实际上使用的时候,要引用的命名空间却是MySQL.Data.EntityFrameworkCore.Extensions
,您看,是MySQL
,我只能说这个包吧,MySQL是请临时工写的吧。
剩下的用起来基本上就是正常用EntityFramework的方法了,网上到处都是例子。
我在macOS上开发,然后部署到我的linux服务器上。我的系统是ubuntu 14.04,linux上的dotnet
安装方式也不复杂,基本上安装官网的步骤就搞定了。不过中间也遇到一些小插曲,我的环境上面有些系统工具没装全,不过那些和本文主线无关我就不说了。
正经方式是在project.json
里写上runtime
,然后运行dotnet publish
,它可以一次性把所有依赖在那个平台上对应的dll都copy到生成目录里面,也就是说整个目录扔到了目标机器上不需要在运行dotnet restore
安装依赖了,可以直接dotnet xxx.dll
运行主程序。
我用的办法是到目标机器上用dotnet restore
、dotnet build
重新编译,不过说起来应该也是差不了太多吧。
web.config真是弱鸡!在ASP.NET Core中它不再是推荐的进行应用程序配置的方式。
在官方的例子中,有一个多环境配置的例子,默认的应用程序配置appsettings.json
,可以给它配置appsettings.{env.EnvironmentName}.json
,两者会做merge,这样的话,appsetting.json
里就全部是生产环节的配置了,在开发和测试环境上通过环境变量来选择用哪个配置文件去覆盖它。
比方说,脚手架里给F5的task配置了注入Development环境,所以会自动用appsettings.Development.json来和默认配置做merge。
我只能说,现代化多了……
官方的指南里推荐的方式是使用nginx做前端,反向代理到应用程序自己host的那个Kestrel服务器。
原本的脚手架里面配置了wwwroot
目录作为静态文件目录,考虑到可以不用把静态文件的流量都放到.NET里来,我直接在nginx里面把/static
给rewrite到了<app>/wwwroot/static
,拦住了绝大部分静态文件,其它少部分,比如favicon.ico
什么的放了就放了罢。
由于不再依附IIS,也没有fast-cgi,这种self hosted运行方式就是单进程容易挂了,比如nodejs、HHVM也是有这种问题,官方指南里写了使用supervisor来监控并重启进程。
supervisor以前在狼厂的时候用得很多,不过都是OP配置好的,我还没自己配过,暂时就先放这儿,等我把程序写完了正式部署的时候再配置吧。
]]>焦虑啊焦虑……
设备&&软件:
键盘:CASIO PX135
音源:4Front TruePianos
摄像:SONY NEX-6 + 24-70/4 ZA
编辑:SONY Vegas
为了把我的练琴记录仪改成多用户App,我需要做一个Weibo OAuth功能,因为练琴记录仪是Single Page App,我不愿意直接跳转到OAuth页面,那样会打断我的应用状态,于是我打算打开一个新窗口来完成OAuth。
这样一来,问题自然就转换为跨窗口通讯问题了。
窗口间通讯毫无疑问首选是window.postMessage
,在cordova当中,原生window.open
是不能用的,官方给的方案是使用cordova-plugin-inappbrowser
插件所提供的cordova.InAppBrowser.open(url, target, options)
来取代window.open
,这两者基本上API差不多一致。
但是IAB插件所返回的对象并不是真正的window
,它没有postMessage
功能,并且在IAB所打开的页面中,也没有window.opener
,于是只能另辟蹊径,找点不靠谱的挫方法来试试了。
OAuth的基本流程这里就不赘述了,简单描述一下
client_id
——也称app key
以及在服务商注册的redirect_url
拼在一起,让用户去访问服务商的authorize
地址。client_id
授权自己的账号,如果是,会跳转到redirect_url?code=xxxxxx
。redirect_url
的访问,用URL参数中的code
和自己的client_id
以及app secret
(相当于密码)去请求服务商的access_token
接口,得到access_token
,这个就是此应用对于这个用户账号的访问凭条。redirect_url
页面根据应用自身需要把获得的access_token
传回应用,完成授权过程。window.open
时的流程var win = window.open(oauth_url)
。redirect_url
。redirect_url
上,把access_token
用window.opener.postMessage
的方式发给应用。win
的onmessage
事件,一旦收到了access_token
就完成授权,可以win.close()
了。然后我先把它写成了一个函数
1 | function crossWindowViaBrowser(url, target, opts, key, timeout) { |
cordova.InAppBrowser.open
时的流程var win = cordova.InAppBrowser.open(oauth_url)
。win.executeScript
并进行轮询,其内容是尝试读取localStorage.getItem(key)
。redirect_url
页面把获取到的access_token
写到localStorage.setItem(key, access_token)
。localStorage.getItem(key)
有值,就可以得到access_token
,然后就可以localStorage.removeItem(key)
,完成授权,win.close()
。然后我也单独写了一个函数
1 | function crossWindowViaCordovaIAB(url, target, opts, key, timeout) { |
1 | function crossWindow(...args) { |
服务端的Redirect Page我是用PHP写的,涉及到上面的cross-browser
的部分大概是:
1 | <script> |
其中$output
是对access_token
接口curl
得到的返回值,虽然微博给的返回值理论上说都是合法的JSON,但出于通用考虑我还是直接把它当字符串传递,让客户端自己在parse
的时候进行try/catch
,而且这样对localStorage
也比较直接。
《神秘海域4》AKA.秘境探险4简评如下,附带和《古墓丽影:崛起》的无恶意简单对比。
惊艳!毫无疑问代表了主机平台目前最高水准的画质。
和TR10比的话,也许TR10在更高配置的PC上可以表现出超过UC4的水准,但是至少在我的农企380上,我觉得是UC4更优。这里的优,不仅是表现在“画质”上面,还有UC4里那些强大的画面细节。虽然没有TressFX和劳拉妹那丧心病狂的飘逸长发,但发型光的质感还是很好的。不仅是头发,在很多方面,UC4都在画面的表现力和质感上很下功夫。就冲这画面,值了那PS4风扇狂转。
攀爬手感方面,个人觉得,是不如TR10的。在后来开启了攀爬镜头辅助以后,感觉有所改善——也许只是我玩了几个小时以后,开始适应了?但UC4在攀爬的扎实感做的是很不错的,也许是TR10的攀岩镐太厉害了,而UC4直到中后期才能拿到那个刀子,而且只能扎一发,这给了更真实的错觉。另一方面,UC4在手感细微调节上也是做的不错的,比如暴风雨那一章,刻意设计了攀岩手滑抓不住的感觉,虽然对于游戏进程没有任何影响,但是这种游戏性和剧情完美融合的感觉就是很赞。
首次读盘的时间相当的长,“从检查点开始”的读盘时间也是真的不短,这在我追求潜行解决战斗的时候,可以说是费了不少时间。
我还是很纠结给10分还是9分的,因为《The Last of Us》在我这里毫无疑问是10分,而UC4在我眼里,剧情叙事方面,是赶不上TLOU的。
但是最后还是给了10分,主要有2个原因:
由于在游戏中大量的双人冒险剧情,UC4也引入了TLOU的隐藏对话系统,这对于游戏性有提高,同时也是一种挺不错的叙事手法。
简直完美,完美。神秘海域4摄影大赛已拉开帷幕!
本来呢我是想打8分的,因为基本上来说,跟前3代战斗机制没有太大变化,最后还是加了1分,加在:
但是,对于战斗,我想吐槽的还是挺多的……
潜行的AI可以说很糟,触发黄色警觉以后,只会原地发呆,远远的看看,不会换岗巡视,菜。更智障的是,敌人看到了同伴的尸体,竟然只会触发黄色警觉,不会进入大规模警戒状态——这两点,是我选的普通难度原因?
主题曲有了一个新的编曲,但说实话,没有TLOU的主题曲那样深入人心。这一点要求似乎太苛刻,毕竟TLOU的立意是要比神秘海域这种爆米花剧情要高一个层次的。但是作为系列的终焉,听到主题曲响起的时候还是会很感动的。
配音因为对德雷克是很熟悉了,所以感觉还是那么棒。
在游戏性方面我觉得和TR10有很大的不同。
TR10胜在有捡垃圾玩法,有角色成长,改枪,对于一个动作冒险游戏,这些虽然都不是核心,但是配合游戏进程的确能让玩家有一些成长的感觉。
UC4延续了系列的特色,在解谜方面做得是很不错的,谜题比TR10的有意思多了。双人冒险其乐无穷,比起劳拉妹一个人飞檐走壁,也是更不孤单。载具段落不多,不过都还算好玩,飞车大战那个章节真是太赞了,要知道前作里面飞车大战都只是别人开车自己战,而这次是可以自己开车了,那感觉……T_T
UC4通关以后可以拿到一些特效,比如子弹时间,还有渲染模式,比如卡通渲染,像素画等等,挺有趣的,不过说实话这些东西多少有点隔靴搔痒的意思,图乐子而已,所以实在是不够支撑在这一项拿到10分呐。
线性剧情,我用了15小时,普通难度,收集程度大约只有1/3。我不能说这游戏容量小,毕竟收集要素我只玩到了很少的一部分,但是相比劳拉妹我觉得内容还是少了点,毕竟劳拉妹有个开放世界可以捡垃圾呢。
当然我没有把网战的部分算进去,不过劳拉妹的简评里也没有把那部分算进去,所以对比还是公平的。
除了一开始的橡皮子弹枪,好像这游戏……基本上就没有小游戏了…………吧?如果把解谜的某些部分算作小游戏也未尝不可。
有些章节场景里有很多可以把玩的物件,这些勉强算小游戏吧,说起来呢,还挺有趣的其实。
哦,还有古惑狼??
收藏品的细节水平我觉得不如TR10。UC4的细节更多体现在演出和场景细节上,比如我之前在知乎上说的宝丽来相机的细节,还有那些像前几作致敬的细节,比如那些可交互物件,还有墙上的画什么的,还有今天突然流传火爆的艾莲娜跟德雷克接吻的时候鼻子的细节等等。应该说从TLOU开始这种风格就挺明显的,从策划到美术,这些细节不是可以做作出来的,而是深入游戏的一种表现力。
附:与TR10各单项对比
神秘海域4 | 古墓丽影:崛起 | |
---|---|---|
画面 | 10 | 10 |
流畅度 | 9 | 9 |
剧情叙事 | 10 | 8 |
场景 | 10 | 10 |
战斗 | 9 | 9 |
音乐音效 | 9 | 9 |
系统 | 9 | 9 |
容量 | 9 | 10 |
小游戏 | 8 | 8 |
细节 | 10 | 9 |
总分 | 9.3 | 9.1 |
DLC赶紧出啊!!!!!买的带季票版本呢!
]]>抽了点时间看了一下Vue 2.0的代码,主要着重于如何实现数据绑定这一块,在小右的指导下基本上算是知道了个六成吧。
代码可以在Vue的GitHub Repo上next
分支里找到。cloc
一下:
1 | $ cloc src/ test/ examples/ |
其中,src
是4000多行,可以不客气的说,Vue完全可以称为是轻量级。
Vue 2不再是Browser-Only的,所以加入了render
和runtime
的概念。
render
是将v-dom树(下文中v-dom和v-tree基本表示一个意思)进行输出的实现层,比如server
就是一个实现。
runtime
是对v-tree进行数据绑定、更新、事件处理等具体操作的实现层,比如web-runtime
就是将抽象dom操作全部实现在DOM API上。
初始化一个Vue Instance的过程,本文不做重点描述,大概如下:
_render
,得到一棵v-tree。上述过程还包括对数据绑定的解析,对vm中的数据字段进行包装,通过getter/setter
触发变化以此实现“Reactivity”,并收集依赖,注册Watcher。这个过程和现在的Vue差不多。
现在,我们有了一棵v-tree,并且它已经mount到了一个dom-tree上,初始化的过程差不多就先介绍到这里吧。
下面以一个简单的计数器例子来介绍一下Vue 2中是如何把getter/setter
与v-dom结合起来实现数据绑定的。
1 | <div id="counter-app"> |
点击“喜+1”的时候,会执行(this.$data.)count++
,这个count
是一个“reactiveSetter”。reactiveSetter
会将这个修改所涉及的,在初始化过程中收集到的一系列依赖进行notify()
。
1 | // /core/observer/index.js |
这里的dep
是一个Dep
实例,dep.notify()
会对其对应的所有注册的Watcher
实例(在最初parse时注册)逐一进行update()
。
1 | // /core/observer/dep.js |
Watcher.prototype.update()
会将自己添加到一个全局的batch queue里面:
1 | // /core/observer/watcher.js |
然后等待下一个tick的来临(批量更新机制)。
当下一个tick来临时,会将batch queue里的每个Watcher
实例都拿出来并且调用它的run()
1 | // /core/observer/watcher.js |
其中的this.get()
:
1 | // /core/observer/watcher.js |
对于vm实例而言,这里的this.getter
绑定的是vm._render
,它会调用this.$options.render
,也就是在初始化时,模板编译所生产的v-dom函数。
1 | // /core/instance/render.js |
于是这里,一个vm所关联的Watcher
实例就通过vm._render()
得到了一棵(更新后的)v-tree。
回到Watcher
里,run()
当中,接下来就会调用this.cb.call(this.vm, value, oldValue)
。上面已经看到value
和oldValue
分别是this.vm
所对应的新、老v-tree。而这里的this.cb
则绑定的是vm._update
。
1 | // /core/instance/lifecycle.js |
可以看到,vm._update
当中,调用了Vue.prototype.__patch__
,那么这个函数又是从哪来的呢?
答案在/entries/web-runtime.js、/platforms/web/runtime/node-ops.js、/core/vdom/patch.js等几个文件里。
在程序启动的时候,xxx-runtime.js(比如web-runtime.js)会作为一个Provider,提供一系列dom操作,如熟悉的createElement()
、insertBefore()
等。把这些操作的具体实现(如web-runtime就是把它们直接落在原生DOM函数上)交给v-dom的createPatchFunction()
。后者则会生成这个__patch__
方法,糅合了通用的tree-diff逻辑,以及因runtime而异的dom操作实现。
1 | // /entries/web-runtime.js |
这个__patch__
函数当中即包含了tree-diff过程又包含了patch过程,并且是在一遍里完成的,在__patch__(oldVTree, newVTree)
被调用之后,oldVTree
所关联的真实backend(在浏览器里,它就是DOM元素)已经被tree-diff算法所patch成newVTree所对应的样子。
上述过程就完成了一次[属性更新 -> UI自动更新]的过程。
优化过程主要是在模板编译阶段通过/compiler/optimizer.js实现的。
主要的方法有两种:
其中第二点,在遇到static sub-tree的时候,会命中oldNode === newNode
的全等逻辑,可以直接跳过整棵子树。不过我发现一些小问题,一个是对于<button @click="count++">喜+1</button>
这种v-dom,我不太确定它应该被当做是纯静态的还是动态的,这个我还没想明白,暂时就先不说了,至少在目前的optimizer中,还是把它当动态的。另一个问题是对于模板中的各种HTML注释和换行所带来的一些空白的TextNode,明显应该是静态的,但却被当做了“动态”节点——之所以加引号是因为这部分节点的确是不会变,但没有提取成static node,所以每次_render
的时候它还是会被render成一个新的v-node,这样就命中不了全等逻辑,然后对它再进行一次比较(尽管是代价非常低的一次比较)。(关于这个问题的例子可以看这个Gist)
另一个问题是,如果使用服务端渲染,初始化会将v-dom直接mount
到服务端输出的dom树上。但在客户端渲染的情况下,直接在浏览器里进行模板编译的话,首次输出会生成一个新的dom节点并mount
到它上面,原版的那个用来当模板的dom节点则没用了。这是个浪费,但可以理解,第一是因为模板里有很多最后不会输出的节点(比如v-if/v-else中未命中的分支),另一个是到了生产环境下应该大多数人都会选择模板预编译吧。
那么关于数据绑定的实现差不多就是这样了,后面有时间(不用掩饰了,肯定要坑)的话,再继续探索一下依赖追踪、computed
属性的实现,以及更多内容(吧……
《古墓丽影:崛起》又名:古墓拆迁、铁臂女侠、无敌铁镐……简评如下。
惊艳!即使只开到High也已经惊艳,Very High我的机器跑不起来,看截图更棒了。
首次读盘时间较长,中途读盘次数合理,速度较快,死亡不能无缝读盘是唯一的鸡蛋里挑骨头。
攀爬动作比TR9更丰富,并且延续了超级流畅的手感,完美。
剧情还是俗套的好莱坞探险大片的路线,但比TR9的稍微好一丁丁点,后期的超自然部分衔接的略微突兀,而且依然是俗。
制作群之后的彩蛋还留悬念,哼!
简直完美,室内精雕细琢简直可以说到每一块石头,室外恢弘大气而且种类很多,山谷、森林、冰川、古建筑……太棒了。
射击的感觉作为一款动作冒险游戏是很棒的,惟独弓箭太强,其他武器基本上都是配角,微微失色。
很多关卡(也许是几乎所有关卡?)都有潜行玩法,丰富了选择。
BGM存在感单薄,但播片当中的BGM是大片水准。音效也是一流的水准,临场感强,相信是强大的游戏工业所提供的保障。
简中配音赞,希望未来引进更多简中配音,并且能有更深度的合作(本作的简中配音是在配音演员看不见画面的情况下配的)。
基本上延续了TR9的技能系统和改枪系统,够玩,缺少一些新意。
加入了若干种资源,这游戏还能捡破烂,真是可以有够玩的……
默认Normal难度个人通关用时17小时(含少量吃饭挂机),完成度71%,尚有大量收集要素可以探索。
由于拆迁游戏的性质,开放世界设计有难度,比如《Ori》当中虽然也是开放地图,但有一个区域一旦触发剧情后就会封锁无法再次进入,影响全收集。TR10有这么大的地图,并且还要在拆迁过后依然保证每个区域的可达性,不容易。
区域挑战基本上还是找多少个牌子、击中多少盏吊灯这些老掉牙的设计,古墓挑战其中很多设计精巧的挑战却不放在主线里,感觉有些浪费。
收集到的古董模型精细程度简直丧心病狂,并且讲解文字全部有配音,单冲这一点就不得不赞。
本文将用一个Pull-to-Refresh的例子来介绍如何使用RxJS进行高度抽象的复杂DOM事件处理。
文中所开发的完整demo代码可以在github找到,在线demo在这里(需要使用手机或开启touch模拟,未作浏览器兼容)。
这个程序将会用到的工具:
Pull to Refresh是一个流行到甚至让人开始觉得有些过时了的交互,也就是所谓的“下拉刷新”。
这个交互简单描述就是:
当一个元素的滚动位置处于其顶端时,做一个下拉手势,将会对元素进行刷新。
由于Web中的限制,在具体实现上有一些妥协,我使用的策略是:
在
touchstart
事件中,检测元素的滚动位置是否在其顶端,若是,则记录起始手指位置,并继续
在touchmove
事件中,检测当前手指位置和起始位置的相对关系,若是下拉,则进入下拉状态
在下拉状态中,继续监听touchmove
事件,并更新UI,通常会拉出一个隐藏的元素,通过其提示用户继续下拉可以刷新
下拉到一定程度,超过阈值,则可以进入Release to Refresh状态,通常也会在UI上做一些提示
在下拉手势结束时,检测下拉程度是否超过阈值,若是,则进行更新,否则恢复原貌
接下来的内容中将会实现一个名为pull-to-refresh
的directive
,在Vue中将其应用在指定的元素上,并指定相关参数,响应对应的回调函数和事件,则可以复用“下拉刷新”的功能。
使用Vue并非是Pull-to-Refresh本身、或者是RxJS依赖Vue,这只是做Demo的一个选择。同样,实现为directive
也只是一个选择,将其实现为component
或者mixin
都是完全可行的。
首先构建一个如图所示的页面框架
其结构为
1 | #app |
其中.body
是一个局部滚动元素,我们将会在.staff
元素上应用pull-to-refresh
,让其相对于body滚动时能够具有下拉刷新功能。
而其他元素不是本文的重点,不在文中赘述了。
Rx中的Rx.Observable
可以使用“事件流”的概念来理解,它将一系列类似的、未来发生的事件整合成一条“流”,我们既可以像遍历一个序列一样去“遍历”它,也可以像对序列那样对它进行map/filter/reduce/flatMap
等等操作,Rx还提供了诸如skip/take/groupBy
等非常实用的操作,甚至是对两条事件流进行“交织”的操作。
RxJS的API,可以在rx-book找到,对于很多流操作它还有图形解释,非常棒。RxMarbles是一个对Rx中各种流操作的图形化学习工具,也是非常直观。
在使用手工处理drag
的时候,我们通常的思路是这样:
touchstart
中记录起始位置,并开始监听touchmove
和touchend
touchmove
中计算当前位置和起始位置之间的offset
,并进行拖拽操作touchend
中取消监听touchmove
和touchstart
,并进行释放操作上面的描述起始是一个“状态机”,而接下来我们要用Rx的风格来处理drag
。
首先我们拥有3条事件流,他们看起来分别是这样:
1 | touchstart ---------@-----------------@------------------- |
对于touchstart
流中的每一个事件,将其map
成一个drag
流,其中每一个元素都由current
和start
两个对象组成,每一条drag
都会在整个touchmove
流中持续,并在touchend
事件时结束。
将上面“图”里的那组事件流进行这样的组合变换,我们可以得到下面这样一个drag
流
1 | touchstart ---------@-----------------@------------------- |
于是就可以通过Rx的订阅函数来处理这条drag
流:
1 | drags.subscribe(drag => drag.subscribe(move => { |
pull-to-refresh
比drag
要稍微复杂一点,不过也复杂不到哪去,下面对着重点代码来梳理一下逻辑,完整代码在src/directives/pull-to-refresh.js
当中。
1 | let touchstart = Rx.Observable.fromEvent(el, 'touchstart') |
首先像drag
那样,建立起touchstart/touchmove/touchend
三个流。
1 | let touchcancel = Rx.Observable.fromEvent(document, 'touchcancel') |
对touchend
和touchcancel
进行无差别处理,将它们merge
成一条end
流,形象描述就是:
1 | touchend ---------#----------------#---- |
对touchstart
流进行过滤,只处理“元素处于其滚动状态顶端”的那些事件,得到一条叫做dragAtTop
的流:
1 | let dragAtTop = touchstart.filter(e => wrapper.scrollTop === 0) |
响应dragAtTop
流,将它map
成与上面类似的drag
流,不过这次我们只关心纵轴上的数据。
1 | let dragTopDown = dragAtTop.map(start => { |
还是用上面那组事件来描述的话,dragTopDown
看起来就是这个样子:
1 | /这个不在顶端,于是被抛弃了 |
现在我们就有了“顶部下拉”的事件流dragTopDown
,对其进行响应,处理交互逻辑:
1 | dragTopDown.forEach(drags => { |
现在我们的pull-to-refresh
这个directive
就已经封装了:
pull-to-refresh-drag-move
事件,可以获知下拉距离offset
和是否超过刷新阈值refresh
pull-to-refresh-drag-release
事件,可以获知本次释放是否超过刷新阈值refresh
它依赖:
touch
事件族的元素el
——通过Vue的directive
机制即可自己获取el
所相对其滚动的容器wrapper
——通过directive
的params
获取on-refresh
回调,返回一个Promise
,在刷新操作完成时resolve
,进行恢复directive
接下来对.staff
元素应用v-pull-to-refresh-
,并且设定其各种参数,响应事件等,只摘主要的代码了
1 | <div class="body" v-el:body> |
上面的代码中对.staff
应用了v-pull-to-refresh
,并且对它绑定on-refresh
回调函数,wrapper
设置为了.body
,留下了v-el:staff
引用,这样我们可以在pull-to-refresh-drag-move
等事件中修改它的UI样式(当然,通过数据绑定来实现也OK)。
1 | export default { |
使用Rx可以将离散的事件转换成Rx.Observable
,我们理解成“流”的概念,“流”虽然是“无定型”的,但我们还是可以把它们当做“序列”来处理。一些原本需要用“状态”来实现的东西现在可以通过对流进行变化和组合来实现了,事件的脉络变得更加清晰。
Observable
Observable
Observable
可以通过toPromise
来转换成Promise
Observable
可以通过toArray
,在其结束时,将它所有的元素转换成数组Observable
over了
为什么我认为对于构建应用程序而言,MVVM/React是比jQuery更容易的方式?
文章比较浅,科普性质,大神们别嫌弃。
用一种“传统”的思路,我们要更新页面某一个部分的UI,应该这么做:
1 | $.get('url', function(data) { |
这个例子应该是一个典型的场景
为什么核心在于“找元素”呢?由于要尽可能的优化UI的性能,只能做最小更新操作,那么就需要找到发生变化的那个字段所需要的元素,单独对其进行操作。
所以jQuery的核心就在于query
,首当其冲就是它能最快捷的帮我们query
出需要的元素来,很好的满足了一个JS库的核心需求。当然它的另一个优势就是它的API设计得太简便了,简直是不会JS都能用,入门成本之低令人发指。
一句话
UI被设计为依赖Model,Model不应该依赖UI。
如果实现成贫血Model层,就会在逻辑代码里面去进行上面的query-update
操作,如果是充血Model层那可能就在Model里。不论怎样,这样做都违背了上述依赖关系。
很简单,当UI发生变化(这种变化在迭代当中非常频繁)的时候,不仅需要修改UI本身,也需要去修改逻辑代码或者Model层,比方说#name
这个ID换掉了,得换个选择器;比方说span
变成了textbox
,得把.html()
换成.val()
;比方说整个UI层重新换了一套CSS命名规范,或者上了一个className混淆方案,可能让所有的addClass/removeClass/hasClass
全瞎;比方说运营需要“重要的事情说三遍”于是同一个字段要被连续展现3次;比方说相册改版,啥没变,惟独从井字格变成轮播图了……
这些本身应该是UI的事儿——毫无业务逻辑在里面——却需要去改逻辑代码,依赖关系颠倒过来了,形成了anti-pattern。
所以现在流行说“单向数据流”,它是对上面所说的依赖关系的一个形象描述。
这概念谁说的来着,好像是Polymer。其实在12年的某个项目里,我就在尝试这个方式,当然,举步维艰。
当时的主要矛盾是,我们也实现了单向数据流,所有UI操作都调用Business层(相当于Controller)的接口,UI保持对Model的严格只读。但Business层修改完了Model之后,下一步就非常难了,为啥难呢?因为“Model变了,Drive不起UI来”。
如果Model只有一个简单粗暴的change
事件,那么UI就倒了八辈子的大霉了,它根本不知道到底变了什么,没法做最小的UI更新,那么性能上基本先Say Goodbye了。
于是实践上的问题就来了,Business层在修改Model的时候需要如履薄冰地触发一个“合理地小”的事件——不能太大,这样UI大面积做无用的更新;不能太碎,这样UI还需要做一个batch更新机制。
这样的结果肯定就是事件的种类会随着use case增多而大幅度增多,而可怕的就是UI必须对这些新增的事件一一作出响应,哪怕它跟之前某一个事件差别相当之小。
这当中自然也就隐含了Model对UI的间接依赖,逻辑代码需要对UI有比较深入的了解,才会知道怎样去触发一个事件它才会“合理地小”。
有了batch update,可以把Model的change
做到字段级别的CRUD事件了,但UI需要关心的事件就会呈一个数量级的增加。等于原本在逻辑代码里集中更新UI,变为了在UI里(借助batch update)分散更新——事儿没变少,就是换了个人在干。
至少是解决了一个依赖倒置的问题,UI通过字段来访问Model,通过事件来订阅更新自己,而Model则几乎不会对UI产生直接依赖了,极端一些,Model对于UI是不是DOM都可以不关心了。
现在有了MVVM和Virtual-DOM了,batch update也都是标配,Business层可以肆无忌惮的对Model进行任何粒度的CRUD。UI也不需要监听Model上的各种事件了——简单的说来,虽然整个数据流没有变,但是每一个环节都变简单了。
所以MVVM和Virtual-DOM解决的问题是数据绑定/数据展现吗?是,也不全是。更深究地说,它们解决的问题是帮助UI和Model之间“脏活累活谁来干”的问题——都没人干,于是只能让框架干了。从此以后,
对于Model而言:“老子就管写,你爱读不读。反正我的值是对的,用户看到展现不对那都赖你。”
对于UI而言:“老子就歇着,你爱咋样就来弄我两下,但是活儿得好,别让我太累,用户嫌卡那就怪你。”
至于Model如何Drive UI,Angular(脏检查)、React(Virtual-DOM)用的办法是主动的发现Model的变化,然后去推动UI更新;Avalon、Vue基于property getter的做法是被动的等Model发生变化。
除了Virtual-DOM以外,都需要对UI进行预处理,解析出一个UI Element -> property之间的依赖关系,知道每一个Element依赖了Model的哪个字段。把这张图反过来,就知道当一个property被修改时,它会影响那些个Element,从而实现最小更新。
而Virtual-DOM的最小化patch方案是通过tree-diff计算出来的,基于现代浏览器“老子for循环跑的飞快”的霸气,执行tree-diff的速度很理想。于是就直接不需要构建依赖关系,用起来更简单粗暴;进而在需要的时候有一定的优化空间,可以通过immutable这种方式来快速跳过tree-diff当中的某些环节。
所以在精心优化的情况下,Virtual-DOM应该最快的无疑,property getter有更强的适应性,天生就很快,但从外部去优化它很难。
React另一个优势是它的启动速度,由于不需要构建依赖关系,甚至是连parse模板都不需要(这一步相当于直接在构建JSX的时候已经做好了),它启动步骤就短多了,夸张地说,直接render
就出来了。
使用property getter的方案对于Model层有非常微弱的侵入性(相比Knockout那是低多了),使用脏检查和Virtual-DOM对Model层都几乎没有侵入性。
当然上面所说的性能差异其实都没有那么大啦……只是因为我自己写过virtual-dom玩具,也看了Vue的源码,一点小结而已。
在一个足够复杂的场景下,如果能践行Model与UI的依赖关系,程序的可测性(React还是谁来着,也管它叫Predictable,可预测)就有了一定的保障。
但是,很多情况下,没有那么理想,比如