js 中的 !! 与 ~~

在各大 js 的开源项目中,时常见到 !! 和 ~~,偶有猜对,却总不得要领。本文旨在深入剖析下这两个运算符的原理,以及使用时的利弊。

!

为了简化问题,我们首先了解下常见的逻辑非运算符 !,EcmaScript 中的定义是:

产生式 UnaryExpression : ! UnaryExpression 按照下面的过程执行:

  • expr 为解析执行 UnaryExpression 的结果
  • oldValue 为 ToBoolean(GetValue(expr))
  • 如果 oldValue 为 true, 返回 false
  • 返回 true

GetValue 处理取值的细节,例如依附于对象的属性、执行 getter 等,不再深究。重点看下 ToBoolean,它能够将各种类型的值最终转化为布尔类型。具体的规则可参考 ES5#9.2 ToBoolean 一节。

接下来的处理很简单,如果 ToBoolen 得到的结果 oldValue 是 true,那就返回 false,否则返回 true。

!!

了解了 ! 之后,!! 就很好解释了,简单来说就是:

产生式 UnaryExpression : !! UnaryExpression 按照下面的过程执行:

  • expr 为解析执行 UnaryExpression 的结果
  • 返回 ToBoolean(GetValue(expr))

是的,比 ! 的运算过程减少了两步,执行完 ToBoolean 后就直接返回了。这也是 !! 最主要的用途:将操作数转化为布尔类型。例如:

!! null // false
!! undefined // false

!! '' // false
!! 'hello' // true

!! 5 // true
!! 0 // false

!! {} // true

值得提示的一点是,!! 实际上等效于 Boolean 被当做函数调用的效果:

!!(value) === Boolean(value)

~

按位非操作符 ~ 比逻辑非操作符 ! 复杂一些,作用是将数值比特位中的 1 变成 0,0 变成 1。EcmaScript 中的定义为:

产生式 UnaryExpression : ~ UnaryExpression 按照下面的过程执行:

  • expr 为解析执行 UnaryExpression 的结果
  • oldValue 为 ToInt32(GetValue(expr))
  • 返回 oldValue 按位取反的结果

与逻辑非执行过程第二步不同,按位非调用的是 ToInt32 而不是 ToBoolean。ToInt32 的处理过程比较复杂,简化为以下四步:

  • number 为调用 ToNumber 将输入参数转化为数值类型的结果
  • 如果 number 是 NaN,+0,-0,+∞ 或者 -∞,返回 +0
  • posInt 为 sign(number) * floor(abs(number))
  • posInt 进行取模处理,转化为在 −2^31 到 2^31−1 之间的 32 位有符号整数并返回

从效果上看,ToInt32 依次做了这样几件事:

  • 类型转换,非数值类型的需要转化为数值类型
  • 特殊值处理,NaN 和 ∞ 都被转化为 0
  • 取整,如果是浮点数,会损失小数点后面的精度
  • 取模,将整数调整到 32 位有符号整数区间内,如果整数原本不在这个区间,会丧失精度

执行完 ToInt32 之后,将得到的 32 位有符号整数进行按位取反,并将结果返回。

需要注意的是,所有的位操作都会先将操作数转化为 32 位有符号整数。

~~

和 !! 与 ! 的关系类似,~~ 实际上是 ~ 的简化版:

产生式 UnaryExpression : ~~ UnaryExpression 按照下面的过程执行:

  • expr 为解析执行 UnaryExpression 的结果
  • 返回 ToInt32(GetValue(expr))

因为第一次执行 ~ 时已经将操作数转化为 32 位有符号整数,第二次执行 ~ 时实际只是将按位取反的结果再次按位取反,相当于取消掉 ~ 处理过程中的第三步。那么 ~~ 的用途也就很明确了:将操作数转化为 32 位有符号整数

下面来看一些具体例子:

~~ null // 0
~~ undefined // 0
~~ NaN // 0
~~ {} // 0
~~ true // 1
~~ '' // 0
~~ 'string' // 0
~~ '1' // 1
~~ Number.POSITIVE_INFINITY // 0

