绯村月

绯村月 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

绯村月 赞了文章 · 2017-09-18

探讨一下常见支付系统的对外接口

作为一个具备用户交易能力的网站,丰富它的支付渠道对于获客和提高日活都有不可估量的积极作用。算起来,我接触过的支付系统也有几十个了,在这里总结一下我所接触过的支付系统对外接口的设计方案。

1. 支付宝

作为国内最大的支付平台,绝大多数网站都会与其对接,当之无愧是最常见的支付渠道,而很多其它小的支付渠道也是参考支付宝来设计其对外接口的,很具有代表性,其支付流程如下图:

支付宝支付流程

上图的流程中其实还隐藏了很多安全校验的细节,例如与支付宝接口之间的数据加密规则和验签规则,异步回调接口的调用者IP白名单,支付宝订单信息反查及与A站点订单信息比对校验(金额、用户、状态等)。另外,还有一些流程是可选的,例如,同步回调这一步,如果将订单确认的流程加在里面,则有可能影响用户体验,所以在这里订单的确认流程可以自己触发另开线程去跑,或者去除同步回调的确认订单功能,完全依赖异步回调来完成订单。而异步回调里的订单反查也不是必须的,如果你认为白名单和验签规则足够可信的话,不反查也可以。

2. 微信支付

成交量也非常可观的支付渠道,其支付流程也具有一定的代表性,流程如下图:

微信支付流程

相比支付宝,其在初始的下单阶段有一些差异,主要表现在,下单动作是由A站点的服务端调用的,而支付宝则是由前端发起调用的。相比起来,支付宝的下单动作由于是在前端调用的,因此,A站点需要将自己的订单信息返回到客户端,然后又客户端发起调用支付宝的下单接口,这样一来,如果安全、加密等做的不到位,很容易被恶意用户篡改信息。而微信的下单接口是服务端调用的,A站服务器只将获得的微信预付订单号返回给客户端,用来发起调用微信客户端支付接口,这样一来,订单信息详情没有暴露在外,相对更安全一些。

3. 深度合作的积分抵扣模式

有时,一些站点或游戏会与某些第三方深度合作,将第三方的积分接入其中,以一定的比例来抵扣和兑换其中的商品或道具。所谓深度合作,则是第三方会直接给出授权互信接口,允许A站点直接调用扣减某用户的积分,用户只需将其第三方站点的账号与A站点的账号绑定即可。这种模式,大多数情况下只有接口级的调用,没有前端收银台界面,流程图如下:

积分直扣流程

从上图可以看出,这种方式在流程上相对比较简单,只要A站点与第三方做好接口加密、签名等安全措施,商定好结算规则,对接起来相对比较容易。另外,这种对接形式需要第三方支持按照A站点的订单号来反查订单信息,方便对账结算和订单异常的自动化处理。

4. 总结:支付系统对外接口的必备要素

4.1 接口部分

  • 下单接口

  • 同步回调通知:通知的信息需要包含双方订单号,订单金额,用户身份信息,订单状态等

  • 异步回调通知:同“同步回调通知”

  • 交易订单查询接口:支持使用双方任一方的订单号来查询订单信息,因为有时A站点不一定能拿到支付渠道产生的订单号,此时则只能拿A站点自己的订单号来查询支付渠道的支付信息。之所以需要查询,一个是在回调时确保订单信息的有效性和准确性,另一个目的则是防止异常情况下,A站点没有收到支付渠道的回调信息,而用户的确在支付渠道成功支付,为确保A站点用户支付都能及时到账发货,需要增加异步的订单查询跑批。

4.2 安全部分

  • 信息加密:https、AES、非对称加密(公钥私钥)等;

  • 签名规则:对传输的信息整体应用签名规则(sha256等),生成签名字符串,用于防止传输信息被篡改;

  • IP白名单:对于服务器间调用的接口,可以将对方服务器IP加入白名单,防止非法IP调用;

  • 订单信息反查:A站点主动向支付渠道查询订单信息,保证订单信息的有效性和准确性。

以上只是我对之前接触过的支付系统对外接口的总结,可能有一些说的不对或有待完善的地方,如果你有任何问题或建议,可以扫描下方二维码或者微信搜索[phpjiagoushier],关注我的微信公众号[PHP架构],参与互动交流。
phpjiagoushier

查看原文

赞 11 收藏 52 评论 0

绯村月 赞了文章 · 2017-07-27

H5与Native交互之JSBridge技术

做过混合开发的很多人都知道Ionic和PhoneGap之类的框架,这些框架在web基础上包了一层Native,然后通过Bridge技术使得js可以调用视频、位置、音频等功能。本文就是介绍这层Bridge的交互原理,通过阅读本文你可以了解到js与ios及android底层的通讯原理及JSBridge的封装技术及调试方法。

一、原理篇

下面分别介绍IOS和Android与Javascript的底层交互原理

IOS

在讲解原理之前,首先来了解下iOS的UIWebView组件,先来看一下苹果官方的介绍:

You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.

上面的意思是说UIWebView是一个可加载网页的对象,它有浏览记录功能,且对加载的网页内容是可编程的。说白了UIWebView有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。

但需要注意的是,Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件,如果你的APP只考虑支持iOS8及以上版本,那么你就可以使用这个新的浏览器控件了。

原生的UIWebView类提供了下面一些属性和方法

属性:

  • loading:是否处于加载中
  • canGoBack:A Boolean value indicating whether the receiver can move backward. (只读)
  • canGoForward:A Boolean value indicating whether the receiver can move forward. (只读)
  • request:The URL request identifying the location of the content to load. (read-only)

方法:

  • loadData:Sets the main page contents, MIME type, content encoding, and base URL.
  • loadRequest:加载网络内容
  • loadHTMLString:加载本地HTML文件
  • stopLoading:停止加载
  • goBack:后退
  • goForward:前进
  • reload:重新加载
  • stringByEvaluatingJavaScriptFromString:执行一段js脚本,并且返回执行结果

Native(Objective-C或Swift)调用Javascript方法

Native调用Javascript语言,是通过UIWebView组件的stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

从上面代码可以看出它其实就是调用了window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge对native调用就好了,所以在这里可以对native的代码做一个简单的封装:

//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

Javascript调用Native(Objective-C或Swift)方法

反过来,Javascript调用Native,并没有现成的API可以直接拿来用,而是需要间接地通过一些方法来实现。UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:jsbridge://methodName?param1=value1&param2=value2

于是在UIWebView的delegate函数中,我们只要发现是jsbridge://开头的地址,就不进行内容的加载,转而执行相应的调用逻辑。

发起这样一个网络请求有两种方式:1. 通过localtion.href;2. 通过iframe方式;
通过location.href有个问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。

使用iframe方式,以唤起Native APP的分享组件为例,简单的封闭如下:

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
    iframe.remove();
}, 100);

然后Webview就可以拦截这个请求,并且解析出相应的方法和参数。如下代码所示:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query
        
        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }
    
            return false
        } else {
            return true
        }
    }

Android

在android中,native与js的通讯方式与ios类似,ios中的通过schema方式在android中也是支持的。

javascript调用native方式

目前在android中有三种调用native的方式:

1.通过schema方式,使用shouldOverrideUrlLoading方法对url协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native代码。
2.通过在webview页面里直接注入原生js代码方式,使用addJavascriptInterface方法来实现。
在android里实现如下:

class JSInterface {
    @JavascriptInterface //注意这个代码一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");

上面的代码就是在页面的window对象里注入了AndroidJS对象。在js里可以直接调用

alert(AndroidJS.getUserData()) //UserDate

3.使用prompt,console.log,alert方式,这三个方法对js里是属性原生的,在android webview这一层是可以重写这三个方法的。一般我们使用prompt,因为这个在js里使用的不多,用来和native通讯副作用比较少。

class YouzanWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 这里就可以对js的prompt进行处理,通过result返回结果
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

Native调用javascript方式

在android里是使用webview的loadUrl进行调用的,如:

// 调用js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");

二、库的封装

js调用native的封装

上面我们了解了js与native通讯的底层原理,所以我们可以封装一个基础的通讯方法doCall来屏蔽android与ios的差异。

YouzanJsBridge = {
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解决连续调用问题
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();
    
        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }
    
        if (UA.isIOS()) {
            $.each(data, function(key, value) {
                if ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = JSON.stringify(value);
                }
            });
            var url = Args.addParameter('youzanjs://' + functionName, data);
            var iframe = document.createElement('iframe');
            iframe.style.width = '1px';
            iframe.style.height = '1px';
            iframe.style.display = 'none';
            iframe.src = url;
            document.body.appendChild(iframe);
            setTimeout(function() {
                iframe.remove();
            }, 100);
        } else if (UA.isAndroid()) {
            window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
        } else {
            console.error('未获取platform信息,调取api失败');
        }
    }
}

