Cam

Cam 查看完整档案

杭州编辑  |  填写毕业院校阿里巴巴  |  前端 编辑 github.com/camsong 编辑
编辑

React.js, Redux, Relay, Ruby, Rails advocatoR. 是不是有 R 的都显得很牛逼

个人动态

Cam 赞了文章 · 2019-02-14

React 最佳实践

欢迎关注我的公众号睿Talk,获取我最新的文章:
clipboard.png

一、前言

在日常开发和 Code Review 的时候,常常会发现一些共性的问题,也有很多值得提倡的做法。本文针对 React 技术栈,总结了一些最佳实践,对编写高质量的代码有一定的参考作用。

二、最佳实践 & 说明

  • 多用 Function Component

如果组件是纯展示型的,不需要维护 state 和生命周期,则优先使用 Function Component。它有如下好处:

  1. 代码更简洁,一看就知道是纯展示型的,没有复杂的业务逻辑
  2. 更好的复用性。只要传入相同结构的 props,就能展示相同的界面,不需要考虑副作用。
  3. 更小的打包体积,更高的执行效率

一个典型的 Function Component 是下面这个样子:

function MenuItem({menuId, menuText, onClick, activeId}) {
    return (
        <div
            menuId={menuId}
            className={`${style} ${activeId === menuId ? active : ''}`}
            onClick={onItemClick}
        >
            {menuText}
        </div>
    );
};
  • 多用 PureComponent

如果组件需要维护 state 或使用生命周期方法,则优先使用 PureComponent,而不是 Component。Component 的默认行为是不论 state 和 props 是否有变化,都触发 render。而 PureComponent 会先对 state 和 props 进行浅比较,不同的时候才会 render。请看下面的例子:

class Child extends React.Component {
  render() {
    console.log('render Child');
    return (
      <div>
        {this.props.obj.num}
      </div>
    );
  }
}

class App extends React.Component {
  state = {
    obj: { num: 1 }
  };

  onClick = () => {
    const {obj} = this.state;
    this.setState({obj});
  }

  render() {
    console.log('render Parent');
    return (
      <div className="App" >
        <button onClick={this.onClick}>
          点我
        </button>
        <Child obj={this.state.obj}/>
      </div>
    );
  }
}

点击按钮后,Parent 和 Child 的 render 都会触发。如果将 Child 改为 PureComponent,则 Child 的 render 不会触发,因为 props 还是同一个对象。如果将 Parent 也改为 PureComponent,则 Parent 的 render 也不会触发了,因为 state 还是同一个对象。

  • 遵循单一职责原则,使用 HOC / 装饰器 / Render Props 增加职责

比如一个公用的组件,数据来源可能是父组件传过来,又或者是自己主动通过网络请求获取数据。这时候可以先定义一个纯展示型的 Function Component,然后再定义一个高阶组件去获取数据:

function Comp() {
    ...
}

class HOC extends PureComponent {

    async componentDidMount() {
        const data = await fetchData();
        this.setState({data});
    }
    
    render() {
        return (<Comp data={this.state.data}/>);
    }
}
  • 组合优于继承

笔者在真实项目中就试过以继承的形式写组件,自己写得很爽,代码的复用性也很好,但最大的问题是别人看不懂。我将复用的业务逻辑和 UI 模版都在父类定义好,子类只需要传入一些参数,然后再覆盖父类的几个方法就好(render的时候会用到)。简化的代码如下:

class Parent extends PureComponent {
    componentDidMount() {
        this.fetchData(this.url);
    }
    
    fetchData(url) {
        ...
    }
    
    render() {
        const data = this.calcData();
        return (
            <div>{data}</data>
        );
    }
}

class Child extends Parent {
    constructor(props) {
        super(props);
        this.url = 'http://api';
    }
    
    calcData() {
        ...
    }
}

这样的写法从语言的特性和功能实现来说,没有任何问题,最大的问题是不符合 React 的组件编写习惯。父类或者子类肯定有一方是不需要实现 render 方法的,而一般我们看代码都会优先找 render 方法,找不到就慌了。另外就是搞不清楚哪些方法是父类实现的,哪些方法是子类实现的,如果让其他人来维护这份代码,会比较吃力。

继承会让代码难以溯源,定位问题也比较麻烦。所有通过继承实现的组件都可以改写为组合的形式。上面的代码就可以这样改写:

class Parent extends PureComponent {
    componentDidMount() {
        this.fetchData(this.props.url);
    }
    
    fetchData(url) {
        ...
    }
    
    render() {
        const data = this.props.calcData(this.state);
        return (
            <div>{data}</data>
        );
    }
}

class Child extends PureComponent {
    calcData(state) {
        ...
    }
    
    render() {
        <Parent url="http://api" calcData={this.calcData}/>
    }
}

这样的代码是不是看起来舒服多了?

  • 如果 props 的数据不会改变,就不需要在 state 或者组件实例属性里拷贝一份

经常会看见这样的代码:

componentWillReceiveProps(nextProps) {
    this.setState({num: nextProps.num});
}

render() {
    return(
        <div>{this.state.num}</div>
    );
}

num 在组件中不会做任何的改变,这种情况下直接使用 this.props.num 就可以了。

  • 避免在 render 里面动态创建对象 / 方法,否则会导致子组件每次都 render

render() {
    const obj = {num: 1}
    
    return(
        <Child obj={obj} onClick={()=>{...}} />
    );
}

在上面代码中,即使 Child 是 PureComponent,由于 obj 和 onClick 每次 render 都是新的对象,Child 也会跟着 render。

  • 避免在 JSX 中写复杂的三元表达式,应通过封装函数或组件实现

render() {
    const a = 8;
    
    return (
        <div>
            {
                a > 0 ? a < 9 ? ... : ... : ...
            }
        </div>
    );    
}

像上面这种嵌套的三元表达式可读性非常差,可以写成下面的形式:

f() {
    ...
}

render() {
    const a = 8;
    
    return (
        <div>
            {
                this.f()
            }
        </div>
    );    
}
  • 多使用解构,如 Function Component 的 props

const MenuItem = ({
    menuId, menuText, onClick, activeId,
}) => {
    return (
        ...
    );
};
  • 定义组件时,定义 PropTypes 和 defaultProps

例子如下:

class CategorySelector extends PureComponent {
    ...
}

CategorySelector.propTypes = {
    type: PropTypes.string,
    catList: PropTypes.array.isRequired,
    default: PropTypes.bool,
};

CategorySelector.defaultProps = {
    default: false,
    type: undefined,
};
  • 避免使用无谓的标签和样式

下面这种情况一般外层的div是多余的,可以将样式直接定义在组件内,或者将定制的样式作为参数传入。例外:当ServiceItem需要在多个地方使用,而且要叠加很多不一样的样式,原写法会方便些。

// bad
<div key={item.uuid} className={scss.serviceItemContainer}>
    <ServiceItem item={item} />
</div>

// good
<ServiceItem key={item.uuid} item={item} className={customStyle} />

三、总结

本文列举了笔者在项目实战和 Code Review 过程中总结的 10 条最佳实践,当中的一些写法和原则只代表个人立场。理解并遵循这些最佳实践,写出来的代码质量会有一定的保证。如果你有不同的意见,或者有补充的最佳实践,欢迎留言交流。

查看原文

赞 54 收藏 34 评论 2

Cam 回答了问题 · 2018-12-11

有谁知道阿里云.数加提供的产品: BI 报表中的仪表盘?