~~ 1.2 // 1
~~ -1.2 // -1
~~ 1.6 // 1
~~ -1.6 // -1
~~ (Math.pow(2, 31) - 1) // 2147483647 = 2^31-1
~~ (Math.pow(2, 31)) // -2147483648 = -2^31
~~ (-Math.pow(2, 31)) // -2147483648 = -2^31
~~ (-Math.pow(2, 31) - 1) // 2147483647 = 2^31-1
~~ (Math.pow(2, 32)) // 0

如果你需要将一个参数转化为 32 位有符号整数,那么 ~~ 将是最简便的方式。不过要切记,它会损失精度,包括小数和整数部分。

参考

前端组件化开发实践

前言

一位计算机前辈曾说过:

Controlling complexity is the essence of computer programming.

随着前端开发复杂度的日益提升,组件化开发应运而生,并随着 FIS、React 等优秀框架的出现遍地开花。这一过程同样发生在美团,面临业务规模的快速发展和工程师团队的不断扩张,我们历经引入组件化解决资源整合问题、逐步增强组件功能促进开发效率、重新打造新一代组件化方案适应全栈开发和共享共建等阶段,努力“controlling complexity”。本文将介绍我们组件化开发的实践过程。

组件化 1.0:资源重组

在美团早期,前端资源是按照页面或者类似业务页面集合的形式进行组织的。例如 order.js 对应订单相关页面的交互,account.css 对应账户相关页面的样式。这种方式在过去的较长一段时间内,持续支撑了整个项目的正常推进,功勋卓著。

legacy-flow

随着业务规模的增加和开发团队的扩张,这套机制逐渐显示出它的一些不足:

  • 资源冗余

    页面的逐渐增加,交互的逐渐复杂化,导致对应的 css 和 js 都有大幅度增长,进而出现为了依赖某个 js 中的一个函数,需要加载整个模块,或者为了使用某个 css 中的部分样式依赖整个 css,冗余资源较多

  • 对应关系不直观

    没有显而易见的对应规则,导致的一个问题是修改某个业务模块的 css 或者 js 时,几乎只能依靠 grep。靠人来维护页面模块 html、css 和 js 之间的依赖关系,容易犯错,常常出现内容已经删除但是 css 或 js 还存在的问题

  • 难于单元测试

    以页面为最小粒度进行资源整合,不同功能的业务模块相互影响,复杂度太高,自动化测试难以推进

2013 年开始,在调研了 FIS、BEM 等方案之后,结合美团开发框架的实际,我们初步实现了一套轻量级的组件化开发方案。主要的改进是:

  • 以页面功能组件为单位聚合前端资源
  • 自动加载符合约定的 css、js 资源
  • 将业务数据到渲染数据的转换过程独立出来
component-flow

举例来说,美团顶部的搜索框就被实现为一个组件。

smart-box

代码构成:

www/component/smart-box/
├── smart-box.js    # 交互
├── smart-box.php   # 渲染数据生产、组件配置
├── smart-box.scss  # 样式
├── smart-box.tpl   # 内容
└── test
    ├── default.js  # 自动化测试
    └── default.php # 单测页面

调用组件变得十足简单:

echo View::useComponent('smart-box', [
    'keyword' => $keyword
]);

对比之前,可以看到组件化的一些特点:

  • 按需加载

    只加载必要的前端资源

  • 对应关系非常清晰

    组件所需要的前端资源都在同一目录,职责明确且唯一,对应关系显著

  • 易于测试

    组件是具备独立展现和交互的最小单元,可利用 Phantom 等工具自动化测试

此外,由于前端资源集中进行调度,组件化也为高阶性能优化提供了空间。例如实现组件级别的 BigRender、通过数据分析进行资源的合并加载等等。

组件化 2.0:趋于成熟

组件化 1.0 上线后,由于简单易用,很快得到工程师的认可,并开始在各项业务中应用起来。新的需求接踵而来,一直持续到 2014 年底,这个阶段我们称之为组件化 2.0。下面介绍下主要的几个改进。

Lifecycle

组件在高内聚的同时,往往需要暴露一些接口供外界调用,从而能够适应复杂的页面需求,例如提交订单页面需要在支付密码组件启动完成后绑定提交时的检查。Web Components、React 等都选择了生命周期事件/方法,我们也是一样。

组件的生命周期:

component-lifecycle

