undefined_segmentfault

undefined_segmentfault 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

undefined_segmentfault 回答了问题 · 2016-03-01

解决在redux中dispatch异步action,并且在异步action完成之后再dispatch另一个action,如何实现

我们在 Redux 应用中也遇到了跟你一样的需求,所以经过各种尝试和摸索之后抽象出了一个 Redux 中间件。

https://github.com/jasonslyvia/redux-sequence-action ,当然要完整实现你的需求,负责处理 ajax 的中间件需要返回 Promise。我们自己设计了请求中间件,不过暂时还没有开源。

关注 4 回答 2

undefined_segmentfault 赞了文章 · 2015-11-25

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

undefined_segmentfault 发布了文章 · 2015-11-09

React 源码剖析系列 - 解密 setState

this.setState() 方法应该是每一位使用 React 的同学最先熟悉的 API。然而,你真的了解 setState 么?先看看下面这个小问题,你能否正确回答。

引子

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

问上述代码中 4 次 console.log 打印出来的 val 分别是多少?

不卖关子,先揭晓答案,4 次 log 的值分别是:0、0、2、3。

若结果和你心中的答案不完全相同,那下面的内容你可能会感兴趣。

同样的 setState 调用,为何表现和结果却大相径庭呢?让我们先看看 setState 到底干了什么。

setState 干了什么

setState 简化调用栈

上面这个流程图是一个简化的 setState 调用栈,注意其中核心的状态判断,在源码(ReactUpdates.js)

function enqueueUpdate(component) {
  // ...

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}

isBatchingUpdates 为 true,则把当前组件(即调用了 setState 的组件)放入 dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新。先不管这个 batchingStrategy,看到这里大家应该已经大概猜出来了,文章一开始的例子中 4 次 setState 调用表现之所以不同,这里逻辑判断起了关键作用。

那么 batchingStrategy 究竟是何方神圣呢?其实它只是一个简单的对象,定义了一个 isBatchingUpdates 的布尔值,和一个 batchedUpdates 方法。下面是一段简化的定义代码:

var batchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    // ...
    batchingStrategy.isBatchingUpdates = true;
    
    transaction.perform(callback, null, a, b, c, d, e);
  }
};

注意 batchingStrategy 中的 batchedUpdates 方法中,有一个 transaction.perform 调用。这就引出了本文要介绍的核心概念 —— Transaction(事务)。

初识 Transaction

熟悉 MySQL 的同学看到 Transaction 是否会心一笑?然而在 React 中 Transaction 的原理和行为和 MySQL 中并不完全相同,让我们从源码开始一步步开始了解。

在 Transaction 的源码中有一幅特别的 ASCII 图,形象的解释了 Transaction 的作用。

/*
 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>
 */

简单地说,一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。

具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。

下面是一个简单使用 Transaction 的例子

var Transaction = require('./Transaction');

// 我们自己定义的 Transaction
var MyTransaction = function() {
  // do sth.
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function() {
    return [{
      initialize: function() {
        console.log('before method perform');
      },
      close: function() {
        console.log('after method perform');
      }
    }];
  };
});

var transaction = new MyTransaction();
var testMethod = function() {
  console.log('test');
}
transaction.perform(testMethod);

// before method perform
// test
// after method perform

当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。

说了这么多 Transaction,它到底是怎么导致上文所述 setState 的各种不同表现的呢?

解密 setState

那么 Transaction 跟 setState 的不同表现有什么关系呢?首先我们把 4 次 setState 简单归类,前两次属于一类,因为他们在同一次调用栈中执行;setTimeout 中的两次 setState 属于另一类,原因同上。让我们分别看看这两类 setState 的调用栈:

componentDidMout 中 setState 的调用栈

componentDidMout 中 setState 的调用栈

setTimeout 中 setState 的调用栈

setTimeout 中 setState 的调用栈

很明显,在 componentDidMount 中直接调用的两次 setState,其调用栈更加复杂;而 setTimeout 中调用的两次 setState,调用栈则简单很多。让我们重点看看第一类 setState 的调用栈,有没有发现什么熟悉的身影?没错,就是 batchedUpdates 方法,原来早在 setState 调用前,已经处于 batchedUpdates 执行的 transaction 中!

那这次 batchedUpdate 方法,又是谁调用的呢?让我们往前再追溯一层,原来是 ReactMount.js 中的 _renderNewRootComponent 方法。也就是说,整个将 React 组件渲染到 DOM 中的过程就处于一个大的 Transaction 中。

接下来的解释就顺理成章了,因为在 componentDidMount 中调用 setState 时,batchingStrategy 的 isBatchingUpdates 已经被设为 true,所以两次 setState 的结果并没有立即生效,而是被放进了 dirtyComponents 中。这也解释了两次打印 this.state.val 都是 0 的原因,新的 state 还没有被应用到组件中。

再反观 setTimeout 中的两次 setState,因为没有前置的 batchedUpdate 调用,所以 batchingStrategy 的 isBatchingUpdates 标志位是 false,也就导致了新的 state 马上生效,没有走到 dirtyComponents 分支。也就是,setTimeout 中第一次 setState 时,this.state.val 为 1,而 setState 完成后打印时 this.state.val 变成了 2。第二次 setState 同理。

扩展阅读

在上文介绍 Transaction 时也提到了其在 React 源码中的多处应用,想必调试过 React 源码的同学应该能经常见到它的身影,像 initialize、perform、close、closeAll、notifyAll 等方法出现在调用栈中,都说明当前处于一个 Transaction 中。

既然 Transaction 这么有用,我们自己的代码中能使用 Transaction 吗?很可惜,答案是不能。不过针对文章一开始例子中 setTimeout 里的两次 setState 导致两次 render 的情况,React 偷偷给我们暴露了一个 batchedUpdates 方法,方便我们调用。

import ReactDom, { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  this.setState(val: this.state.val + 1);
  this.setState(val: this.state.val + 1);
});

当然因为这个不是公开的 API,后续存在废弃的风险,大家在业务系统里慎用哟!

注释

  1. test-react 文中测试代码已放在 Github 上,需要自己实验探索的同学可以 clone 下来自己断点调试。

  2. 为了避免引入更多的概念,上文中所说到的 batchingStrategy 均指 ReactDefaultBatchingStrategy,该 strategy 在 React 初始化时由 ReactDefaultInjection 注入到 ReactUpdates 中作为默认的 strategy。在 server 渲染时,则会注入不同的 strategy,有兴趣的同学请自行探索。

查看原文

赞 37 收藏 78 评论 6

认证与成就

  • 获得 37 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-10-26
个人主页被 454 人浏览