标签存档: JavaScript

推开窗户看世界 — Objective-C之外

引子

App Store 如今正风靡世界,许多人都想尝试去自己开发一个 iOS App,不过苹果官方推荐使用的是 Objective-C 这个语言。而 Objective-C 与 C、Java 之类的语言风格差异有些大,短时间并不容易掌握,又或者许多人只是想尝试开发一个可以运行在 iOS 上的程序而已,不想再去额外去学一门新的语言,那这个时候就可以考虑另外的一些技术来开发 iOS App。

在移动开发领域,得益于现在手机的性能越来越好,浏览器的功能越来越强大,JavaScript 也成为一门很流行的程序开发语言,而对应到 iOS 平台上,也有多种使用 JavaScript 来开发 iOS App 的技术。这次要介绍的三种技术之中,就有两种使用了 JavaScript。

另外,许多开发跨平台移动应用开发的解决方案,首选要支持的就是 iOS,因此在 iOS 这个平台上可以看到很多种不使用 Objective-C 去开发 iOS App 的技术,而且其中技术所编写出来的程序,除了可以在 iOS 上运行之外,还可以运行在 Android 以及其他系统平台上。能少写一份代码,何乐而不为呢~

不使用 Objective-C

这次要介绍的是三个不使用 Objective-C 来开发 iOS App 的解决方案:

这三个解决方案各有特点,它们的运行机制也有所不同,至于在开发 iOS App 时选用何种方案,可以根据每种方案不同的特点去选择适合自己的。

Titanium Mobile

Titanium Mobile 是 AppCelerator 推出的一个基于 JavaScript 的跨平台移动应用开发技术。通过 Titanium Mobile,可以直接使用 JavaScript 开发能运行于 iOS 和 Android 平台的应用程序,而代码只需要写一次。

Titanium Mobile 是通过将 JavaScript 代码映射到对应平台的 Native Code,在 Titanium Mobile 中操作某一个 UI 对象,或者是其他对象时,实时上是在操作对应平台中实际的 UI 对象,例如使用 Ti.UI.createView() 创建一个视图,它在 iOS 中运行时会实际对应到 UIView,使用 Ti.UI.createTabGroup() 就会对应到 iOS 中的 UITabBarController。这样做的好处就是,可以直接使用 JavaScript 来创建出原生的 UI,而不需要使用额外的代码来让程序 UI 更像系统的 UI。

另外,AppCelerator 在收购了 Apatna 之后,推出了适用于 Titanium 的 IDE Titanium Studio,通过使用 Titanium Studio,可以很方便的创建项目、带智能提示的编辑器,以及很方便的调试项目。

在 Titanium Studio 中调试 iOS 项目时,可以做到单步调试,也就意味着,可以拥有不弱于使用 Xcode 开发项目的体验。断点、单步、变量查看功能一应具全。如果需要发布到 App Store,也可以很方便的通过图形化界面来打包。

当然,使用 Titanium Studio 也需要配合 Xcode 来使用,必须要在安装了 Xcode 的情况下,才可以使用 Titanium Studio 来调试和测试项目。

在 Titanium Studio 推出之前,Titanium Mobile 是靠一个 Developer Tool 来进行打包等操作的,相对比较繁琐,而有了 Titanium Studio 之后,让一切变得简单,我也是从那时开始关注 Titanium Mobile。

对于前端开发工程师来说,只需要理解了 iOS 开发中的一些概念,就可以使用 Titanium Mobile 来开发一个像模像样的 iOS App。并且 Titanium Mobile 实现了 CommonJS 规范,可以很方便来模块化程序代码。

PhoneGap

PhoneGap 是一个使用 HTML+CSS+JavaScript 来开发移动 App 的解决方案,使用它来开发 App 只需要有 Web 开发基础即可。它在今年10月份被 Adobe 收购了,然后加入到 Apache Software Foundation 进行孵化。

目前 PhoneGap 已经支持了市面上大多数的智能手机平台,其中就有 iOS。

在我看来,PhoneGap 其实只是提供了一个运行于各种智能手机平台的浏览器的壳,通过这个壳,PhoneGap 的 JavaScript 库可以和系统进行沟通,从而实现在 Web 页面中与系统交互的功能。

因为 PhoneGap 只提供了系统功能的 API 调用,而没有提供任何和界面相关的 API,那么界面就只能自己来折腾了。

幸好开源的世界是强大的,已经有了一堆这样的界面库来供我们选择,像 jQTouchjQuery MobileSencha Touch 之类,都提供了类似于 iOS 的界面组件,可以让我们省去许多界面上的编码工作。

使用浏览器来运行 App 的一个坏处就是:慢。因为整个 App 就是一个网页,如果编码不当,整个程序使用起来体验会比较差,和原生的应用会有很大区别。

在上面提到的几个 UI 框架之中,我比较喜欢的是 jQTouch,它只提供了基本 UI 框架和一些视图切换效果,做出来的程序是一个单页面 App,在整个程序的反应速度上会感觉比较好。不过 jQTouch 也缺少一些常用的组件像对话框,而这些在 jQuery Mobile 和 Sencha Touch 之中又有提供。选择使用哪个框架就要看个人喜好了,它们的网站都提供了在线预览功能

其实,如果用不到系统的 API,只是想把自己的程序包装成一个很像 App 的 App 的话,只要用一用 PhoneGap 提供的壳就行了,它的 API 几乎可以不用看,配合一些 UI 框架可以快速的产出一个 iOS App 来。

当然了,PhoneGap 最大的好处就是,几乎跨了几乎所有的主流智能手机平台,这对于初创团队来说,是迅速推出各个平台客户端的一个好方法。另外 PhoneGap 官方网站也提供了一系列配套服务,例如使用 PhoneGap Build 可以直接编译各个平台的程序安装包,而不需要开发者自己在本地配置每个平台的编译环境。

Mono Touch

Mono Touch 是一个使用 C# 来编写跨平台应用的框架,同样支持了 iOS 和 Android 两大平台。

Mono Touch 对于 .NET 程序员来说应该是一个好消息,除了调试,其他的都可以在 Windows 中搞定,调试的时候可以通过在虚拟机里运行 Mac OS X 来解决。Mono Touch 因为是基于 Mono 的,也有个配套的 IDE 可以用:Mono Develop。因为都是 .NET,当然也可以用 Vistual Studio 这个更强大的工具了。

