Coder Social home page Coder Social logo

blog's People

Contributors

leowang721 avatar

Watchers

 avatar  avatar

blog's Issues

[2017-03-22] JavaScript 中的数据结构

数组

JavaScript引擎都在尽力的优化以接近C类语言的性能。

对于数组而言,可以直接使用,但最好是触发Fast Elements,具体可查看:《高性能的JavaScript开发》的数组部分

栈、队列

使用数组就可以轻易的模拟了,最多封装个类方便使用。

栈: push pop
队列:unshift pop

Set

es6已经有了,直接使用吧!

链表

我一直认为 链表 是数组长度固定化的产物,js的数组是动态的,不觉得它还有什么存在的必要, arr.splice(10, 1, newNode) 这种代码不是比链表插入要优美得多?而且还有一堆方法可以直接用:shift, unshift, pop, push, forEach, map ...

遍历也可以直接用 for...of 使用Iterator的方式

当然如果有不同的意见,我也可以改正。

如果是为了练习代码能力,那倒是不妨自己实现一个。

HashTable、Dictionary

js中的对象起到的作用已经超过了基础的数据结构 DictionaryHashTable,也不必费心去实现了,而且es6还有Map

hash算法倒是个有意思的东西,不过不在当前范畴。

这里还是数学概念:

  • 自由树是一个连通的、无回路的无向图。
  • 有根树就是多了个根节点,由此就有了“父子层级”的概念。
  • 有序树指的是子级节点有序的树。
  • 二叉树是指最多有两个子节点的树。
  • 完全二叉树是指子节点是满的,只要有就一定是两个,并且叶节点的深度一定相同(就是说左右子树的层数相等)。

树的最大特点是:任意两个顶点由唯一一条简单路径相连

二叉树 => 二叉查找树 => 红黑树 => B树 => B+树 .... ????

二叉查找树

节点存值,左子树都比它小,右子树都比它大。

执行基本操作的时间与树的高度成正比。n个节点的完全二叉树,操作最坏运行时间为O(lgn);如果是线性链(子级节点都跑一边去了),最坏是O(n)。

一颗随机构造的二叉树期望高度为O(lgn),基本操作的平均时间为O(lgn)。

一个简陋的二叉查找树的实现

经典的图定义是 G = (V, E):

// 至少我曾经会这么写……
class Graph {
    nodes = [];
    edges = [];
}

但实际上有两种表示方法:邻接表 or 邻接矩阵,上面那个一般是邻接矩阵,当然要写成边集的话那也没办法~~

来看图示:

邻接表无向图
image

邻接表有向图
image

邻接矩阵网上找的图

image

邻接表适合于稀疏图(|E|远小于|V|^2),因为表示出来比较紧凑
邻接矩阵则适合于稠密图,或必须很快判别两个给定顶点是否存在连接边(顶点间最短路径算法

待续...

[2017-03-21] 高性能的JavaScript开发

引擎暂时只是v8, JavaScriptCore、SpiderMonkey、Chakra、Carakan等等这些回头再看看做做对比啥的吧……

其他语言是以C/C++为例。

测试环境为Mac,并列出浏览器 or 环境的版本

  • Chrome: 56.0.2924.87 (64-bit)
  • Firefox: 51.0.1 (64 位)
  • Safari: 10.0.3 (12602.4.8)
  • Node.js: v7.7.2

持续更新中... 只是个持续更新的笔记而已,只是希望能说明白罢了!!

前言

C的访问机制,基本是编译确定了位置,偏移信息共享,使用时直接使用偏移量,所以非常高效。而js则不然,它在执行阶段才能确定结构,而且还能够增减对象的属性,在查找值时需要匹配属性名才能找到正确的值,这是很浪费时间的。

不过引擎们做了很多努力,已经在逐步接近其他语言的性能了,例如隐藏类。

这里说到的东西目的主要是为了避免让引擎的优化被浪费,甚至是倒退。

其实呢,对于WebGL这类的貌似更有用一些,对于一般开发倒是意义不大。

数据类型

在v8中,除了基础类型 BooleanNumberStringNullUndefined以外,其他都是对象。

在v8中,数据以两种形式来表示:

  • 值:例如 String、对象等,它们是变长的,而且内容的类型也不一样。
  • 句柄:大小是固定的,句柄中包含了指向数据的指针

v8实际上将所有的数据交由GC管理的,被定义在内部,我们不能直接操作,只能通过Handle(句柄类)来进行操作,所以访问的时候都是先找到Handle,再通过指针去访问实际的值,修改的时候也是修改Handle中的指针。

除了极少数的数据例如整型,其他的内容都是从堆里申请内存来存储的。

Handle本身能够存放特定数据,这也就使得这些数据不需要再从堆中分配,这也就减少了内存的使用并增加了访问速度。

在v8中,Handle对象的大小是4/8字节(32/64位机器),在JavaScriptCore中是8字节。JavaScript的数字尽管按照标准都应是64位浮点格式来表示,但是对于整数实际的操作是基于32位整数,这也就意味着可以在Handle中直接存储整数以达到快速访问的目的。

但针对着32位的Handle对象而言,它至少要区分存储的数据是整数还是指针,而指针本身的最后两位都是00,其实是不需要的,所以,就用这两位来表示句柄中包含的数据的类型,最后一位如果为0表示是整数,如果是1则表示是其他类型。

所以v8中实际能够直接快速使用的整数是小整数,31位的有符号整数。

v8中的Handle数据表示:
tagged values

超过31位的数字,则被转换为double,放到一个Object中,再用指针指过去。

开发指引

  1. 尽可能的使用31位有符号的整数,它很快![-1073741825, 1073741824]

数组

我们都知道数组这种数据结构,它的经典实现就是:内存中一段连续的位置分配来存储数据的线性集合。在其他语言中,它是需要在声明时固定类型、指定长度,以便在栈区分配内存,并且长度是不可变的。

数组能够高效的访问,其实就在于内存的“固定”分配机制,元素类型固定,每次偏移的长度也是固定的,那么对于访问而言,仅需要计算地址位移信息而已,仅需要几个机器语言指令就可以存、取及使用。

js的数组本质是个对象,但在v8中,以非负整数为key的元素被称为Element,每个对象都有一个指向Element数组的指针,其存放和其他属性是分开的,这其实也是针对数组的优化。

那么Element,其实也就是数组元素,它有两种类型:

  • Fast Elements: C语言模式的数组内存分配
  • Dictionary Elements:hash table

毫无疑问,Dictionary Elements要比Fast Elements慢很多,所以从性能角度而言,我们要做的事情是避免Dictionary Elements的出现。

什么状况下会导致Dictionary Elements的出现呢?

  1. 过大的预分配数组,通常阈值是 64K(PagesPerChunk的大小限制?)
  2. 在非预分配的数组中,过多的超越当前数组长度的下标赋值,这个阈值由kMaxGap决定,通常是1024
  3. 删除元素也有可能会导致

至于过多的“洞”出现在数组中会不会导致转换为Dictionary Elements暂时还没有明确的结论,不过不建议这样的用法。

示例:

// for 1
let arr = new Array(65000);
// for 2
let arr = [];
arr[1024] = 1024;

再来看Fast Elements,它其实也有三种类型:

  1. Fast integer values
  2. Fast doubles
  3. Fast values

这里其实是由元素类型导致的不同类型,所以可以再看一下上面的数据类型这部分

首先是整型,v8为了提高效率,31位有符号整数是由Handle保存的,所以处理起来是极快的。
再来是 doubles,常见的触发条件就是在整数数组中增加了一个浮点数,这会导致整个数组都被展开转换为double来存储,通过增加一个隐藏类实现的
最后是 values,例如再加入了一个true|undefined|null,这时候就要再次增加隐藏类,指明要存储的是Object

如果涉及到了这些转化,都会是性能的消耗,所以我们要尽可能的避免这种情况的出现。

然而如果一开始就初始化如:let arr = [1, 2, 1.5, true],并没有这样的转换问题,所以复杂类型的字面直接初始化是一个好主意,但是更好的主意是:尽量的在数组中存储相同类型的元素。

在Google IO 2012年的v8讲解中有这方面的讲解,清晰易懂,建议查看一下:视频 PPT

上面的说到的东西谈及了一个概念:隐藏类,这个会在后面的部分再细说,在这里只需要知道这个就行了:数组的Fast Elements模式默认是小整数,随着不同类型值的进入,会导致数组类型发生转换,都是通过隐藏类来实现的,而这个转换往往是一种不必要的消耗。

下面的代码注释部分就是在说明隐藏类

var a = new Array();
a[0] = 77;   // integer 无洞
a[1] = 88;
a[2] = 0.5;   // double 无洞
a[3] = true; // Object 无洞

可以看出,有洞、无洞也会是隐藏类的一个标记,所以洞的出现不可避免的会造成性能的下降。

实验

测试预分配与动态分配

var length = 10000000;
function a(){
    var arr = [];
    var i = 0;
    for (i = 0; i < length; i++) {
        arr[i] = i;
    }
}
function b(){
    var arr = new Array(length);
    var i = 0;
    for (i = 0; i < length; i++) {
        arr[i] = i;
    }
}

计算10次运行的平均值,结果依次为 a 和 b,Safari不支持performance.mark,所以都用了new Date()计算:

1000W次运算的结果

  • Chrome: 295.8, 72.3
  • Node.js: 290.4, 65.7 (它也是v8)
  • Firefox: 77.1, 73.1
  • Safari: 53.2, 13.4

100W:

  • Chrome: 24.8, 4.9
  • Node.js: 24.1, 5.5 (它也是v8)
  • Firefox: 6.4, 5.9
  • Safari: 5.7, 1.5

10W及以下几可无视差别。

其实还尝试了小额内存预分配(<64K)且试探性的内存延展(1024),不过从实验结果上来看由于引入了额外的计算,反而时间较内存自动分配有所增加。

逆向赋值的测试

var length = 10000000;
function a(){
    var arr = new Array(length);
    var i = 0;
    for (; i < length; i++) {
        arr[i] = i;
    }
}
function b(){
    var arr = new Array(length);
    var i = length - 1;
    for (; i >= 0; i--) {
        arr[i] = i;
    }
}

测试结果几近相同,b的表现偶尔会变坏一点点,但是不知是不是触发什么bug了,Firefox直接在逆向赋值崩掉了,刷出了61.3, 5782这样令人崩溃的结果。

anyway,不要逆向赋值了!!

开发指引

  1. 使用非负整数作为数组下标,不要使用负数、浮点数、字符串等会被认为是一般性的Object的key
  2. 在数组中存储同一类型的元素
  3. 尽量的避免使用不连续的索引值,而且从0开始
  4. 如果非要存不同类型的元素,那么使用字面直接量初始化而不是一个一个的存入
  5. 预先分配数组大小,这在大多数状况下都有较大的性能提升,可以忽略掉64K的限制,但是小于万量级的话差别几可无视
  6. 不要逆向赋值!!不要逆向赋值!!不要逆向赋值!!
  7. 最好不要随便删除数组元素,这可能会导致转为Dictionary Elements,据说洞变少可能会被v8优化回紧凑结构,但是这是不可依赖的行为
  8. 先赋值,再访问,避免使用 arr[100] == null 或者隐式转换的判断性访问。

对象(隐藏类)

之前说过,js中除了那五种基础类型,其他的都是对象。而js又是个弱类型的语言,为了能够达到优化的目的(还记得与C类语言的特点么?就在前言部分。),即提升读取性能,v8利用动态创建隐藏内部类的方式动态地将属性的内存地址记录在对象内,从而提升整体的属性访问速度。

避免了通过字符串匹配的方式来查找属性值。

隐藏类是为Object服务的,相同结构的Object会共享隐藏类,当结构发生了改变,对应的隐藏类也会发生改变,要么复用,要么新增。

而且会将使用过的隐藏类结构通过内嵌缓存(inline cache)缓存起来,以便复用时可以快速的访问偏移值。

复用的一个最佳例子就是类的使用了:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);
var p2 = new Point(3, 4);  // 复用隐藏类
p2.z = 5;  // p2使用了不同的隐藏类!!很可能触发了一个新增处理