一个组件的完整生命周期包括:

  • init,初始化组件根节点和配置
  • fetch,加载 css 和 js 资源
  • render,内容渲染,默认的渲染内容方式是 BigRender
  • ready,进行数据绑定等操作
  • update,数据更新
  • destroy,解除所有事件监听,删除所有组件节点

组件提供 pause、resume 方法以方便进行生命周期控制。各个阶段使用 Promise 串行进行,异步的管理更清晰。使用自定义语义事件,在修改默认行为、组件间通信上充分利用了 YUI 强大的自定义事件体系,有效降低了开发维护成本。

举个例子,页面初始化时组件的启动过程实际也是借助生命周期实现的:

var afterLoadList = [];
Y.all('[data-component]').each(function (node) {
    var component = new Y.mt.Component(node);
    // 绑定 init 生命周期事件,在 init 默认行为完成后执行回调
    component.after('init', function (e) {
        // 如果配置了延迟启动
        if (e.config.afterLoad) {
            // 暂停组件生命周期
            e.component.pause();
            // 压入延迟启动数组
            afterLoadList.push(e.component);
        }
    });
    // 开始进入生命周期
    component.start();
});

Y.on('load', function () {
    // 在页面 load 事件发生时恢复组件生命周期
    afterLoadList.forEach(function (component) {
        component.resume();
    });
});

回过头来看,引入生命周期除了带来扩展性外,更重要的是理顺了组件的各个阶段,有助于更好的理解和运用。

Data Binding

数据绑定是我们期盼已久的功能,将 View 和 ViewModel 之间的交互自动化无疑会节省工程师的大量时间。在组件化减少关注点和降低复杂度后,实现数据绑定变得更加可能。

我们最终实现的数据绑定方案主要参考了 Angular,通过在 html 节点上添加特定的属性声明绑定逻辑,js 扫描这些内容并进行相应的渲染和事件绑定。当数据发生变化时,对应的内容全部重新渲染。

<ul class="addressList">
    <li
        mt-bind-repeat="addr in addrList"
        mt-bind-html="addr.text"
    >
    </li>
</ul>

<script>
Y.use(['mt-bind', 'mt-scope'], function () {
    Y.mt.bind.init(document.body);
    var scope = Y.one('.addressList').getScope();
    // 将 scope.addrList 设置为一个数组,DOM 上将自动渲染其内容   
    scope.$set('addrList', [
        { text: "first address" },
        { text: "second address" }
    ]);
});
</script>

使用属性声明绑定逻辑的好处是可以同时支持后端渲染,这对于美团团购这样的偏展现型业务是非常必要的,用户可以很快看到页面内容。

Flux

实现数据绑定后,我们不得不面对另外一个问题:如何协同多个组件间的数据。因为某个组件的数据变化,很有可能引起其他组件的变化。例如当修改购买数量,总金额会变化,而总金额超过 500 后,还需要展示大额消费提醒。

为了解决这个问题,我们引入了 Flux,使用全局消息总线的思路进行跨组件交互。

例如因为交互复杂而一直让我们非常头疼的项目购买页,在应用组件 + Flux 重构后,各模块之间的互动更加清晰:

component-flux

其他方面的改进还有很多,包括引入模板引擎 LightnCandy 约束模板逻辑、支持组件任意嵌套、支持异步加载并自动初始化等。

随着组件化 2.0 的逐步完善,基本已经可以从容应对日常开发,在效率和质量方面都上了一个台阶。

组件化 3.0:重启征程

时间的车轮滚滚前行,2014 年底,我们遇到一些新的机遇和挑战:

  • 基于 Node 的全栈开发模式开始应用,前后端渲染有了更多的可能性
  • YUI 停止维护,需要一套新的资源管理方案
  • 新业务不断增加,需要找到一种组件共享的方式,避免重复造轮子

结合之前的实践,以及在这一过程中逐渐积累的对业内方案的认知,我们提出了新的组件化方案:

  • 基于 React 开发页面组件,使用 NPM 进行分发,方便共建共享
  • 基于 Browserify 二次开发,建设资源打包工具 Reduce,方便浏览器加载
  • 建设适应组件化开发模式的工程化开发方案 Turbo,方便工程师将组件应用于业务开发中

React