与 Titanium Mobile 和 PhoneGap 不同的是,Mono Touch 如果要编译到设备,或者发布到 App Store 的话,是需要收费的。

另外,因为 Mono Touch 也是编译成可执行代码再部署到设备上的,因此运行速度相对于 PhoneGap 所制作的应用来说,应该会快上一些。

不过 Mono Touch 要收费,并且收费还不是很便宜,可能会影响到它的普及率。

小结

在上面讲的三个解决方案之外,还有其他好多大大小小的移动应用开发解决方案,但是用于 iOS 上的解决方案,主要也就是映射代码、或者是包装的形式了,对于应用而不是游戏类型来说,一般都是够用的,在具体选择的时候,也可以根据自己更为熟悉哪种语言,或者框架来挑选。

如果你发现了其他有意思的移动应用开发技术,也可以和我分享一下 :)

参考资料

  1. http://www.appcelerator.com/products/titanium-mobile-application-development
  2. http://developer.appcelerator.com/apidoc/mobile/latest
  3. http://phonegap.com/about
  4. https://build.phonegap.com
  5. http://www.mono-project.com/Main_Page
  6. http://xamarin.com/monotouch
  7. https://github.com/xamarin/monotouch-samples
  8. http://blog.zhaojie.me/2010/09/develop-ios-app-with-monotouch-in-visual-studio-1.html

— EOF —

使用 node.js + nginx 建设网站

昨天搞定了一个小网站的搭建,用了 node.js,另外为了能在一个 VPS 上搭建多个网站,用了 nginx 作为反向代理。

软件介绍

嗯,从维基上复制了一下~

node.js

Node.js是一个事件驱动I/O伺服端JavaScript环境,基于V8。目的是为了提供撰写可扩充网络程式,如web服务。第一个版本由Ryan Dahl于2009年释出,后来,Joyent雇用了Dahl,并协助发展Node.js。

nginx

nginx(发音同engine x)是一款由俄罗斯程序员Igor Sysoev所开发轻量级的网页服务器、反向代理服务器以及电子邮件(IMAP/POP3)代理服务器。

cluster

在 node.js 0.6.0 之前,有一个第三方的 node.js 模块 cluster,用来进行多核服务器上运行 node.js,以及提供扩展的支持。但是在 node.js 0.6.0 之后,node.js 本身就提供了 cluster 的支持,另外,第三方的 cluster 也与 node.js 0.6 有兼容性问题。目前 node.js 的稳定版本是 0.6.5,因此需要使用原生的 cluster 来代替第三方的 cluster。

幸好内置的 cluster 也足够简单,如果只是为了多核负载均衡,以及支持即时服务重启的话,只需要写一点的代码就可以完成这些功能了。

server.js

var path = require('path');
var http = require('http');
var cluster = require('cluster');

var NODE_ENV = process.env.NODE_ENV || 'production';
var appName = path.basename(__dirname);
var appPort = 9000;

var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    process.title = appName + ' master';
    console.log(process.title, 'started');

    // 根据 CPU 个数来启动相应数量的 worker
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }

    process.on('SIGHUP', function() {
        // master 进程忽略 SIGHUP 信号
    });

    cluster.on('death', function(worker) {
        console.log(appName, 'worker', '#' + worker.pid, 'died');
        cluster.fork();
    });

} else {
    process.title = appName + ' worker ' + process.env.NODE_WORKER_ID;
    console.log(process.title, '#' + process.pid, 'started');

    process.on('SIGHUP', function() {
        // 接收到 SIGHUP 信号时,关闭 worker
        process.exit(0);
    });

    http.Server(function(req, res) {
        res.writeHead(200);
        res.end('Worker ' + process.env.NODE_WORKER_ID);
    }).listen(8000);
}

运行服务 node server.js

nodejs master started
nodejs worker 1 #38928 started
nodejs worker 3 #38930 started
nodejs worker 2 #38929 started
nodejs worker 4 #38931 started

如果直接 kill 掉某一个 worker,kill 38928

nodejs worker #38931 died
nodejs worker 5 #38934 started

可以看到一个新的 worker 会马上启动,这就保证了服务的不间断性。

Virtual Host 支持

通常情况下,我们不会在一个 IP 上只部署一个网站。在使用 node.js 时,可以使用 connect 提供的 vhost 支持 Virtual Host,但是,这也限制了服务器只能用 node.js,而不能同时使用其他的服务,例如再安装一个 PHP 服务之类。

这时就可以使用 nginx 的反向代理来解决了,用户在访问网站时,请求先到 nginx 进行处理,如果是 node.js 站点的话,将请求转发到 node.js 的服务,然后再将 node.js 服务的结果返回给用户。

在 nginx 中设置反向代理很简单,一句 proxy_pass 就可以搞定:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:9000;
    }
}

在添加了 Virtual Host 之后,就可以把一些静态资源,例如 CSS、JavaScript 之类的文件,直接交给 nginx 来处理,而不是什么请求都需要到 node.js 这一层去处理,也省去反向代理这一关的消耗。

Session 支持

使用 express 这个 node.js web framework 来创建网站时,可以配合 connect 这个中间件来实现 session 支持。

默认情况下,connect 的 session 是使用内置的内存存储来存放 session 信息,这时如果 node.js 服务一旦重启,所有的 session 信息都会丢失,这对于用户来说不是个好体验,那么我们可以用外部的存储来存放 session 信息,例如 redis。

要让 connect 使用 redis 作为 session 存储的话也是很方便的:

var express = require('express');
var RedisStore = require('connect-redis')(express);

var app = express.createServer(
    express.session({ secret: 'keyboard cat', store : new RedisStore() })
);

监测文件改动

在调试的时候,经常需要重新启动 node.js 以便修改过的文件生效,原来第三方的 cluster 有一个配置项,可以很方便的配置监测时间间隔,文件改动后自动重新启动 worker,但是原生的 cluster 就没有这个功能了,需要自己来实现。

fs 模块提供了 watch 函数,可以方便的监测文件修改,使用这个就可以来实现文件修改后自动重启 woker 功能了。