需要注意的是,隐藏类将属性这些东西解析为树,所以顺序不同的初始化会导致隐藏类的解析结果也不同。

所有的Object类型都是这样。

开发指引

  1. 一次性的初始化所有的属性,而不是后续的动态增加
  2. 属性初始化的顺序应当一致,以便保证能够复用隐藏类。
  3. delete 会触发隐藏类的改变,如果是为了内存回收,设置为null是更好的选择

函数

避免触发优化回滚

v8有一个优化行为,针对于热点函数,会使用Crankshaft编译器去乐观且猜测性的生成高效的本地代码,这通常是建立在变量类型不改变的前提下,如果发现类型变化了,那么v8就会使用优化回滚(Deoptimization)机制来回滚到之前的一个没有经过特别优化的代码。

例如:

var counter = 0;
function waitThenGo() {
    counter++;
    if (counter < 10000000) {
        return counter;
    }
    var now = new Date();
    console.log(now);
}

或许目的只是为了等一段时间之后打印当前时间,但是对于v8来说,waitThenGo执行了很多次之后,就可能会触发Crankshaft编译器来生成优化的代码了,它认为已经知道了所有的信息了,例如类型。

但是实际上,没运行到 now 这一行之前,我们都不知道 now 的类型,所以当运行到这一行时,v8就别无选择的只能回滚到一个通用的状态了。

开发指引

  1. 随用随声明是好的,但是如果前面的代码会触发大量的执行return,那么提前声明后面的变量是能够在一定程度上起到优化效果的

[2015-08-24] React Native 浅入门 —— 绘图篇

权当一个笔记,再写写或许更明白点

原本搞了个模拟的,就使用元素来模拟绘图,例如直线……

不过确实是很麻烦…… 不过再去扫了扫 Issues,果然发现有人问过,且 facebook 也有相应的方案:

绘图可使用:ReactART,位置:Libraries/ART/ReactNativeART.js

var ReactART = {
    LinearGradient: LinearGradient,
    RadialGradient: RadialGradient,
    Pattern: Pattern,
    Transform: Transform,
    Path: Path,
    Surface: Surface,
    Group: Group,
    ClippingRectangle: ClippingRectangle,
    Shape: Shape,
    Text: Text,
};

例子可暂时扫扫 https://github.com/reactjs/react-page/blob/art/

继续记笔记:

依赖

使用这个之前,安装 art 库:

npm i art --save

并且需要在项目中添加依赖:

  1. 右键点击项目 -> 'Add Files to ProjectName -> 选择 node_modules/react-native/React/Libraries/ART/ART.xcodeproj'
  2. libART.a 添加到 Linked Frameworks and Libraries

基础元素

  • Surface - 长方形的容器,可渲染的区域,是其他元素的容器!
  • Group - 可容纳形状、文本和其他的分组
  • Shape - 一个向量路径定义的形状,可填充
  • Text - 文本

即一个图形,必须是这样的:最外层是一个 Surface,去定义容器的大小,就像是 Canvas 元素一样,然后其子元素由 Group 进行组装,使用 Shape 和 Text 确定内容

公共的 props

  • x,y: 坐标
  • originX, originY: 原始坐标,这个给 scale 和 rotate 使用的
  • transform: {Array} 变形相关
  • visible: {boolean} 决定了 opacity 的值是 0|1
  • opacity: {number} opacity 的值
  • scale, scaleX, scaleY {number} 缩放
  • rotation: {string} 旋转 0deg