在组件化 2.0 的过程中,我们发现很多功能和 React 重合,例如 Data Binding、Lifecycle、前后端渲染,甚至直接借鉴的 Flux。除此之外,React 的函数式编程思想、增量更新、兼容性良好的事件体系也让我们非常向往。借着前端全栈开发的契机,我们开始考虑基于 React 进行组件化 3.0 的建设。

NPM + Reduce

NPM + Reduce 构成了我们新的资源管理方案,其中:

  • NPM 负责组件的发布和安装。可以认为是“分”的过程,粒度越小,重用的可能性越大
  • Reduce 负责将页面资源进行打包。可以认为是“合”的过程,让浏览器更快地加载

一个典型的组件包:

smart-box/
├── package.json    # 组件包元信息
├── smart-box.jsx   # React Component
├── smart-box.scss  # 样式
└── test
    └── main.js     # 测试

NPM 默认只支持 js 文件的管理,我们对 NPM 中的 package.json 进行了扩展,增加了 style 字段,以使打包工具 Reduce 也能够对 css 和 css 中引用的 image、font 进行识别和处理:

{
    "style": "./smart-box.scss"
}

只要在页面中 require 了 smart-box,经过 Reduce 打包后,js、css 甚至图片、字体,都会出现在浏览器中。

var SmartBox = require('@mtfe/smart-box');
// 页面
var IndexPage = React.createClass({
    render: function () {
        return (
            <Header>
                <SmartBox keyword={ this.props.keyword } />
            </Header>
            ...
        );
    }
});
module.exports = IndexPage;

整体思路和组件化 1.0 如出一辙,却又那么不同。

Turbo

单单解决分发和打包的问题还不够,业务开发过程如果变得繁琐、难以 Debug、性能低下的话,恐怕不会受到工程师欢迎。

为了解决这些问题,我们在 Node 框架的基础上,提供了一系列中间件和开发工具,逐步构建对组件友好的前端工程化方案 Turbo。主要有:

  • 支持前后端同构渲染,让用户更早看到内容
  • 简化 Flux 流程,数据流更加清晰易维护
  • 引入 ImmutableJS,保证 Store 以外的数据不可变
  • 采用 cursor 机制,保证数据修改/获取同步
  • 支持 Hot Module Replacement,改进开发流自动化

通过这些改进,一线工程师可以方便的使用各种组件,专注在业务本身上。开发框架层面的支持也反过来促进了组件化的发展,大家更乐于使用一系列组件来构建页面功能。

小结

发现痛点、分析调研、应用改进的解决问题思路在组件化开发实践中不断运用。历经三个大版本的演进,组件化开发模式有效缓解了业务发展带来的复杂度提升的压力,并培养工程师具备小而美的工程思想,形成共建共享的良好氛围。毫无疑问,组件化这种“分而治之”的思想将会长久地影响和促进前端开发模式。我们现在已经准备好,迎接新的机遇和挑战,用技术的不断革新提升工程师的幸福感。

剖析 Promise 之基础篇

随着浏览器端异步操作的复杂程度日益增加,以及以 Evented I/O 为核心思想的 NodeJS 的火爆,Promise、Async 等异步操作封装由于解决了异步编程上面临的诸多挑战,得到了飞速发展。本文旨在剖析 Promise 的内部机制,从实现原理层面深入探讨,从而达到“知其然且知其所以然”,在使用 Promise 上更加熟练自如。如果你还不太了解 Promise,推荐阅读下 promisejs.org 的介绍。

是什么

Promise 是一种对异步操作的封装,可以通过独立的接口添加在异步操作执行成功、失败时执行的方法。主流的规范是 Promises/A+

Promise 较通常的回调、事件/消息,在处理异步操作时具有显著的优势。其中最为重要的一点是:Promise 在语义上代表了异步操作的主体。这种准确、清晰的定位极大推动了它在编程中的普及,因为具有单一职责,而且将份内事做到极致的事物总是具有病毒式的传染力。分离输入输出参数、错误冒泡、串行/并行控制流等特性都成为 Promise 横扫异步操作编程领域的重要砝码,以至于 ES6 都将其收录,并已在 Chrome、Firefox 等现代浏览器中实现。

内部机制

自从看到 Promise 的 API,我对它的实现就充满了深深的好奇,一直有心窥其究竟。接下来,将首先从最简单的基础实现开始,由浅入深的逐步探索,剖析每一个 feature 后面的故事。