上面android端我们使用了addJavascriptInterface方法来注入一个AndroidJS对象。

项目通用方法抽象

在项目的实践中,我们逐渐抽象出一些通用的方法,这些方法基本上都是可以满足项目的需求。如下所示:

1.getData(datatype, callback, extra) H5从Native APP获取数据

使用场景:H5需要从Native APP获取某些数据的时候,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
callbackFunction回调函数
extraObject传递给Native APP的数据对象

示例代码:

JSBridge.getData('userInfo',function(data) {
    console.log(data);
});

2.putData(datatype, data) H5告诉Native APP一些数据

使用场景:H5告诉Native APP一些数据,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
dataObject{ username: 'zhangsan', age: 20 }传递给Native APP的数据对象

示例代码:

JSBridge.putData('userInfo', {
    username: 'zhangsan',
    age: 20
});

3.gotoWebview(url, page, data) Native APP新开一个Webview窗口,并打开相应网页

参数类型是否必须示例值说明
urlStringhttp://www.youzan.com网页链接地址,一般都只要传递URL参数就可以了
pageStringweb网页page类型,默认为web
dataObject额外参数对象

示例代码:

// 示例1:打开一个网页
JSBridge.gotoWebview('http://www.youzan.com');

// 示例2:打开一个网页,并且传递额外的参数给Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {
    goods_id: 10000,
    title: '这是商品的标题',
    desc: '这是商品的描述'
});

4.gotoNative(page, data) 从H5页面跳转到Native APP的某个原生界面

参数类型是否必须示例值说明
pageStringloginPageNative页面标示符,例如loginPage
dataObject{ username: 'zhangsan', age: 20 }额外参数对象

示例代码:

// 示例1:打开Native APP登录页面
JSBridge.gotoNative('loginPage');

// 示例2:打开Native APP登录页面,并且传递用户名给Native APP
JSBridge.gotoNative('loginPage', {
    username: '张三'
});

5.doAction(action, data) 功能上的一些操作

参数类型是否必须示例值说明
actionStringcopy操作功能类型,例如分享、复制
dataObject{ content: '这是要复制的内容' }额外参数

示例代码:

// 示例1:调用Native APP复制一段文本到剪切板
JSBridge.doAction('copy', {
    content: '这是要复制的内容'
});

// 示例2:调用Native APP的分享组件,分享当前网页到微信
JSBridge.doAction('share', {
    title: '分享标题',
    desc: '分享描述',
    link: 'http://www.youzan.com',
    imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});

三、调试篇

使用Safari进行UIWebView的调试

(1)首先需要打开Safari的调试模式,在Safari的菜单中,选择“Safari”→“Preference”→“Advanced”,勾选上“Show Develop menu in menu bar”选项,如下图所示。
2-1
(2)打开真机或iPhone模拟器的调试模式,在真机或iPhone模拟器中打开设置界面,选择“Safari”→“高级”→“Web检查器”,选择开启即可,如下图所示。
2-2
(3)将真机通过USB连上电脑,或者开启模拟器,Safari的“Develop”菜单下便会多出相应的菜单项,如图所示。

Paste_Image.png

(4)Safari连接上UIWebView之后,我们就可以直接在Safari中直接修改HTML、CSS,以及调试Javascript。

Paste_Image.png

四、参考链接

本文由 @kk @劲风 共同创作,首发于有赞技术博客: http://tech.youzan.com/jsbridge/

查看原文

赞 106 收藏 180 评论 4

绯村月 赞了回答 · 2016-05-31

解决搞不清FastCgi与PHP-fpm之间是个什么样的关系

刚开始对这个问题我也挺纠结的,看了《HTTP权威指南》后,感觉清晰了不少。

首先,CGI是干嘛的?CGI是为了保证web server传递过来的数据是标准格式的,方便CGI程序的编写者。

web server(比如说nginx)只是内容的分发者。比如,如果请求/index.html,那么web server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。好了,如果现在请求的是/index.php,根据配置文件,nginx知道这个不是静态文件,需要去找PHP解析器来处理,那么他会把这个请求简单处理后交给PHP解析器。Nginx会传哪些数据给PHP解析器呢?url要有吧,查询字符串也得有吧,POST数据也要有,HTTP header不能少吧,好的,CGI就是规定要传哪些数据、以什么样的格式传递给后方处理这个请求的协议。仔细想想,你在PHP代码中使用的用户从哪里来的。

当web server收到/index.php这个请求后,会启动对应的CGI程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以规定CGI规定的格式返回处理后的结果,退出进程。web server再把结果返回给浏览器。

好了,CGI是个协议,跟进程什么的没关系。那fastcgi又是什么呢?Fastcgi是用来提高CGI程序性能的。

提高性能,那么CGI程序的性能问题在哪呢?"PHP解析器会解析php.ini文件,初始化执行环境",就是这里了。标准的CGI对每个请求都会执行这些步骤(不闲累啊!启动进程很累的说!),所以处理每个时间的时间会比较长。这明显不合理嘛!那么Fastcgi是怎么做的呢?首先,Fastcgi会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是fastcgi的对进程的管理。

那PHP-FPM又是什么呢?是一个实现了Fastcgi的程序,被PHP官方收了。

大家都知道,PHP的解释器是php-cgi。php-cgi只是个CGI程序,他自己本身只能解析请求,返回结果,不会进程管理(皇上,臣妾真的做不到啊!)所以就出现了一些能够调度php-cgi进程的程序,比如说由lighthttpd分离出来的spawn-fcgi。好了PHP-FPM也是这么个东东,在长时间的发展后,逐渐得到了大家的认可(要知道,前几年大家可是抱怨PHP-FPM稳定性太差的),也越来越流行。

好了,最后来回来你的问题。
网上有的说,fastcgi是一个协议,php-fpm实现了这个协议

对。

有的说,php-fpm是fastcgi进程的管理器,用来管理fastcgi进程的

对。php-fpm的管理对象是php-cgi。但不能说php-fpm是fastcgi进程的管理器,因为前面说了fastcgi是个协议,似乎没有这么个进程存在,就算存在php-fpm也管理不了他(至少目前是)。 有的说,php-fpm是php内核的一个补丁

以前是对的。因为最开始的时候php-fpm没有包含在PHP内核里面,要使用这个功能,需要找到与源码版本相同的php-fpm对内核打补丁,然后再编译。后来PHP内核集成了PHP-FPM之后就方便多了,使用--enalbe-fpm这个编译参数即可。

有的说,修改了php.ini配置文件后,没办法平滑重启,所以就诞生了php-fpm

是的,修改php.ini之后,php-cgi进程的确是没办法平滑重启的。php-fpm对此的处理机制是新的worker用新的配置,已经存在的worker处理完手上的活就可以歇着了,通过这种机制来平滑过度。

还有的说PHP-CGI是PHP自带的FastCGI管理器,那这样的话干吗又弄个php-fpm出

不对。php-cgi只是解释PHP脚本的程序而已。

关注 269 回答 24

绯村月 赞了文章 · 2016-01-26

Reflux系列01:异步操作经验小结

写在前面

在实际项目中,应用往往充斥着大量的异步操作,如ajax请求,定时器等。一旦应用涉及异步操作,代码便会变得复杂起来。在flux体系中,让人困惑的往往有几点:

  1. 异步操作应该在actions还是store中进行?

  2. 异步操作的多个状态,如pending(处理中)、completed(成功)、failed(失败),该如何拆解维护?

  3. 请求参数校验:应该在actions还是store中进行校验?校验的逻辑如何跟业务逻辑本身进行分离?

本文从简单的同步请求讲起,逐个对上面3个问题进行回答。一家之言并非定则,读者可自行判别。

本文适合对reflux有一定了解的读者,如尚无了解,可先行查看 官方文档 。本文所涉及的代码示例,可在 此处下载。

Sync Action:同步操作

同步操作比较简单,没什么好讲的,直接上代码可能更直观。

var Reflux = require('reflux');

var TodoActions = Reflux.createActions({
    addTodo: {sync: true}
});

var state = [];
var TodoStore = Reflux.createStore({
    listenables: [TodoActions],
    onAddTodo: function(text){
        state.push(text);
        this.trigger(state);
    },
    getState: function(){
        return state;
    }
});

TodoStore.listen(function(state){
    console.log('state is: ' + state);    
});
TodoActions.addTodo('起床');
TodoActions.addTodo('吃早餐');
TodoActions.addTodo('上班');

看下运行结果

➜  examples git:(master) ✗ node 01-sync-actions.js
state is: 起床
state is: 起床,吃早餐
state is: 起床,吃早餐,上班

Async Action:在store中处理