楼主说的是 QuickBI(https://bi.aliyun.com) 吧,QuickBI 仪表板中的图表种类和功能非常丰富。

clipboard.png

简单看了一下,像图中线柱面地图等常见图表,楼主要徒手实现这些效果还是比较复杂的。
首先你需要有前端开发基础知识 HTML/JS/CSS,然后对Canvas/SVG/WebGL至少一种绘图技术有比较细致了解。但即使你掌握了这些,自己开发效率还是比较低的,你可以基于D3/G2库来帮你快速开发。至于动画等领域需要再钻研。拖拽学一下 HTML Drag and Drop API 就能大概知道原理。

还有一个方法就是直接参考最业界流行的图表库源码,依葫芦画瓢,比如目前star数最高的canvas图库 Chart.js,希望这篇文章能帮到你。
图表库源码剖析 - Chart.js 最流行的 Canvas 图表库

关注 2 回答 1

Cam 发布了文章 · 2016-01-13

CSS Modules详解及React中实践

CSS Modules

CSS 是前端领域中进化最慢的一块。由于 ES2015/2016 的快速普及和 Babel/Webpack 等工具的迅猛发展,CSS 被远远甩在了后面,逐渐成为大型项目工程化的痛点。也变成了前端走向彻底模块化前必须解决的难题。

CSS 模块化的解决方案有很多,但主要有两类。一类是彻底抛弃 CSS,使用 JS 或 JSON 来写样式。Radiumjsxstylereact-style 属于这一类。优点是能给 CSS 提供 JS 同样强大的模块化能力;缺点是不能利用成熟的 CSS 预处理器(或后处理器) Sass/Less/PostCSS,:hover:active 伪类处理起来复杂。另一类是依旧使用 CSS,但使用 JS 来管理样式依赖,代表是 CSS Modules。CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力,API 简洁到几乎零学习成本。发布时依旧编译出单独的 JS 和 CSS。它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular/jQuery 中使用。是我认为目前最好的 CSS 模块化解决方案。近期在项目中大量使用,下面具体分享下实践中的细节和想法。

CSS 模块化遇到了哪些问题?

CSS 模块化重要的是要解决好两个问题:CSS 样式的导入和导出。灵活按需导入以便复用代码;导出时要能够隐藏内部作用域,以免造成全局污染。Sass/Less/PostCSS 等前仆后继试图解决 CSS 编程能力弱的问题,结果它们做的也确实优秀,但这并没有解决模块化最重要的问题。Facebook 工程师 Vjeux 首先抛出了 React 开发中遇到的一系列 CSS 相关问题。加上我个人的看法,总结如下:

  1. 全局污染
    CSS 使用全局选择器机制来设置样式,优点是方便重写样式。缺点是所有的样式都是全局生效,样式可能被错误覆盖,因此产生了非常丑陋的 !important,甚至 inline !important 和复杂的选择器权重计数表,提高犯错概率和使用成本。Web Components 标准中的 Shadow DOM 能彻底解决这个问题,但它的做法有点极端,样式彻底局部化,造成外部无法重写样式,损失了灵活性。

  2. 命名混乱
    由于全局污染的问题,多人协同开发时为了避免样式冲突,选择器越来越复杂,容易形成不同的命名风格,很难统一。样式变多后,命名将更加混乱。

  3. 依赖管理不彻底
    组件应该相互独立,引入一个组件时,应该只引入它所需要的 CSS 样式。但现在的做法是除了要引入 JS,还要再引入它的 CSS,而且 Saas/Less 很难实现对每个组件都编译出单独的 CSS,引入所有模块的 CSS 又造成浪费。JS 的模块化已经非常成熟,如果能让 JS 来管理 CSS 依赖是很好的解决办法。Webpack 的 css-loader 提供了这种能力。

  4. 无法共享变量
    复杂组件要使用 JS 和 CSS 来共同处理样式,就会造成有些变量在 JS 和 CSS 中冗余,Sass/PostCSS/CSS 等都不提供跨 JS 和 CSS 共享变量这种能力。

  5. 代码压缩不彻底
    由于移动端网络的不确定性,现在对 CSS 压缩已经到了变态的程度。很多压缩工具为了节省一个字节会把 '16px' 转成 '1pc'。但对非常长的 class 名却无能为力,力没有用到刀刃上。

上面的问题如果只凭 CSS 自身是无法解决的,如果是通过 JS 来管理 CSS 就很好解决,因此 Vjuex 给出的解决方案是完全的 CSS in JS,但这相当于完全抛弃 CSS,在 JS 中以 Object 语法来写 CSS,估计刚看到的小伙伴都受惊了。直到出现了 CSS Modules。

CSS Modules 模块化方案

CSS Modules 内部通过 ICSS 来解决样式导入和导出这两个问题。分别对应 :import:export 两个新增的伪类。

:import("path/to/dep.css") {
  localAlias: keyFromDep;
  /* ... */
}
:export {
  exportedKey: exportedValue;
  /* ... */
}

但直接使用这两个关键字编程太麻烦,实际项目中很少会直接使用它们,我们需要的是用 JS 来管理 CSS 的能力。结合 Webpack 的 css-loader 后,就可以在 CSS 中定义样式,在 JS 中导入。

启用 CSS Modules

// webpack.config.js
css?modules&localIdentName=[name]__[local]-[hash:base64:5]

加上 modules 即为启用,localIdentName 是设置生成样式的命名规则。

/* components/Button.css */
.normal { /* normal 相关的所有样式 */ }
.disabled { /* disabled 相关的所有样式 */ }
/* components/Button.js */
import styles from './Button.css';

console.log(styles);

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

生成的 HTML 是

<button class="button--normal-abc53"> Processing... </button>

注意到 button--normal-abc5436 是 CSS Modules 按照 localIdentName 自动生成的 class 名。其中的 abc5436 是按照给定算法生成的序列码。经过这样混淆处理后,class 名基本就是唯一的,大大降低了项目中样式覆盖的几率。同时在生产环境下修改规则,生成更短的 class 名,可以提高 CSS 的压缩率。

上例中 console 打印的结果是:

Object {
  normal: 'button--normal-abc546',
  disabled: 'button--disabled-def884',
}

CSS Modules 对 CSS 中的 class 名都做了处理,使用对象来保存原 class 和混淆后 class 的对应关系。

通过这些简单的处理,CSS Modules 实现了以下几点:

  • 所有样式都是 local 的,解决了命名冲突和全局污染问题

  • class 名生成规则配置灵活,可以此来压缩 class 名

  • 只需引用组件的 JS 就能搞定组件所有的 JS 和 CSS

  • 依然是 CSS,几乎 0 学习成本

样式默认局部

使用了 CSS Modules 后,就相当于给每个 class 名外加加了一个 :local,以此来实现样式的局部化,如果你想切换到全局模式,使用对应的 :global

.normal {
  color: green;
}

/* 以上与下面等价 */
:local(.normal) {
  color: green; 
}

/* 定义全局样式 */
:global(.btn) {
  color: red;
}

/* 定义多个全局样式 */
:global {
  .link {
    color: green;
  }
  .box {
    color: yellow;
  }
}

Compose 来组合样式

对于样式复用,CSS Modules 只提供了唯一的方式来处理:composes 组合

/* components/Button.css */
.base { /* 所有通用的样式 */ }

.normal {
  composes: base;
  /* normal 其它样式 */
}

.disabled {
  composes: base;
  /* disabled 其它样式 */
}
import styles from './Button.css';

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

生成的 HTML 变为

<button class="button--base-abc53 button--normal-abc53"> Processing... </button>

由于在 .normal 中 composes 了 .base,编译后会 normal 会变成两个 class。

composes 还可以组合外部文件中的样式。

/* settings.css */
.primary-color {
  color: #f40;
}

/* components/Button.css */
.base { /* 所有通用的样式 */ }

.primary {
  composes: base;
  composes: $primary-color from './settings.css';
  /* primary 其它样式 */
}

对于大多数项目,有了 composes 后已经不再需要 Sass/Less/PostCSS。但如果你想用的话,由于 composes 不是标准的 CSS 语法,编译时会报错。就只能使用预处理器自己的语法来做样式复用了。

class 命名技巧

CSS Modules 的命名规范是从 BEM 扩展而来。BEM 把样式名分为 3 个级别,分别是:

  • Block:对应模块名,如 Dialog

  • Element:对应模块中的节点名 Confirm Button

  • Modifier:对应节点相关的状态,如 disabled、highlight

综上,BEM 最终得到的 class 名为 dialog__confirm-button--highlight。使用双符号 __-- 是为了和区块内单词间的分隔符区分开来。虽然看起来有点奇怪,但 BEM 被非常多的大型项目和团队采用。我们实践下来也很认可这种命名方法。

CSS Modules 中 CSS 文件名恰好对应 Block 名,只需要再考虑 Element 和 Modifier。BEM 对应到 CSS Modules 的做法是:

/* .dialog.css */
.ConfirmButton--disabled {
}

你也可以不遵循完整的命名规范,使用 camelCase 的写法把 Block 和 Modifier 放到一起:

/* .dialog.css */
.disabledConfirmButton {
}

如何实现CSS,JS变量共享

上面提到的 :export 关键字可以把 CSS 中的 变量输出到 JS 中。下面演示如何在 JS 中读取 Sass 变量:

/* config.scss */
$primary-color: #f40;

:export {
  primaryColor: $primary-color;
}
/* app.js */
import style from 'config.scss';

// 会输出 #F40
console.log(style.primaryColor);

CSS Modules 使用技巧

CSS Modules 是对现有的 CSS 做减法。为了追求简单可控,作者建议遵循如下原则:

  • 不使用选择器,只使用 class 名来定义样式

  • 不层叠多个 class,只使用一个 class 把所有样式定义好

  • 所有样式通过 composes 组合来实现复用

  • 不嵌套

上面两条原则相当于削弱了样式中最灵活的部分,初使用者很难接受。第一条实践起来难度不大,但第二条如果模块状态过多时,class 数量将成倍上升。

一定要知道,上面之所以称为建议,是因为 CSS Modules 并不强制你一定要这么做。听起来有些矛盾,由于多数 CSS 项目存在深厚的历史遗留问题,过多的限制就意味着增加迁移成本和与外部合作的成本。初期使用中肯定需要一些折衷。幸运的是,CSS Modules 这点做的很好:

如果我对一个元素使用多个 class 呢?

没问题,样式照样生效。

如何我在一个 style 文件中使用同名 class 呢?

没问题,这些同名 class 编译后虽然可能是随机码,但仍是同名的。

如果我在 style 文件中使用了 id 选择器,伪类,标签选择器等呢?
没问题,所有这些选择器将不被转换,原封不动的出现在编译后的 css 中。也就是说 CSS Modules 只会转换 class 名相关样式。

但注意,上面 3 个“如果”尽量不要发生。

CSS Modules 结合 React 实践

className 处直接使用 css 中 class 名即可。

/* dialog.css */
.root {}
.confirm {}
.disabledConfirm {}
import classNames from 'classnames';
import styles from './dialog.css';

export default class Dialog extends React.Component {
  render() {
    const cx = classNames({
      [styles.confirm]: !this.state.disabled,
      [styles.disabledConfirm]: this.state.disabled
    });

    return <div className={styles.root}>
      <a className={cx}>Confirm</a>
      ...
    </div>
  }
}

注意,一般把组件最外层节点对应的 class 名称为 root。这里使用了 classnames 库来操作 class 名。
如果你不想频繁的输入 styles.**,可以试一下 react-css-modules,它通过高阶函数的形式来避免重复输入 styles.**

CSS Modules 结合历史遗留项目实践

好的技术方案除了功能强大炫酷,还要能做到现有项目能平滑迁移。CSS Modules 在这一点上表现的非常灵活。

外部如何覆盖局部样式

当生成混淆的 class 名后,可以解决命名冲突,但因为无法预知最终 class 名,不能通过一般选择器覆盖。我们现在项目中的实践是可以给组件关键节点加上 data-role 属性,然后通过属性选择器来覆盖样式。

// dialog.js
  return <div className={styles.root} data-role='dialog-root'>
      <a className={styles.disabledConfirm} data-role='dialog-confirm-btn'>Confirm</a>
      ...
  </div>
// dialog.css
[data-role="dialog-root"] {
  // override style
}

因为 CSS Modules 只会转变类选择器,所以这里的属性选择器不需要添加 :global

如何与全局样式共存

前端项目不可避免会引入 normalize.css 或其它一类全局 css 文件。使用 Webpack 可以让全局样式和 CSS Modules 的局部样式和谐共存。下面是我们项目中使用的 webpack 部分配置代码:

module: {
  loaders: [{
    test: /\.jsx?$/,
    loader: 'babel'
  }, {
    test: /\.scss$/,
    exclude: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css?modules&localIdentName=[name]__[local]!sass?sourceMap=true'
  }, {
    test: /\.scss$/,
    include: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css!sass?sourceMap=true'
  }]
}
/* src/app.js */
import './styles/app.scss';
import Component from './view/Component'

/* src/views/Component.js */
// 以下为组件相关样式
import './Component.scss';

目录结构如下:

src
├── app.js
├── styles
│   ├── app.scss
│   └── normalize.scss
└── views
    ├── Component.js
    └── Component.scss

这样所有全局的样式都放到 src/styles/app.scss 中引入就可以了。其它所有目录包括 src/views 中的样式都是局部的。

总结

CSS Modules 很好的解决了 CSS 目前面临的模块化难题。支持与 Sass/Less/PostCSS 等搭配使用,能充分利用现有技术积累。同时也能和全局样式灵活搭配,便于项目中逐步迁移至 CSS Modules。CSS Modules 的实现也属轻量级,未来有标准解决方案后可以低成本迁移。如果你的产品中正好遇到类似问题,非常值得一试。

原发于知乎专栏 http://zhuanlan.zhihu.com/purerender/20495964

查看原文

赞 22 收藏 100 评论 5

Cam 发布了文章 · 2015-12-01

抛弃jQuery,拥抱原生JavaScript

前端发展很快,现代浏览器原生 API 已经足够好用。我们并不需要为了操作 DOM、Event 等再学习一下 jQuery 的 API。同时由于 React、Angular、Vue 等框架的流行,直接操作 DOM 不再是好的模式,jQuery 使用场景大大减少。因此我们项目组在双十一后抽了一周时间,把所有代码中的 jQuery 移除。下面总结一下:

Why not jQuery

1. 模式变革

jQuery 代表着传统的以 DOM 为中心的开发模式,但现在复杂页面开发流行的是以 React 为代表的以数据/状态为中心的开发模式

应用复杂后,直接操作 DOM 意味着手动维护状态,当状态复杂后,变得不可控。React 以状态为中心,自动帮我们渲染出 DOM,同时通过高效的 DOM Diff 算法,也能保证性能。我们在 React 应用实践中也发现,大部分当你想直接操作 DOM 的时候,就意味着你可能做错了。

2. 不支持同构渲染

重构就是前后端运行同一份代码,后端也可以渲染出页面,这对 SEO 要求高的场景非常合适。由于 React 等流行框架天然支持,已经具有可行性。当我们在尝试把现有应用改成同构时,因为代码要运行在服务器端,但服务器端没有 DOM,所以引用 jQuery 就会报错。这也是要移除 jQuery 的迫切原因。同时不但要移除 jQuery,在很多场合也要避免直接操作 DOM。

3. 原生 API 足够好用

由于浏览器的历史原因,曾经的前端开发为了兼容不同浏览器怪癖,需要增加很多成本。jQuery 由于提供了非常易用的 API,屏蔽了浏览器差异,极大地提高了开发效率。这也导致很多前端只懂 jQuery。其实这几年浏览器更新很快,也借鉴了很多 jQuery 的 API,如 querySelectorquerySelectorAll 和 jQuery 选择器同样好用,而且性能更优。

4. 性能

前端开发一般不需要考虑性能问题,但你想在性能上追求极致的话,一定要知道 jQuery 性能很差。原生 API 选择器相比 jQuery 丰富很多,如 document.getElementsByClassName 性能是 $(classSelector) 的 50 多倍!

jQuery VS Native API

测试链接:http://jsperf.com/jquery-vs-native-api

5. 时机成熟

差的浏览器(IE)已经淘汰的差不多了。

If We Didn't Spend So Much on IE Support, We Could Be Taking Vacations on Mars
Christian Alfoni

我们的主打产品现在有千万用户,因为我们一直引导用户升级浏览器,上个月统计 IE 9 以下用户只占不到 3%。但为了这 3% 的用户我们前端开发却增加了很多工作量,也限制了我们升级我们的架构,因此放弃支持他们利大于弊。当然这要根据产品来定,比如这是百度统计的国内浏览器占有率,IE8 竟然还有 22%。有些产品可以为了保证用户体验,在旧的浏览器上投入很大成本,甚至做到了极致。其实我觉得产品更应该做的是引导用户升级浏览器。微软也宣布 2016年1月12号停止支持 IE 11 以下浏览器,继续使用旧浏览器就会有安全风险,我们更应该主动引导,只要产品有足够吸引力,大部分用户升级并不困难。

浏览器分布率

数据来源 百度统计

下面是国际上 IE 占有率,IE8 已经跌出前 10,IE 11 比较多,还好支持他们并不难。

国际浏览器分布率

数据来源 W3 Counter

如何移除 jQuery

1. 替换代码

移除 jQuery 可以很顺利,我们把整个过程详细整理了,并开源。

打开 https://github.com/oneuijs/You-Dont-Need-jQuery 对 API 查找替换即可。

同时我们简单封装了一些方法:

刚去了 jQuery 又引了新的库,这不是玩我吗??其实以上两个库很简单,只是常用方法的简单封装,建议你看一下代码。你当然可以不用。

以上的库都用于我们的生产环境,我们会长期维护,保证更新。

2. 旧浏览器自动跳转

代码替换后,当用户用旧浏览器打开时,你还要做一个跳转,把用户定位到提示页面,提示用户下载升级浏览器。IE9 以下浏览器都支持条件判断语句,可以在 </head> 标签结束前添加如下代码做自动跳转

<!--[if lte IE 9]>
  <script>if (!/update\.htm/.test(location.href)) window.location = '//abc.com/update.htm'; </script>
<![endif]-->

总结

本文并不是强迫你一定要移除 jQuery,jQuery 为支持旧浏览器节省了很多成本。但条件成熟的情况下,移除 jQuery,参照 You Don't Need jQuery 拥抱原生 JavaScript 能同样保证开发效率,也可以给产品带来更好的性能,同时也能提高开发者水平。

原发于我的博客,原文地址:https://github.com/camsong/blog/issues/4

查看原文

赞 16 收藏 91 评论 10

Cam 赞了文章 · 2015-11-23

React Mixin 的前世今生

在 React component 构建过程中,常常有这样的场景,有一类功能要被不同的 Component 公用,然后看得到文档经常提到 Mixin(混入) 这个术语。此文就从 Mixin 的来源、含义、在 React 中的使用说起。

使用 Mixin 的缘由

Mixin 的特性一直广泛存在于各种面向对象语言。尤其在脚本语言中大都有原生支持,比如 Perl、Ruby、Python,甚至连 Sass 也支持。先来看一个在 Ruby 中使用 Mixin 的简单例子,

module D
  def initialize(name)
    @name = name
  end
  def to_s
    @name
  end
end

module Debug
  include D
  def who_am_i?
    "#{self.class.name} (\##{self.object_id}): #{self.to_s}"
  end
end

class Phonograph
  include Debug
  # ...
end

class EightTrack
  include Debug
  # ...
end

ph = Phonograph.new("West End Blues")
et = EightTrack.new("Real Pillow")
puts ph.who_am_i?  # Phonograph (#-72640448): West End Blues
puts et.who_am_i?  # EightTrack (#-72640468): Real Pillow

在 ruby 中 include 关键词即是 mixin,是将一个模块混入到一个另一个模块中,或是一个类中。为什么编程语言要引入这样一种特性呢?事实上,包括 C++ 等一些年龄较大的 OOP 语言,有一个强大但危险的多重继承特性。现代语言为了权衡利弊,大都舍弃了多重继承,只采用单继承。但单继承在实现抽象时有着诸多不便之处,为了弥补缺失,如 Java 就引入 interface,其它一些语言引入了像 Mixin 的技巧,方法不同,但都是为创造一种 类似多重继承 的效果,事实上说它是 组合 更为贴切。

在 ES 历史中,并没有严格的类实现,早期 YUI、MooTools 这些类库中都有自己封装类实现,并引入 Mixin 混用模块的方法。到今天 ES6 引入 class 语法,各种类库也在向标准化靠拢。

封装一个 Mixin 方法

看到这里,我们既然知道了广义的 mixin 方法的作用,那不妨试试自己封装一个 mixin 方法来感受下。

const mixin = function(obj, mixins) {
  const newObj = obj;
  newObj.prototype = Object.create(obj.prototype);

  for (let prop in mixins) {
    if (mixins.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixins[prop];
    }
  }

  return newObj;
}

const BigMixin = {
  fly: () => {
    console.log('I can fly');
  }
};

const Big = function() {
  console.log('new big');
};

const FlyBig = mixin(Big, BigMixin);

const flyBig = new FlyBig(); // 'new big'
flyBig.fly(); // 'I can fly'

对于广义的 mixin 方法,就是用赋值的方式将 mixins 对象里的方法都挂载到原对象上,就实现了对对象的混入。

是否看到上述实现会联想到 underscore 中的 extend 或 lodash 中的 assign 方法,或者说在 ES6 中一个方法 Object.assign()。它的作用是什么呢,MDN 上的解释是把任意多个的源对象所拥有的自身可枚举属性拷贝给目标对象,然后返回目标对象。

因为 JS 这门语言的特别,在没有提到 ES6 Classes 之前没有真正的类,仅是用方法去模拟对象,new 方法即为创建一个实例。正因为这样地弱,它也那样的灵活,上述 mixin 的过程就像对象拷贝一样。

那问题是 React component 中的 mixin 也是这样的吗?

React createClass

React 最主流构建 Component 的方法是利用 createClass 创建。顾名思义,就是创造一个包含 React 方法 Class 类。这种实现,官方提供了非常有用的 mixin 属性。我们就先来看看它来做 mixin 的方式是怎样的。

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
  mixins: [PureRenderMixin],

  render() {
    return <div>foo</div>;
  }
});