说明:
x, y 每个都可有,它指定了元素的位置。
originX, originY则是一个相对位置,它主要是给 scale 和 rotate 使用的,默认是0, 0,这样就意味着,默认是以起点为中心旋转,或者从起点开始缩放!

Surface

容器,接受的 props:

  • width: {number}
  • height: {number}

Group

定义分组,它是一个 Component,实际上使用了 NativeGroup,可接受的 props:

  • onMouseDown
  • onMouseUp

Shape

形状,与 ART 或者 ReactART 不同的是,当前不支持 width 和 height,可接受的 props:

  • fill: 定义笔刷,有一个用法已废弃,不多述,主要是生成颜色:new Color(colorOrBrush),值可为:
    • {string} 预定义颜色:maroon: '#800000', red: '#ff0000', orange: '#ffA500', yellow: '#ffff00', olive: '#808000', purple: '#800080', fuchsia: "#ff00ff", white: '#ffffff', lime: '#00ff00', green: '#008000', navy: '#000080', blue: '#0000ff', aqua: '#00ffff', teal: '#008080', black: '#000000', silver: '#c0c0c0', gray: '#808080'
    • rgb/rgba 颜色:rgba(0,0,0,0.1)
    • hex #0000FF
    • hsb
    • hsl
  • stroke: 定义颜色,跟 fill 其实用法一样
  • strokeDash: {Object} 画虚线
    • strokeDash.count {number} array 的个数,必须有此值才能生效,必须=array.length
    • strokeDash.array:{Array} 虚线是如何交替绘制
      • 10,10 表示先绘制10个点,再跳过10个点,如此反复
      • 10, 20, 10 先绘制10个点,跳过20个点,绘制10个点,跳过10个点,再绘制20个点,如此反复
  • strokeCap: {string} 设置线条终点形状,可取值:butt(0) , square(2) 有个默认值 round(1)
  • strokeJoin: {string} 设置线条连接点的风格,该属性支持如下三个值 可取值:miter(0), bevel(2) 有个默认值 round(1)
  • strokeWidth: {number} 线宽 默认1

Text

文字

  • path 文字的路径
  • font 字体相关
    • {string} 例如 'normal|bold|italic 13px 字体名称' 可选单位如 px、pt、em 等
    • {Object}
      • font.fontFamily {string} 字体,可以使用逗号分隔
      • fontSize {number}
      • fontWeight {string} bold|normal
      • fontStyle {string} italic|normal

其余几个跟 Shape 一致的

Transform

变形相关

var t = new Transform();
t.transformTo(1, 0, 0, 1, 0, 0)
    .move(props.x || 0, props.y || 0)
    .rotate(props.rotation || 0, props.originX, props.originY)
    .scale(scaleX, scaleY, props.originX, props.originY);

transform 变形矩阵

参数为(xx, yx, xy, yy[, x, y]),即 arguments

 result       current      arguments
xx  xy  x     xx  xy  x     xx  xy  x
yx  yy  y  =  yx  yy  y  *  yx  yy  y
0   0   1     0   0   1     0   0   1

xx - (number) 沿 X 轴的缩放 (default: 1)
yx - (number) 旋转? multiplier to skew the x axis (default: 0)
xy - (number) 旋转? multiplier to skew the y axis (default: 0)
yy - (number) 沿 Y 轴的缩放 (default: 1)
x - (number) 水平移动 (default: 0)
y - (number) 竖直移动 (default: 0)

transformTo

重置矩阵为某个值,其实就是重置 current,跟构造函数参数一致

translate & move & moveTo

translate(x, y)
move(x, y) 水平位移和竖直位移
moveTo(x, y) 移动到

rotate & rotateTo

(deg, x, y)

scale & scaleTo

(x, y)
缩放

resizeTo

resizeTo(width, height)

inversePoint & point

好像是点,不过没确认作用

Path

是在 art/core/path的基础上扩展了一小下的 path,做了一点处理,使用跟 path 一样:

所有的起点都是当前位置,往往是所在 Group 的x, y

基础

move
moveTo
line
lineTo
reset
close
toJSON

高级

  • curve 曲线,三阶贝塞尔曲线,参数为 c1x, c1y, c2x, c2y, ex, ey
    • (c1x, c1y): 第一个控制点坐标
    • (c2x, c2y): 第二个控制点坐标
    • (ex, ey): 曲线终点,取值是相对于起点的距离
  • curveTo 曲线,三阶贝塞尔曲线,跟 curve 唯一的区别是 (ex, ey) 是坐标值
  • arc 弧,参数x, y, rx, ry, outer, counterClockwise, rotation
    • (x, y) 终点坐标举例起点坐标的相对距离
    • (rx, ry) 这个还没搞清楚是啥,会影响弧度
    • outer 外侧圆弧么??
    • counterClockwise 顺时针方向么?
    • rotation 角度???
      arcTo 跟 curve 唯一的区别是 (x, y) 是坐标值
      counterArc
      counterArcTo

Pattern(url, width, height, left, top)

主要是笔刷

ClippingRectangle

其实还是个 Group,不过似乎是有路径规划了还是神马,没确定完呢
props

  • x, y
  • width, height

LinearGradient 和 RadialGradient

似乎是笔刷定义,暂没确认

to be continued

[2015-08-12] React Native 浅入门 —— 交互篇

权当一个笔记,再写写或许更明白点

习惯了前端世界的交互模式之后(其实就是 DOM 事件),在这个入门的过程中感觉 React Native 的交互处理就是个不适应。

如果要做 React Native 的交互,首先至少要知道这样几个东西:

不过干读这几个文档的话,基本就是一头雾水……

还是一点一点来看罢:

普通行为

TouchableHighlight、TouchableOpacity、TouchableWithoutFeedback 这几个很好弄,官方贴心的直接封装了最基础的 Touch 行为,在任何需要点击的 View 外面直接包上这样的标签就行了。

TouchableHighlight 在点击时表现为高亮
TouchableOpacity 在点击时表现为透明
TouchableWithoutFeedback 在点击时无反馈

这几个效果都是封装好了的,无需开发者操心。

Sample Code

<TouchableOpacity
    onPressIn={this._onPressInCircle.bind(this)}
    onPressOut={this._onPressOutCircle.bind(this)}>
    <View style={styles.gridItem}></View>
</TouchableOpacity>

然而这些只适用于按钮系……

支持事件:

  • onPress
  • onPressIn
  • onPressOut
  • onLongPress

支持参数:

  • delayLongPress {number} 单位ms
  • delayPressIn {number} 单位ms
  • delayPressOut {number} 单位ms

再特殊点的行为,例如划过,就不用想用这几个货直接实现了。

Gesture Responder System

中文翻译叫:手势应答系统。

主要就是搞手势识别处理的,那其实也就是复杂点的触摸:例如一边摸一遍动啊,摸着还动出花样画个 L 啥的的那种。

事件

决定是否成为处理器

冒泡的:

  • onStartShouldSetResponder touchStart/mouseDown行为发生,是否当前的元素成为处理器
  • onMoveShouldSetResponder touchMove/mouseMove行为发生,是否当前行为成为处理器

不冒泡/未来不冒泡的:

  • onScrollShouldSetResponder 滚动行为发生了,是否当前的元素成为处理器
  • onSelectionChangeShouldSetResponder 选择行为发生了,是否当前元素成为处理器

是否接管成为处理器(因为冒泡是从最深处开始,可以在父级的元素使用此类方法接管):

  • onStartShouldSetResponderCapture touchStart/mouseDown行为发生,是否当前的元素代替最深层的子元素成为处理器
  • onMoveShouldSetResponderCapture touchStart/mouseDown行为发生,是否当前的元素代替最深层的子元素成为处理器