下面是个简单的异步操作的例子。这里通过addToServer这个方法来模拟异步请求,并通过isSucc字段来控制请求的状态为成功还是失败

可以看到,这里对前面例子中的state进行了一定的改造,通过state.status来保存请求的状态,包括:

  • pending:请求处理中

  • completed:请求处理成功

  • failed:请求处理失败

var Reflux = require('reflux');

/**
 * @param {String} options.text 
 * @param {Boolean} options.isSucc 是否成功
 * @param {Function} options.callback 异步回调
 * @param {Number} options.delay 异步延迟的时间
 */
var addToServer = function(options){
    var ret = {code: 0, text: options.text, msg: '添加成功 :)'};

    if(!options.isSucc){
        ret = {code: -1, msg: '添加失败!'};
    }
    
    setTimeout(function(){
        options.callback && options.callback(ret);
    }, options.delay);
};


var TodoActions = Reflux.createActions(['addTodo']);

var state = {
    items: [],
    status: ''
};

var TodoStore = Reflux.createStore({

    init: function(){
        state.items.push('睡觉');
    },

    listenables: [TodoActions],

    onAddTodo: function(text, isSucc){
        var that = this;

        state.status = 'pending';
        that.trigger(state);

        addToServer({
            text: text,
            isSucc: isSucc,
            delay: 500,
            callback: function(ret){
                if(ret.code===0){
                    state.status = 'success';
                    state.items.push(text);
                }else{
                    state.status = 'error';
                }
                that.trigger(state);
            }
        });
    },
    getState: function(){
        return state;
    }
});

TodoStore.listen(function(state){
    console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});

TodoActions.addTodo('起床', true);
TodoActions.addTodo('吃早餐', false);
TodoActions.addTodo('上班', true);

看下运行结果:

➜  examples git:(master) ✗ node 02-async-actions-in-store.js 
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: success, current todos is: 睡觉,起床
status is: error, current todos is: 睡觉,起床
status is: success, current todos is: 睡觉,起床,上班

Async Action:在store中处理 潜在的问题

首先,祭出官方flux架构示意图,相信大家对这张图已经很熟悉了。flux架构最大的特点就是单向数据流,它的好处在于 可预测易测试

一旦将异步逻辑引入store,单向数据流被打破,应用的行为相对变得难以预测,同时单元测试的难度也会有所增加。

enter image description here

ps:在大部分情况下,将异步操作放在store里,简单粗暴有效,反而可以节省不少代码,看着也直观。究竟放在actions、store里,笔者是倾向于放在actions里的,读者可自行斟酌。

毕竟,社区对这个事情也还在吵个不停。。。

Async 操作:在actions中处理

还是前面的例子,稍作改造,将异步的逻辑挪到actions里,二话不说上代码。

reflux是比较接地气的flux实现,充分考虑到了异步操作的场景。定义action时,通过asyncResult: true标识:

  1. 操作是异步的。

  2. 异步操作是分状态(生命周期)的,默认的有completedfailed。可以通过children参数自定义请求状态。

  3. 在store里通过类似onAddTodoonAddTodoCompletedonAddTodoFailed对请求的不同的状态进行处理。

var Reflux = require('reflux');

/**
 * @param {String} options.text 
 * @param {Boolean} options.isSucc 是否成功
 * @param {Function} options.callback 异步回调
 * @param {Number} options.delay 异步延迟的时间
 */
var addToServer = function(options){
    var ret = {code: 0, text: options.text, msg: '添加成功 :)'};

    if(!options.isSucc){
        ret = {code: -1, msg: '添加失败!'};
    }
    
    setTimeout(function(){
        options.callback && options.callback(ret);
    }, options.delay);
};


var TodoActions = Reflux.createActions({
    addTodo: {asyncResult: true}
});

TodoActions.addTodo.listen(function(text, isSucc){
    var that = this;
    addToServer({
        text: text,
        isSucc: isSucc,
        delay: 500,
        callback: function(ret){
            if(ret.code===0){
                that.completed(ret);
            }else{
                that.failed(ret);
            }
        }
    });
});


var state = {
    items: [],
    status: ''
};

var TodoStore = Reflux.createStore({

    init: function(){
        state.items.push('睡觉');
    },

    listenables: [TodoActions],

    onAddTodo: function(text, isSucc){
        var that = this;

        state.status = 'pending';
        this.trigger(state);
    },

    onAddTodoCompleted: function(ret){
        state.status = 'success';
        state.items.push(ret.text);
        this.trigger(state);
    },

    onAddTodoFailed: function(ret){
        state.status = 'error';
        this.trigger(state);
    },

    getState: function(){
        return state;
    }
});

TodoStore.listen(function(state){
    console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});

TodoActions.addTodo('起床', true);
TodoActions.addTodo('吃早餐', false);
TodoActions.addTodo('上班', true);

运行,看程序输出

➜  examples git:(master) ✗ node 03-async-actions-in-action.js 
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: success, current todos is: 睡觉,起床
status is: error, current todos is: 睡觉,起床
status is: success, current todos is: 睡觉,起床,上班

Async Action:参数校验

前面已经示范了如何在actions里进行异步请求,接下来简单演示下异步请求的前置步骤:参数校验。

预期中的流程是:

流程1:参数校验 --> 校验通过 --> 请求处理中 --> 请求处理成功(失败)
流程2:参数校验 --> 校验不通过 --> 请求处理失败

预期之外:store.onAddTodo 触发

直接对上一小节的代码进行调整。首先判断传入的text参数是否是字符串,如果不是,直接进入错误处理。

var Reflux = require('reflux');

/**
 * @param {String} options.text 
 * @param {Boolean} options.isSucc 是否成功
 * @param {Function} options.callback 异步回调
 * @param {Number} options.delay 异步延迟的时间
 */
var addToServer = function(options){
    var ret = {code: 0, text: options.text, msg: '添加成功 :)'};

    if(!options.isSucc){
        ret = {code: -1, msg: '添加失败!'};
    }
    
    setTimeout(function(){
        options.callback && options.callback(ret);
    }, options.delay);
};


var TodoActions = Reflux.createActions({
    addTodo: {asyncResult: true}
});

TodoActions.addTodo.listen(function(text, isSucc){
    var that = this;

    if(typeof text !== 'string'){
        that.failed({ret: 999, text: text, msg: '非法参数!'});
        return;
    }

    addToServer({
        text: text,
        isSucc: isSucc,
        delay: 500,
        callback: function(ret){
            if(ret.code===0){
                that.completed(ret);
            }else{
                that.failed(ret);
            }
        }
    });
});


var state = {
    items: [],
    status: ''
};

var TodoStore = Reflux.createStore({

    init: function(){
        state.items.push('睡觉');
    },

    listenables: [TodoActions],

    onAddTodo: function(text, isSucc){
        var that = this;

        state.status = 'pending';
        this.trigger(state);
    },

    onAddTodoCompleted: function(ret){
        state.status = 'success';
        state.items.push(ret.text);
        this.trigger(state);
    },

    onAddTodoFailed: function(ret){
        state.status = 'error';
        this.trigger(state);
    },

    getState: function(){
        return state;
    }
});

TodoStore.listen(function(state){
    console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});

// 非法参数
TodoActions.addTodo(true, true);

运行看看效果。这里发现一个问题,尽管参数校验不通过,但store.onAddTodo 还是被触发了,于是打印出了status is: pending, current todos is: 睡觉

而按照我们的预期,store.onAddTodo是不应该触发的。

➜  examples git:(master) ✗ node 04-invalid-params.js 
status is: pending, current todos is: 睡觉
status is: error, current todos is: 睡觉

shouldEmit 阻止store.onAddTodo触发

好在reflux里也考虑到了这样的场景,于是我们可以通过shouldEmit来阻止store.onAddTodo被触发。关于这个配置参数的使用,可参考文档

看修改后的代码

var Reflux = require('reflux');

/**
 * @param {String} options.text 
 * @param {Boolean} options.isSucc 是否成功
 * @param {Function} options.callback 异步回调
 * @param {Number} options.delay 异步延迟的时间
 */
var addToServer = function(options){
    var ret = {code: 0, text: options.text, msg: '添加成功 :)'};

    if(!options.isSucc){
        ret = {code: -1, msg: '添加失败!'};
    }
    
    setTimeout(function(){
        options.callback && options.callback(ret);
    }, options.delay);
};


var TodoActions = Reflux.createActions({
    addTodo: {asyncResult: true}
});

TodoActions.addTodo.shouldEmit = function(text, isSucc){
    if(typeof text !== 'string'){
        this.failed({ret: 999, text: text, msg: '非法参数!'});
        return false;
    }
    return true;
};