以官方封装的 PureRenderMixin 来举例,在 createClass 对象参数中传入一个 mixins 的数组,里面封装了我们所需要的模块。mixins 也可以增加多个重用模块,使用多个模块,方法之间的有重合会对普通方法和生命周期方法有所区分。

在不同的 mixin 里实现两个名字一样的普通方法,在常规实现中,后面的方法应该会覆盖前面的方法。那在 React 中是否一样会覆盖呢。事实上,它并不会覆盖,而是在控制台里报了一个在 ReactClassInterface 里的 Error,说你在尝试定义一个某方法在 component 中多于一次,这会造成冲突。因此,在 React 中是不允许出现重名普通方法的 Mixin。

如果是 React 生命周期定义的方法呢,是会将各个模块的生命周期方法叠加在一起,顺序执行。

因为,我们看到 createClass 实现的 mixin 为 Component 做了两件事:

  • 工具方法

    • 这是 mixin 的基本功能,如果你想共享一些工具类方法,就可以定义它们,直接在各个 Component 中使用。

  • 生命周期继承,props 与 state 合并

    • 这是 react mixin 特别也是重要的功能,它能够合并生命周期方法。如果有很多 mixin 来定义 componentDidMount 这个周期,那 React 会非常智能的将它们都合并起来执行。

    • 同样地,mixins 也可以作用在 getInitialState 的结果上,作 state 的合并,同时 props 也是这样合并。

未来的 React Classes

当 ECMAScript 发展到今天,这已经是一个百家争鸣的时代,各种优异的语言特性都出现在 ES6 和 ES7 的草案中。

React 在发展过程中一直崇尚拥抱标准,尽管它自己看上去是一个异类。当 React 0.13 释出的时候,React 增加并推荐使用 ES6 Classes 来构建 Component。但非常不幸,ES6 Classes 并不原生支持 mixin。尽管 React 文档中也未能给出解决方法,但如此重要的特性没有解决方案,也是一件十分困扰的事。

为了可以用这个强大的功能,还得想想其它方法,来寻找可能的方法来实现重用模块的目的。先回归 ES6 Classes,我们来想想怎么封装 mixin。

让 ES6 Class 与 Decorator 跳舞

要在 Class 上封装 mixin,就要说到 Class 的本质。ES6 没有改变 JavaScript 面向对象方法基于原型的本质,不过在此之上提供了一些语法糖,Class 就是其中之一,换汤不换药。

对于 Class 具体用法可以参考 MDN。目前 Class 仅是提供一些基本写法与功能,随着标准化的进展,相信会有更多的功能加入。

那对于实现 mixin 方法来说就没什么不一样了。但既然讲到了语法糖,就来讲讲另一个语法糖 Decorator,正巧可以来实现 Class 上的 mixin。