开始处理了

  • onResponderStart 当前处理开始
  • onResponderGrant 现在正在响应触摸事件
  • onResponderMove 用户正移动他们的手指
  • onResponderEnd 当前处理结束
  • onResponderRelease 在触摸最后被引发,即touchUp

跟拦截相关的(当前应答器的身份)

  • onResponderReject 当前视图的应答器不是“我”了,并且还不释放让我来当。
  • onResponderTerminationRequest 其他的东西想成为应答器。应该释放应答吗?返回 true 就是允许释放
  • onResponderTerminate 应答器已经转交给别人担当了。可能在调用onResponderTerminationRequest 之后被其他视图获取,也可能是被操作系统在没有请求的情况下获取了(发生在 iOS 的 control center/notification center)

以上都是在ResponderEventPlugin.js里面实现的,我们直接使用视图 View 配置

行为生命周期

单个元素的行为生命周期

这个图画的我头晕啊……

几个特性

冒泡

之前说到,有两个东西是冒泡的:

  • onScrollShouldSetResponder
  • onSelectionChangeShouldSetResponder

然则默认是触发最深的那个元素,也就是子级元素,如果父级要拦截作为处理器,则需要处理:

  • onStartShouldSetResponderCapture
  • onMoveShouldSetResponderCapture

这两个事件的触发顺序是从父级开始的,所以如果父级设置了返回 true,则会执行父级的处理。

但是如果任一返回了 false,则依然使用子级元素作为处理器。

不过如果父级的 onStartShouldSetResponder 如果返回 false,干脆不会触发父级的验证,onStartShouldSetResponderCapture 返回 true 也没用,Move 也是同理。

拦截

  • onResponderReject 当前视图的应答器不是“我”了,并且还不释放让我来当。
  • onResponderTerminationRequest 其他的东西想成为应答器。应该释放应答吗?返回 true 就是允许释放
  • onResponderTerminate 应答器已经转交给别人担当了。可能在调用onResponderTerminationRequest 之后被其他视图获取,也可能是被操作系统在没有请求的情况下获取了(发生在 iOS 的 control center/notification center)

话说这个还没搞明白怎么用……

简单用法

直接写属性,作为 prop:

<View onResponderStart={(evt) => true} />

也可以使用...运算符:

class GridItem extends Component {
    get touchProps() {
        return {
            onStartShouldSetResponder: (evt) => true,
            onResponderGrant: (evt) => {
                console.log('child');
                console.log(evt);
            },
            onResponderTerminationRequest: (evt) => true
        };
    }
    render() {
        return (
            <View {...this.touchProps}></View>
        )
    }
}

也可以使用 PanResponder(这个会在实际处理的事件前加个 Pan,输出时又会去掉,而且会增加一个参数 gestureState):

class GridItem extends Component {
    componentWillMount() {
        this._panGesture = PanResponder.create({
            onStartShouldSetResponder: (evt, gestureState) => true,
            onPanResponderGrant: (evt, gestureState) => {
                console.log('child');
                console.log(evt);
                console.log(gestureState);
            },
            onPanResponderTerminationRequest: (evt, gestureState) => true
        });
    }
    render() {
        return (
            <View {...this._panResponder.panHandlers}></View>
        )
    }
}

特殊行为

划走切换的效果

可以参考:http://www.terlici.com/2015/04/06/simle-slide-menu-react-native.html
就是使用 PanResponder + Animation做的。

这个回头我自己再搞个出来。

手势解锁

因为不想用 WebView 做,所以这里都是从纯 React Native 的角度去考虑的。

因为生命周期中,TouchIn 是起点,所以如果在外面按住了划过元素,元素是不会有反应的……

单纯的子级接管作为处理器然后释放也是没用的,如果同时设置 Capture,父级的优先级大……

那么是否可以这样呢?父级判断碰撞,然后释放处理权?但是拦截的判断只在最开始触发的时候能搞,所以似乎还是行不通的。而且都碰撞到了,如果能直接处理子元素不是更简便么?

没那么简单,需要看看这三个方法:

子元素的获取

使用 refs:

<View style={styles.gridView} {...this._panResponder.panHandlers} >
    <View style={styles.gridLine}>
        <GridItem ref="item1" />
        <GridItem ref="item2" />
        <GridItem ref="item3" />
    </View>
    <View style={styles.gridLine}>
        <GridItem ref="item4" />
        <GridItem ref="item5" />
        <GridItem ref="item6" />
    </View>
    <View style={styles.gridLine}>
        <GridItem ref="item7" />
        <GridItem ref="item8" />
        <GridItem ref="item9" />
    </View>
</View>

这样就可以直接通过 this.refs[name] 获取到子元素了。

获取子元素的位置

这是从 React Native 的 Issue 1374 拿到的方法:

var RCTUIManager = require('NativeModules').UIManager;
var view = this.refs[name];
var handle = React.findNodeHandle(view);
RCTUIManager.measure(handle, (x, y, width, height, pageX, pageY) => {
    // x,y 似乎是当前container的坐标
    // width, height 是宽高
    // pageX, pageY 是在屏幕中的坐标(起始坐标)
})

所以,元素在屏幕中的范围是:pageX ~ pageX + width, pageY ~ pageY + height

至少是个简单的正方形,如果是其他形状例如圆形,可能还需要计算圆心和半径的大小。

获取当前 touch 的坐标

之前说过 PanResponder 会给事件方法增加一个参数 gestureState

一个 gestureState 对象有以下属性:

  • stateID:gestureState 的ID-在屏幕上保持至少一个触发动作的时间
  • moveX:最近动态触发的最新的屏幕坐标
  • x0:应答器横向的屏幕坐标
  • y0:应答器纵向的屏幕坐标
  • dx:触发开始后累积的横向动作距离
  • dy:触发开始后累积的纵向动作距离
  • vx:当前手势的横向速度
  • vy:当前手势的纵向速度
  • numberActiveTouch:屏幕上当前触发的数量

那么 touch 位置的坐标可以这么获得:[x0 + dx, y0 + dy]

当然也可以使用都有的 evt 参数:

changedTouches - Array of all touch events that have changed since the last event
identifier - The ID of the touch
locationX - The X position of the touch, relative to the element
locationY - The Y position of the touch, relative to the element
pageX - The X position of the touch, relative to the screen
pageY - The Y position of the touch, relative to the screen
target - The node id of the element receiving the touch event
timestamp - A time identifier for the touch, useful for velocity calculation
touches - Array of all current touches on the screen

直接用[pageX, pageY] 就行了。

这样就可以进行简单的碰撞计算了,计算位置是否在某个子元素的范围内就行了。

实际上至此手势解锁的几个关键问题已经解决,正在写一个手势解锁的组件:k-react-native-swipe-unlock 玩耍。

To Be Continued.

[2015-08-10] React Native 浅入门 —— 结构篇

权当一个笔记,再写写或许更明白点

结构布局

说到布局,首先作为一个前端同学,我想到的是:div、p、span、section ... 以及 article、header、footer...

这些非语义化的&语义化的标签,在前端的世界中,是用于按层级、按语义去实现一个可读的结构。

一个经典的 HTML 结构:

<header>
    <nav>
        <ul>
            <li>首页</li>
            <li>文章列表</li>
        </ul>
    </nav>
</header>
<section id="article-list">
    <h1>文章列表</h1>
    <article>
        <header>
            <hgroup>
                <h1>title</h1>
                <h2>作者</h2>
            </hgroup>
        </header>
        <section>
            <p>第一段</p>
            <p>第二段</p>
        </section>
        <aside>
            <ul>
                <li>tag1</li>
                <li>tag2</li>
            </ul>
        </aside>
    </article>
    ...