TodoActions.addTodo.listen(function(text, isSucc){
    var that = this;

    addToServer({
        text: text,
        isSucc: isSucc,
        delay: 500,
        callback: function(ret){
            if(ret.code===0){
                that.completed(ret);
            }else{
                that.failed(ret);
            }
        }
    });
});


var state = {
    items: [],
    status: ''
};

var TodoStore = Reflux.createStore({

    init: function(){
        state.items.push('睡觉');
    },

    listenables: [TodoActions],

    onAddTodo: function(text, isSucc){
        var that = this;

        state.status = 'pending';
        this.trigger(state);
    },

    onAddTodoCompleted: function(ret){
        state.status = 'success';
        state.items.push(ret.text);
        this.trigger(state);
    },

    onAddTodoFailed: function(ret){
        state.status = 'error';
        this.trigger(state);
    },

    getState: function(){
        return state;
    }
});

TodoStore.listen(function(state){
    console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});

// 非法参数
TodoActions.addTodo(true, true);
setTimeout(function(){
    TodoActions.addTodo('起床', true);
}, 100)

再次运行看看效果。通过对比可以看到,当shouldEmit返回false,就达到了之前预期的效果。

➜  examples git:(master) ✗ node 05-invalid-params-shouldEmit.js 
status is: error, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: success, current todos is: 睡觉,起床

写在后面

flux的实现细节存在不少争议,而针对文中例子,reflux的设计比较灵活,同样是使用reflux,也可以有多种实现方式,具体全看判断取舍。

最后,欢迎交流。

查看原文

赞 3 收藏 8 评论 1

绯村月 赞了文章 · 2016-01-18

中文 React Router

React/Router

文档已废弃,关闭更新,详情请往 这里:

React Router 一个针对React而设计的路由解决方案、可以友好的帮你解决React components 到URl之间的同步映射关系。

概览

在阐明React Router可以帮你解决的问题之前我们来举一个没有引用React Router 的简单例子。

没使用 React Router

var About = React.createClass({
  render: function () {
    return <h2>About</h2>;
  }
});

var Inbox = React.createClass({
  render: function () {
    return <h2>Inbox</h2>;
  }
});

var Home = React.createClass({
  render: function () {
    return <h2>Home</h2>;
  }
});

var App = React.createClass({
  render () {
    var Child;
    switch (this.props.route) {
      case 'about': Child = About; break;
      case 'inbox': Child = Inbox; break;
      default:      Child = Home;
    }

    return (
      <div>
        <h1>App</h1>
        <Child/>
      </div>
    )
  }
});

function render () {
  var route = window.location.hash.substr(1);
  React.render(<App route={route} />, document.body);
}

window.addEventListener('hashchange', render);
render(); // render initially

在hash值改变的时候,App 将会根据this.props.route 值的改变来动态渲染 <Child/> component。
这样子的做法看起来很直接,但是这也会让整个应用程序变得更加复杂。
我们可以想想,如果组件 Inbox 有一些内嵌的子组件需要根据 例如 inbox/message/:id 或者 inbox/unread 等这样的路由规则做动态渲染的时候。我们需要一些更加智能的手段来把路由信息传递给我们的App,这样Inbox 组件可以根据URL的映射关系来控制哪些子组件应该需要被渲染。我们的很多组件应该根据URL的规则来做动态渲染。在不使用路由规则的前提下,复杂一点的路由需求就需要我们写很多条件判断的代码去去解决实RL和层级组件的同步问题。

引入路由

解决复杂的URL和层级组件之间的映射关系式React Router 的核心。我们使用声明式的方式为我们举的例子引入路由。我们使用JSX的方式来进行路由的配置,这样我们可以通过属性的方式来配置页面视图的层级关系。
先来看看路由的配置

var Router = require('react-router');
var Route = Router.Route;

// declare our routes and their hierarchy
var routes = (
  <Route handler={App}>
    <Route path="about" handler={About}/>
    <Route path="inbox" handler={Inbox}/>
  </Route>
);

我们删除掉一些在组件内判断路由逻辑的代码。然后用<RouteHandle/> 替换 <Child/>.然后代码变成下面这个样子。

var RouteHandler = Router.RouteHandler;

var App = React.createClass({
  render () {
    return (
      <div>
        <h1>App</h1>
        <RouteHandler/>
      </div>
    )
  }
});

最后我们需要监听url的变化来动态渲染应用,加入下面的代码。

  Router.run(routes, Router.HashLocation, (Root) => {
  React.render(<Root/>, document.body);
});

Root 是 React Router 路由匹配后决定渲染的最高层级的组件,告诉 RouterHandle 应该渲染的内容是什么。
<Router/> 组件是不会被渲染的。只是一个创建内部路由规则的配置对象。

接下来我们为应用添加更多的UI组件
现在我们计划给Inbox UI 添加Inbox message 子组件。首先我们需要添加一个新的Message组件。然后我们在原有的inbox路由下面为 Message 组件添加新的路由,这样就可以得到嵌套的UI。

var Message = React.createClass({
  render () {
    return <h3>Message</h3>;
  }
});

var routes = (
  <Route handler={App}>
    <Route path="about" handler={About}/>
    <Route path="inbox" handler={Inbox}>
      <Route path="messages/:id" handler={Message}/>
    </Route>
  </Route>
);

现在我们访问 inbox/message/jKei3c32c的URL就可以匹配到新的路由规则并可以匹配到App->Inbox->Message 这个分支下的UI。

获取url的参数
我们需要获取到一些Url的信息,这样我们可以根据这些参数从服务器端获取数据。我们把交给<Route/>匹配好的组件称为RouterHandler. RouterHandler 实例可以获取到一些非常有用的属性当你渲染组件的时候。特别是一些从URL动态获取的参数信息。比如在我们举例中得 :id

var Message = React.createClass({
  componentDidMount: function () {
    // from the path `/inbox/messages/:id`
    var id = this.props.params.id;
    fetchMessage(id, function (err, message) {
      this.setState({ message: message });
    })
  },
  // ...
});

嵌套的UI和多层级的URLs是 不需要耦合的。
有了React Router,我们不需要用嵌套UI的方式来对应多层级的URL。反过来,获取嵌套组件的UI,我们也不需要有多层级的URL与它对应。

比如说我们有/about/company 这样的URL,我们不需要嵌套UI组件到About组件中。

var routes = (
  <Route handler={App}>
    <Route path="about" handler={About}/>
    <Route path="about/company" handler={Company}/>
  </Route>
);

虽然说我们的URL是有层级嵌套的,但是我们UI组件中得 About 组件和 Company 组件却可以是相邻展平在同级目录的。

现在让我们往路由中添加url /archive/messages/:id 然后让该路由嵌套到inbox UI里面,即使 这个URL不跟上层 Router 的URL 嵌套。我们需要做三件事让匹配下面规则的路由正常工作。

1、url 要以 / 这样的绝对路径开头,这代表不会从父路由继承路由规则。
2、嵌套在Inbox route 中的router 会导致UI组件的层级嵌套。
3、确定你已经有必需的动态URL片段,在这里我们只有 :id ,所以处理起来相当简单。

var routes = (
  <Route handler={App}>
    <Route path="inbox" handler={Inbox}>
      <Route path="messages/:id" handler={Message}/>
      <Route path="/archive/messages/:id" handler={Message}/>
    </Route>
  </Route>
);

这就是React Router的核心,应用的UI组件是层层嵌套的。现在我们可以让这些嵌套的UI组件和URL规则保持同步了。

Route 配置

DefaultRoute

一个RefaultRoute 是一个已匹配父组件会默认展示的子组件。
你期望在没有子组件被匹配的时候一个子RouterHandler总是能够渲染到页面。

Props
handle
RouterHandler 是你需要渲染到页面的匹配规则的组件
name (可选)
当你使用linking 和 transitioning 的路由名字

举例

<Route path="/" handler={App}>

  <!--
    When the url is `/`, this route will be active. In other
    words, `Home` will be the `<RouteHandler/>` in `App`.
  -->
  <DefaultRoute handler={Home}/>

  <Route name="about" handler={About}/>
  <Route name="users" handler={Users}>
    <Route name="user" handler={User} path="/user/:id"/>

    <!-- when the url is `/users`, this will be active -->
    <DefaultRoute name="users-index" handler={UsersIndex}/>

  </Route>
</Route>

NotFoundRoute

NotFoundRoute 会在父组件匹配成功但没有一个同级组件被匹配的时候会被激活。
你可以使用它来处理不合法的链接。

提示
NotFoundRoute不是针对当资源没有被找到而设计的。路由没有匹配到特定的URL和通过一个合法的URL没有查找到资源是有却别的。url course/123 是一个合法的url并能够匹配到对应的路由,所以它是找到了的意思。但是通过123 去匹配资源的时候却没有找到,这个时候我们并不像跳转到一个新的路由,我们可以设置不同的状态来选软不同的UI组件,而不是通过NotFoundRoute 来解决。