Decorator 在 ES7 中定义的新特性,与 Java 中的 pre-defined Annotations 相似。但与 Java 的 annotations 不同的是 decorators 是被运用在运行时的方法。在 Redux 或其他一些应用层框架中渐渐用 decorator 实现对 component 的『修饰』。现在,我们来用 decorator 来现实 mixin。

core-decorators.js 为开发者提供了一些实用的 decorator,其中实现了我们正想要的 @minxin。我们来解读一下核心实现。

import { getOwnPropertyDescriptors } from './private/utils';

const { defineProperty } = Object;

function handleClass(target, mixins) {
  if (!mixins.length) {
    throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
  }

  for (let i = 0, l = mixins.length; i < l; i++) {
       // 获取 mixins 的 attributes 对象
    const descs = getOwnPropertyDescriptors(mixins[i]);

     // 批量定义 mixin 的 attributes 对象
    for (const key in descs) {
      if (!(key in target.prototype)) {
        defineProperty(target.prototype, key, descs[key]);
      }
    }
  }
}

export default function mixin(...mixins) {
  if (typeof mixins[0] === 'function') {
    return handleClass(mixins[0], []);
  } else {
    return target => {
      return handleClass(target, mixins);
    };
  }
}

它实现部分的源代码十分简单,它将每一个 mixin 对象的方法都叠加到 target 对象的原型上以达到 mixin 的目的。这样,就可以用 @mixin 来做多个重用模块的叠加了。

import React, { Component } from 'React';
import { mixin } from 'core-decorators';

const PureRender = {
  shouldComponentUpdate() {}
};

const Theme = {
  setTheme() {}
};

@mixin(PureRender, Theme)
class MyComponent extends Component {
  render() {}
}

细心的读者有没有发现这个 mixin 与 createClass 上的 mixin 有区别。上述实现 mixin 的逻辑和最早实现的简单逻辑是很相似的,之前直接给对象的 prototype 属性赋值,但这里用了 getOwnPropertyDescriptor defineProperty 这两个方法,有什么区别呢?

事实上,这样实现的好处在于 defineProperty 这个方法,也是定义与赋值的区别,定义则是对已有的定义,赋值则是覆盖已有的定义。所以说前者并不会覆盖已有方法,后者是会的。本质上与官方的 mixin 方法都很不一样,除了定义方法级别的不能覆盖之外,还得加上对生命周期方法的继承,以及对 State 的合并。

再回到 decorator 身上,上述只是作用在类上的方法,还有作用在方法上的,它可以控制方法的自有属性,也可以作 decorator 工厂方法。在其它语言里,decorator 用途广泛,具体扩展不在本文讨论的范围。

讲到这里,对于 React 来说我们自然可以用上述方法来做 mixin。但 React 开发社区提出了『全新』的方式来取代 mixin,那就是 Higher-Order Components。

Higher-Order Components(HOCs)

Higher-Order Components(HOCs)最早由 Sebastian Markbåge(React 核心开发成员)在 gist 提出的一段代码。

Higher-Order 这个单词相信都很熟悉,Higher-Order function(高阶函数)在函数式编程是一个基本概念,它描述的是这样一种函数,接受函数作为输入,或是输出一个函数。比如常用的工具方法 mapreducesort 都是高阶函数。

而 HOCs 就很好理解了,将 Function 替代成 Component 就是所谓的高阶组件。如果说 mixin 是面向 OOP 的组合,那 HOCs 就是面向 FP 的组合。先看一个 HOC 的例子,

import React, { Component } from 'React';

const PopupContainer = (Wrapper) =>
  class WrapperComponent extends Component {
    componentDidMount() {
      console.log('HOC did mount')
    }

    componentWillUnmount() {
      console.log('HOC will unmount')
    }

    render() {
      return <Wrapper {...this.props} />;
    }
  }

上面例子中的 PopupContainer 方法就是一个 HOC,返回一个 React Component。值得注意的是 HOC 返回的总是新的 React Component。要使用上述的 HOC,那可以这么写。

import React, { Component } from 'React';

class MyComponent extends Component {
  render() {}
}

export default PopupContainer(MyStatelessComponent);

封装的 HOC 就可以一层层地嵌套,这个组件就有了嵌套方法的功能。对,就这么简单,保持了封装性的同时也保留了易用性。我们刚才讲到了 decorator,也可以用它转换。

import React, { Component } from 'React';

@PopupContainer
class MyComponent extends Component {
  render() {}
}

export default MyComponent;

简单地替换成作用在类上的 decorator,理解起来就是接收需要装饰的类为参数,返回一个新的内部类。恰与 HOCs 的定义完全一致。所以,可以认为作用在类上的 decorator 语法糖简化了高阶组件的调用。

如果有很多个 HOC 呢,形如 f(g(h(x)))。要不很多嵌套,要不写成 decorator 叠罗汉。再看一下它,有没有想到 FP 里的方法?

import React, { Component } from 'React';

// 来自 https://gist.github.com/jmurzy/f5b339d6d4b694dc36dd
let as = T => (...traits) => traits.reverse().reduce((T, M) => M(T), T);

class MyComponent extends as(Component)(Mixin1, Mixin2, Mixin3(param)) { }

绝妙的方法!或用更好理解的 compose 来做

import React, { Component } from 'React';
import R from 'ramda';

const mixins = R.compose(Mixin3(param), Mixin2, Mixin1);

class MyComponent extends mixins(Component) {}

讲完了用法,这种 HOC 有什么特殊之处呢,

  1. 从侵入 class 到与 class 解耦,React 一直推崇的声明式编程优于命令式编程,而 HOCs 恰是。

  2. 调用顺序不同于 React Mixin,上述执行生命周期的过程类似于 堆栈调用didmount -> HOC didmount -> (HOCs didmount) -> (HOCs will unmount) -> HOC will unmount -> unmount

  3. HOCs 对于主 Component 来说是 隔离 的,this 变量不能传递,以至于不能传递方法,包括 ref。但可以用 context 来传递全局参数,一般不推荐这么做,很可能会造成开发上的困扰。

当然,HOCs 不仅是上述这一种方法,我们还可以利用 Class 继承 来写,再来一个例子,

const PopupContainer = (Wrapper) =>
  class WrapperComponent extends Wrapper {
    static propTypes = Object.assign({}, Component.propTypes, {
      foo: React.PropTypes.string,
    });

    componentDidMount() {
      super.componentDidMount && super.componentDidMount();
      console.log('HOC did mount')
    }

    componentWillUnmount() {
      super.componentWillUnmount && super.componentWillUnmount();
      console.log('HOC will unmount')
    }
  }

其实,这种方法与第一种构造是完全不一样的。区别在哪,仔细看 Wrapper 的位置处在了继承的位置。这种方法则要通用得多,它通过继承原 Component 来做,方法都是可以通过 super 来顺序调用。因为依赖于继承的机制,HOC 的调用顺序和 队列 是一样的。

didmount -> HOC didmount -> (HOCs didmount) -> will unmount -> HOC will unmount -> (HOC will unmount)

细心的你是否已经看出 HOCs 与 React Mixin 的顺序是反向的,很简单,将 super 执行放在后面就可以达到正向的目的,尽管看上去很怪。这种不同很可能会导致问题的产生。尽管它是未来可能的选项,但现在看还有不少问题。

总结

未来的 React 中 mixin 方案 已经有伪代码现实,还是利用继承特性来做。

而继承并不是 "React Way",Sebastian Markbåge 认为实现更方便地 Compsition(组合)比做一个抽象的 mixin 更重要。而且聚焦在更容易的组合上,我们才可以摆脱掉 "mixin"。

对于『重用』,可以从语言层面上去说,都是为了可以更好的实现抽象,实现的灵活性与写法也存在一个平衡。在 React 未来的发展中,期待有更好的方案出现,同样期待 ES 未来的草案中有增加 Mixin 的方案。就今天来说,怎么去实现一个不复杂又好用的 mixin 是我们思考的内容。

资源

查看原文

赞 11 收藏 50 评论 0

Cam 赞了文章 · 2015-11-03

SegmentFault 创始人祁宁对话 C# 之父 Anders Hejlsberg

导读

上周,C#、Delphi 之父 Anders Hejlsberg 亲临帝都,就 TypeScript 做了一场技术分享,并与众多开发者就此进行了技术探讨。Anders Hejlsberg 加入微软的 19 年里,一直致力于 C# 和 .NET 的研发工作。同时,作为 TypeScript 的核心开发者和该开源项目的重要领导人物,他这次来帝都,主要也是为 TypeScript 布道。以下是讲座主要内容。

图片描述

Anders Hejlsberg 简述 TypeScript

(Anders)大家下午好!首先我介绍一下这是我第一次到中国来,在这儿逗留的这一天令我非常高兴,而且此前我也是非常激动的。今天在这会给大家介绍一下我一直在做的事情,包括大家都在谈论的 TypeScript 以及相关的 Open Source 的一般性的工作。

我做软件开发工具已经有 35 年的时间了,而且编写了不同语言的编程。以前都是专有的源,是封闭式的,现在我看到它有更新的变化,有一些新的方法论。

首先我们来看一下相关的背景。RedMonk 在上个季度刚刚发出的报告,JavaScript 在 GitHub 和 StackOverflow 被提及的次数排名都很靠前,和以前比有显著的上升。我觉得这里面有一些比较重要的原因。首先,JavaScript 是真正跨平台的语言。以前的语言,如 Java,其实现在都不算是(跨平台语言)了。另外,JavaScript 的性能近年来有 10~100 倍的飞速提升,它一个语言就可以在所有的层面上编程,这个是非常不一样的地方。比如这两年的 Node.js,还有移动端 ReactNative、NativeScript 等等,都可以用它来编写。在不同层面使用同一个语言开发,避免了学习很多新的东西。

其次,现在 JavaScript 已经被 ECMAScript 标准化了。今年已经出到第六版的标准,看起来非常雄心勃勃,因为他们的目的是为了解决 JavaScript 的一些遗留问题,而且另外添加一些新的模块、class 等等。现在第六版的标准我觉得有很多东西是非常有意思的,像 Mondule、Class、Arrow Function 等,JavaScript 的核心实际就是这样的。首先它是动态的,是两个家族系列(Dynamic 和 Static),对动态的界定是它不需要编译,直接打印就可以了,静态的是要求你静态地进行编译。我们可以分别来看一下动态和静态之间都各有什么优点?优点大家都知道就是它非常简化,可以让你直接上手工作,而且可以互动不需要编程,而且非常容易。静态的这块更多是强调修正一些错误,强调性能升级还有更好的工具。实际上现实来讲是说动态的和静态的都各有所长,而且我们两者都需要有。