</section>
<footer>
    CopyRight
</footer>

写 HTML 的时候,我们往往是直接就设计完了这样的一个结构,一个 big picture,当然我们还是先按大块设计,再去设计细节!不过在设计结构的时候,基本一直都能看到完整的东西。

而在 React Native 中,讲究的是 Component 的复用,是不断的分层/块去编写 Component,但是只有在那个 Component 中才能看到再进一步的结构,因为往往是“你只需要使用我就好了,不需要关心我怎么显示的。”

React 也是一样,不信去看最终 React 展现到浏览器中的样子吧,还是那么一堆 div 标签,干读基本读晕。

话题转回来,我们要这么展现文章列表:

    ...
    <ArticleList />
    ...

所以,我们需要设计个列表页:

class ArticleList extends React.Component {
    ...
    render() {
        return (
            <Text>文章列表</Text>
            <ListView
                dataSource={this.state.datasource}
                renderRow={this._renderArticle />
        );
    }
    _renderArticle(rowData: string, sectionID: number, rowID: number) {
        ...
        return (
            <TouchableHighlight onPress={() => this._pressArticle(rowID)}>
                <Article id="{articleId}" />
            </TouchableHighlight>
        )
    }
}

在这里确定了:我要以 ListView 的方式展现列表,点击的时候,使用 Article 的视图展现内容。

所以还需要写个 Article(在写的时候直接按全屏搞的,导航什么的留给 Navigator 搞定,我才不管。)

class Article extends React.Component {
    render() {
        return (
            <View style="{articleStyles.header}">
                <View styles="{articleStyles.title}">
                    <Text>title</Text>
                </View>
                <ArticleAuthor name="Leo" />
            </View>
            <View style="{articleStyles.content}">
                <Text>第一段</Text>
                <Text>第二段</Text>
            </View>
            <View style="{articleStyles.aside}">
                <ArticleTags />
            </View>
        );
    }
}

这样的过程,导致我们不能对当前的视图有一个像 HTML 那样的总览,看到的永远是块,而不是结构。而且由于 App 的场景专注性是极强的,所以它实际上更多的是 Navigator 式的浏览,而不是像 Web 大多数时候都要展现一个完整的 Nav 放在那里。

所以在文章列表后,点击展现 Article 的时候,我们使用 navigator 提供了一个返回按钮(最多再提供上一篇、下一篇),不需要再提供 Nav 了,而且这些都跟 Article 没啥关系,Article 只关注自己要展现的东西,而且以全屏的方式展现。

事实上很多 Web 站点也开始使用这种方式了。

这样的好处是,独立性很强,复用性也很强。坏处是:如果你“查看元素”,只会吐槽什么鬼东西!

不过思路是 OK 的,不过说实话我更喜欢 webcomponents,这样的结构看起来不是更好么?基本也是一个玩路

webcomponents 理念下的一个 DOM 结构

标签映射

已经存在的基础映射

如图所示:当前 React Native 提供的跟基础 HTML 元素相关的Component(默认0.8-stable)

缺了那么多的标签,让被惯坏了的前端怎么玩……

别的还好,View 和 Text 这两个东西,基本就要冒充很多标签的行为了:

先说Text

在 React Native 的世界中,只有一种文本标记:Text。只要你要显示文字,就必须放在 Text 标签中,没的商量。

所以 Text 对于前端同学来说,就约等于 p、div、h1~h6、section、span 等等标签,先吐会儿……

标题基本就是:

<Text style="{某个styles.title}">标题内容</Text>

段落可能就是:

<Text style="{某个styles.paragraph}">标题内容</Text>

我们还是更细一些,在 Web 的世界中,文本可能为(不会出现在移动领域的就不提了,例如鼠标悬浮的 title):

Text Types

不是简单的一一对应的关系,可能需要不断的组合。并且在 Web 中重新实现 UI 的方式,在这里依然存在,所以有可能对应的就是某个你自行实现的或者引用自外部库的 Component。

在 React Native 中,文本是信息,所以一个 Text 就够了,将文本的组合,或许引入交互,或许引入图片等等 视为 Component,转到组件的世界里去玩,而不是平铺的画板上直接画内容。

再说View

在 React Native 中,View 是最基本的视图:
View is a container that supports layout with flexbox, style, some touch handling, and accessibility controls, and is designed to be nested inside other views and to have 0 to many children of any type

所以其实所有的 视图Component 最终都是一个 View 而已。

所以从基础元素的角度来说:View 当前对于我们来说,约等于 div、section 等等所有跟布局相关的元素。

尼玛要用盒模型?要用 flexbox?用 View 吧,还支持无限嵌套的。同时作为容器,全局的行为神马的都往 View 身上扔吧,我就是新的 div,那个爆炸扩散嵌套的 div!

UI 控件

这时候,来说一下 UI 控件,就是在 Web 届很喜欢搞的东东,先看已经提供了的:
当前 React Native 提供的 ui Component(默认0.8-stable)

当然我们可以把基础的 View 和 Text 砍掉后剩下的东西也视为 ui 控件。

Web 届的 UI 种类如此之多,但我们可以庆幸的是,在 APP 中,解决了一个大问题:控件交互的各异性。APP 中是普适性的 UI 交互,就这一种,都这么用,不用去实现什么3、4、5、6种日历控件神马的了。

在 React Native 中,需要不断的根据需求进行可复用性评估,然后不断的开发出 ui Component、业务 Component、通用 Component 等等,因为我的 APP 就是用这些 Component 拼出来的视图!

而这么多自行定义的 Component,你说你不搞各异性体验的 UI??

还有呢

  1. 怎么画图?Canvas 木有了,怎么搞手势解锁呢?
    直接 View 拼接?直接调用 Native 的组件?还是像 lwansbrough/react-native-canvas 现在占位用 WebView 内嵌个 Canvas?还是自行实现一个 Canvas To UIView 绘图的东西?
    我想要的是一个通用的东西,写一个 SwipeUnlocker 出来,是期望 Web、M 站、APP 上都直接可用的,哪个解决方案能满足呢?

2015-08-24 更新
绘图可使用:ReactART,位置:Libraries/ART/ReactNativeART.js

var ReactART = {
  LinearGradient: LinearGradient,
  RadialGradient: RadialGradient,
  Pattern: Pattern,
  Transform: Transform,
  Path: Path,
  Surface: Surface,
  Group: Group,
  ClippingRectangle: ClippingRectangle,
  Shape: Shape,
  Text: Text,
};

例子可暂时查看 https://github.com/reactjs/react-page/blob/art/

关于绘图,再单独记录一篇笔记罢:[React Native 浅入门 —— 绘图篇]

  1. Form 没有了,不过还有 tcomb-form-native,不过这东西的使用模式需要适应下……

  2. WebView 有多大的性能问题呢?