if (cluster.isMaster) {
    process.title = appName + ' master';
    console.log(process.title, 'started');

    var workers = [];

    // 根据 CPU 个数来启动相应数量的 worker
    for (var i = 0; i < numCPUs; i++) {
        var worker = cluster.fork();
        workers.push(worker.pid);
    }

    process.on('SIGHUP', function() {
        // master 进程忽略 SIGHUP 信号
    });

    // 监测文件改动,如果有修改,就将所有的 worker kill 掉
    fs.watch(__dirname, function(event, filename) {
        workers.forEach(function(pid) {
            process.kill(pid);
        });
    });

    cluster.on('death', function(worker) {
        var index = workers.indexOf(worker.pid);
        if (index != -1) {
            workers.splice(index, 1);
        }
        console.log(appName, 'worker', '#' + worker.pid, 'died');
        worker = cluster.fork();
        workers.push(worker.pid);
    });

}

这样,每次文件保存之后,node.js 都会自动重启,从而避免了每次保存文件要手动重启服务的麻烦。

当然,在使用监测文件自动重启的时候,最好加上 NODE_ENV 的判断,在 development 的时候才进行自动重启,而 production 的时候使用手动重启就够了。

小结

总的来说,使用 node.js 来构建网站还是很方便的,加上 nginx 反向代理之后,与使用 PHP 之前也没有很大的区别,又可以享受到 node.js 的高效。

嗯,就这样了,希望此文对你有所帮助。

参考资料

  1. https://zh.wikipedia.org/wiki/Node.js
  2. http://nodejs.org/docs/v0.6.5/api/cluster.html
  3. http://learnboost.github.com/cluster/
  4. http://expressjs.com/
  5. http://senchalabs.github.com/connect/
  6. http://redis.io/

— EOF —

慎用 script 节点的 src 属性来传递参数

在有些使用 javascript 来渲染数据的时候,为了能动态获取不同的数据,并且保持 javascript 代码的可扩展性,会将 javascript 代码中获取数据的部分需要的参数提取出来,做为参数放在 script 节点的外部。

一般来说,传递参数到 javascript 文件内部的方法有两种,一种是将参数写在一个 script 节点中,写成全局变量的方式的传递给紧接着这个 script 节点的外部 javascript 中,Google Analytics 就是使用这样的方式:

<script type="text/javascript">
var p1 = "v1", p2 = "v2";
</script>
<script type="text/javascript" src="foo.js"></script>

另外一种是将参数直接写在 script 节点的 src 属性中,相当于一个页面的查询字符串一样:

<script type="text/javascript" src="foo.js?p1=v1&p2=v2"></script>

不过,使用 script 节点的 src 属性来传递参数需要注意一个很重要的问题,那就是动态变化的 src 属性会导致缓存失效

现在,为了网站性能的需要,一般都会将 javascript 文件放在独立的服务器上,并设置一个较长的过期时间,这样客户端只会在第一次访问网站时需要去下载这个 javascript 文件。但是,如果使用 src 来传递参数,就可能会使这种缓存策略失效。特别是 src 中存在动态参数的情况,例如统计脚本中如果有一个 ip 参数,那么访客每次连上线时,可能 ip 都会不同,就会导致 javascript 缓存失效了。

解决这个问题的方法也很简单,简单地的将 src 属性中的参数放到 script 节点的一个自定义属性中就可以了,例如 data-args,而 src 属性只需要保留一个时间戳就可以了。因为使用 src 属性来传递参数本来就需要定位 script 节点,所以改由 data-args 自定义属性来传递参数并不会增加额外的代码。只不过页面会通不过 w3c 的验证罢了 :)

&lt;script type="text/javascript" src="foo.js" data-args="p1=v1&amp;p2=v2"&gt;&lt;/script&gt;

再次提醒,慎用 script 节点的 src 属性来传递参数 :)

使用 arguments.caller 实现自动回调

这是一篇介绍类似于 js hack 的文章,只是说明了一种可行的途径,并且可能会增加代码复杂程度,具体项目中是否可以使用还请自辨:)

在前端开发过程中,有许多业务流程可能是需要用户进行登录的,并且登录过程是放在弹出层中,这样就可以不用刷新页面,增强用户体验。在登录时,用户的操作就会被打断,为了进一步增强用户体验,我们可能需要在登录完成后自动继续进行用户在登录前想进行的操作。

假设有这样一个场景,用户需要发表一个留言,但是发表留言是需要登录的,而发表留言的输入框是一直显示的,这也就要求在用户点击了发表按钮时对用户登录状态进行验证,传统的做法是将在用户登录状态检查封装在一个函数之中,这个函数接收一个回调参数,如果登录验证通过,则执行回调函数。这样的逻辑可以用以下代码表示:

function doAction() {
checkLogin(function() {
// 处理业务逻辑
});
}
function checkLogin(callback) {
if (isLogin) {
callback();
} else {
showLogin(callback);
}
}
function showLogin(callback) {
document.getElementById(“login-btn”).onclick = function() {
isLogin = true;
callback();
};
}

    总觉得这样的方式会将业务逻辑放到一个匿名函数中,而不是放在了按钮的事件响应函数中,感觉上不是那么好。这对整个事件响应函数的改动比较大,主要的业务逻辑都放在了登录验证函数的回调中。而希望可以是下面这样:

    function doAction() {
    // 直接在函数开始进行登录验证,将业务逻辑独立出来
    if (!doLogin()) {
    return;
    }
    // 处理业务逻辑
    }
    function doLogin() {
    if (isLogin) {
    return true;
    } else {
    var callback = function() {}; // 自动组装 callback
    showLogin(callback);
    return false;
    }
    }
    function showLogin(callback) {
    document.getElementById(“login-btn”).onclick = function() {
    isLogin = true;
    callback();
    };
    }

      这里的关键在于怎么“自动组装 callback”,很幸运的是,JavaScript 的 Function 提供了一个属性 caller 可以用来获取调用当前函数的函数是什么。并且还可以用 caller 的 arguments 属性来获取 caller 执行时的参数是什么,这也就使我们自动组装 callback 并恢复 caller 的原参数成为可能。

      <div id=”result”></div>
      <input type=”button” value=”Sumit” onclick=”sendPost()” id=”post” />
      <input type=”button” value=”Clear login” onclick=”isLogin=false” id=”reset” />
      <div id=’login’ style=”border:1px solid #000;padding:10px;margin:10px;display:none”>
      <strong>Login</strong><br />
      <input type=”button” id=’loginbtn’ value=”Login” />
      </div>
      <script type=”text/javascript”>
      // 是否登录
      var isLogin = false;
      // 检查登录状态
      var checkLogin = function(cfg) {
      if (isLogin) {
      return true;
      }
      cfg = cfg || {};
      var callback = null;
      // 自动执行回调
      if (cfg.autoCallback && arguments.callee.caller) {
      try {
      var caller  = arguments.callee.caller,
      args    = caller.arguments || [],
      scope   = cfg.callbackScope || {},
      newArgs = [];
      for (var i = 0; i < args.length; ++i) {
      newArgs.push(args[i]);
      }
      callback = caller ? function() {
      try {
      caller && caller.apply(scope, newArgs);
      } catch (e) {
      //alert(e.message);
      }
      } : null;
      } catch (e) {
      callback = null;
      }
      }
      showLogin(callback);
      return false;
      };
      function sendPost() {
      if (!checkLogin({autoCallback:true})) {
      return;
      }
      document.getElementById(‘result’).innerHTML += ‘Hello<br />’;
      }
      function showLogin(callback) {
      document.getElementById(‘login’).style.display = ‘;
      document.getElementById(‘loginbtn’).onclick = function() {
      document.getElementById(‘login’).style.display = ‘none’;
      isLogin = true;
      callback && callback();
      }
      }
      </script>

      其实弄出这么个方法来实现登录自动回调也是为了方便写代码,不用每次去将一堆业务逻辑放到 callback 中,直接在函数开始回一个 if (!checkLogin({autoCallback:true})) 就行。

      caller 属性在 MDC 中被标记为“非标准”的,但是在主流浏览器中,都是支持这个属性的,也就是说,在浏览器兼容性上使用 caller 是没有问题的。

      不过,使用奇技淫巧容易伤身体,慎用:)

      YUI学习笔记(4)

      YUI学习笔记(4)

      by xujiwei (http://www.xujiwei.com/)

      YAHOO.util.Subscriber 与 YAHOO.util.CustomEvent。

      1. YAHOO.util.Subscriber (event.js)

      这 应该算是设计模式中的观察者模式了,Subscriber 订阅一个事件,在 Publisher 触发那个事件后,会逐个通知 Subscriber。

      对 于一般开发者来说,并不需要去关心 Subscriber 的实现,因为 Subscriber 主要是 CustomEvent 用来分发动作以及删 除 Subscriber 的。

      Subscriber 类只定义了 3 个属性:fn、obj 以及 override,3 个方 法:getScope、contains、toString,其中 fn 为订阅者的回调函数,obj 为要传递给回调函数的一个额外参 数,override 如果是布尔型的 true 值,那么表示使用 obj 属性为回调函数执行时的上下文,或者直接使用一个对象来作为回调函数执行的 上下文。

      Subscriber 的 3 个方法中比较有用的是 getScope 和 contains,toString 只是简单的 将 Subscriber 对象转换成一个字符串。getScope 会根据 Subscriber 对象的 override 属性来获取回调函数执行 的上下文,contains 用来判断 Subscriber 对象与指定的回调函数和 obj 是否一致。

      2. YAHOO.util.CustomEvent (event.js)

      CustomEvent 的 作用相当在观察者模式中发布者的身份,可以通过它来实现一个自己的事件发布者。

      CustomEvent 构造函数的定义如下:

      CustomEvent = function(type, oScope, silent, signature)

      在 创建 CustomEvent 对象时,几个参数的用途如下:

      type 是自定义事件的名称,在使用回调函数的参数格式 为 YAHOO.util.Event.LIST 时,回调函数的第一个参数就是 CustomEvent 对象的名称;

      oScope 是 执行回调函数时的上下文对象,也就是在回调函数中可以用 this 来引用这个对象;

      silent 参数是用指示是否 在 YUI 为 debug 版本时禁用调试信息;

      signature 用来指示回调函数参数的格式,可以为 YAHOO.util.Event.FLAT 或 YAHOO.util.Event.LIST, 默认是 YAHOO.util.Event.LIST。

      在使用 CustomEvent 之前,先要了解一下 CustomEvent 中 回调函数参数的格式,CustomEvent 的回调函数可以有两种格式,一种为 YAHOO.util.Event.LIST,这种格式的回调函数具有 三个参数,分别是事件名称、参数数组和附加对象参数;另外一种回调函数参数格式为 YAHOO.util.Event.FLAT,这个时候回调函数只有两 个参数,一个为 CustomEvent 对象调用 fire 方法时的第一个参数,另外一个是订阅时的额外对象参数。

      在创 建 CustomEvent 对象时,CustomEvent 构造函数还会首先创建一个内部的自定义事件,用来处理该自定义事件被订阅的事件,这 在 EventProvider 中用到,这里暂且不提。

      CustomEvent 对象使用一个名为 subscribers 的数组来保 存所有订阅者的列表,并且通过维护这个列表来维护该自定义事件的订阅者。

      CustomEvent 对象提供了 subscribe、 unsubscribe、unsubscribeAll、fire 这几个方法来处理自定义事件的订阅、退订以及触发等动作,而这几个就是观察者模式中的 主要动作了。

      subscribe 的签名为 subscribe: function(fn, obj, override), 三个参数分别对应了 Subscriber 类构造函数的三个参数,分别对应了回调函数、额外对象参数以及是否使用额外对象参数作为执行上下文。 subscribe 只是简单的判断参数 fn 是否有定义,然后会触发自定义事件订阅事件,最后使用这三个参数创建一个 Subscriber 对象添 加到 CustomEvent 对象的 subscribers 属性中。

      unsubscribe 方法用来取消事件的订阅,它的函数签名 为 unsubscribe: function(fn, obj),两个参数分别是回调函数和额外对象参数,如果使用无参数调 用 unsubscribe 方法,那么会直接调用 unsubscribeAll 来删除所有订阅者,否则会逐一判断 subscribers 中的每 个对象,通过使用 Subscriber 对象的 contains 方法来判断给定的 fn 和 obj 与其是否一致,如果一致,就使用一个私有方 法 _delete 来删除这个 Subscriber。

      unsubscribeAll 方法没有参数,它只是简单的直接删除自定义事件的 所有订阅者,最后直接给 subscribers 赋值一个空数组来避免有可能出现漏删订阅者的情况。

      内部方法 _delete 的参数 是 Subscriber 对象在 subscribers 数组中的索引,它会先删除 Subscriber 对象的 fn 和 obj 属性,最后使 用 splice 方法将 Subscriber 对象从数组中删除。

      var s = this.subscribers[index];
      if (s) {
      delete s.fn;
      delete s.obj;
      }
      this.subscribers.splice(index, 1);

      使用 delete 删除 Subscriber 对象的 fn 和 obj 属性是为了去除 回调函数及额外对象参数的引用,以免引起不必要的内 存泄露。

      CustomEvent 对象最重要的方法就是 fire 了,就是通过这个方法来通知所有了订阅者这个自定义事件被触发了。 fire 方法先使用 Array 的 slice 方法将调用 fire 方法时的参数转化成数组,这样就可以在调用 Subscriber 的回调函 数时可以传递参数给它们。

      在遍历 subscribers 中的 Subscriber 前,fire 方法先使用 了 subscribers 的 slice 方法来创建一个 subscribers 的副本,这样避免在执行 fire 的过程中 有 Subscirber 取消订阅了这个自定义事件会导致错误。

      在执行 Subscriber 的回调函数前,先使 用 Subscriber 对象的 getScope 方法来获取执行回调函数时的上下文对象,再根据 CustomEvent 对象 的 signature 属性来决定怎么去调用 Subscriber 的回调函数。

      如 果 signature 为 YAHOO.util.CustomEvent.FLAT,那么就把调用 fire 方法时的第一个参数做为回调函数的第一 个参数,再把 Subscriber 对象的 obj 属性做为第二个参数:

      s.fn.call(scope, param, s.obj)

      如 果 signature 为 YAHOO.util.CustomEvent.LIST,那么就除了把整个 fire 方法的参数列表传递给回调函数外, 还要传递当前 CustomEvent 的名称给回调函数:

      s.fn.call(scope, this.type, args, s.obj)

      Subscriber 的 回调函数如果在执行过程中出现了错误,那么 CustomEvent 的 lastError 属性就是指向错误对象的引用,另外,如 果 YAOO.util.Event.throwErrors 为 true,那么会把这个错误再次抛出。

      另外,Subscriber 对 象也可以控制事件通知是否继续,如果 Subscriber 对象的回调函数执行后的返回一个 false,那么在 fire 方法中就会停止通知剩下 的 Subscriber 对象,通常情况下,先订阅自定义事件的 Subscriber 可以阻止后订阅的 Subscriber 接收到通知。

      使 用 YUI 的自定义事件(CustomEvent)可以很方便地实现观察者模式,更好地组织 JavaScript 程序的结构。

      YUI学习笔记(3)

      YUI学习笔记(3)

      by xujiwei (http://www.xujiwei.com/)

      YAHOO.lang.later,YAHOO.lang.trim,YAHOO.lang.isXXX 以及 YAHOO.lang.hasOwnProperty。

      1. YAHOO.lang.later(yahoo/yahoo.js)

      later 方法用来延迟执行方法,是对 setInterval 和 setTimeout 的封装,并且可以传递参数到延迟执行的函数或者使用参数数组批量执行指定的函数。

      later 方法的签名为:

      later: function(when, o, fn, data, periodic)

      when 是用来指定在多长时间后执行指定的函数,以毫秒计算;

      o 是上下文对象,即在要执行的函数里使用 this 是会引用这个 o 对象;

      fn 就是要延迟执行的函数了,可以传递一个函数引用,也可以传递一个字符串,later 方法会在 o 对象中查找对应名称的属性来做为要执行的方法;

      data 就是传递给延迟执行的函数的参数了,可以为一个参数,或者是一个参数数组,那么如果我们要传递的参数本身就是一个数组的话,就要自己先把这个参数数组包装成一个数组;

      peridoic 参数是一个布尔值,用来表示延迟执行的函数是否需要周期执行而不是只执行一次。

      later 方法执行后会返回一个对象,包含了一个名 interval 的属性用来表示函数是以 setInterval 来执行的还是以 setTimeout 来执行的,以及一个方法 cancel 用来取消执行被延迟执行的函数。嗯,不过只有在 peridoic 为 true 时这个 cancel 方法比较有用,毕竟如果 peridoic 为 false 时函数执行一次就不会再执行了,cancel 也没有什么意义。

      {
      interval: periodic,
      cancel: function() {
      if (this.interval) {
      clearInterval(r);
      } else {
      clearTimeout(r);
      }
      }
      };

        2. YAHOO.lang.trim (yahoo/yahoo.js)

        trim 方法用来去除字符串两边的空白字符,其实也就用了一个正则来匹配字符串两端的空白字符并替换成空白字符串。

        不过在 YAHOO.lang.trim 中,它使用了一个 try … catch 来在调用的参数不为字符串时直接返回原来的对象。

        3. 对象类型判断 YAHOO.lang.isXXX (yahoo/yahoo.js)

        YAHOO.lang 中包含了一堆用于判断对象是否为某个类型的方法,例如 isObject、isString、isNumber 等。

        YAHOO.lang.isArray 用来判断一个对象是否为数组,YUI 中并不是使用的 obj instanceof Array 或者 obj.constructor == Array,而是判断指定的对象是否有两个数组应该具有的经典属性和方法 length 和 splice,这是因为如果要判断的对象是属于另外一个 frame 中时,除非你能获得另外一个 frame 中 Array 定义的费用,否则 instanceof 和 constructor == Array 都是返回 false 的,我们可以使用以下代码来测试一下:

        test.html

        <iframe id=”frame” src=”iframe.html”></iframe>
        <script type=”text/javascript”>
        <!–
        var f = document.getElementById(‘frame’);
        setTimeout(function() {
        alert(f.contentWindow.arr.constructor == Array);
        }, 1000);
        //–>
        </script>

          iframe.html

          <script type=”text/javascript”>
          <!–
          var arr = [1, 2, 3];
          //–>
          </script>

            而如果把 Array 换成 arr 所在 frame 中 Array 的引用,结果就是为 true 了:

            test.html

            <iframe id=”frame” src=”iframe.html”></iframe>
            <script type=”text/javascript”>
            <!–
            var f = document.getElementById(‘frame’);
            setTimeout(function() {
            // 这里将 Array 换成 f.contentWindow.Array
            alert(f.contentWindow.arr.constructor == f.contentWindow.Array);
            }, 1000);
            //–>
            </script>

              但是因为 isArray 是一个通用的方法,在使用时不可能要求调用者同时也传递一个目标 frame 的引用过来,所以只好通过判断两个经典的属性来代替了。

              YUI 的注释中提到是 Safari 不支持跨框架通过 instanceof 的检测,但是我测试的结果是几个主流浏览器都不支持跨框架的 instanceof 检测。

              YAHOO.lang.isBooleanYAHOO.lang.isFunctionYAHOO.lang.isStringYAHOO.lang.isUndefined 这 4 个方法是直接通过使用 typeof 来判断对象类型是否为 boolean、function、string 或 undefined 的。

              YAHOO.lang.isNumber 也是通过 typeof 来判断的,不过附加了一个检测条件,使用 isFinite 来检测数字是否为有限的,即不是 NaN、负无穷或正无穷。

              YAHOO.lang.isObject 通过 typeof 来判断对象类型是否 object 以及使用 isFunction 方法来检测对象是否为一个 Function,在 YAHOO.lang.isObject 方法的眼中,Object 与 Function 都是 Object。

              YAHOO.lang.isNull 直接将判断对象与 null 进行严格相等比较。

              YAHOO.lang.isValue 用来判断传递给它的参数是否为一个有值的对象,它是将 isObject、isString、isNumber 以及 isBoolean 四个方法对参数进行判断的结果进行或操作来作为结果的,对 null/undefined/NaN 这三个值进行 isValue 判断时就会返回 false,因为 0、false、’ 这三个值在分别在 isNumber、isBoolean、isString 时判断为 true,所有 isValue 的结果也为 true。

              4. YAHOO.lang.hasOwnProperty (yahoo/yahoo.js)

              嗯,这个方法也是对 Object.prototype.hasOwnProperty 的封装,前提是当前上下文的 Object.prototype 有 hasOwnProperty 方法,如果 Object.prototype.hasOwnProperty 不存在,那么 YAHOO.lang.hasOwnProperty 就实现了自己的 hasOwnProperty 方法,它是通过判断对象的属性与对象 prototype 的同名属性是否为同一个对象的判断的。如果对象的属性不是从原型继承过来的,那么两个属性值就有可能不一致:

              o.constructor.prototype[prop] !== o[prop]

              不过这里也有个问题,在 YAHOO.lang.hasOwnProperty 的注释中也提到了,就是 YUI 自己实现的 hasOwnProperty 是无法分辨在属性值一样时,这个属性是从原型继承过来的还是对象自身的:

              var YAHOO = {
              lang : {
              isUndefined : function(o) {
              return typeof o === “undefined”;
              },
              hasOwnProperty : function(o, prop) {
              return !this.isUndefined(o[prop]) &&
              o.constructor.prototype[prop] !== o[prop];
              }
              }
              }
              var A = function() {};
              A.prototype.foo = ‘foo’;
              var a = new A();
              a.foo = ‘foo’;
              alert(a.hasOwnProperty(‘foo’)); // true
              alert(YAHOO.lang.hasOwnProperty(a, ‘foo’)); // 这里返回 false,事实上应该是 true

                但是现在 Object.prototype 中没有 hasOwnProperty 的浏览器应该基本没有了,所以这个问题也不是很大。

                5. YAHOO.lang.augmentProto 以及 YAHOO.lang.extend 的别名 (yahoo/yahoo.js)

                在 yahoo.js 中,将 YAHOO.lang.augment 以及 YAHOO.augment 指向了 YAHOO.lang.augmentProto,YAHOO.extend 指向了 YAHOO.lang.extend,可以一定程度上减少代码的编写。

                YUI学习笔记(2)

                YUI学习笔记(2)

                by xujiwei (http://www.xujiwei.com/)

                YAHOO.lang.dump 与 YAHOO.lang.substitute。

                1. YAHOO.lang.dump(yahoo.js)

                dump 方法用来将一个对象转储为一个字符串,并且可以指定转储的深度。

                在 dump 过程中,对于基础类型例如 Number、String、Boolean,是直接返回字符串的,对 HTMLElement 对象是返回 HTMLElement 本身,也就是不做处理,对于函数 Function 则是返回字符串“f(){…}”。

                对于数组,dump 返回的格式就如我们定义时一样“[item1, item2 item3, …]”,对于对象 Object,则是使用键值对的形式“key => value”,与 PHP 里面的数组定义方式相似。

                例如一个对象定义如下:

                var obj = {
                num: 1,
                str: “string”,
                bool: true,
                date: new Date(),
                obj: {
                obj_num: 1,
                obj_str: “obj_string”
                },
                foo: function() {
                }
                }

                  dump 之后的字符串如下:

                  {num => 1, str => string, bool => true, date => Wed Jan 7 15:57:52 UTC+0800 2009,obj => {obj_num => 1, obj_str => obj_string}, foo => f(){…}}

                  字符串没有被引号引起来,这个方法只适合用来展示对象的结构,与 JSON 序列化差得有点远了。

                  2. YAHOO.lang.substitute(yahoo.js)

                  substitute 实现的功能与 C# 中的 String.Format 方法类似,用来格式化一个字符串,但是它是一个字符串来代表一个占位符,而不像 C# 中是使用数字,或许这个用法和 python 中的字符串格式化更像。

                  substitute 调用的格式为:substitute(formatString, valueObject [,formatCallback]),其中 formatString 是作为格式化字符串的字符串,其中包含着一些格式为 {key} 这样的占位符,valueObject 是包含要取代占位符的值的对象,它的结构为 { key1 : value1, key2 : value2 },最后的 formatCallback 参数则是额外的格式化处理函数,用来在格式化字符串进行一些额外的处理。

                  substitute 格式化字符串的占位符格式为“{key [meta]}”,其中 meta 是可选的,用来表示额外的格式化属性,而要替换的数据则是 valueObject 中名称为占位符 key 的属性,例如,使用 { name : “xujiwei” } 做为数据,那么“{name}”将被替换为“xujiwei”。

                  基本数据类型例如 String、Number 在 substitute 中是直接替换的,但是如果要替换的值为一个数组,那么 subtitute 会先使用 YAHOO.lang.dump 方法将对象转储为一个字符串再进行替换,深度为 10,而在替换的值为一个对象时,则会先检测 meta 数据中是否包含了 dump 关键字,或者这个对象的 toString 方法是否与 Object 对象原型的 toString 方法一样,如果是的话就使用 YAHOO.lang.dump 方法来将对象转储为字符串进行替换,否则就调用对象的 toString 方法得到字符串进行格式化。

                  如果替换的数据类型不是 Object、Array、String、Number 中的一种的话,那么这个占位符就不进行替换。

                  一个覆盖了所有类型数据的例子如下:

                  YAHOO.lang.substitute(
                  “String : {key1}\nNumber : {key2}\nObject : {key3}\nArray : {key4}”,
                  {
                  key1 : “xujiwei”,       // String
                  key2 : 123456,          // Number
                  key3 : {
                  firstName : “Jiwei”,
                  lastName  : “Xu”
                  },                      // Object
                  key4 : [1, 2, 3]        // Array
                  });

                    它的输出如下:

                    String : xujiwei

                    Number : 123456

                    Object : {firstName => Jiwei, lastName => Xu}

                    Array : [1, 2, 3]

                    继续阅读 »

                    YUI学习笔记(1)

                    YUI学习笔记(1)

                    by xujiwei (http://www.xujiwei.com/)

                    今天开始学习 YUI,加强一下对 JavaScript 的理解。

                    1. 命名空间 YAHOO.namespace(yahoo.js)

                    YUI 中使用了命名空间的概念,在 JS 中使用命名空间是为了模块以及代码组织清晰的需要,通过使用命名空间可以将功能相似或同一模块中的函数、变量等放到同一个命名空间下。

                    其实 JS 中的命名空间就是一个嵌套的对象而已,即子命名空间相当于父命名空间中的一个属性,它本身也是一个对象,这样子命名空间也可以有自己的子命名空间。

                    在 YUI 中,命名空间的格式与 C# 中类似,是以点号分隔的字符串,可以使用 YAHOO 对象的静态方法 namespace 来创建命名空间,需要注意的是,以 namespace 方法创建命名空间时,所有的对象都是附加在 YAHOO 这个对象上的,如果调用 namespace 方法创建一个“com.xujiwei.ajax”这样的命名空间,其中 ajax 的完整路径将是 YAHOO.com.xujiwei.ajax,而不是 com.xujiwei.ajax,也就是说 namespace 不会产生新的顶层对象,一切以 YAHOO 对象为基础。

                    namespace 方法接受一个或多个字符串来生成命名空间,但是它只返回最后一个参数所代表的命名空间的最后一个域的对象,例如使用 namespace(“com.xujiwei.js”, “com.xujiwei.ajax”) 时它的返回值是代表 js 这个模块的的对象:

                    YAHOO.namespace(“com.xujiwei.js”, “com.xujiwei.ajax”).get = function() {
                    alert(“get method of com.xujiwei.ajax”);
                    }
                    YAHOO.com.xujiwei.ajax.get();

                    2. YAHOO.lang.extend(yahoo.js)

                    YAHOO.lang.extend 负责扩展一个现在的函数对象,相当于面向对象中的继承,并且可以附加一个参数重写继承的方法,它的函数声明是:

                    extend: function(subc, superc, overrides)

                    其中 subc 为要扩展的函数对象,superc 是要继续的函数对象,overrides 中包含着要重写父类的方法。

                    YAHOO.lang.extend 是通过原型链的方式来扩展对象,即创建一个父类的实例做为子类的原型对象,在 extend 方法中,首先构造了一个空函数,将空函数的 prototype 指向父类的 prototype,以免在父类的构造函数需要参数时创建父类实例时会出错,然后创建这个空函数的实例赋值给子类的 prototype 对象,并且给子类添加一个 superclass 属性指定它的父类,另外还指定了子类 prototype 的 constructor 属性为子类构造函数,以免创建子类实例时会丢失 constructor 属性,这个问题我在《慎用 somefunction.prototype》有提到。

                    var F = function() {};
                    F.prototype = superc.prototype;
                    subc.prototype = new F();
                    subc.prototype.constructor = subc;
                    subc.superclass = superc.prototype;

                    接着处理的是要重写的函数,首先遍历 overrides 参数,并且使用 hasOwnProperty 方法判断属性是否为 overrides 自身的属性,而不是从 Object 对象的 prototype 继承过来的,如果是 overrides 自身的属性,那么就覆盖子类 prototype 的相同成员。

                    在处理 overrides 的最后,还修复了 IE 中不枚举与内部函数同名的函数的 bug,即例如 overrides 中包含了 toString 或 valueOf 的定义,但是这两个函数也是 Object 对象所具有的,在 IE 中使用 for(p in overrides) 就不会得到这两个函数,因此需要修复,这是 YAHOO.lang 中 _IEEnumFix 函数所做的工作。

                    if (overrides) {
                    for (var i in overrides) {
                    if (L.hasOwnProperty(overrides, i)) {
                    subc.prototype[ i ]=overrides[ i ];
                    }
                    }
                    L._IEEnumFix(subc.prototype, overrides);
                    }

                    _IEEnumFix 只是简单的判断当前浏览器是否为 IE,如果为 IE 则从一个已经定义的列表中获取在 IE 中枚举不到的函数名列表,逐一判断是否为源对象自身的属性并且与 Object 原型中的同名函数不一样,如果是则添加到目标对象。如果浏览器不是 IE 则 _IEEnumFix 直接就是一个空函数,直接不处理。

                    _IEEnumFix: (YAHOO.env.ua.ie) ? function(r, s) {
                    for (var i=0;i<ADD.length;i=i+1) {
                    var fname=ADD[ i ],f=s[fname];
                    if (L.isFunction(f) && f!=Object.prototype[fname]) {
                    r[fname]=f;
                    }
                    }
                    } : function(){},

                    3. YAHOO.lang.augmentObject(yahoo.js)

                    augmentObject 方法用来引入目标对象的属性,并且处理重写,通常用于合并配置对象与默认配置对象。

                    augmentObject 有三种调用方法,分别是:

                    augmentObject(result, source),一般情况下用些方法,从 source 引入 result 中没有的属性;

                    augmentObject(result, source, override),override 设置是否覆盖,如果是 true 的话,那么 source 中所有属性会覆盖 result 中已有的属性;

                    augmentObject(result, source, ovrride_name, …),指定要覆盖属性的列表,source 参数后为要覆盖属性名称的列表,例如覆盖 result 中的 toString 和 valueOf,那么可以调用 augmentObject(result, source, “toString”, “valueOf”)来实现。

                    合并对象的最后同样调用了 _IEEnumFix 方法来修正 IE 中不枚举与内部属性同名属性的 bug。

                    augmentProto 与 augmentObject 功能基本相同,只不过后者是处理对象,而前者是处理函数的 prototype,它是通过使用两个参数的 prototype 来构造一个参数列表,传递给 augmentObject 对象来实现 augmentProto 的功能。

                    var a=[r.prototype,s.prototype];
                    for (var i=2;i<arguments.length;i=i+1) {
                    a.push(arguments[ i ]);
                    }
                    L.augmentObject.apply(this, a);

                    因为 augmentProto 是调用 augmentObject 来实现的,因此它的调用格式也是 augmentObject 一样,具体可以看看上面 augmentObject 的三种调用方法。

                    另外,YAHOO.lang.merge 方法也是通过调用 augmentObject 方法来实现对象的合并。

                    [JavaScript] 慎用 somefunction.prototype

                    在写 JavaScript 脚本的时候,为了创建一个类,如果不使用框架,一般情况我们都会使用 prototype 来给要创建的类增加公有方法,例如:

                    • // code from xujiwei.cn
                    • function Person(name) {
                    • this.Name = name;
                    • }
                    • Person.prototype.SayHello = function() {
                    • alert(‘Hello, ‘ + this.Name);
                    • }
                    • Person.prototype.SayBye = function() {
                    • alert(‘Goodbye, ‘ + this.Name);
                    • }

                    不过,有的时候,为了书写以及维护的方便,我们会把公有方法的声明写到一个对象里,然后赋值给 Person.prototype,例如:

                    • // code from xujiwei.cn
                    • function Person(name) {
                    • this.Name = name;
                    • }
                    • Person.prototype = {
                    • SayHello: function() {
                    • alert(‘Hello, ‘ + this.Name);
                    • },
                    • SayBye: function() {
                    • alert(‘Goodbye, ‘ + this.Name);
                    • }
                    • }

                    使用这种方式,在这个类具有大量公有方法的时候,就不需要维护许多的 Person 标识符,如果某一天这个类的名字需要改变,那么要改的地方只有两个,一个是 function 的声明,一个是 prototype 前面的标识符,如果是使用前一种方式的话,那么有多少个公有方法,就需要维护 N+1 个标识符了,虽然可以使用查找替换,但是从稳定上来说,查找替换可能会引起一些错误,这增加了维护的成本。

                    这种方式虽然给我们的维护增加了便利,但也引发了另外一个隐藏的问题,就是类的 constructor 属性丢失的问题

                    • // code from xujiwei.cn
                    • function Person1(name) {
                    • this.Name = name;
                    • }
                    • Person1.prototype.SayHello = function() {
                    • alert(‘Hello, ‘ + this.Name);
                    • }
                    • Person1.prototype.SayBye = function() {
                    • alert(‘Goodbye, ‘ + this.Name);
                    • }
                    • // code from xujiwei.cn
                    • function Person2(name) {
                    • this.Name = name;
                    • }
                    • Person2.prototype = {
                    • SayHello: function() {
                    • alert(‘Hello, ‘ + this.Name);
                    • },
                    • SayBye: function() {
                    • alert(‘Goodbye, ‘ + this.Name);
                    • }
                    • }
                    • alert(new Person1(‘Bill’).constructor);
                    • alert(new Person2(‘Steve’).constructor);

                    运行上面的测试代码我们可以发现,Person1 的 constructor 属性为 Person1 类的构造函数,但是 Person2 的 constructor 属性却是 Object,那么在需要使用 constructor 属性来判断对象类型的时候,就会出现问题。

                    因此,在写 JavaScript 类的时候,如果不需要使用 constructor 属性来获取对象的类型,那么个人比较倾向于使用第二种写法,但是如果需要使用 constructor 属性以实现自己的反射机制或 GetType 函数等等,那么就要使用第一种写法。

                    当然,如果在实现自己反射机制或 GetType 函数时并不依赖 constructor 属性,那么两种写法都是可以的了,例如额外维护一个成员变量,用于标识自身的类型等。也可以使用一些现成的JS框架,有一些框架已经实现了JS中类的实现等,例如 js.class,这就看个人需要进行取舍了。

                    JavaScript 中为 Date 类实现 DateAdd 方法

                    JavaScript 中的并没有提供像 VBScript 里的 DateAdd 方法用于日期的操作,像加一年,减一个月什么的,这在服务端经常用到,比如设置 Cookie 的到期时间为现在时间的后一年,那么就需要使用这个方法了。

                    虽然 JavaScript 中没有 DateAdd 方法,但是 Date 类却有设置年月日时分秒的方法,比如 setFullYear、setMonth 之类的,而且,这些个方法的参数是可以为负的,在设置之后, Date 类会自行进行调整,每个月是30天还是31天,年份是不是闰年都不用我们来管了,只管设置值就是。

                    有了这个特性之后,我们就可以很方便的来为 Date 类添加 add 方法了。之所以不添加一个 DateAdd 方法而是给 Date 类添加一个 add 方法是因为我觉得那样更加方便,当然你也可以将 Date 类的 add 方法转换成为一个全局函数 DateAdd。

                    为了对每一个 Date 类的实例都有效,这里用到了 prototype 对象。

                    在 VBScript 里的 DateAdd 方法是用一个字符串来控制所加的量是年还是月还是其他的,所以在这里我也模仿 VBScript 里的 DateAdd 方法,使用一个字符串来控制所加量对应的部分,比如 y 代表年,m 代表月。

                    最后得到的代码如下:

                    • Date.prototype.add = function(part, value) {
                    • value *= 1;
                    • if(isNaN(value)) {
                    • value = 0;
                    • }
                    • switch(part) {
                    • case “y”:
                    • this.setUTCFullYear(this.getUTCFullYear() + value);
                    • break;
                    • case “m”:
                    • this.setUTCMonth(this.getUTCMonth() + value);
                    • break;
                    • case “d”:
                    • this.setUTCDate(this.getUTCDate() + value);
                    • break;
                    • case “h”:
                    • this.setUTCHours(this.getUTCHours() + value);
                    • break;
                    • case “n”:
                    • this.setUTCMinutes(this.getUTCMinutes() + value);
                    • break;
                    • case “s”:
                    • this.setUTCSeconds(this.getUTCSeconds() + value);
                    • break;
                    • default:
                    • }
                    • }

                    代码里的 getUTCFullYear 等等也可以换成通常用的 getFullYear,因为是相对调整,所以用哪一个是没有区别的。

                    希望此文对你有所帮助。

                    xujiwei