props

handler
RouterHandler 是你需要渲染到页面的匹配规则的组件

举例

<Route path="/" handler={App}>
  <Route name="course" path="course/:courseId" handler={Course}>
    <Route name="course-dashboard" path="dashboard" handler={Dashboard}/>

    <!-- ie: `/course/123/foo` -->
    <NotFoundRoute handler={CourseRouteNotFound} />
  </Route>

  <!-- ie: `/flkjasdf` -->
  <NotFoundRoute handler={NotFound} />
</Route>

最后一个 NotFoundRoute 将会渲染到 APP 内。 第一个将会被渲染到Course 内。

Redirect

Recirect 可以跳转到另外一个路由中。

props

from
你想开始redirect的地址,包括一些动态的地址。默认为* ,这样任何匹配不到路由规则的情况多回被重定向到另外一个地方。
to
你想要重定向到得路由名字。
params
默认情况下,这些参数将会自动传递到新的路由,你也可以指定他们,特别是你不需要的时候。
query
params一样

举例

<!--
  lets say we want to change from `/profile/123` to `/about/123`
  and redirect `/get-in-touch` to `/contact`
-->
<Route handler={App}>
  <Route name="contact" handler={Contact}/>
  <Route name="about-user" path="about/:userId" handler={UserProfile}/>
  <Route name="course" path="course/:courseId">
    <Route name="course-dashboard" path="dashboard" handler={Dashboard}/>
    <Route name="course-assignments" path="assignments" handler={Assignments}/>
  </Route>

  <!-- `/get-in-touch` -> `/contact` -->
  <Redirect from="get-in-touch" to="contact" />

  <!-- `/profile/123` -> `/about/123` -->
  <Redirect from="profile/:userId" to="about-user" />

  <!-- `/profile/me` -> `/about-user/123` -->
  <Redirect from="profile/me" to="about-user" params={{userId: SESSION.USER_ID}} />
</Route>

<Redirect> 能够被放置到路由的任何层级。如果你选择把它放在路由某一层级的下方,那么from路径也会匹配到它上层的路径。

Route

Route 用于声明式地映射路由规则到你多层嵌套的应用组件。
props
name(可选)
name 在路由中是唯一的,被使用在 Link 组件和路由转换的方法中。
path(optional)
在url中使用的路径,如果不填写的话,路径就是name,如果name也没有的话,默认就是 /.
handler
当路由被匹配的时候会被 RouteHander 渲染的组件。
children
路由是可以嵌套的,如果子路由的路径被匹配,那么父路由也处于激活状态。

ignoreScrollBehavior
当路由或者路由的params 改变的时候,路由会根据scrollBehavior 来调整页面滚动条的位置。但是 你也可以不选择这项功能,特别是在一些搜索页面或者是 tab切换的页面。

Top-Level

Router.create

创建一个新的路由。

Signature
Router.create(options)

Options
routes

location

scrollBehavior

onAbort
Used server-side to know when a route was redirected.

Method
run(callback)
启动路由,和Router.run 一样

举例

// the more common API is
Router.run(routes, Router.HistoryLocation, callback);

// which is just a shortcut for
var router = Router.create({
  routes: routes,
  location: Router.HistoryLocation
});

router.run(callback);

Router.run

The main API into react router. It runs your routes, matching them against a location, and then calls back with the next state for you to render.

signature

Router.run(routes,[location,],callback)
参数
routes
location (可选)
默认值是Router.HashLocation 如果你设置了Location 那么它的改变会被监听。如果你设置了一个字符路劲,那么路由会立即匹配并执行回调函数。

举例

// Defaults to `Router.HashLocation`
// callback is called whenever the hash changes
Router.run(routes, callback);

// HTML5 History
// callback is called when history events happen
Router.run(routes, Router.HistoryLocation, callback);

// Server rendering
// callback is called once, immediately.
Router.run(routes, '/some/path', callback);

callback(Root,state)
回调函数接收两个参数
1、 Root
2、 state

Root
是一个包含了所有匹配组件的一个组件,它用来渲染你的组件。
state
一个包含了匹配状态的对象。
state.path
带有查询参数的当前URL
state.action
一个触发路由改变的操作
state.pathname
不带查询参数的URL
state.params
当前被激活路由匹配路径对应的参数 如 /:id 对应的id值.

state.query
当前被激活路由匹配路径对应的查询参数
state.routes
包含了匹配路由的数组,在组件渲染之前获取数据会显得很有帮助。
可以查看 exampleasync-data

举例

基本用法

javascript
Router.run(routes, function (Root) {
// whenever the url changes, this callback is called again
React.render(<Root/>, document.body);
});

var resolveHash = require('when/keys').all;

var SampleHandler = React.createClass({
statics: {

// this is going to be called in the `run` callback
fetchData: function (params) {
  return fetchStuff(params);
}

},
// ...
});

Router.run(routes, Router.HistoryLocation, function (Root, state) {

// create the promises hash
var promises = state.routes.filter(function (route) {

// gather up the handlers that have a static `fetchData` method
return route.handler.fetchData;

}).reduce(function (promises, route) {

// reduce to a hash of `key:promise`
promises[route.name] = route.handler.fetchData(state.params);
return promises;

}, {});

resolveHash(promises).then(function (data) {

// wait until we have data to render, the old screen stays up until
// we render
React.render(<Root data={data}/>, document.body);

});
});

something.serve(function (req, res) {
Router.run(routes, req.path, function (Root, state) {

// could fetch data like in the previous example
fetchData(state.matches).then(function (data) {
  var html = React.renderToString(<Root data={data} />);
  res.send(html);
});

});
});


#Components

##Link
用于在应用程序中导航的一种主要方式。``Link``将会渲染出标签属性href 变得容易被理解。
当``Link``定位的路由被激活的时候自动 显示为 定义的 ``activeClassName`` 和/或者
``activeStyle`` 定义的样式。

**Props**
``to``
要被定位到的路由名字,或者是完整的路径

``params``

包含了名字/值的对象,和路由地址的动态段对应一致。

``query``

一个包含名字/值 的对象,它会成为地址中的查询参数

**举例**

// given a route config like this
<Route name="user" path="/users/:userId"/>

// create a link with this
<Link to="user" params={{userId: "123"}}/>

// though, if your user properties match up to the dynamic segements:
<Link to="user" params={user}/>


``query``

一个包装成javascript对象的字符串查询参数

``activeClassName``

当路由被激活是 ``Link`` 接收的 className,默认值为 ``active``

``activeStyle ``

当路由被激活是链接元素 展示的style样式。

``onClick``

对点击时间的常规处理,仅仅在标签``<a>`` 上起效。调用 ``e.preventDefault`` 或者是返回false 将会阻止阻止事件发射。通过 ``e.stopPropagation()`` 将会阻止时间冒泡。

**others**

你也可以在<a>上传递 props,例如 title,id , className 等。

**举例**

提供一个形式像 ``<Router name="user" path="/user/:userid" />:`` 这样的路由

<Link to="user" params={{userId: user.id}} query={{foo: bar}}>{user.name}</Link>
<!-- becomes one of these depending on your router and if the route is
active -->
Michael
Michael

<!-- or if you have the full url already, you can just pass that in -->
<Link to="/users/123?foo=bar">{user.name}</Link>

<!-- change the activeClassName -->
<Link activeClassName="current" to="user" params={{userId: user.id}}>{user.name}</Link>

<!-- change style when link is active -->
<Link style={{color: 'white'}} activeStyle={{color: 'red'}} to="user" params={{userId: user.id}} query={{foo: bar}}>{user.name}</Link>


##Root
React router 创建的应用顶层组件。

**举例**

Router.run(routes, (Root) => {
React.render(<Root/>, document.body);
});

**说明**
当前路由的实例和 ``Root`` 一致。

var MyRouter = Router.create({ routes });

MyRouter.run((Root) => {
Root === MyRouter; // true
});

当前这仅仅是一个实现的细节,我们会逐步将它设计成一个公共的API。


##Route Handler
用户定义的一个组件,作为传递给``Routes`` 的一个 ``handler`` 属性。 路由会在你通过 ``RouterHandler`` 渲染组件的时候给你注入一些属性值。同时在路由转换的时候调用一些生命周期的静态方法。

**注入的属性**

``params``

url 中的动态段。

``query``

url中的查询参数

``path``

完整的url 路劲

**举例**

// given a route like this:
<Route path="/course/:courseId/students" handler={Students}/>

// and a url like this:
"/course/123/students?sort=name"