  3. 当前来说 OC <-> JS 的 Component 映射是不是还是主流模式?

[2017-03-21] JavaScript引擎

JavaScript引擎是能够将JavaScript代码处理并执行的运行环境,不要把它跟浏览器内核搞混了!内核(如Webkit、Trident、Gecko等)主要是将页面转变成可视化的图像结果,即渲染出来,所以通常也被称为渲染引擎。

渲染引擎和JavaScript引擎是浏览器处理网页过程的两个核心部分,它们是相互合作的关系。

一般来说,渲染引擎根据JavaScript提供的桥接接口提供给JavaScript访问DOM的能力,很多HTML5也是通过桥接接口实现的。

桥接接口复杂而不高效往往是一个性能瓶颈,所以才说DOM的频繁使用会导致性能问题。

常见的JavaScript引擎

v8

Google开源的使用C++编写的引擎,有其他语言(如Java)的封装,BSD协议,自体还有其他协议。

Github Wiki

ChromeChromium都是使用v8的,另外Android上的浏览器默认也是使用v8作为js引擎,当然还有Node.js

还有个有趣的: TrifleJS,一个使用 .NET WebBrowser Class 及 v8引擎的IE浏览器。

JavaScriptCore

是WebKit的默认JavaScript引擎,它其实是跟着 Webkit 一起发布的,源码在Source/JavaScript,东家是苹果,BSD/LGPL

它有不少别名: SquirrelFishSquirrelFish Extreme,在Safari中还有NitroNitro Extreme,但其实项目、库的名字始终都是JavaScriptCore。

Code Wiki

Safari使用JavaScriptCore,无论是PC还是M。PhantomJS是基于Webkit搞出来的,也是使用的JavaScriptCore。

iOS的开发能够直接用 JavaScriptCore 的,一家的。

Chakra

微软系的引擎,微软以ChakraCore为名在Github上开放了源代码,MIT协议。

IE9+浏览器及Edge都是使用Chakra,Win10的应用也是使用了它。

SpiderMonkey / OdinMonkey

Mozilla系的就是多,标题取了第一个和最新的一个……

SpiderMonkey,第一款JavaScript引擎,由Brendan Eich在Netscape Communications时编写,用于Mozilla Firefox 1.0~3.0版本。

Rhino,由Mozilla基金会管理,开放源代码,完全以Java编写。

TraceMonkey,基于实时编译的引擎,其中部份代码取自Tamarin引擎,用于Mozilla Firefox 3.5~3.6版本。

JaegerMonkey,德文Jäger原意为猎人,结合追踪和组合码技术大幅提高性能,部分技术借凿了V8、JavaScriptCore、WebKit,用于Mozilla Firefox 4.0以上版本。

IonMonkey,可以对JavaScript编译后的结果进行优化,用于Mozilla Firefox 18.0以上版本。

OdinMonkey,可以对asm.js进行优化,用于Mozilla Firefox 22.0以上版本。

Carakan

自Opera10.50版本开始使用。

其他

JerryScript

三星搞出的一个用于物联网的引擎,可以运行在受限制的设备上,例如微控制器:

只有几 KB RAM 能运行引擎的设备(<64 KB RAM)

只能为代码引擎提供有限 ROM 空间的设备(<200 KB ROM)

该引擎支持设备上编译,提供从 JavaScript 到外设的访问

[2015-08-13] React Native 浅入门 —— 变形、动画篇

权当一个笔记,再写写或许更明白点

本不想这么快就写这个的,不过用到了,顺便记录一下。

变形(2D 或 3D 转换)

支持的东西在变化,请查看 /Libraries/StyleSheet/TransformPropTypes.js 确定当前支持的属性。

查看现有官方文档中 View 的 style 支持,会发现这么几个: rotation scaleX scaleY transformMatrix translateX translateY

但是很不幸滴,如果去看 TransformPropTypes.js 就会发现,这几个已经被标成了:DEPRECATED

甭这么用了,就算现在能用也是一样。

不过官方还给了这么一段:

transform: ReactPropTypes.arrayOf(
    ReactPropTypes.oneOfType([
        ReactPropTypes.shape({perspective: ReactPropTypes.number}),
        ReactPropTypes.shape({rotate: ReactPropTypes.string}),
        ReactPropTypes.shape({rotateX: ReactPropTypes.string}),
        ReactPropTypes.shape({rotateY: ReactPropTypes.string}),
        ReactPropTypes.shape({rotateZ: ReactPropTypes.string}),
        ReactPropTypes.shape({scale: ReactPropTypes.number}),
        ReactPropTypes.shape({scaleX: ReactPropTypes.number}),
        ReactPropTypes.shape({scaleY: ReactPropTypes.number}),
        ReactPropTypes.shape({translateX: ReactPropTypes.number}),
        ReactPropTypes.shape({translateY: ReactPropTypes.number})
    ])
  )

所以实际上已经整体将变形、动画相关的切到了 transform 上。

来看一个简单的例子,看了再对比上面那段,就知道怎么用了:

var styles = StyleSheet.create({
    line: {
        transform: [
            {rotate: '45deg'}  // 旋转45度
        ]
    }
});

这些属性都能在 css3 中找到,所以就分类看下好了。

perspective(0.9.0-stable开始支持)

这个说明来自于:W3school
perspective 属性定义 3D 元素距视图的距离,以像素计。该属性允许您改变 3D 元素查看 3D 元素的视图。
当为元素定义 perspective 属性时,其子元素会获得透视效果,而不是元素本身。
注意:perspective 属性只影响 3D 转换元素。

旋转

值类型为字符串,例如 '9deg',表示9度。

  • rotate:2D旋转,绕着中心顺时针旋转多少度。
  • rotateX, rotateY, rotateZ:3D旋转,分别是绕着 X 轴、Y 轴和 Z 轴。(0.9.0-stable开始支持)

82598b70-37af-11e5-8a72-b8b0d8d47521

缩放

值类型为数字,表示倍数。

  • scale:X、Y 同时缩放
  • scaleX:仅 X 轴方向缩放
  • scaleY:仅 Y 轴方向缩放

移动

值行为数字:单位应该是坐标轴单位

  • translateX:X 轴方向移动
  • translateY:Y 周方向移动

动画

上面的都是设置了 style,达到一个变形的效果,那么能不能让它们动起来呢?

简单的 View,再设置 state 的做法是不行的,就是一个跳跃的效果。

现存的翻译过的文档都没有详细些的内容,这也是因为动画部分官方正在开发中,不过已经放出来了,而且E文的文档已经有更新,就翻译一些过来,顺便做点实验:

Animated 库被设计用来简便的实现风格多样的动画行为,同时又有较高的性能。

Animated 主要是通过声明初始/目标值,并且配置过渡的动画方式,然后通过简单的 start/stop 方法来控制动画的执行。

按官方说法要比设置 state 和 prerender 快很多滴。

支持元素

如例子中的 Animated.Image,默认支持:Image、Text和 View。

不过可以通过 Animated.createAnimatedComponent 方法来创造你想要的 Component。

动画效果(速度算法)

  • sprint:【弹性】简单的单弹簧物理模型,弹性的动画效果,效果符合 Origami
    • friction: 摩擦力…… 默认 7
    • tension: 张力 默认 40
  • decay: 【渐缓】以一个初始速度开始,并逐渐减慢直至停止
    • velocity: 初始速度,必须
    • deceleration: 减速率. 默认 0.997
  • timing: 指定时间长度的平缓动画
    • duration: 时间长度,单位 ms, 默认 500
    • easing: 定义曲线的平缓函数. 可查看 Easing module,其中已预定义了一些函数. iOS 默认是 Easing.inOut(Easing.ease).
    • delay: 延迟多少 ms 执行动画,默认 0

动画的执行(APIs)

执行 start 方法开始动画效果,start 可以接受一个 callback 参数,在动画完成时触发执行。
如果动画正常执行结束,callback 会接收到一个参数 {finished: true},如果是非正常结束,例如被手势或其他动画打断了,finished 则为 false。

一个 Component 展现时自动触发动画

主要是使用 componentDidMount

class SampleAnimation extends Component {
    constructor(props: any) {
        super(props);
        this.state = {
            bounceValue: new Animated.Value(0)
        };
    }
    render(): ReactElement {
        return (
            <Animated.View // 支持: Image, Text, View
                style={
                    {
                        flex: 1,
                        transform: [
                            {scale: this.state.bounceValue}
                        ]
                    }
                }
            />
        );
    }
    componentDidMount() {
        this.state.bounceValue.setValue(1.5);  // 目标值
        Animated.spring(  // 支持: spring, decay, timing,过渡的动画方式
            this.state.bounceValue,
            {
                toValue: 0.8,  // 目标值
                friction: 1 // 动画方式的参数
            }
        ).start();  // 开始
    }
}

动画的组合

可以使用以下方法来进行组合:

  • parallel 并行
  • sequence 顺序执行
  • stagger 交错??
  • delay 延迟执行

这四个方法都接受数组参数,元素内容是动画效果。

如果任何一个动画被 stoped,则其他动画也会终止。
Parallel有一个参数:stopTogether,如果被设置为 false,则不会执行这种终止的效果。

Animated.sequence([            // spring to start and twirl after decay finishes
  Animated.decay(position, {   // coast to a stop
    velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
    deceleration: 0.997,
  }),
  Animated.parallel([          // after decay, in parallel:
    Animated.spring(position, {
      toValue: {x: 0, y: 0}    // return to start
    }),
    Animated.timing(twirl, {   // and twirl
      toValue: 360,
    }),
  ]),
]).start();                    // start the sequence group

改变输入输出值域映射:interpolate

例如:[0, 1]映射[0, 100]为:

value.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 100],
});

同样支持多重值域:

value.interpolate({
    inputRange: [-300, -100, 0, 100, 101],
    outputRange: [300,    0, 1,   0,   0],
});

这意味着:

-300 => 300
-200 150
-100 0
-50 0.5
0 1
50 0.5
100 0
101 0

这几个很容易理解,主要是边界值:
最左侧的[-300, -100]对应着[300, 0],这意味着,这种映射会一直向负数方向延伸下去,因此 -400 => 450
最右侧是[100, 101]对应这[0, 0],这意味着此后的数目都对应0,即 200 => 0

interpolate 支持动画计算函数,很多已经在 Libraries/Animation/Animated/Easing.js 中定义了。

interpolate可配置,默认是 extend,不过 clamp 在防止输出超出值域上同样十分有用。

toValue 可以接受另一个 Animated.Value 作为参数

Animated.events

输入事件,可以让手势或其他事件来直接对应动画的值,这当然需要一个结构化的数据才能支持:
第一层是数组,元素是对象:

scrollX 对应着 event.nativeEvent.contentOffset.x
pan.x and pan.y 对应着 gestureState.dx 和 gestureState.dy

onScroll={Animated.event(
    [{nativeEvent: {contentOffset: {x: scrollX}}}]   // scrollX = e.nativeEvent.contentOffset.x
)}
onPanResponderMove={Animated.event([
    null,                                          // ignore the native event
    {dx: pan.x, dy: pan.y}                         // extract dx and dy from gestureState
]);

spring.stopAnimation(callback) 终止动画

spring.addListener(callback) 增加一个 listener,在动画执行时

LayoutAnimation

允许全局的进行动画的创建和更新,这会在下次的渲染时执行:

var App = React.createClass({
  componentWillMount() {
    // Animate creation
    LayoutAnimation.spring();
  },

  getInitialState() {
    return { w: 100, h: 100 }
  },

  _onPress() {
    // Animate the update
    LayoutAnimation.spring();
    this.setState({w: this.state.w + 15, h: this.state.h + 15})
  },

  render: function() {
    return (
      <View style={styles.container}>
        <View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
        <TouchableOpacity onPress={this._onPress}>
          <View style={styles.button}>
            <Text style={styles.buttonText}>Press me!</Text>
          </View>
        </TouchableOpacity>
      </View>
    );
  }
});

requestAnimationFrame

一般来说用不到这个 来自于 browser 的 API,Animations 基本封装好了。

不过,我还是来个简单的例子吧,一个超级简陋的时钟秒针的动画(这结合了绘图篇的知识):

class Timing extends Component {
    constructor(props) {
        super(props);
        this.state = {
            secondRotation: (new Date()).getSeconds() * 6;
        };

    }

    animate() {
        var secondRotation = (new Date()).getSeconds() * 6;
        this.setState({secondRotation});
        requestAnimationFrame(this.animate.bind(this));
    }

    componentDidMount() {
        requestAnimationFrame(this.animate.bind(this));
    }

    render() {
        return (
            <View>
                <Surface
                    width={300}
                    height={400}
                    style={
                        {backgroundColor: 'blue'}
                    }>
                    <Group x={150} y={150}>
                        <Shape stroke="#000000" strokeWidth={4} d={line} rotation={this.state.secondRotation} />
                    </Group>
                </Surface>
            </View>
        );
    }
}

to be continued.

[2017-03-13] koa的中间件机制原理及async await版本实现

个人还是很喜欢这种机制,也想搞一个这般的 Workflow 辅助类出来,不过不想用 Generator,所以折腾了一小下……

不得不说的 yield *

yield*最大的作用,就是将Generator嵌在另一个Generator的内部执行。

function *a() {
    console.log(1);
    b();
    console.log(3);
}
function *b() {
    console.log(2);
}

执行结果是13,是的,2不会被输出,但是如果换成:

    yield *b();

就等同于:

function *a() {
    console.log(1);
    console.log(2);
    console.log(3);
}

输出就是123就好了。

而这也是koa2的中间件核心实现机制,笑话一下自己最开始搞出来的遍历行为,傻在next上了,囧~~

async/await模式的实现

如果还不明白,那么就看这个:

function *a() {
    console.log(1);
    yield* b();
    console.log(5);
}
function *b() {
    console.log(2);
    yield* c();
    console.log(4);
}
function *c() {
    console.log(3);
}

循环内嵌之后,你只需要遍历a的Iterator就行了,会展开嵌入后面所有的Generator!!

所以核心问题就变成了,如何定义出所需要的方法a而已。

koa的middlewares是一个数组,为了能够得到要访问的方法,其实就是middlewares[0]的封装。我们知道每个async function其实就是一个Generator function,那么我只需要把参数next指向下一个中间件函数,并且确保调用时使用yield*去调用,并且参数绑定ctx和下一个next。

所以我需要的可能是这样的:

let nextc = function* () {
    console.log(3);
}
let nextb = function* () {
    console.log(2);
    yield* nextc();
    console.log(4);
}
let nexta = function* () {
    console.log(1);
    yield * nextb();
    console.log(5);
}

nexta().next();  // 执行,输出 1 2 3 4 5

再换一下,用async/await:

// 注意这里我开始引入 next 了
async function a(next) {
    console.log(1);
    await next();
    console.log(5);
}
async function b(next) {
    console.log(2);
    await next();
    console.log(4);
}
async function c(next) {
    console.log(3);
}

// 然后我们来手动定义next
let nextc = async function () {
    c.call(this);
};

let nextb = async function () {
    b.call(this, nextc);
}

let nexta = async function () {
    a.call(this, nextb);
}
// 执行
nexta();
// 输出了 1 2 3 4 5

发现规律了么……

nexta调用的是函数a,将nextb作为next参数传入,而nextb调用b,将nextc作为参数传入,同理继续。最后一个指向空就行了,啥也不干。

那么:

function compose(middlewares) {
    let i = middlewares.length;
    let next = async function(){};  // noop
    // 循环算出来next
    for (let i = middlewares.length - 1; i >=0; i--) {
        next = getCallback(middlewares[i], next);
    }

    return next;
}

function getCallback(method, next) {
    return async function() {
        method(next);
    };
}

let callback = compose([a, b, c]);

callback();  // 输出1 2 3 4 5

貌似大功告成?还得继续……

中间件是需要传入数据的,简单理解为:一个数据要从第一个中间件进入,最终从第一个中间件的after出来,核心目的是改变这个数据,因此在当前的模式下,最好不要传值,我们来看:

app.use(async (ctx, next) => {
    // do sth with ctx
    await next();
    // do sth with ctx after
});

根据上面的调用模式,如果一直是值引用,那么就要 return 处理后的值,与代码中的await next()不符了,而且使用还会变得麻烦,所以koa选择使用context的概念,传入一个Object的引用,然后一直去修改它。

那么我们接下来要做的事情,其实就是支持 ctx 参数了:

// 定义中间件,这看起来就跟 koa 一样了是吧
async function a(ctx, next) {
    ctx.output.push(1);
    await next();
    ctx.output.push(5);
}
async function b(ctx, next) {
    ctx.output.push(2);
    await next();
    ctx.output.push(4);
}
async function c(ctx, next) {
    ctx.output.push(3);
}

// 定义compose,因为参数的位置变化了,所以下面的执行也要修改一下了
// 最简单的方式,加一个参数
function compose(middlewares, ctx) {
    let i = middlewares.length;
    let next = async function(){};  // noop
    // 循环算出来next
    for (let i = middlewares.length - 1; i >=0; i--) {
        next = getCallback(middlewares[i], ctx, next);
    }

    return next;
}

function getCallback(method, ctx, next) {
    return async function() {
        method(ctx, next);
    };
}

let ctx = {output: []};
let callback = compose([a, b, c], ctx);

await callback();

console.log(ctx.output);  // 输出1 2 3 4 5

能不能更简便些呢?

好吧,反正我还是没看明白 koa 的 compose 结果,怎么被使用的,直接传一个 ctx 作为参数就达到效果了。

我其实怀疑是 koa-convert 搞的鬼,默认都是 Generator,都被转换成 async 了,然后 call 了ctx 和 next。

应该是这样吧……

[2017-03-13] Iterator, Generator 和 Async

Iterator

Iterator为不同的数据结构提供统一的访问机制,只要它部署Iterator接口,就可以完成遍历操作。

它的流程其实是跟链表的遍历很相似的:创建一个指针对象,指向当前数据结构的起始位置,然后不断的next(),每次返回的是一个对象数据,包含value和done两个字段供访问。

for...of循环实际上会去自动找Iterator接口。

ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator这个key上,一个数据结构只要具有Symbol.iterator方法,就可以认为是“可遍历的”。

再次提醒,千万注意:Symbol.iterator里面的i是小写!!!

为啥要用Iterator

跟其他语言一样,提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。

原生支持Iterator的类型

原生的表述数据集的类型:ArrayObjectMapSet

而实际上原生支持Iterator的数据结构是:ArrayMapSet

Object默认不支持,其实非要手写支持的话,还不如直接用Map

字符串也支持,因为它类似于Array

手动支持Iterator

Symbol.iterator是一个函数,返回的是一个遍历器,执行next(),不断的返回包含value和done两个字段的对象数据。

类似于Array的Object

例如arguments,对于这种,其实我们可以直接使用ArrayIterator接口:

let obj = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
};