大概是 5 年前我们的客户包括自己内部的团队就跟我们说,他们在写大的 JavaScript 代码时会遇到一些问题,所以我们想,怎么做才能加强 JavaScript 代码,让其强的更强,同时不破坏和损坏既有的价值。这就导致了我们现在所说的 TypeScript 的出现——实际上它是 JavaScript 的超级集合,把两者(动态的、静态的)最好的东西都集合在一起。这样实际上带来了两个好处,一个是我们在这上面添加了静态的,这样就有更加好的工具,还有一个好处在于你可以现在获取未来才有的一些相关的属性,可以在任何浏览器、任何的托管主机和操作系统中使用,而且它整个都是开源的,这才是跨平台的。(ECMAScript6 实际上在 5 年之后有了非常大的、多的 browser 部署才行)

……

以上是 Anders Hejlsberg 关于 TypeScript 基本的介绍。接下来是圆桌对话,是进一步的深入了解。圆桌对话由 SegmentFault CTO 祁宁、LeanCloud CEO 江宏以及云适配 CTO 马铎共同参与。

圆桌对话:深入了解 TypeScript

江宏:大家好,我是 LeanCloud 的江宏。LeanCloud 是移动应用的后端云服务。我是很小的时候学编程时就开始使用 Anders 创造的 Turbo Pascal,直到后来的 Delphi。今天很高兴有机会跟大师一起交流。刚才 Anders 介绍了 TypeScript,那么我就以一个这方面的问题开场吧。TypeScript 在 JavaScript 之上加了 class,而据我所知,很多 JavaScript 程序员,包括一些很优秀的程序员,都对在 JavaScript 里模拟 class 有强烈的反感。除了宗教原因以外,你觉得从技术上看 class 和 prototype 有优劣之分吗?

Anders Hejlsberg:你的这个问题非常好,JavaScript 没有 class,所以有一些习惯于 class 的开发者认为不大好用,而加上 class 也是因为有一部分的开发者是要用这样的东西。从 TypeScript 的 compiler 来讲,JavaScript 原有的 function、closure、prototype 都不受影响。对我来说,我认为它是中性的,你可以用这个也可以用那个,有两种方法来开发。

马铎:我了解 Anders Hejlsberg 先生的技术有十来年,他在开发界里面名气很大,可谈论的问题是非常多,但现在我们把问题回到 TypeScript 上,我想到一个问题,TypeScript 这种语言这么灵活,那么我想提的问题是,在灵活与规范化之间,TypeScript 是如何做好平衡的,这是不是 TypeScript 设计之初做的主要的考虑?

Anders Hejlsberg:TypeScript 作为一个语言来讲,技术上是非常有意思的。它有一个 GridView (77:21) Type 或者是 (77:22) Type 这样的东西可以用,如果这样用的话就可以查。如果不写类型的时候,实际上就相当于 JavaScript,问题是你怎么来写类型,传统上来讲编程的语言就是两个,一个是静态、一个是动态的,或者是 DodeType,所有的都是用静态类型来写的,所以你可以选用哪一种形式的类型。

祁宁:TypeScript 和 ECMAScript 的关系会是怎样的?如果以后 ECMAScript 支持了越来越多的 TypeScript 里的新特性,我们还需要 TypeScript 吗?

Anders Hejlsberg:我想就这个问题说两件事,首先是对于刚才提到的 ECMAScript6 的标准化,我们是非常致力于这个工作的,它就是微软的,我们也不是希望要把这个语言变成一个完全不同的东西。另外,它实际上有一些核心的东西,一个是 TypeScript 从静态类型提供了一个更好的工具,同时在编码的时候我们把它叫做 Downlevel,我觉得可能是更简化的意思。另外 ECMAScript6 标准化的时候是在不断地演变的,但我并不认为这个标准化有静态类型的系统,之后 6 还会有新版本,而现在的 browser 是不会支持它的。

祁宁:我们知道微软在开源社区中一直颇受微辞,它也有一些颇受诟病的传统,那么现在的微软真的走向开放了吗?

Anders Hejlsberg:有关你提的问题应该更多地看一下我们的行动,你们有自己的评判。如果看一下我们所谓工作的流程还有整个的开源社区对这个工作的深度的介入,还有包括 Google 已经在写 Angular 2,而且是在 TypeScript 上写的,这些是事实胜于雄辩的。我们微软也是在变的过程中,在这个过程中我们也学习了很多的东西。

江宏:程序设计语言在过去几十年里经历了一个演化过程。GUI 和桌面程序催生了面向对象的语言,多核处理器和对平行计算的需求让函数式语言再次流行起来,同时也让 Erlang、Go 等一些新的语言出现。您认为下一个新的语言是什么样的,让它出现的动机会是什么?

Anders Hejlsberg:有关这点其实很难说未来会出现什么新的语言的类型,但我可以说一下哪些事情或者是因素影响了现在的工作,比如说我们说的函数式的语言,这个对很多其他的东西来讲都是非常能够带来灵感的源泉。因为函数式语言有了 C# 里的很多特性,当然微软也有自己的函数式语言 F#。另外移动应用还有云等等都影响了现在的很多工作,他们要求系统之间可以进行网络通讯,能够容忍延迟,所以有了异步的编程。ECMAScript6 的标准之后会是 7,但编程的模型也是会继续向前演进的,可是编程语言的演进是非常缓慢的,因为它不是基于硬件的演变,而更多是涉及到人方面的因素。我们也知道在过去的 30 多年里,人大脑的尺寸没有发生太多的变化,因此编程语言方面它的演进是非常慢的。

马铎:我这有两个小问题想请教一下,第一是关于 TypeScript 的开源以及微软开源的事情,但这个问题其实可以不用问了,因为我也是一个 TypeScript 的使用者,关于开源这块其实已经有答案了。第二个问题是 TypeScript 现在发展的一种状况,我想听听 Anders Hejlsberg 先生在全球范围内以及在中国的 TypeScript 发展的状况是不是跟你所想象的一样,是好还是不好?

Anders Hejlsberg:有关这个问题我认为 TypeScript 在过去一年半的时间里是经历了非常非常快速的或者是很多轮的增长,现在我们看到了 Angular 2 还有 Google 也加入了,自从他们加入了以来又有了更大的增长。另外,从每个月有关这个下载的相关应用来讲,也是超过了 5000 万。再有,看到社区也是非常非常热闹的,因为他们现在在 GitHub 上工作了一年半的时间了,这里面有很多相关的问题,这说明在过去的一年半时间里肯定是有很多相关的问题和使用。我们看到了它的发展。TypeScript 在很多会议上或者是用户集团会议上都是有很多使用的。

祁宁:我们知道这是您第一次来中国,您对中国的开发者印象如何?您能从您自己的经历中为这些年轻人提出建议吗?

Anders Hejlsberg:首先,我觉得这是我第一次来中国,这个经历令我非常激动。另外和当地开发者社区有一些互动,另外我们看到了整个的开发者社区热情也是非常大的,所以这些对我们来说都是非常震撼的,另外我们也在微软本地的研究看了相关的项目,我发现也是特别棒的,很多东西非常务实、非常有用、非常有意思,其中很多东西我都想把它放在我们最终的产品里。

再有,其实我很难给大家一些相关的建议,对我来说编程的工作应该是大家都富有热情的来做的,因为我在上学的时候就开始接触这个工作,当时个人电脑都没有,什么东西都得从头学起,但我就是想学,谁也不能阻止我,并不是说我希望得到更高的薪酬,我完全是凭热情。我非常幸运我的爱好和工作是结合在一起的,但不是人人都像我这么幸运,最重要的是你要找到你的热情所在,而这个又能带给你快乐,这就是带来更好的工作成就,如果你面临一个选择,一方面是工作能多给你钱,另一方面是你很热爱的,这样的话才能工作得非常带劲,你也能够成长,同时也能学到非常多的东西。

江宏:您在微软多半是有某些管理职责的,不管是对人还是对项目,请问您是怎么平衡对于技术工作的热情和自己的其他职责的?我相信很多技术人都会有这个问题。比如我之前在 Google 工作的时候很多工程师遇到这个问题。后来创业,很多技术出身的创业者也遇到这个问题。

Anders Hejlsberg:这个问题相对来说比较好回答,我是自主选择做技术方面的工作,实际上我不管人,也没有人向我汇报,但在技术方面我负的责任是非常大的。因为对我的工作描述是我必须要创作出在全球开发者社区都非常有影响的产品,所以我是不管人的,我更愿意从事我非常擅长的技术方面的工作而不是管理人,这是我的专长所在,也是我的热情所在。

江宏:您在职业生涯里经历了不少编程语言和工具的生命周期。比如我以前用您主导开发的 Delphi。从技术上说 Delphi 是一个非常好的开发桌面应用的语言和开发环境,但是现在很少有人用了,似乎 Delphi 的发展没有达到应有的高度。如果那个时候有比较多的开源社区参与,您认为事情会不一样吗?您觉得决定一个语言的生死和是否能流行的主要因素有哪些?

Anders Hejlsberg:这个我也是很难说的,因为当时还没有开源,现在有了开源我认为它的影响对编程语言来讲是非常非常大的,而且如果没有开源将没有任何的编程语言是受欢迎的,因为开发者现在已经对私有的封闭式的东西是不敢兴趣的,而且我认为语言的设计 90% 的编程语言都是一样的,那 10% 是新的东西,新的东西发展是非常非常缓慢的,因为它和硬件如何发展都没有关系,它是和人脑和数学的发展是有关系的,所以是非常缓慢的。另外我不认为有任何的编程语言是完美无瑕的,我经常跟别人这么说,你给我看一个完美无瑕的编程语言,我马上给你展示一个根本没有用户的编程语言。一旦编程语言有了用户,它的代码就要考虑向后的兼容性。很多技术都有很多糟糕的地方,而这个正是它们成功的表象。

图片描述

以上为圆桌对话主要内容,圆桌结束后,Anders Hejlsberg 还与在场的开发者就编程方面的问题进行了交流。对于本次帝都之行,他表示非常感谢中国如此多的开发者能够支持 TypeScript,也希望在之后越来越多的开发者能参与到 TypeScript 的工作中来。

查看 GitHub 该项目:Microsoft/TypeScript

活动现场照片

图片描述

图片描述

图片描述

图片描述

图片描述

图片描述

查看原文

赞 7 收藏 9 评论 2

Cam 发布了文章 · 2015-10-26

Immutable 详解及 React 中实践

Immutable data

Shared mutable state is the root of all evil(共享的可变状态是万恶之源)
-- Pete Hunt

有人说 Immutable 可以给 React 应用带来数十倍的提升,也有人说 Immutable 的引入是近期 JavaScript 中伟大的发明,因为同期 React 太火,它的光芒被掩盖了。这些至少说明 Immutable 是很有价值的,下面我们来一探究竟。

JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。如 foo={a: 1}; bar=foo; bar.a=2 你会发现此时 foo.a 也被改成了 2。虽然这样做可以节约内存,但当应用复杂后,这就造成了非常大的隐患,Mutable 带来的优点变得得不偿失。为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。