var Students = React.createClass({
render () {

this.props.params.courseId; // "123"
this.props.query.sort; // "name"
this.props.path; // "/course/123/students?sort=name"
// ...

}
});


**静态的生命周期方法**

你可以定义一些在路由转换时会调用的静态方法到你的路由handler 对应的组件中。

``willTransitionTo(transition,params,query,callback)``

当一个handler 将要被渲染的时候被调用。为你提供了中断或者是重定向的机会。你可以在异步调用的时候暂停转换,在完成之后可以调用``callback(error)``  方法。或者在参数列表中省略callback。

``willTranstionFrom(transition,component,callback)``

当一个被激活路由将要跳出的时候给你提供了中断跳出的方法。``component`` 是当前的组件。你可能需要检查一下 state 的状态来决定是否需要跳出。

**关于 ``callback`` 的参数**

 如果你在参数列表中添加了callback,你需要在最后的时候调用它,即使你使用的是重定向。

**举例**

var Settings = React.createClass({
statics: {

willTransitionTo: function (transition, params, query, callback) {
  auth.isLoggedIn((isLoggedIn) => {
    transition.abort();
    callback();
  });
},

willTransitionFrom: function (transition, component) {
  if (component.formHasUnsavedData()) {
    if (!confirm('You have unsaved information,'+
                 'are you sure you want to leave this page?')) {
      transition.abort();
    }
  }
}

}

//...
});

查看原文

赞 14 收藏 134 评论 5

绯村月 赞了回答 · 2015-10-12

解决谁能用简单明白的一句话总结下执行上下文是什么意思

有时候是这样的,译者在翻译书的的时候不知道拿什么来对应相应的英文,就根据自己的感觉,好像大概是这个意思来强加给广大读者,让很多人不知所以然。

@原磨豆浆 的大意是对的,上下文的原意是 context, 作用域的原意是scope, 这两个不是一个东西。

每一个函数的调用(function invocation) 都有对应的scopecontext.

scope 指的是 函数被调用的时候, 各个变量的作用区域
context 指的是 current scope and its enclosing scope. 就是当前scope 和包裹它外面的scope. 如果一个变量在当前scope没找到,那么它会自底向上继续找enclosing scope 直到找到为止。很像javascript 的prototype那样的找法。经常在javascript中,函数被调用的时候, 查看this 指向哪个object, 那么那个object 就是当前的 "上下文"。
图片描述
还有就是要知道JavaScript中 函数被调用有四种方式:

  • The Method Invocation Pattern
  • The Function Invocation Pattern
  • The Constructor Invocation Pattern
  • The Apply Invocation Pattern

为什么要区分这些,因为每一种中的this指向的对象是不同的。

以最后一个为例解释一下context

// 一个函数, 自己执行的时候没意义,因为它不知道 this 是谁
function foo() { this.saySomething('good') };

obj1 = { saySomething: function(x) { console.log('im from obj1' + x); } }

obj2 = { saySomething: function(x) { console.log('im from obj2' + x); } }

foo.call(obj1); // 把this 绑定给 obj1, context 就是 obj1
foo.call(obj2);// 把this 绑定给 obj2, context 就是  obj2

后面还有一些语言上的小坑比如 函数里面的函数(closure) 里 默认的 this指向的是global, 而不是当前的object。 所以你经常会看到 var that = this 或者 var self = this的 小技巧 这就是保存context, 以便后面可以引用。

推荐书目:

关注 31 回答 12

绯村月 赞了文章 · 2015-06-02

How to choose template engine

前言

后端渲染页面的开发方式目前还占据着主导的地位,虽然像Backbone、Angular、Reactjs等技术很火,但是在国内的普及和应用还远远不够。单页,前端渲染页面的方式想要成为主流还有很长的路要走。在我接触的语言中,后端开发的模式基本以MVC为主,V当然所谓的View了,如Java的Jsp、Velocity、Freemark,再如Node的Ejs、Jade、Handlebars等等,可以说不同语言对应很多的模板引擎,那么我们该如何选择模板引擎呢?我从实际开发的维护性来谈几点。

Layouts

使用layouts可以抽出我们页头的公共代码,使我们只需关注单个页面的内容。一般的Html页面都是这样的

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>title</title>       
    <link rel="stylesheet" href="css/bootstrap.css"/>
    <link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<div class="container" id="container"></div>
</body>
</html>

如果不使用layouts,每个页面我们都得复制一遍上面的代码,然后改变body里的内容,如果有十个页面我们就得复制十遍,更别说复杂的应用了页面都是几十往上。想想这样的情景:你维护着没使用layouts的应用,大概一百左右的页面吧,有一天需要在header里加一个meta标签...别说做了,想想我就想吐了。使用了layouts维护起来就像这样的

#default.xx
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>title</title>       
    <link rel="stylesheet" href="css/bootstrap.css"/>
    <link rel="stylesheet" href="css/style.css"/>
</head>
<body>
    {body}
</body>
</html>

#index.xx
<div class="container" id="container"></div>

Partials

partials和layouts的核心都是抽出相同的代码,便于维护。比如页面有一个公共的footer结构,98%的页面需要这个结构,剩下的页面不需要。不使用partials的话,就看看哪些页面需要,然后后一个个复制粘贴,好不容易复制完了,设计跑来说:不好意思哈,刚footer里一个标签使用错了,把span改成div。丫的还得一个个去改,累死累活完成了,设计又跑过来了:那啥,还是改回span吧。估计那会儿不管是谁,心里都会默念草泥马。大把的时间都浪费在复制粘贴反复修改上面了。使用了partials,需要footer的页面就引一下,就像这样

#footer.xx
<div>footer</div>

#index.xx
<div>content</div>
{include footer}

这个时候footer里随便改啥都轻松搞定,因为只需要维护一份代码

Block

Block可以理解为占位符,例如上面的layouts default.xx只有一份代码,title是固定的,那么渲染出来每个页面的title都是相同的。实际上不同的页面title是不同的,那么怎么办?有人会说了,title通过后端传递值。我个人觉得页面级的内容最好不要丢到后端去维护,会造成后端代码的冗余,不能为了渲染而渲染。这个时候我们就需要Block了。

#default.xx
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>{title}</title>     
    <link rel="stylesheet" href="css/bootstrap.css"/>
    <link rel="stylesheet" href="css/style.css"/>
</head>
<body>
    {body}
</body>
</html>

#page1.xx
{define title 'page1'}
<div>page1 content</div>

#page2.xx
{define title 'page2'}
<div>page2 content</div>

其实就是占个坑,然后在具体的页面里指定内容。

Marco

在模板中调用工具方法,举一个最常见的例子,日期格式化,如果在模板中使用不了工具方法。那么我们就得在后端的逻辑代码中先格式化时间,虽然能抽出一个公共的方法调用,但是我还是觉得不能为了渲染而渲染,将这些渲染逻辑丢到业务中。如果模板支持宏,拿上面的例子来说就像这样

#index.xx
{format time 'YYYY-MM-DD'}
<div>page1 content</div>

Set

前面四个内容我觉得是一个模板引擎的标配,随便少了哪样,都会使维护性变得更差,除非有其他类似的替代方案。至于现在的set其实就是可以在模板中声明变量,并可以进行简单的逻辑判断,变量声明这个功能很多模板是不支持的。那么有什么具体的作用呢?拿上面的footer的例子来说,虽然只有2%的页面不需要footer,我们仍然没办法将footer放进layouts中,因为放进layouts中意味着所有的页面都出现footer,当然了你可以说服你的产品经理或者交互工程师,要求所有的页面的都添加footer。可惜这样完美的情况是少数的,我们还得在98%的页面中添加下面一段代码

{include footer}

如果哪天footer改名了,你知道怎么做了吧...如果模板引擎支持变量声明和逻辑判断,那么可以试着这么干

#default.xx
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>{title}</title>     
    <link rel="stylesheet" href="css/bootstrap.css"/>
    <link rel="stylesheet" href="css/style.css"/>
</head>
<body>
    {body}
    {if !noFooter}
        {include footer}
    {endif}
</body>
</html>

#sb.xx
{set noFooter=true}
<div>劳资就不要footer</div>

顿时妈妈再也不用担心文件改名了。

小结

撇开性能暂且不谈,无论什么样的模板引擎,都应该大大简化我们的工作。如果使用了模板引擎,对页面管理和代码组织毫无用处,那离死也就不远了!

查看原文

赞 3 收藏 7 评论 10

绯村月 赞了文章 · 2015-05-26

一起来实现co(Promise化的4.X版)

前言