为了让语言上更加准确和简练,本文做如下约定:

  • Promise:代表由 Promises/A+ 规范所定义的异步操作封装方式;
  • promise:代表一个 Promise 实例。

基础实现

为了增加代入感,本文从最为基础的一个应用实例开始探索:通过异步请求获取用户id,然后做一些处理。在平时大家都是习惯用回调或者事件来处理,下面我们看下 Promise 的处理方式:

// 例1

function getUserId() {
    return new Promise(function (resolve) {
        // 异步请求
        Y.io('/userid', {
            on: {
                success: function (id, res) {
                    resolve(JSON.parse(res).id);
                }
            }
        });
    });
}

getUserId().then(function (id) {
    // do sth with id
});

JS Bin

查看全文
YUI经验谈-自定义事件默认行为

纵观主流JS库和框架,YUI在自定义事件方面做的尤为出色。如果需要挑出一个代表性的feature,那么非事件默认行为莫属。

是什么

YUI自定义事件在总体上模仿了DOM事件的设计思想。DOM中的一些事件是有默认行为的,详细见DOM3 Event - Default actions and cancelable events一节。简单来说,所谓默认行为,是指该事件在通常情况下所表现出来的动作,例如:

  • 一个链接节点的click事件,默认行为是转向该链接href属性对应的地址
  • 表单的submit事件,默认行为是将表单包含的数据提交给表单的action

说通常情况下,是因为有时开发者会在事件的回调函数中调用

e.preventDefault();

来阻止默认行为的发生。

查看全文
[译]优秀与伟大程序员的区别

在程序员的职业生涯中,总有一些理念让你眼前一亮,犹如夜空中一颗颗明亮的星,指引你不断前行。Quora上Russel Simmons的这个答案就是曾让我深受触动的三个理念,翻译出来给E文一般的童鞋。E文好的童鞋请移步,原著更加准确生动些。

原文链接:Software Engineering: What distinguishes a good software engineer from a great one?

我并不打算给出一个全面的答案,不过我已注意到几个伟大程序员具备却不常被提及的特质:

  • 能够平衡实效与完美。伟大工程师具备进行娴熟、快速而粗略的hack,和设计优雅、精炼、健壮的解决方案的两种能力,以及,根据给定问题选择恰当方式的智慧。一些稍普通的程序员们看起来缺乏对问题关键细节的极致关注,另一些人则太过坚持完美主义。
  • 不排斥调试代码和修复bug。平庸的程序员害怕、厌恶调试,甚至对自己的代码也是如此。伟大程序员会以丘吉尔般顽强的精神潜心钻研。如果问题被证明不在他们的代码中,伟大程序员会有些不愉快,他们会最终找出问题所在。
  • 合理质疑。优秀程序员会找到看似可行的解决办法并一直沿用。伟大程序员则倾向于质疑自己的代码,直到测试完备。而这一过程往往需要大量的数据分析和系统管理。一般程序员可能看到一个似乎没什么危害的细微异常,然后忽略掉了它。如果换作伟大程序员,他们会怀疑这可能是一个更大问题的线索,并投入精力研究透彻。伟大程序员乐于做更多的交叉校验和完备性检查,通过这种方式发现隐含的问题。
YUI3在美团的实践

美团网在2010年引爆了团购行业,并在2012年销售额超过55亿,实现了全面盈利。在业务规模不断增长的背后,作为研发队伍中和用户最接近的前端团队承担着非常大的压力,比如用户量急剧上升带来的产品多样化,业务运营系统的界面交互日益复杂,代码膨胀造成维护成本增加等等。面对这些挑战,我们持续改进前端技术架构,在提升用户体验和工作效率的同时,成功支撑了美团业务的快速发展,这一切都得益于构建在YUI3框架之上稳定高效的前端代码。在应用YUI3的过程中,我们团队积累了一些经验,这里总结成篇,分享给大家。

为什么选择YUI3

使用什么前端基础框架是建立前端团队最重要的技术决策之一。美团项目初期因为要加快开发进度,选择了当时团队最熟悉的YUI2(前框架时代杰出的类库),保证美团能够更快更早地上线,抢占市场先机。不久由于前端技术发展很快,YUI2的缺点逐渐凸显,例如开发方式落后、影响工作效率等等,于是我们开始考虑基础库的迁移。