Immutable 可以很好地解决这些问题。

什么是 Immutable Data

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:

Immutable 原理动画

目前流行的 Immutable 库有两个:

immutable.js

Facebook 工程师 Lee Byron 花费 3 年时间打造,与 React 同期出现,但没有被默认放到 React 工具集里(React 提供了简化的 Helper)。它内部实现了一套完整的 Persistent Data Structure,还有很多易用的数据类型。像 CollectionListMapSetRecordSeq。有非常全面的mapfiltergroupByreduce`find`函数式操作方法。同时 API 也尽量与 Object 或 Array 类似。

其中有 3 种最重要的数据结构说明一下:(Java 程序员应该最熟悉了)

  • Map:键值对集合,对应于 Object,ES6 也有专门的 Map 对象

  • List:有序可重复的列表,对应于 Array

  • Set:无序且不可重复的列表

seamless-immutable

与 Immutable.js 学院派的风格不同,seamless-immutable 并没有实现完整的 Persistent Data Structure,而是使用 Object.defineProperty(因此只能在 IE9 及以上使用)扩展了 JavaScript 的 Array 和 Object 对象来实现,只支持 Array 和 Object 两种数据类型,API 基于与 Array 和 Object 操持不变。代码库非常小,压缩后下载只有 2K。而 Immutable.js 压缩后下载有 16K。

下面上代码来感受一下两者的不同:

// 原来的写法
let foo = {a: {b: 1}};
let bar = foo;
bar.a.b = 2;
console.log(foo.a.b);  // 打印 2
console.log(foo === bar);  //  打印 true

// 使用 immutable.js 后
import Immutable from 'immutable';
foo = Immutable.fromJS({a: {b: 1}});
bar = foo.setIn(['a', 'b'], 2);   // 使用 setIn 赋值
console.log(foo.getIn(['a', 'b']));  // 使用 getIn 取值,打印 1
console.log(foo === bar);  //  打印 false

// 使用  seamless-immutable.js 后
import SImmutable from 'seamless-immutable';
foo = SImmutable({a: {b: 1}})
bar = foo.merge({a: { b: 2}})   // 使用 merge 赋值
console.log(foo.a.b);  // 像原生 Object 一样取值,打印 1
console.log(foo === bar);  //  打印 false

Immutable 优点

1. Immutable 降低了 Mutable 带来的复杂度

可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。

比如下面一段代码:

function touchAndLog(touchFn) {
  let data = { key: 'value' };
  touchFn(data);
  console.log(data.key); // 猜猜会打印什么?
}

在不查看 touchFn 的代码的情况下,因为不确定它对 data 做了什么,你是不可能知道会打印什么(这不是废话吗)。但如果 data 是 Immutable 的呢,你可以很肯定的知道打印的是 value

2. 节省内存

Immutable.js 使用了 Structure Sharing 会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。

import { Map} from 'immutable';
let a = Map({
  select: 'users',
  filter: Map({ name: 'Cam' })
})
let b = a.set('select', 'people');

a === b; // false
a.get('filter') === b.get('filter'); // true

上面 a 和 b 共享了没有变化的 filter 节点。

3. Undo/Redo,Copy/Paste,甚至时间旅行这些功能做起来小菜一碟

因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。

后面我会提供 Flux 做 Undo 的示例。

4. 并发安全

传统的并发非常难做,因为要处理各种数据不一致问题,因此『聪明人』发明了各种锁来解决。但使用了 Immutable 之后,数据天生是不可变的,并发锁就不需要了

然而现在并没什么卵用,因为 JavaScript 还是单线程运行的啊。但未来可能会加入,提前解决未来的问题不也挺好吗?

5. 拥抱函数式编程

Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。

像 ClojureScript,Elm 等函数式编程语言中的数据类型天生都是 Immutable 的,这也是为什么 ClojureScript 基于 React 的框架 --- Om 性能比 React 还要好的原因。

Immutable 缺点

1. 需要学习新的 API

No Comments

2. 增加了资源文件大小

No Comments

3. 容易与原生对象混淆

这点是我们使用 Immutable.js 过程中遇到最大的问题。写代码要做思维上的转变。

虽然 Immutable.js 尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。

Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用 map.get('key') 而不是 map.keyarray.get(0) 而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。

当使用外部库的时候,一般需要使用原生对象,也很容易忘记转换。

下面给出一些办法来避免类似问题发生:

  1. 使用 Flow 或 TypeScript 这类有静态类型检查的工具

  2. 约定变量命名规则:如所有 Immutable 类型对象以 $$ 开头。

  3. 使用 Immutable.fromJS 而不是 Immutable.MapImmutable.List 来创建对象,这样可以避免 Immutable 和原生对象间的混用。

更多认识

Immutable.is

两个 immutable 对象可以使用 === 来比较,这样是直接比较内存地址,性能最好。但即使两个对象的值是一样的,也会返回 false

let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
map1 === map2;             // false

为了直接比较对象的值,immutable.js 提供了 Immutable.is 来做『值比较』,结果如下:

Immutable.is(map1, map2);  // true