大名鼎鼎的co正是TJ大神基于ES6的一些新特性开发的异步流程控制库,基于它所开发的koa更是被视为未来主流的web框架。之前在论坛也看了不少大神们关于co源码的分析,不过co在升级为4.X版本时,代码进行了一次颇有规模的重构,从先前的基于thunkify函数,改变成了现在的基于Promise,并且可能在未来版本移除对thunkify函数的支持。正好小弟最近也在看TJ大神的源码,也来分享一下co(4.X)的实现思路以及源码注解。

进入正题

以下是两段co(4.X)的经典使用示例:

jsco(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});
jsco(function *(){
  // resolve multiple promises in parallel 
  var a = Promise.resolve(1);
  var b = Promise.resolve(2);
  var c = Promise.resolve(3);
  var res = yield [a, b, c];
  console.log(res);
  // => [1, 2, 3] 
}).catch(onerror);

再结合co的文档,我们可以把co的主要功能分为:

  • 异步流程控制,依次执行generator函数内的每个位于yield后的Promise对象,并在Promise的状态改变后,把其将要传递给reslove函数的结果传递给reject函数的错误返回出来,可供外部来进行传递值等操作,这些Promise是串行执行的。
  • yield后是Promise对象的数组属性值是Promise对象的对象,则返回出结构相同的Promise执行结果数组/对象,并且这些Promise是并行执行的。
  • co自身的返回值也是一个Promise对象,可供继续使用。

好了,我们要实现以上这几个功能,其实就是解决以下几个问题:

  1. 确保每一个yield动作的串行执行,并正确的返回异步结果。
  2. 若在yield后的是Promise数组属性值为Promise对象的对象,则并行执行这些Promise。
  3. 检查每一个yield后的对象是否符合要求,若不,则尝试进行转换。
  4. 最后整体返回一个Promise。

我们来逐个击破。

解决 1、4

以下是co源码中的解决 1、4 的方案:

jsfunction co(gen) {
  var ctx = this;

  /**
   * 我们看到,co函数整个的返回值便是一个Promise实例,包装了
   * 传递的generator函数内所有Promise的执行,
   * 我们暂且称它为"外壳Promise"吧,而在传递进来的generator函数内的所有Promise我们都先称为“内部Promise”,
   * 用以区分。
   */
  return new Promise(function(resolve, reject) {

    //判断传递给co的参数,若是一个generator函数,则执行之,得到一个generator对象
    if (typeof gen === 'function') gen = gen.call(ctx);
    //此时gen对象还不是一个generator对象的话,
    //则调用“外壳Promise”的resolve,直接结束整个“外壳Promise”,把参数值作为结果传出
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    //入口,这个函数的详细分析请往下看两行。。
    onFulfilled();

    //这个onFulfilled函数主要有两个用途:
    //第一个用途就是上面的入口函数的功能,将generator执行到第一个yield处开启第一个异步调用
    //第二个用途便是当作所有”内部Promise“的resolve方法,处理异步结果,并继续调用下一个Promise
    function onFulfilled(res) {
      var ret;
      try {
        //当作为”内部Promise“的resolve方法时,res参数自然便是本次Promise的执行结果了
        //利用generator函数的特性,调用next方法时的参数,会当做yield的返回值,这样我们就做到了将异步的结果返回出来
        ret = gen.next(res);
      } catch (e) {
        //若报错,则直接调用"外壳Promise"的reject方法,直接结束整个“外壳Promise”,把错误对象作为结果传出
        return reject(e);
      }

      //将generator.next的执行结果传入next函数,实现串行调用。关于next函数的分析也请往下看。。
      next(ret);
    }

    //这个onRejected函数的用途自然便是当作"内部Promise"的reject方法啦
    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    //这个next函数便是用来执行串行调用
    function next(ret) {
      //如果“内部Promise”,全部执行完毕,done的值便已经是generator函数中的return出的值了,
      //把这个结果传递给“外壳Promise”的resolve函数,暴露给处理整个co结果的后续调用
      if (ret.done) return resolve(ret.value);

      //若“内部Promise”尚未执行完毕,那么确保ret.value是一个Promise对象,并进而调用它
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

好,到这儿我们已经把co4.X的核心串行调用的实现过程给搞定了。整个的执行流程可以归结为:

  • 进入“外壳Promise”
  • 通过入口onFulfilled(),取得第一个Promise对象
  • 将每一个“内部Promise”通过then(onFulfilled, onRejected)开始执行,并将resolve函数都封装为onFulfilled(),reject函数都封装为onRejected()
  • 通过在onFulfilled()onRejected()内调用next方法,实现在一个异步调用结果返回后继续执行下一个,通过将结果作为参数传递给next方法,实现将异步结果返回给外部,依次往复。
  • 所有内部Promise执行完毕,将最后结果暴露给外壳Promise的处理函数,结束

解决 2、3

在TJ大神的源码中,2其实伴随在3(即上面代码中的toPromise方法)中解决的,我们来看一下:

js/**
 * 主要任务便是将传入的参数对象转换为Promise对象
 */
function toPromise(obj) {
  //确保obj有意义
  if (!obj) return obj;
  //如果已经是Promise对象,则直接把obj返回出去
  if (isPromise(obj)) return obj;
  //若是generator函数或现成的generator对象,则直接把obj作为参数传入co函数,并把这个co函数
  //返回出来的"外壳Promise"作为return出来的Promise
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  //若obj是函数,则直接视为符合thunk规范的函数(thunk函数是啥这里就不细说了哈。。),直接转换
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  //若是Promise数组,则调用arrayToPromise方法
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  //若是属性值是Promise对象的对象,则调用objectToPromise方法
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

5个if其实就是一堆的判断并执行响应转换咯(废话。。),看来已经解决了问题3,问题2的精髓自然就是arrayToPromiseobjectToPromise,好,我们来攻克这最后一道壁垒:

先是arrayToPromise

js/**
 * 利用ES6规范中的Promise.all方法,充当全部结果的返回者
 * 利用Array.map方法,实现了并行操作,分别对数组中的每一个元素递归执行toPromise方法,把这些子Promise接着
 * 返回co中来获取执行结果,最后等待这些子Promise全部得到结果后,Promise.all执行成功,
 * 返回执行结果数组
 * 这实现,请收下我的膝盖。。。
 */
function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}

接着是objectToPromise

js/**
 * 与数组略有不同,利用for循环来实现并行的异步调用,
 * Promise.all()仅充当一个类计数器,并返回最终结果
 */
function objectToPromise(obj){
  //results是将用于返回的对象,使用和obj相同的构造函数
  var results = new obj.constructor();
  //Object.keys方法用于返回对象的所有的属性名
  var keys = Object.keys(obj);
  //寄存所有对象属性的Promise的数组
  var promises = [];

  //利用for循环来实现异步
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    //确保obj[key]为Promise对象,然后调用defer推入promises数组等待执行,否则直接将结果返回给result[key]
    var promise = toPromise.call(this, obj[key]);
    if (promise && isPromise(promise)) defer(promise, key);
    else results[key] = obj[key];
  }

  //传入的是promise.then()返回的空Promise,所以此处Promise.all仅充当一个计数器,确保所有异步操作的resolve操作中对results对象的属性都赋值完毕后,返回最终的results对象
  return Promise.all(promises).then(function () {
    return results;
  });

  //执行异步操作,并在操作结果赋值给results[key]
  function defer(promise, key) {
    results[key] = undefined;
    promises.push(promise.then(function (res) {
      results[key] = res;
    }));
  }
}

好了,到这我们已经把co(4.X)的核心源码实现都搞定,它的庐山真面目就是这样啦~

如有任何不正确之处,欢迎指出^ ^

参考

查看原文

赞 7 收藏 37 评论 1

绯村月 赞了文章 · 2015-05-26

Node.js 不深也不浅得了解下编码

背景:目前,在开发基于微信的Web App应用,也就是借助微信所有资源,如公众号,账号系统和扫描JS-JDK等。后端是用node做中间件,依赖API服务(坑爹的是,API服务是用base64保存图片...)。现需实现一功能:用户选择图片,然后调用微信JS-JDK API,再上传至微信服务器(不能直接从微信里发送选择的图片),最后重微信服务器下载下来保存至我们自己服务器里。(下载下来后,需要转成data URIs)

关键代码:

controller.action('image', function * (next) {
  ......

  token = yield base.getAccessToken(); // 获取access_token
  url = resource.genFetchImage(token, mediaId); // 组合请求图片的链接

  response = yield request.get(url); // 通过co-request向微信服务器发出请求

  // 处理响应,编码成base64
  type = response.headers["content-type"];
  prefix = "data:" + type + ";base64,";
  base64 = new Buffer(response.body, 'binary').toString('base64');

  this.body = prefix + base64;
  yield next;
});

最后的响应结果也是有模有样的,看起来也很像data URIs,坑爹的这货是假的,假的。。。Google,百度,啥啥的都找了一遍,还是没结果(我寻找问题答案时,时常定位不准。。。),有以下答案的:

方案一:

// 别说了,网上的十有八九就是我上面那种方式!

方案二:

...
base64 = new Buffer(response.body).toString('base64');

方案三:

...
base64 = response.body.toString('base64');

方案四:

...
base64 = new Buffer(response.body, 'utf8').toString('base64');

一一试了个遍,结果千奇百怪,千万个草泥马奔腾啊!!!都不行,后来还是解决了,才有这文章。其实原理很简单,只是对编码的理解不够而已。(小白我前端一枚,目前在较为深入学习node,想走全栈,反正路还很长呢!!)

正确方案:

...
// 通过co-request向微信服务器发出请求
response = yield request.get({
  url: url,
  encoding: null // 指定编码
});

response = response.body.toString('base64');

重点在request.get的参数上,{ encoding: null },我会慢慢讲解下去(大神勿喷!!)

--------------------------分割线--------------------------

在这里会涉及几个关键知识

  • Buffer对象
  • 字符编码,uf8,binary和base64等
  • co-request和request NPM包

Buffer对象

Buffer是一个像Array的对象,但它主要用于操作字节,也就是二进制数据,它的元素为16进制的两位数,即0到255的数值。(欲想较为深入了解,看《深入浅出nodejs》)

关键API,具体参考new Bufferbuf.toString

new Buffer(str[, encoding]) 

buf.toString([encoding][, start][, end])

字符编码,ascii, utf8,binary和base64等

字符编码,简单讲就是将我们显示器看到的字符编码成计算机识别的位(bit),比如:

  • 小写字母 a 通过ascii编码成 0110 0001,十六进制表示成 0x61,占8位,1字节
  • 中文 通过utf8 编码成 1110 0110 1000 1000 1001 0001,十六进制表示成 0xE68891,占24位,3字节

这里有几个关键点:

  • utf8编码是常用的字符编码,它向下兼容ascii编码。并不是所有的 byte串 都能成功解码成人们能识别的 chat串,它是有解码算法(参考wiki),所以我们像���\u001d�)u�m\u001f�\u001a�͸��E这样常见的乱码是由于解码出错造成的。
  • 对于不能识别的byte串会解码成重点这货竟然有相应的utf8编码,编码为0xFFFD。这里有个关键点,很多byte串是无法正确解码的,但他们都会用表示,而字符又只有一种编码,所以对二进制数据如:图片,视频等,通过utf8编码并保存到变量后,是无法通过utf8原样解码成原来二进制的。
  • binary编码,也就是二进制编码,通常通过consle打印,为了“好看”会打印成16进制。
  • Base64是一种基于64个可打印字符来表示二进制数据的表示方法。对于我通常会用于将图片转换成data URLs,为了减少请求,或充分利用localStorage等。

co-request和request NPM包

request是非常非常强大的模拟浏览器发送HTTP请求的模块,非常非常强大!!而co-request,是通过TJ大神写的co模块简单对request包装了下,实现 yield + promise 优雅实现异步控制流,摆脱倒金字塔的利器!!

--------------------------分割线--------------------------

好,基础知识就差不多了,回到我之前遇到的问题上,并对其讲解下:

  response = yield request.get(url); // 通过co-request向微信服务器发出请求

  // 处理响应,编码成base64
  type = response.headers["content-type"];
  prefix = "data:" + type + ";base64,";
  base64 = new Buffer(response.body, 'binary').toString('base64');

为什么这段代码有问题??对于request方法它有一个关键的参数encoding,默认值是utf8,所以response.body的值已经乱码的字符,再所以new Buffer(response.body, 'binary')将其转换成二进制时,已经不是原来的二进制了,这解释了为什么“最后的响应结果也是有模有样的,看起来也很像data URIs,坑爹的这货是假的,假的。。。”。而通过将encoding设为null,也就不对原始数据编码,保持原始的二进制图片数据。

网上的大多数的方案一,其实是没有错的!!!错在我遇到的错误和解决方案不拉边,它适用于通过[fs.createReadStream(path[, options])](https://nodejs.org/api/all.ht... 读取本地图片,并转换成base64编码。

而方案四我觉得特别有趣

...
base64 = new Buffer(response.body, 'utf8').toString('base64');

我想,response.body竟然是utf8编码的,那我可以通过new Buffer(response.body, 'utf8'),将其反编码成二进制数据(也就是binary),然后再转换成base64,结果试了不行!!!最后痛苦的想了一遍,才发现自己脑短路了,也许这问题对很对大神来说很明显错误,可我还是觉得有趣,不懂的可以仔细想想。

到了最后了,我想答案很明显了,原理很简单,无非原始的二进制数据才是转换base64的正确数据。。。其它的错误都是“白忙活”惹的祸!!

小白之手,大神勿喷,欢迎意见,及时添正!

查看原文

赞 24 收藏 31 评论 10

绯村月 赞了回答 · 2015-04-17

lua 函数条件与返回的区别是什么?

有一小点点区别,我这里先说一下查看Lua(我指的是其官方实现)汇编的方法吧。
luac -l src.lua就可以在stdout上打印出src.lua生成的Lua VM字节码,并且是已经反汇编好了的。有了这个小技巧,我们就可以开始之后的讨论了。
我把你的这两个函数分别改名为dum1()dum2(),顺带帮你写了个dum3(),程序内容和Lua反汇编结果如下:

 ~/ cat -n test.lua
     1  function dum1(t)
     2          if t then return 1 end
     3          return 0
     4  end
     5
     6  function dum2(t)
     7          if t then return 1 else return 0 end
     8  end
     9
    10  function dum3(t)
    11          return t and 1 or 0
    12  end
    13      
 ~/ luac -l test.lua

main <test.lua:0,0> (7 instructions at 0x7f85f1405a00)
0+ params, 2 slots, 1 upvalue, 0 locals, 3 constants, 3 functions
        1       [4]     CLOSURE         0 0     ; 0x7f85f1405c40
        2       [1]     SETTABUP        0 -1 0  ; _ENV "dum1"
        3       [8]     CLOSURE         0 1     ; 0x7f85f1405fe0
        4       [6]     SETTABUP        0 -2 0  ; _ENV "dum2"
        5       [12]    CLOSURE         0 2     ; 0x7f85f14060e0
        6       [10]    SETTABUP        0 -3 0  ; _ENV "dum3"
        7       [12]    RETURN          0 1

function <test.lua:1,4> (7 instructions at 0x7f85f1405c40)
1 param, 2 slots, 0 upvalues, 1 local, 2 constants, 0 functions
        1       [2]     TEST            0 0
        2       [2]     JMP             0 2     ; to 5
        3       [2]     LOADK           1 -1    ; 1
        4       [2]     RETURN          1 2
        5       [3]     LOADK           1 -2    ; 0
        6       [3]     RETURN          1 2
        7       [4]     RETURN          0 1

function <test.lua:6,8> (8 instructions at 0x7f85f1405fe0)
1 param, 2 slots, 0 upvalues, 1 local, 2 constants, 0 functions
        1       [7]     TEST            0 0
        2       [7]     JMP             0 3     ; to 6
        3       [7]     LOADK           1 -1    ; 1
        4       [7]     RETURN          1 2
        5       [7]     JMP             0 2     ; to 8
        6       [7]     LOADK           1 -2    ; 0
        7       [7]     RETURN          1 2
        8       [8]     RETURN          0 1

function <test.lua:10,12> (8 instructions at 0x7f85f14060e0)
1 param, 2 slots, 0 upvalues, 1 local, 2 constants, 0 functions
        1       [11]    TEST            0 0
        2       [11]    JMP             0 3     ; to 6
        3       [11]    LOADK           1 -1    ; 1
        4       [11]    TEST            1 1
        5       [11]    JMP             0 1     ; to 7
        6       [11]    LOADK           1 -2    ; 0
        7       [11]    RETURN          1 2
        8       [12]    RETURN          0 1

从实际的运行效果来说,这3个版本的dum()都是一样的(如果t不是false或者nil就返回1,否则返回0),但是实际运行起来略有一小点差别:dum3()会比前两个版本要多执行一个判断(因为Lua编译器没做什么优化,所以那个1还是要判断一下的)。dum1()dum2()之间虽然后者比前者多编译出了一条JMP语句,但是由于这条JMP语句是在RETURN之后的,所以并不会影响程序实际的运行流程。
总结起来就是,这3个版本的dum()语义没有差别,运行的结果也一样,但是编译出来的语句略有差别:其中dum1()dum2()运行流程一致,dum3()会多对常数1进行判断。

关注 1 回答 1

认证与成就

  • 获得 27 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-11-26
个人主页被 594 人浏览