经过一段时间对主流前端库、框架的反复考量,我们认为YUI3是最适合我们团队使用的基础框架。

首先,国内的开源框架及其社区刚开始起步,在代码质量、架构设计和理念创新上还难以跟YUI3比肩,所以基本排除在外。其次,国外像YUI3这样面向用户产品、文档丰富、扩展性良好的成熟框架屈指可数,例如ExtJS和Dojo则更适合业务复杂的传统企业级开发。最后,使用jQuery这种类库构建同YUI3一样强大的框架对创业团队来说并不可取,美团快速发展、竞争多变的业务特点决定了我们必须把主要精力放在更高一层的业务开发上,而不是去重复发明一个蹩脚的YUI。

YUI3成为最终选择有以下几个直接的原因:

  • 非常优秀,是真正的框架,真正的重型武器,具有强劲的持续开发能力,可以应对业务的快速发展。不管是规模不断增长的用户产品,还是交互日趋复杂的业务系统(美团有超过100个业务系统作全电子化的运营支撑),YUI3都游刃有余。
  • 代码整齐规范,容易维护,适合有洁癖的工程师,同时能够显著提高团队协作时的开发效率。因为人手紧缺,后端工程师也需要参与前端开发,一致的代码风格使前后端配合轻松简单。
  • 有出色的架构设计,是很好的框架范本,通过研究学习可以帮助工程师成长,培养良好的工程思维。人是美团最重要的产品。

随着团队成长,我们最后引入了YUI3,在迁移过程中,遇到了很多技术上的和工程上的挑战,但是我们一直在前进,一直在行进中开火。从结果来看,YUI3为我们团队提供了先进生产力,为快速开发、快速部署、快速迭代提供了源源不断的力量。

YUI3的优秀主要表现在模块和组件框架的出色设计,下面我们着重介绍这两方面的一些实践经验。

查看全文
YUI事件体系之Y.delegate

relay baton

在介绍了YUI自定义事件体系和对DOM事件的封装后,本篇文章重点阐述事件方面的一种常用技术——事件代理。事件代理(Event Delegation,又称事件委托)充分运用事件传播模型,用一种十分优雅的方式实现了批量节点事件监听。具体的原理和优点请移步zakas比较古老的一篇文章Event delegation in JavaScript。事件代理在YUI中的实现为Y.delegate

基本用法

为方便讨论,约定以下名称:

  • 代理节点:实际监听事件的节点。在事件传播到此节点时判断是否符合代理条件,符合则执行回调函数。
  • 被代理节点:希望监听事件的节点。如果不采用事件代理,那么应该直接监听这些节点的事件。
  • 目标节点:事件发生的目标节点,即event.target

三者的层次关系从内到外依次为:目标节点 <= 被代理节点 <= 代理节点。

查看全文
YUI事件体系之Y.Event

mouse event

在介绍了由Y.DoY.CustomEventY.EventTarget构建的自定义事件体系后,本篇文章将为大家介绍建立在这一体系之上,YUI对DOM事件的封装——Y.Event

Y.DOMEventFacade

众所周知,浏览器之间存在大量的不兼容问题,在事件方面尤其如此。Y.DOMEventFacade主要用来处理DOM事件对象的浏览器兼容问题,提供跨浏览器的简洁接口。事实上,我们常在Y.one('.selector').on('click', function (e) {})中使用的e就是Y.DOMEventFacade的实例。

具体来说,兼容处理的属性主要有:

  • target,专门处理了target为文本节点的情况,统一为元素节点,方便操作
  • relativeTarget,关联目标节点,在mouseover/mouseout等事件中设置
  • keyCode/charCode等输入信息
  • pageX/clientX等位置信息

兼容处理的方法主要有:

  • stopPropagation/stopImmediatePropagation,不支持停止立即传播时,仅能在YUI层面模拟,不会阻止通过原生方法添加的同层回调,即,在YUI监听过el的click事件后,又通过el.addEventListener('click', nativeCallback)监听,如果在YUI的回调中调用e.stopImmediatePropagation的话,nativeCallback仍然会执行
  • preventDefault

另外,为了方便同时停止传播和阻止默认行为,YUI还提供了halt方法。

查看全文
Proudly powered by Express. Designed by Spring.