Immutable.is 比较的是两个对象的 hashCodevalueOf(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。

后面会使用 Immutable.is 来减少 React 重复渲染,提高性能。

另外,还有 moricortex 等,因为类似就不再介绍。

与 Object.freeze、const 区别

ES6 中新加入的 Object.freezeconst 都可以达到防止对象被篡改的功能,但它们是 shallowCopy 的。对象层级一深就要特殊处理了。

Cursor 的概念

这个 Cursor 和数据库中的游标是完全不同的概念。

由于 Immutable 数据一般嵌套非常深,为了便于访问深层数据,Cursor 提供了可以直接访问这个深层数据的引用。

import Immutable from 'immutable';
import Cursor from 'immutable/contrib/cursor';

let data = Immutable.fromJS({ a: { b: { c: 1 } } });
// 让 cursor 指向 { c: 1 }
let cursor = Cursor.from(data, ['a', 'b'], newData => {
  // 当 cursor 或其子 cursor 执行 update 时调用
  console.log(newData);
});

cursor.get('c'); // 1
cursor = cursor.update('c', x => x + 1);
cursor.get('c'); // 2

实践

与 React 搭配使用,Pure Render

熟悉 React 的都知道,React 做性能优化时有一个避免重复渲染的大招,就是使用 shouldComponentUpdate(),但它默认返回 true,即始终会执行 render() 方法,然后做 Virtual DOM 比较,并得出是否需要做真实 DOM 更新,这里往往会带来很多无必要的渲染并成为性能瓶颈。

当然我们也可以在 shouldComponentUpdate() 中使用使用 deepCopy 和 deepCompare 来避免无必要的 render(),但 deepCopy 和 deepCompare 一般都是非常耗性能的

Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 ===is 比较就能知道是否需要执行 render(),而这个操作几乎 0 成本,所以可以极大提高性能。修改后的 shouldComponentUpdate 是这样的:

import { is } from 'immutable';
shouldComponentUpdate: (nextProps, nextState) => {
  return !(this.props === nextProps || is(this.props, nextProps)) ||
         !(this.state === nextState || is(this.state, nextState));
}

使用 Immutable 后,如下图,当红色节点的 state 变化后,不会再渲染树中的所有节点,而是只渲染图中绿色的部分:

react reconciliation

你也可以借助 React.addons.PureRenderMixin 或支持 class 语法的 pure-render-decorator 来实现。

setState 的一个技巧

React 建议把 this.state 当作 Immutable 的,因此修改前需要做一个 deepCopy,显得麻烦:

import '_' from 'lodash';

const Component = React.createClass({
  getInitialState() {
    return {
      data: { times: 0 }
    }
  },
  handleAdd() {
    let data = _.cloneDeep(this.state.data);
    data.times = data.times + 1;
    this.setState({ data: data });
    // 如果上面不做 cloneDeep,下面打印的结果会是已经加 1 后的值。
    console.log(this.state.data.times);
  }
}

使用 Immutable 后:

  getInitialState() {
    return {
      data: Map({ times: 0 })
    }
  },
  handleAdd() {
    this.setState({ data: this.state.data.update('times', v => v + 1) });
    // 这时的 times 并不会改变
    console.log(this.state.data.get('times'));
  }

上面的 handleAdd 可以简写成:

  handleAdd() {
    this.setState(({data}) => ({
      data: data.update('times', v => v + 1) })
    });
  }

与 Flux 搭配使用

由于 Flux 并没有限定 Store 中数据的类型,使用 Immutable 非常简单。

现在是实现一个类似带有添加和撤销功能的 Store:

import { Map, OrderedMap } from 'immutable';
let todos = OrderedMap();
let history = [];  // 普通数组,存放每次操作后产生的数据

let TodoStore = createStore({
  getAll() { return todos; }
});

Dispatcher.register(action => {
  if (action.actionType === 'create') {
    let id = createGUID();
    history.push(todos);  // 记录当前操作前的数据,便于撤销
    todos = todos.set(id, Map({
      id: id,
      complete: false,
      text: action.text.trim()
    }));
    TodoStore.emitChange();
  } else if (action.actionType === 'undo') {
    // 这里是撤销功能实现,
    // 只需从 history 数组中取前一次 todos 即可
    if (history.length > 0) {
      todos = history.pop();
    }
    TodoStore.emitChange();
  }
});

与 Redux 搭配使用

Redux 是目前流行的 Flux 衍生库。它简化了 Flux 中多个 Store 的概念,只有一个 Store,数据操作通过 Reducer 中实现;同时它提供更简洁和清晰的单向数据流(View -> Action -> Middleware -> Reducer),也更易于开发同构应用。目前已经在我们项目中大规模使用。

由于 Redux 中内置的 combineReducers 和 reducer 中的 initialState 都为原生的 Object 对象,所以不能和 Immutable 原生搭配使用。

幸运的是,Redux 并不排斥使用 Immutable,可以自己重写 combineReducers 或使用 redux-immutablejs 来提供支持。

上面我们提到 Cursor 可以方便检索和 update 层级比较深的数据,但因为 Redux 中已经有了 select 来做检索,Action 来更新数据,因此 Cursor 在这里就没有用武之地了。

总结

Immutable 可以给应用带来极大的性能提升,但是否使用还要看项目情况。由于侵入性较强,新项目引入比较容易,老项目迁移需要评估迁移。对于一些提供给外部使用的公共组件,最好不要把 Immutable 对象直接暴露在对外接口中。

如果 JS 原生 Immutable 类型会不会太美,被称为 React API 终结者的 Sebastian Markbåge 有一个这样的提案,能否通过现在还不确定。不过可以肯定的是 Immutable 会被越来越多的项目使用。

资源

原文发于知乎,会持续更新,更多内容请关注我们的专栏

查看原文

赞 38 收藏 163 评论 6

Cam 赞了文章 · 2015-10-05

JavaScript 原型系统的变迁,以及 ES6 class

概述

JavaScript 的原型系统是最初就有的语言设计。但随着 ES 标准的进化和新特性的添加。它也一直在不停进化。这篇文章的目的就是梳理一下早期到 ES5 和现在 ES6,新特性的加入对原型系统的影响。

如果你对原型的理解还停留在 function + new 这个层面而不知道更深入的操作原型链的技巧,或者你想了解 ES6 class 的知识,相信本文会有所帮助。

这篇文章是我学习 You Don't Know JS 的副产品,推荐任何想系统性地学习 JavaScript 的人去阅读此书。

JavaScript 原型简述

很多人应该都对原型(prototype)不陌生。简单地说,JavaScript 是基于原型的语言。当我们调用一个对象的属性时,如果对象没有该属性,JavaScript 解释器就会从对象的原型对象上去找该属性,如果原型上也没有该属性,那就去找原型的原型。这种属性查找的方式被称为原型链(prototype chain)。

对象的原型是没有公开的属性名去访问的(下文再谈 __proto__ 属性)。以下为了方便称呼,我把一个对象内部对原型的引用称为 [[Prototype]]。

JavaScript 没有类的概念,原型链的设定就是少数能够让多个对象共享属性和方法,甚至模拟继承的方式。在 ES5 以前,如果我们想设置对象的 [[Prototype]],只能通过 new 关键字,比如:

function User() {
  this._name = 'David'
}

User.prototype.getName = function() {
  return this._name
}

var user = new User()
user.getName()                  // "David"
user.hasOwnProperty('getName')  // false

User 函数被 new 关键字调用时,它就类似于一个构造函数,其生成的对象的 [[Prototype]] 会引用 User.prototype 。因为 User.prototype 也是一个对象,它的 [[Prototype]] 是 Object.prototype

一般我们对这种构造函数命名都会采用 CamelCase ,并把它称呼为“类”,这不仅是为了跟 OOP 的理念保持一致,也是因为 JavaScript 的内建“类”也是这种命名。

SomeClass 生成的对象,其 [[Prototype]] 是 SomeClass.prototype。除了稍显繁琐,这套逻辑是可以自圆其说的,比如:

  1. 我们用 {..} 创建的对象的 [[Prototype]] 都是 Object.prototype,也是原型链的顶点。

  2. 数组的 [[Prototype]] 是 Array.prototype

  3. 字符串的 [[Prototype]] 是 String.prototype

  4. Array.prototypeString.prototype 的 [[Prototype]] 是 Object.prototype

模拟继承

模拟继承是自定义原型链的典型使用场景。但如果用 new 的方式则比较麻烦。一种常见的解法是:子类的 prototype 等于父类的实例。这就涉及到定义子类的时候调用父类的构造函数。为了避免父类的构造函数在类定义过程中的潜在影响,我们一般会建造一个临时类去做代替父类 new 的过程。

function Parent() {}
function Child() {}

function createSubProto(proto) {
  // fn 在这里就是临时类
  var fn = function() {}
  fn.prototype = proto
  return new fn()
}

Child.prototype = createSubProto(Parent.prototype)
Child.prototype.constructor = Child

var child = new Child()
child instanceof Child   // true
child instanceof Parent  // true

ES5: 自由地操控原型链

既然原型链本质上只是建立对象之间的关联,那我们可不可以直接操作对象的 [[Prototype]] 呢?

在 ES5(准确的说是 5.1)之前,我们没有办法直接获取对象的原型,只能通过 [[Prototype]] 的 constructor

var user = new User()
user.constructor.prototype          // User
user.hasOwnProperty('constructor')  // false

类可以通过 prototype 属性获取生成的对象的 [[Prototype]]。[[Prototype]] 里的 constructor 属性又会反过来引用函数本身。因为 user 的原型是 User.prototype ,它自然也能够通过 constructor 获取到 User 函数,进而获取到自己的 [[Prototype]]。比较绕是吧?

ES5.1 之后加了几个新的 API 帮助我们操作对象的 [[Prototype]],自此以后 JavaScript 才真的有自由操控原型的能力。它们是:

  • Object.prototype.isPrototypeOf

  • Object.create

  • Object.getPrototypeOf

  • Object.setPrototypeOf

注:以上方法并不完全是 ES5.1 的,isPrototypeOf 是 ES3 就有的,setPrototypeOf 是 ES6 才有的。但它们的规范都在 ES6 中修改了一部分。

下面的例子里,Object.create 创建 child 对象,并把 [[Prototype]] 设置为 parent 对象。Object.getPrototypeOf 可以直接获取对象的 [[Prototype]]。isPrototypeOf 能够判断一个对象是否在另一个对象的原型链上。

var parent = {
  _name: 'David',
  getName: function() { return this._name },
}

var child = Object.create(parent)

Object.getPrototypeOf(child)           // parent
parent.isPrototypeOf(child)            // true
Object.prototype.isPrototypeOf(child)  // true
child instanceof Object                // true

既然有 Object.getPrototypeOf,自然也有 Object.setPrototypeOf 。这个函数可以修改任何对象的 [[Prototype]] ,包括内建类型。

var anotherParent = {
  name: 'Alex'
}

Object.setPrototypeOf(child, anotherParent)
Object.getPrototypeOf(child)  // anotherParent

// 修改数组的 [[Prototype]]
var a = []
Object.setPrototypeOf(a, anotherParent)
a instanceof Array        // false
Object.getPrototypeOf(a)  // anotherParent

灵活使用以上的几个方法,我们可以非常轻松地创建原型链,或者在已知原型链中插入自定义的对象,玩法只取决于想象力。我们以此修改一下上面的模拟继承的例子:

function Parent() {}
function Child() {}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

因为 Object.create(..) 传入的参数会作为 [[Prototype]] ,所以这里有一个有意思的小技巧。我们可以用 Object.create(null) 创建一个没有任何属性的对象。这个技巧适合做 proxy 对象,有点类似 Ruby 中的 BasicObject

尴尬的私生子 __proto__

说到操作 [[Prototype]] 就不得不提 __proto__ 。这个属性是一个 getter/setter ,可以用来获取和设置任意对象的 [[Prototype]] 。

child.__proto__           // equal to Object.getPrototypeOf(child)
child.__proto__ = parent  // equal to Object.setPrototypeOf(child, parent)

它本来不是 ES 的标准,无奈众多浏览器早早地都实现了这个属性,而且应用得还挺广泛的。到了 ES6 为了向下兼容性只好接纳它成为标准的一部分。这是典型的现实倒逼标准的例子。

看看 MDN 的描述都充满了怨念。

The use of proto is controversial, and has been discouraged. It was never originally included in the EcmaScript language spec, but modern browsers decided to implement it anyway. Only recently, the proto property has been standardized in the ECMAScript 6 language specification for web browsers to ensure compatibility, so will be supported into the future. It is deprecated in favor of Object.getPrototypeOf/Reflect.getPrototypeOf and Object.setPrototypeOf/Reflect.setPrototypeOf (though still, setting the [[Prototype]] of an object is a slow operation that should be avoided if performance is a concern).

__proto__ 是不被推荐的用法。大部分情况下我们仍然应该用 Object.getPrototypeOfObject.setPrototypeOf 。什么是少数情况,待会再讲。

ES6: class 语法糖

不得不说开发者世界受 OO 的影响非常之深,虽然 ES5 给了我们足够灵活的 API ,但是:

  • 很多人还是倾向于用 class 来组织代码。

  • 很多类库、框架创造了自己的 API 来实现 class 的功能。

产生这一现象的原因有很多,但事实如此。而且如果用别人的轮子,有些事是我们无法选择的。也许是看到了这一现象,ES6 时代终于有了 class 语法,有望统一各个类库和框架不一致的类实现方式。来看一个例子:

class User {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  fullName() {
    return `${this.firstName} ${this.lastName}`
  }
}

let user = new User('David', 'Chen')
user.fullName()  // David Chen

以上的类定义语法非常直观,它跟以下的 ES5 语法是一个意思:

function User(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

User.prototype.fullName = function() {
  return '' + this.firstName + this.lastName
}

ES6 并没有改变 JavaScript 基于原型的本质,只是在此之上提供了一些语法糖。class 就是其中之一。其他的还有 extendssuperstatic 。它们大多数都可以转换成等价的 ES5 语法。

我们来看看另一个继承的例子:

class Child extends Parent {
  constructor(firstName, lastName, age) {
    super(firstName, lastName)
    this.age = age
  }
}

其基本等价于:

function Child(firstName, lastName, age) {
  Parent.call(this, firstName, lastName)
  this.age = age
}

Child.prototype = Object.create(Parent.prototype)
Child.constructor = Child

无疑上面的例子更加直观,代码组织更加清晰。这也是加入新语法的目的。不过虽然新语法的本质还是基于原型的,但新加入的概念或多或少会引起一些连带的影响。

extends 继承内建类的能力

因为语言内部设计原因,我们没有办法自定义一个类来继承 JavaScript 的内建类的。继承类往往会有各种问题。ES6 的 extends 的最大的卖点,就是不仅可以继承自定义类,还可以继承 JavaScript 的内建类,比如这样:

class MyArray extends Array {
}

这种方式可以让开发者继承内建类的功能创造出符合自己想要的类。所有 Array 已有的属性和方法都会对继承类生效。这确实是个不错的诱惑,也是继承最大的吸引力。

但现实总是悲催的。extends 内建类会引发一些奇怪的问题,很多属性和方法没办法在继承类中正常工作。举个例子:

var a = new Array(1, 2, 3)
a.length  // 3

var b = new MyArray(1, 2, 3)
b.length  // 0

如果说语法糖可以用 Babel.js 这种 transpiler 去编译成 ES5 解决 ,扩充的 API 可以用 polyfill 解决,但是这种内建类的继承机制显然是需要浏览器支持的。而目前唯一支持这个特性的浏览器是………… Microsoft Edge 。

好在这并不是什么致命的问题。大多数此类需求都可以用封装类去解决,无非是多写一点 wrapper API 而已。而且个人认为封装和组合反而是比继承更灵活的解决方案。

super 带来的新概念(坑?)

super 在 constructor 和普通方法里的不同

在 constructor 里面,super 的用法是 super(..)。它相当于一个函数,调用它等于调用父类的 constructor 。但在普通方法里面,super 的用法是 super.prop 或者 super.method()。它相当于一个指向对象的 [[Prototype]] 的属性。这是 ES6 标准的规定。

class Parent {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  fullName() {
    return `${this.firstName} ${this.lastName}`
  }
}

class Child extends Parent {
  constructor(firstName, lastName, age) {
    super(firstName, lastName)
    this.age = age
  }

  fullName() {
    return `${super.fullName()} (${this.age})`
  }
}

注意:Babel.js 对方法里调用 super(..) 也能编译出正确的结果,但这应该是 Babel.js 的 bug ,我们不该以此得出 super(..) 也可以在非 constructor 里用的结论。

super 在子类的 constructor 里必须先于 this 调用

如果写子类的 constructor 需要操作 this ,那么 super 必须先调用!这是 ES6 的规则。所以写子类的 constructor 时尽量把 super 写在第一行。

class Child extends Parent {
  constructor() {
    this.xxx()  // invalid
    super()
  }
}

super 是编译时确定,不是运行时确定

什么意思呢?先看代码:

class Child extends Parent {
  fullName() {
    super.fullName()
  }
}

以上代码中 fullName 方法的 ES5 等价代码是:

fullName() {
  Parent.prototype.fullName.call(this)
}

而不是

fullName() {
  Object.getPrototypeOf(this).fullName.call(this)
}

这就是 super 编译时确定的特性。不过为什么要这样设计?个人理解是,函数的 this 只有在运行时才能确定。因此在运行时根据 this 的原型链去获得上层方法并不太符合 class 的常规思维,在某些情况下更容易产生错误。比如 child.fullName.call(anotherObj)

super 对 static 的影响,和类的原型链

static 相当于类方法。因为编译时确定的特性,以下代码中:

class Child extends Parent {
  static findAll() {
    return super.findAll()
  }
}

findAll 的 ES5 等价代码是:

findAll() {
  return Parent.findAll()
}

static 貌似和原型链没关系,但这不妨碍我们讨论一个问题:类的原型链是怎样的?我没查到相关的资料,不过我们可以测试一下:

Object.getPrototypeOf(Child) === Parent             // true
Object.getPrototypeOf(Parent) === Object            // false
Object.getPrototypeOf(Parent) === Object.prototype  // false

proto = Object.getPrototypeOf(Parent)
typeof proto                             // function
proto.toString()                         // function () {}
proto === Object.getPrototypeOf(Object)  // true
proto === Object.getPrototypeOf(String)  // true

new proto()  //TypeError: function () {} is not a constructor

可见自定义类的话,子类的 [[Prototype]] 是父类,而所有顶层类的 [[Prototype]] 都是同一个函数对象,不管是内建类如 Object 还是自定义类如 Parent 。但这个函数是不能用 new 关键字初始化的。虽然这种设计没有 Ruby 的对象模型那么巧妙,不过也是能够自圆其说的。

直接定义 object 并设定 [[Prototype]]

除了通过 classextends 的语法设定 [[Prototype]] 之外,现在定义对象也可以直接设定 [[Prototype]] 了。这就要用到 __proto__ 属性了。“定义对象并设置 [[Prototype]]” 是唯一建议用 __proto__ 的地方。另外,另外注意 super 只有在 method() {} 这种语法下才能用。

let parent = {
  method1() { .. },
  method2() { .. },
}

let child = {
  __proto__: parent,

  // valid
  method1() {
    return super.method1()
  },

  // invalid
  method2: function() {
    return super.method2()
  },
}

总结

JavaScript 的原型是很有意思的设计,从某种程度上说它是更加纯粹的面向对象设计(而不是面向类的设计)。ES5 和 ES6 加入的 API 能更有效地操控原型链。语言层面支持的 class 也能让忠于类设计的开发者用更加统一的方式去设计类。虽然目前 class 仅仅提供了一些基本功能。但随着标准的进步,相信它还会扩充出更多的功能。

本文的主题是原型系统的变迁,所以并没有涉及 getter/setter 和 defineProperty 对原型链的影响。想系统地学习原型,你可以去看 You Don't Know JS: this & Object Prototypes

参考资料

You Don't Know JS: this & Object Prototypes
You Don't Know JS: ES6 & Beyond
Classes in ECMAScript 6 (final semantics)
MDN: Object.prototype.__proto__

查看原文

赞 25 收藏 121 评论 11

Cam 发布了文章 · 2015-09-30

传统 Ajax 已死,Fetch 永生

image

原谅我做一次标题党,Ajax 不会死,传统 Ajax 指的是 XMLHttpRequest(XHR),未来现在已被 Fetch 替代。

最近把阿里一个千万级 PV 的数据产品全部由 jQuery 的 $.ajax 迁移到 Fetch,上线一个多月以来运行非常稳定。结果证明,对于 IE8+ 以上浏览器,在生产环境使用 Fetch 是可行的。

由于 Fetch API 是基于 Promise 设计,有必要先学习一下 Promise,推荐阅读 MDN Promise 教程。旧浏览器不支持 Promise,需要使用 polyfill es6-promise

本文不是 Fetch API 科普贴,其实是讲异步处理和 Promise 的。Fetch API 很简单,看文档很快就学会了。推荐 MDN Fetch 教程 和 万能的WHATWG Fetch 规范

Why Fetch

XMLHttpRequest 是一个设计粗糙的 API,不符合关注分离(Separation of Concerns)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的 Promise,generator/yield,async/await 友好。

Fetch 的出现就是为了解决 XHR 的问题,拿例子说明:

使用 XHR 发送一个 json 请求一般是这样:

var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';

xhr.onload = function() {
  console.log(xhr.response);
};

xhr.onerror = function() {
  console.log("Oops, error");
};

xhr.send();

使用 Fetch 后,顿时看起来好一点

fetch(url).then(function(response) {
  return response.json();
}).then(function(data) {
  console.log(data);
}).catch(function(e) {
  console.log("Oops, error");
});

使用 ES6 的 箭头函数 后:

fetch(url).then(response => response.json())
  .then(data => console.log(data))
  .catch(e => console.log("Oops, error", e))

现在看起来好很多了,但这种 Promise 的写法还是有 Callback 的影子,而且 promise 使用 catch 方法来进行错误处理的方式有点奇怪。不用急,下面使用 async/await 来做最终优化:

注:async/await 是非常新的 API,属于 ES7,目前尚在 Stage 1(提议) 阶段,这是它的完整规范。使用 Babel 开启 runtime 模式后可以把 async/await 无痛编译成 ES5 代码。也可以直接使用 regenerator 来编译到 ES5。

try {
  let response = await fetch(url);
  let data = response.json();
  console.log(data);
} catch(e) {
  console.log("Oops, error", e);
}
// 注:这段代码如果想运行,外面需要包一个 async function

duang~~ 的一声,使用 await 后,写异步代码就像写同步代码一样爽await 后面可以跟 Promise 对象,表示等待 Promise resolve() 才会继续向下执行,如果 Promise 被 reject() 或抛出异常则会被外面的 try...catch 捕获。

Promise,generator/yield,await/async 都是现在和未来 JS 解决异步的标准做法,可以完美搭配使用。这也是使用标准 Promise 一大好处。最近也把项目中使用第三方 Promise 库的代码全部转成标准 Promise,为以后全面使用 async/await 做准备。

另外,Fetch 也很适合做现在流行的同构应用,有人基于 Fetch 的语法,在 Node 端基于 http 库实现了 node-fetch,又有人封装了用于同构应用的 isomorphic-fetch

注:同构(isomorphic/universal)就是使前后端运行同一套代码的意思,后端一般是指 NodeJS 环境。

总结一下,Fetch 优点主要有:

  1. 语法简洁,更加语义化

  2. 基于标准 Promise 实现,支持 async/await

  3. 同构方便,使用 isomorphic-fetch

Fetch 启用方法

下面是重点↓↓↓

先看一下 Fetch 原生支持率:
image

原生支持率并不高,幸运的是,引入下面这些 polyfill 后可以完美支持 IE8+ :

  1. 由于 IE8 是 ES3,需要引入 ES5 的 polyfill: es5-shim, es5-sham

  2. 引入 Promise 的 polyfill: es6-promise

  3. 引入 fetch 探测库:fetch-detector

  4. 引入 fetch 的 polyfill: fetch-ie8

  5. 可选:如果你还使用了 jsonp,引入 fetch-jsonp

  6. 可选:开启 Babel 的 runtime 模式,现在就使用 async/await

Fetch polyfill 的基本原理是探测是否存在 window.fetch 方法,如果没有则用 XHR 实现。这也是 github/fetch 的做法,但是有些浏览器(Chrome 45)原生支持 Fetch,但响应中有中文时会乱码,老外又不太关心这种问题,所以我自己才封装了 fetch-detectorfetch-ie8 只在浏览器稳定支持 Fetch 情况下才使用原生 Fetch。这些库现在每天有几千万个请求都在使用,绝对靠谱

终于,引用了这一堆 polyfill 后,可以愉快地使用 Fetch 了。但要小心,下面有坑:

Fetch 常见坑

  • Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: 'include'})

  • 服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。