如之前所述,能否支持遍历,仅是检查Symbol.iterator这个key而已,所以普通的对象也能支持Iterator,单还不如用Set

普通对象部署Iterator

let obj = {
    a: 'a',
    b: 'b',
    [Symbol.iterator]() {
        const self = this;
        const keys = Object.keys(self);  // 举个例子而已
        let i = 0;
        return {
            next() {
                i++;
                return {
                    done: i >= keys.length,
                    value: self[keys[i]]
                };
            }
        }
    }
};

需要注意的是,Iterator的作用是使得数据结构的成员能够按某种次序排列,所以偶尔蛋疼的这么设定,可能仅仅是为了定义一个复杂的访问顺序,但是使用的时候仅仅for...of就好了。

因此也不能强说这么做是不对的。

不过,对于这种,用类不是更好一点么?例如:

class ListIteratorByAge {
    constructor(list) {
        this.list = list;
    }

    [Symbol.iterator]() {
        const self = this;
        const arr = self.list.slice(0).sort((a, b) => a.age >= b.age);
        let i = -1;
        return {
            next() {
                i++;
                return {
                    done: i >= arr.length,
                    value: arr[i]
                };
            }
        }
    }
}

注意这里的Symbol.iterator的定义,能不能有更简便的方法呢:

class ListIteratorByAge {
    constructor(list) {
        this.list = list;
    }
    [Symbol.iterator]() {
        const arr = self.list.slice(0).sort((a, b) => a.age >= b.age);
        return arr[Symbol.iterator]();
    }
}

再次强调,只是在定义某种次序的访问,没规定要怎么访问!!只是在默认支持的数据结构中按顺序访问了而已。

Iterator的使用

一个经典的硬性使用Iterator的示例:

let arr = [1, 4, 2, 10];
let ite = arr[Symbol.iterator]();
let eachResult = ite.next();  // 调用一次访问到第一个元素
while(!eachResult.done) {
    // do sth with eachResult.value
    eachResult = ite.next();
}

其实直接用for...of更简便:

let arr = [1, 4, 2, 10];
for (value of arr) {
    // do sth with value
}

几个默认使用 Iterator 的场景

总结自 ECMAScript6 入门

  1. 解构赋值,例如let [x,y] = set
  2. 扩展运算符,例如['a', ...arr, 'd']
  3. yield*,例如yield* [2,3,4];
  4. 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合都符合条件
    • for...of
    • Array.from()
    • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
    • Promise.all()
    • Promise.race()

Generator

简而言之,结合着上面的Iterator,可以理解Generator实际上是生成了一个可遍历的结果数据集,数据集的元素有哪些,由yieldreturn定义,函数执行的结果就是数据集的Iterator

例如:

function* testGenerator() {
    yield 1;
    yield 2;
    return 3;
}
let ite = testGenerator();

那么不断执行ite.next()并以!ite.next().done为判断条件的执行结果为:1,2,3

next可以传入值,作为执行时上一个yield的执行返回结果:

function* counterGen() {
    let i = 0;
    while(1) {
        let signal = yield i++;
        // console.log('now is ' + i);
        if (signal) {
            i = 0;
        }
    }
}
let counter = counterGen();

counter.next();  // {value: 0, done: false}
...
counter.next(true);  // {value: 0, done: false}  重置了

需要注意的是,在执行next(true)时,counterGen 中那个注释掉的console语句中,i其实是6,后面置的0。
执行的最终结果中,value是0。
也就是说,yield的结果其实是可以被后面的语句给改掉的,或者可以将例子中while语句{}包起来的部分认为是一个函数,最后返回i是最终的结果。
也可以这么理解:执行到下一个yield时,才返回了之前的结果,大概、或许是这样吧……

总结一小下:

  1. 定义时,function 后面跟着一个*
  2. 内部用yield定义一个可被遍历到的元素(状态)
  3. return也会被认为是一个可被遍历到的元素(状态),如果没有return或没跟返回值,value会是undefined
  4. 状态会被后面的同步代码所改变,直到下一次yield之前都可以

不得不说的 yield *

本来不想说这个,感觉要变成基础知识集了,不过研究中间件偏偏遇到了,不得不说……

yield*最大的作用,就是将Generator嵌在另一个Generator的内部执行。

function *a() {
    console.log(1);
    b();
    console.log(3);
}
function *b() {
    console.log(2);
}

执行结果是13,是的,2不会被输出,但是如果换成:

    yield *b();

就等同于:

function *a() {
    console.log(1);
    console.log(2);
    console.log(3);
}

输出就是123就好了。

而这也是koa2的中间件核心实现机制,笑话一下自己搞出来的遍历行为,然后傻在next的调用上了,囧~~

可以查看

Generator更多的用法

建议查看 ECMAScript6 入门

Async

语法糖,是对Generator的改进。

返回的结果是一个Promise

错误机制

只需注意几点就行了。

  1. 返回结果是Promise,所以按Promise的错误处理就行了
  2. async内部的await指定的状态,是同步的,全都成功整体状态才会成功,任何一个失败了,整体也就失败了(后面的不执行了),除非用try...catch包起来或者用.catch处理一下

for await of

let body = '';
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.