竟然没有提到 IE,这实在太不科学了,现在来详细说下 IE

IE 使用策略

所有版本的 IE 均不支持原生 Fetch,fetch-ie8 会自动使用 XHR 做 polyfill。但在跨域时有个问题需要处理。

IE8, 9 的 XHR 不支持 CORS 跨域,虽然提供 XDomainRequest,但这个东西就是玩具,不支持传 Cookie!如果接口需要权限验证,还是乖乖地使用 jsonp 吧,推荐使用 fetch-jsonp。如果有问题直接提 issue,我会第一时间解决。

标准 Promise 的不足

由于 Fetch 是典型的异步场景,所以大部分遇到的问题不是 Fetch 的,其实是 Promise 的。ES6 的 Promise 是基于 Promises/A+ 标准,为了保持简单简洁,只提供极简的几个 API。如果你用过一些牛 X 的异步库,如 jQuery(不要笑) 、Q.js 或者 RSVP.js,可能会感觉 Promise 功能太少了。

没有 Deferred

Deferred 可以在创建 Promise 时可以减少一层嵌套,还有就是跨方法使用时很方便。
ECMAScript 11 年就有过 Deferred 提案,但后来没被接受。其实用 Promise 不到十行代码就能实现 Deferred:es6-deferred。现在有了 async/await,generator/yield 后,deferred 就没有使用价值了。

没有获取状态方法:isRejected,isResolved

标准 Promise 没有提供获取当前状态 rejected 或者 resolved 的方法。只允许外部传入成功或失败后的回调。我认为这其实是优点,这是一种声明式的接口,更简单。

缺少其它一些方法:always,progress,finally

always 可以通过在 then 和 catch 里重复调用方法实现。finally 也类似。progress 这种进度通知的功能还没有用过,暂不知道如何替代。

最后

Fetch 替换 XHR 只是时间问题,现在看到国外很多新的库都默认使用了 Fetch。

最后再做一个大胆预测:由于 async/await 这类新异步语法的出现,第三方的 Promise 类库会逐渐被标准 Promise 替代,使用 polyfill 是现在比较明智的做法。

转至我的博客,原文地址:https://github.com/camsong/blog/issues/2

想不想加入阿里巴巴一起玩 ES7,React,FRP 等最新技术,欢迎简历到 neosoyn@gmail.com

查看原文

赞 199 收藏 779 评论 39

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2012-02-20
个人主页被 2.3k 人浏览