Arcthur

Arcthur 查看完整档案

杭州编辑杭州电子科技大学  |  软件工程 编辑阿里巴巴  |  前端开发专家 编辑 www.zhihu.com/people/arcthur 编辑
编辑

现任职于阿里巴巴集团,花名流形
现任阿里巴巴大数据对外产品前端团队负责人
专注在React、Node等领域

个人动态

Arcthur 收藏了文章 · 2018-06-21

React + TS 2.8:终极组件设计模式指南

原文:Ultimate React Component Patterns with Typescript 2.8, Martin Hochel

本文的写作灵感来自于《React Component Patterns》,线上演示地址>>点我>>

熟悉我的朋友都知道,我不喜欢写无类型支持的 JavaScript,所以从 TypeScript 0.9 开始我就深深地爱上它了。
除了类型化的 JavaScript,我也非常喜欢 React,React 和 TypeScript 的结合让我感觉置身天堂:D。
在整个应用中,类型安全和 VDOM 的无缝衔接,让开发体验变得妙不可言!

所以本文想要分享什么信息呢?
尽管网上有很多关于 React 组件设计模式的文章,但是没有一篇介绍如何使用 TypeScript 来实现。
与此同时,最新版的 TypeScript 2.8 也带来了令人激动人心的功能,比如支持条件类型(Conditional Types)、标准库中预定义的条件类型以及同态映射类型修饰符等等,这些功能使我们能够更简便地写出类型安全的通用组件模式。

本文非常长,但是请不要被吓到了,因为我会手把手教你掌握终极 React 组件设计模式!

文中所有的设计模式和例子都使用 TypeScript 2.8 和严格模式

准备

磨刀不误砍柴工。首先我们要安装好 typescripttslib,使用 tslib 可以让我们生成的代码更加紧凑。

yarn add -D typescript
# tslib 弥补编译目标不支持的功能,如
yarn add tslib

然后,就可以使用 tsc 命令来初始化项目的 TypeScript 配置了。

# 为项目创建 tsconfig.json ,使用默认编译设置
yarn tsc --init

接着,安装 reactreact-dom 和它们的类型文件。

yarn add react react-dom
yarn add -D @types/{react,react-dom}

非常棒!现在我们就可以开始研究组件模式了,你准备好了么?

无状态组件

无状态组件(Stateless Component)就是没有状态(state)的组件。大多数时候,它们就是纯函数
下面让我们来使用 TypeScript 随便编写一个无状态的按钮组件。

就像使用纯 JavaScript 一样,我们需要引入 react 以支持 JSX 。
(译注:TypeScript 中,要支持 JSX,文件拓展名必须为 .tsx

import React from 'react'
const Button = ({ onClick: handleClick, children }) => (
  <button onClick={handleClick}>{children}</button>
)

不过 tsc 编译器报错了:(。我们需要明确地告诉组件它的属性是什么类型。所以,让我们来定义组件属性:

import React, { MouseEvent, ReactNode } from 'react'
type Props = { 
 onClick(e: MouseEvent<HTMLElement>): void
 children?: ReactNode 
}
const Button = ({ onClick: handleClick, children }: Props) => (
  <button onClick={handleClick}>{children}</button>
)

很好!这下终于没有报错了!但是我们还可以做得更好!

@types/react 类型模块中预定了 type SFC<P>,它是 interface StatelessComponent<P> 的类型别名,并且它预定义了 childrendisplayNamedefaultProps 等属性。所以,我们用不着自己写,可以直接拿来用。

于是,最终的代码长这样:

Stateless Component

状态组件

让我们来创建一个有状态的计数组件,并在其中使用我们上面创建的 Button 组件。

首先,定义好初始状态 initialState

const initialState = { clicksCount: 0 }

这样我们就可以使用 TypeScript 来对它进行类型推断了。

这种做法可以让我们不用分别独立维护类型和实现,如果实现变更了类型也会随之自动改变,妙!

type State = Readonly<typeof initialState>

同时,这里也明确地把所有属性都标记为只读。在使用的时候,我们还需要显式地把状态定义为只读,并声明为 State 类型。

readonly state: State = initialState

为什么声明为只读呢?
这是因为 React 不允许直接更新 state 及其属性。类似下面的做法是错误的

this.state.clicksCount = 2
this.state = { clicksCount: 2 }

该做法在编译时不会出错,但是会导致运行时错误。通过使用 Readonly 显式地把类型 type State 的属性都标记为只读属性,以及声明 state 为只读对象,TypeScript 可以实时地把错误用法反馈给开发者,从而避免错误。

比如:

Compile time State type safety

由于容器组件 ButtonCounter 还没有任何属性,所以我们把 Component 的第一个泛型参数组件属性类型设置为 object,因为 props 属性在 React 中总是 {}。第二个泛型参数是组件状态类型,所以这里使用我们前面定义的 State 类型。

Stateful Component

你可能已经注意到,在上面的代码中,我们把组件更新函数独立成了组件类外部的纯函数。这是一种常用的模式,这样的话我们就可以在不需要了解任何组件内部细节的情况下,单独对这些更新函数进行测试。此外,由于我们使用了 TypeScript ,而且已经把组件状态设置为只读,所以在这种纯函数中对状态的修改也会被及时发现。

const decrementClicksCount = (prevState: State) 
                      => ({ clicksCount: prevState.clicksCount-- })

// Will throw following complile error:
//
// [ts]
// Cannot assign to 'clicksCount' because it is a constant or a read-only property.

是不是很酷呢?;)

默认属性

现在让我们来拓展一下 Button 组件,给它添加一个 string 类型的 color 属性。

type Props = { 
    onClick(e: MouseEvent<HTMLElement>): void
    color: string 
}

如果想给组件设置默认属性,我们可以使用 Button.defaultProps = {...} 实现。这样的话,就需要把类型 Propscolor 标记为可选属性。像下面这样(多了一个问号):

type Props = { 
    onClick(e: MouseEvent<HTMLElement>): void
    color?: string 
}

此时,Button 组件就变成了下面的模样:

const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
    <button style={{ color }} onClick={handleClick}>
        {children}
    </button>
)

这种实现方式工作起来是没毛病的,但是却存在隐患。因为我们是在严格模式下,所以可选属性 color 的类型其实是联合类型 undefined | string

假如后续我们需要用到 color,那么 TypeScript 就会抛出错误,因为编译器并不知道 color 已经被定义在 Component.defaultProps 了。

Default Props issue

为了告诉 TypeScript 编译器 color 已经被定义了,有以下 3 种办法:

  • 使用! 操作符(Bang Operator)显式地告诉编译器它的值不为空,像这样 <button onClick={handleClick!}>{children}</button>
  • 使用三元操作符(Ternary Operator)告诉编译器值它的值不为空:<button onClick={handleClick ? handleClick: undefined}>{children}</button>
  • 创建一个可复用的高阶函数(High Order Function)withDefaultProps,该函数会更新我们的属性类型定义并且设置默认属性。是我见过的最纯粹的解决办法。

多亏了 TypeScript 2.8 新增的预定义条件类型,withDefaultProps 实现起来非常简单。

withDefaultProps High order function generic helper

注意: Omit 并没有成为 TypeScript 2.8 预定义的条件映射类型,因此需要自行实现: declare type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

下面我们用它来解决上面的问题:

Define default props on our Button component

或者更简单的:

Define default props on inline with Component implementation

现在,Button 的组件属性已经定义好,可以被使用了。在类型定义上,默认属性也被标记为可选属性,但是在是现实上仍然是必选的。

{
  onClick(e: MouseEvent<HTMLElement>): void
  color?: string
}

button with default props

在使用方式上也是一模一样:

render(){
  return (
    <ButtonWithDefaultProps 
      onClick={this.handleIncrement}
    >
      Increment
    </ButtonWithDefaultProps>
  )
}

withDefaultProps 也能用在直接使用 class 定义的组件上,如下图所示:

inline class

这里多亏了 TS 的类结构源,我们不需要显式定义 Props 泛型类型

ButtonViaClass 组件的用法也还是保持一致:

render(){
  return (
    <ButtonViaClass
      onClick={this.handleIncrement}
    >
      Increment
    </ButtonViaClass>
  )
}

接下来我们会编写一个可展开的菜单组件,当点击组件时,它会显示子组件内容。我们会用多种不同的组件模式来实现它。

渲染回调/渲染属性模式

要想让一个组件变得可复用,最简单的办法是把组件子元素变成一个函数或者新增一个 render 属性。这也是渲染回调(Render Callback)又被称为子组件函数(Function as Child Component)的原因。

首先,让我们来实现一个拥有 render 属性的 Toggleable 组件:

toggleable component

存在不少疑惑?

让我们来一步一步看各个重要部分的实现:

const initialState = { show: false }
type State = Readonly<typeof initialState>

这个没什么新内容,就跟我们前文的例子一样,只是声明状态类型。

接下来我们需要定义组件属性。注意:这里我们使用映射类型 Partial 来把属性标记为可选,而不是使用 ? 操作符。

type Props = Partial<{
  children: RenderCallback
  render: RenderCallback
}>
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
type ToggleableComponentProps = { 
  show: State['show']
  toggle: Toggleable['toggle'] 
}

我们希望同时支持子组件函数和渲染回调函数,所以这里把它们都标记为可选的。为了避免重复造轮子,这里为渲染函数创建了 RenderCallback 类型:

type RenderCallback = (args: ToggleableComponentProps) => JSX.Element

其中,看起来可能令人疑惑的是类型 type ToggleableComponentProps

type ToggleableComponentProps = { 
  show: State['show']
  toggle: Toggleable['toggle'] 
}

这个其实是用到了 TS 的类型查询功能,这样的话我们就不需要重复定义类型了:

  • show: State['show']:使用在状态中已经定义的类型来为 show 声明类型
  • toggle: Toggleable['toggle']:通过类型推断和类结构获取方法类型。优雅而强大!

其他部分的实现是很直观的,标准的渲染属性/子组件函数模式:

export class Toggleable extends Component<Props, State> {
  // ...
  render() {
    const { children, render } = this.props
    const renderProps = { show: this.state.show, toggle: this.toggle }
    if (render) {
      return render(renderProps)
    }
    return isFunction(children) ? children(renderProps) : null
  }
  // ...
}

至此,我们就可以通过子组件函数来使用 Toggleable 组件了:

children as a function

或者给 render 属性传递渲染函数:

render prop

得益于强大的 TS ,我们在编码的时候还可以有代码提示和正确的类型检查:

soundness

如果我们想复用它,可以简单的创建一个新组件来使用它:

ToggleableMenu

这个全新的 ToggleableMenu 组件现在就可以用在菜单组件中了:

Menu Component

而且效果也正如我们所预期:

menu demo

这种方式非常适合用在需要改变渲染内容本身,而又不想使用状态的场景。因为我们把渲染逻辑移到了 ToggleableMenu 的子组件函数中,同时又把状态逻辑留在 Toggleable 组件中。

组件注入

为了让我们的组件更加灵活,我们还可以引入组件注入(Component Injection)模式。

何为组件注入模式?如果你熟悉 React-Router 的话,那么在定义路由的时候就是在使用这个模式:

<Route path="/foo" component={MyView} />

所以,除了传递 render/children 属性,我们还可以通过 component 属性来注入组件。为此,我们需要把行内渲染回调函数重构成可复用的无状态组件:

import { ToggleableComponentProps } from './toggleable'
type MenuItemProps = { title: string }
const MenuItem: SFC<MenuItemProps & ToggleableComponentProps> = ({
  title,
  toggle,
  show,
  children,
}) => (
  <>
    <div onClick={toggle}>
      <h1>{title}</h1>
    </div>
    {show ? children : null}
  </>
)

这样的话,ToggleableMenu 也需要重构下:

type Props = { title: string }
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
  <Toggleable
    render={({ show, toggle }) => (
      <MenuItem show={show} toggle={toggle} title={title}>
        {children}
      </MenuItem>
    )}
  />
)

接下来,让我们来定义新的 component 属性。
首先,我们需要更新下属性成员:

  • children 可以是函数或者是 ReactNode
  • component 是新成员,它的值为组件,该组件的属性需要实现 ToggleableComponentProps,同时它又必须支持默认为 any 的泛型类型,这样它不会仅仅用于实现了 ToggleableComponentProps 属性的组件。
  • props 是新成员,用来往下传递任意属性,这也是一种通用模式。它被定义为类型是 any 的索引类型,所以这里我们其实丢失了严格的安全检查。
// 使用任意属性类型来声明默认属性,props 默认为空对象
const defaultProps = { props: {} as { [name: string]: any } }
type Props = Partial<
  {
    children: RenderCallback | ReactNode
    render: RenderCallback
    component: ComponentType<ToggleableComponentProps<any>>
  } & DefaultProps
>
type DefaultProps = typeof defaultProps

接着,需要把新的 props 同步到 ToggleableComponentProps,这样才能使用 props 属性 <Toggleable props={...}/>

export type ToggleableComponentProps<P extends object = object> = {
  show: State['show']
  toggle: Toggleable['toggle']
} & P

最后还需要修改下 render 方法:

render() {
    const { 
     component: InjectedComponent, 
     children, 
     render, 
     props 
    } = this.props
    const renderProps = { 
     show: this.state.show, toggle: this.toggle 
    }
    // 当使用 component 属性时,children 不是一个函数而是 ReactNode
    if (InjectedComponent) {
      return (
        <InjectedComponent {...props} {...renderProps}>
          {children}
        </InjectedComponent>
      )
    }
    if (render) {
      return render(renderProps)
    }
    // children as a function comes last
    return isFunction(children) ? children(renderProps) : null
  }

把前面的内容都综合起来,就实现了一个支持 render 属性、函数子组件和组件注入的 Toggleable 组件:

full toggleable component

其使用方式如下:

ToggleableMenu with component injection pattern

这里要注意:我们自定义的 props 属性并没有安全的类型检查,因为它被定义为索引类型 { [name: string]: any }

We can pass anything to our props prop :(

在菜单组件的渲染中,ToggleableMenuViaComponentInjection 组件的使用方式跟原来一致:

export class Menu extends Component {
  render() {
    return (
      <>
        <ToggleableMenuViaComponentInjection title="First Menu">
          Some content
        </ToggleableMenuViaComponentInjection>
        <ToggleableMenuViaComponentInjection title="Second Menu">
          Another content
        </ToggleableMenuViaComponentInjection>
        <ToggleableMenuViaComponentInjection title="Third Menu">
          More content
        </ToggleableMenuViaComponentInjection>
      </>
    )
  }
}

泛型组件

在前面我们实现组件注入模式时,有一个大问题是 props 属性失去了严格的类型检查。如何解决这个问题?你可能已经猜到了!我们可以把 Toggleable 实现为泛型组件。

首先,我们需要把属性泛型化。我们可以使用默认泛型参数,这样的话,当我们不需要传 props 时就可以不用显式传递该参数了。

type Props<P extends object = object> = Partial<
  {
    children: RenderCallback | ReactNode
    render: RenderCallback
    component: ComponentType<ToggleableComponentProps<P>>
  } & DefaultProps<P>
>

此外,还需要使 ToggleableComponentProps 泛型化,不过它现在其实已经是了,所以这块不需要重写。

唯一需要改动的是 type DefaultProps ,因为目前的实现方式中,它是没有办法获取泛型类型的,所以我们需要把它改为另一种方式:

type DefaultProps<P extends object = object> = { props: P }
const defaultProps: DefaultProps = { props: {} }

马上就要完成了!

最后把 Toggleable 组件变成泛型组件。同样地,我们使用了默认参数,因为只有在使用组件注入时才需要传参,其他情况时则不需要。

export class Toggleable<T = {}> extends Component<Props<T>, State> {}

大功告成!不过,真的么?我们如何才能在 JSX 中使用泛型类型?

很遗憾,并不能。

所以,我们还需要引入 ofType 泛型组件工厂模式:

export class Toggleable<T extends object = object> extends Component<Props<T>, State> {
  static ofType<T extends object>() {
    return Toggleable as Constructor<Toggleable<T>>
  }
}

完整的实现版本如下:

generic props

有了 static ofType 静态方法之后,我们就可以创建正确的类型检查泛型组件了:

ofType

一切都跟之前一样,但是这次我们的 props 有了类型检查!

type safe

高阶组件

既然我们的 Toggleable 组件已经实现了 render 属性,那么实现高阶组件(High Order Component, HOC)就很容易了。渲染回调模式的最大好处之一就是,它可以直接用于实现 HOC。

下面让我们来实现这个 HOC。

我们需要新增以下内容:

  • displayName(用于调试工具展示,便于阅读)
  • WrappedComponent (用于访问原组件,便于测试)
  • 使用 hoist-non-react-statics 包的 hoistNonReactStatics 方法

hoc implemention

这样我们就可以以 HOC 的方式来创建 Toggleable 菜单项了, 而且仍然保持了对属性的类型检查。

const ToggleableMenuViaHOC = withToggleable(MenuItem)

Proper type annotation

受控组件

压轴大戏来了!
我们来实现一个可以通过父组件进行高度配置的 Toggleable ,这种是一种非常强大的模式。

可能有人会问,受控组件(Controlled Component)是什么?在这里意味着,我想要同时控制 Menu 组件中所有 ToggleableMenu 的内容是否显示,看看下面的动态你应该就知道是什么了。

controlled component

为了实现该目标,我们需要修改下 ToggleableMenu 组件,修改后的内容如下:

ToggleableMenu

然后,我们还需要在 Menu 中新增一个状态,并且把它传递给 ToggleableMenu

Stateful Menu component

最后,还需要修改 Toggleable 最后一次,让它变得更加无敌和灵活。
修改内容如下:

  1. 新增 show 属性到 Props
  2. 更新默认属性(因为 show 是可选的)
  3. 更新默认状态,使用属性 show 的值来初始化状态 show,因为我们希望该值只能来自于其父组件
  4. 使用 componentWillReceiveProps 来利用公开属性更新状态

1 & 2 对应的修改:

const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }

type State = Readonly<typeof initialState>
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>

3 & 4 对应的修改:

export class Toggleable<T = {}> extends Component<Props<T>, State> {
  static readonly defaultProps: Props = defaultProps
  // Bang operator used, I know I know ...
  state: State = { show: this.props.show! }

  componentWillReceiveProps(nextProps: Props<T>) {
    const currentProps = this.props
    
    if (nextProps.show !== currentProps.show) {
      this.setState({ show: Boolean(nextProps.show) })
    }
  }
}

至此,终极 Toggleable 组件诞生了:

final Toggleable

同时,使用 ToggleablewithToggleable 也还要做些轻微调整,以便传递 show 属性和类型检查。

withToggleable Hoc with controllable functionality

总结

使用 TS 来实现对 React 组件进行正确的类型检查其实是相当难的。但是随着 TS 2.8 新功能的发布,我们几乎可以随意使用通用的 React 组件模式来实现类型安全的组件。

在本篇超长文中,多亏了 TS,我们学习了如何实现具有多种模式且类型安全的组件。

综合来看,其实最强大的模式非属性渲染(Render Prop)莫属,有了它,我们可以不费吹灰之力就可以实现组件注入和高阶组件。

文中所有的示范代码托管于作者的 GitHub 仓库

最后,还有一点要强调的是,本文中涉及的类型安全模板可能只适用于使用 VDOM/JSX 的库:

  • 使用语言服务的 Angular 模板也具备类型检查,但是在有些地方也还是会失效,比如 ngFor
  • Vue 模板目前也还没有类似 Angular ,所以它的模板和数据绑定实际上是魔术字符串。不过这可能在未来会改变。虽然也可以对模板字符串使用 VDOM,不过用起来应该会很笨重,因为有太多属性类型定义。(snabdom 表示:怪我咯)。
查看原文

Arcthur 关注了用户 · 2017-03-07

uglee @uglee

关注 440

Arcthur 赞了文章 · 2017-01-19

Redux story-1:who creates it?

前言

 这是一个系列文章,旨在分享在react及相关技术栈实践过程中的个人感悟,心得。如果有不好的地方,欢迎各位批评指正。

 由于对react本身还未深入了解,今天我们先来谈一谈redux相关的问题。

Who creates it?

Dan Abramovredux的作者,同时,他也是Create React App, React Hot Loader作者。当然1年前,他也由于redux及相关的开源贡献加入了facebook(他大二就辍学了,之前还当过.net工程师)。

 在我最初了解到他的时候,我觉得他非常有礼貌。同时,也为了更多的了解redux,我计划开始阅读他的每一条tweet,原先计划的是从15年7月开始,后来因为进展缓慢,而且react版本也已经发生很大变化了,于是便从16年1月1日开始阅读,目前记录到7月15日了。事实也证明,在这个过程中,的的确确学习到了很多东西。包括redux的文档及redux-links的作者Mark Erikson,以及国外很多写过redux系列的朋友们。

 如果你有兴趣的话,可以看看我摘录的一些片段。其中除了知识性的内容外,还有一些关于它自己生活,经历,学习方法,如何面对JS疲劳等等的摘录。也让我渐渐的了解到了国外的程序员们的一些观点,兴趣,梗等等。

正文

 好了,暂时先介绍到这里了。切回redux本身,下面是学习源码过程中自己的一些体会。

createStore

createStore的第3个参数为enhancer,如果enhancer有多个,那么应该使用compose的方式组合多个enhancer

 且每个enhancer的模板为export default createStore => (reducer, preloadedState, enhancer) => {...}

 因为在createStore中执行了:return enhancer(createStore)(reducer, preloadedState)

 另外,上面的提到的形如(reducer, preloadedState, enhancer) => {...} 这个样子的其实都可以叫做createStore

 这也是社区有那么多enhancer的原因,他们可以形成一个enhancer链,我调用你的createStore,然后返回我的createStore供下一级调用

 所以在自己的createStore的函数体中经常能看到诸如var store = createStore(reducer, preloadedState, enhancer);这样的用法,目的就是让自己这一级之前的enhancer产生一个store出来,而之前的enhancer里的createStore又会调用之前的,到最尽头,就是redux本身的createStore

applyMiddleware

applyMiddleware的目的是返回一个enhancer,这个enhancer存储了1个或者多个中间件,中间件在上一级的dispatch方法的基础上增添自己的逻辑,然后返回自己的dispatch方法

 对于中间件而言,中间件的模板为:export default store => next => action => {...}。有的地方也写成export default _ref => next => action => {...}或者export default ({getState, dispatch}) => next => action => {...},看自己喜好了

 实际的调用顺序如下(定义在reduxapplyMiddleware.js中):

1. middleware(middlewareAPI);
/*
    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    };
    
    chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    第1步即为第1次执行中间件,用redux自己的dipatch初始化各个中间件里的dispatch(也就是中间件的next参数)和getState。
    从而确保至少redux本身能够正常工作,中间件的store或者_ref即为这里的middlewareAPI
* */

2. dispatch = compose(...chain)(store.dispatch);
/*
    第2步即为第2次执行中间件
    即用compose的形式链式调用第1步返回的中间件集合,如果中间件是定义在applyMiddleware的最后一个
    那么中间件里的next为store.dispatch,否则next为上一个中间件返回的结果,可以理解为上一个中间件
    返回的是封装了dispatch的自己的dispatch,这里的原理其实和enhancer一模一样

    enhancer的目的是封装多次createStore并用compose的方式进行调用
    middleware的目的是封装多次dispatch并用compose的方式进行调用
* */

总结:
/*
    所以最后在redux的createStore.js中return的enhancer(createStore)(reducer, preloadedState)的结果就是一个增强
    版的store,而这个增强版的store中存放的是增强版的dispatch
* */

/* ××××××××××××××××关于combineReducers×××××××××××××××
*   从执行上来说,combineReducers实际上最后就是变成对reducers进行深度优先遍历并执行的过程
*   从结构上来说,combineReducers决定了我们的state状态树的最终结构或者说形状,他是呈一个树型结构的
*   combineReducers(reducerA, reducerB),reducerA里面嵌套combineReducers(reducerA-child1, reducerA-child2)
*   实际上对应状态树而已就是第一层有两个节点A,B,而A节点下面有两个子节点A-child1,A-child2
*
*   所以在最初设计的时候,我们要设想我们最终的状态树的样子,然后合理划分reducer,就像设计数据库的表结构一样。
    当然这是比较概括的说法,事实上reducer的设计或者说state的划分有太多太多值得研究的东西,这个我们以后再谈了。
* */

bindActionCreator

bindActionCreator实际就是给actionCreator外层再添加了一层函数,而这层函数存放了对dispatch的引用

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

所以我们可以一般在组件里直接调用bindActionCreator返回的actionCreator,即this.props.loadSomething(...)。而不用写成dispatch(actionCreator(...args)),实际上他们是等价的

connect

 既然提到了redux,由于目前我是采用react进行开发,所以不得不提到相关的react-redux。其中最重要的莫属于connect这个函数了。

 传入connect的组件在挂载到页面上后会调用store.subscribe进行订阅,订阅的目的是我们调用dispatch的时候,表明我们的状态树即将发生变化,这个时候我们希望我们的组件对应发生变化,而组件变化的唯一方式就是setState

 订阅就是告诉redux,这个组件是依赖于状态树的某部分工作的,所以当你变化的时候,记得获取最新的state,然后通知我,至于我如何响应,那就是我自己的事了,你只管通知我状态树发生了变化并把它传给我就行了。值得一提的是,connect内部进行了大量的性能优化,避免不必要的渲染,关于此以及mapStateToPropsmapDispathToProps,我们放到以后再谈。

结语

 篇幅有限,这一篇文章暂时就先这样啦,更多的内容,我想放在下一篇来分享,同时自己也在不断学习,希望能理解得更好。

 值得一提的是,我们也许会认为我们了解到的redux,mobx,rxjs等等完全不同理念的库,他们的作者也许也是"极端"的,是排斥他人及理念的。实际上,这是不正确的,早在16年5月,Dan就和mobx的作者在twitter上有过互动,他们达成了共识,那就是和对方一起合作,一起推动自身以及react的发展。

 对于redux-thunk,文档中也许会首先建议使用这个简单的库来处理异步相关的问题。对于复杂的应用,他们也推荐使用redux-saga这样的库去重构自己的代码。在twitter上,Dan也多次提到过库的应用场景的问题,建议大家用之前先了解自己为什么要使用,它解决了哪些痛点,然后再去使用。甚至特意提过issue,来了解react-router-redux的作用。

 除此之外,也提到在时间充裕的情况下,学习react,应该先从本身入手,ES6,webpack,jsx,redux等等和react本身都是没有直接联系的,在学习完react之后,我们知道了他本身的哪些不足,哪些地方需要加强,哪些地方需要引入第三方库去解决,解决的是哪些痛点,我们再去了解这些工具,才能真正体会到他们的威力。

 说到这里,稍微有一点远了,不过我觉得还是有必要提及一下。那就是,我们身处一个浮躁的社会,无论是在现实中对待朋友,亲人,陌生人,由于学习,工作,生活的压力,周遭的浮躁氛围的影响,多多少少也会让自己带着些许暴戾之气。在网络上,由于约束的放宽,我们也许更会将压抑的情感释放给广袤的网络世界,在微博,贴吧,知乎上,我们或多或少书写着,察觉着这样的行为。

 但是,作为一名程序猿,我还是期待能够看到我们这个圈子更多的将时间,精力,努力花费在对现有技术的改进,对未知世界的探索,追寻程序,库,框架,思想的本质,结交志同道合的朋友,一起交流,分享,思考对技术的看法。而不是卷入无休止的撕逼,用了某某而产生的优越,甚至借贬低他人来抬高自己。

 我们可以理解一时的愤懑之情,因为我们大多,真是只是普普通通的社会人,喜怒哀乐再平常不过。但若我们一直保持这种状态,永远在上面这些场景都留下对人不对事的话语,讥讽,甚至谩骂。希望大家能为我们的后代想想。

查看原文

赞 4 收藏 5 评论 0

Arcthur 回答了问题 · 2017-01-18

【请教算法】请问,这个分页算法可有优化的地方?

几个问题

  1. document.getElementById('ws_pager_total')不应该出现

  2. 分页你的需求如果够了这样也可以,不然还要再拆分原子化

  3. 最好支持 smart 和 dumped

关注 3 回答 2

Arcthur 关注了问题 · 2017-01-18

【请教算法】请问,这个分页算法可有优化的地方?

interface INumberPagerItem {
    url: string;
    text: string;
    page: number;
    title: string;
    target: string;
    iscurrent: boolean;
    enabled: boolean;
}
interface INumberPager {
    total: number;
    current: number;
    showcount: number;
    items: INumberPagerItem[];
}

class NumberPagerItemModel implements INumberPagerItem {
    url: string;
    text: string;
    page: number;
    target: string;
    iscurrent: boolean;
    title: string;
    enabled: boolean;

    constructor(url: string, text: string, page: number, title: string, target: string, iscurrent: boolean, enabled: boolean) {
        this.url = url;
        this.text = text;
        this.page = page;
        this.title = title;
        this.target = target;
        this.iscurrent = iscurrent;
        this.enabled = enabled;
    }
}

class NumberPagerItem extends React.Component<INumberPagerItem, any>{
    render() {
        var a = null;
        if (this.props.enabled) {
            a = <a title={this.props.title} href={this.props.url + this.props.page} className={"ws-pager " + (this.props.iscurrent ? "ws-pager-current-page" : "ws-pager-page")} target={this.props.target} >{this.props.text}</a>
        }
        else {
            a = <span title={this.props.title} className={"ws-pager " + (this.props.iscurrent ? "ws-pager-current-page" : "ws-pager-page")} >{this.props.text}</span>
        }
        return (a);
    }
}

class NumberPager extends React.Component<INumberPager, any>{
    constructor(props: INumberPager) {
        super(props);
    }

    render() {
        var items = this.props.items.map((i) => {
            return (<NumberPagerItem key={i.text.toString() + i.page.toString()} title={i.title} enabled={i.enabled} text={i.text} url={i.url} page={i.page} iscurrent={i.iscurrent} target={i.target || '_self'} />);
        });

        return (
            <span>
                {items}
            </span>);
    }
}
interface IFastPager {
    current: number;
    totalpages: number;
    fastToPage(next: number): void;
}
class FastPager extends React.Component<IFastPager, any>{
    next: number;
    constructor() {
        super();

        this.state = { current: 1, godisabled: true }
    }
    componentWillMount() {
        this.state.current = this.props.current;
        this.next = this.props.current;
    }
    handleChange(e) {
        this.next = parseInt(e.target.value);
        this.state.godisabled = this.next == this.state.current;
        this.setState(this.state);
    }
    handleClick(e) {
        if (this.next != this.state.current) {
            this.next = this.next > this.props.totalpages ? this.props.totalpages : this.next;
            this.props.fastToPage(this.next);
        }
    }
    render() {
        return (
            <span className="ws-pager-fast">
                <label>
                    到第
                <input type="number" name="ws-pager-fast-number" className="ws-pager-fast-page-number" defaultValue={this.state.current} onChange={e => this.handleChange(e)} />
                    页
                </label>
                <input type="button" className="ws-pager-fast-confrim" value="确定" disabled={this.state.godisabled} onClick={e => this.handleClick(e)} />
            </span>
        );
    }
}

interface IWsPager {
    url: string;
    target?: string;
    total: number;
    size: number;
    current: number;
    showcount: number;
    needfast: boolean;
}

interface IWsPagerState {
    current: number;
    items: INumberPagerItem[];
}

class WsPager extends React.Component<IWsPager, IWsPagerState>{
    constructor(props: IWsPager) {
        super(props);

        this.state = { current: this.props.current, items: [] };
    }
    componentWillMount() {
        let numbers: number[] = [];

        var total = parseInt((document.getElementById('ws_pager_total') as HTMLInputElement).value);
        var current = parseInt((document.getElementById('ws_pager_current') as HTMLInputElement).value);
        this.state.current = current;

        let totalpage = Math.ceil(total / this.props.size);
        let prepage = this.state.current - 1;
        let nextpage = this.state.current + 1;

        numbers.push(-10);

        if (totalpage - this.props.showcount <= 0) {
            for (var i = 1; i <= totalpage; i++) {
                numbers.push(i);
            }
        }
        else {
            let half = (this.props.showcount - 1) / 2;
            let pre = this.state.current - half;
            let next = this.state.current + half;

            numbers.push(1);

            if (pre > half) {
                numbers.push(-9);
            }
            var start = pre;
            if (pre < 2) {
                start = 2;
                var end = start + this.props.showcount > totalpage ? totalpage : start + this.props.showcount
                for (var i = start; i < end; i++) {
                    numbers.push(i);
                }
            }
            else {
                start = pre;
                if (next >= totalpage) {
                    next = totalpage - 1;
                    start = next - this.props.showcount;

                    for (var i = start; i <= next; i++) {
                        numbers.push(i);
                    }
                } else {
                    for (var i = pre; i < this.state.current; i++) {
                        numbers.push(i);
                    }

                    if (this.state.current != 1) {
                        numbers.push(this.state.current);
                    }

                    for (var i = this.state.current + 1; i <= next; i++) {
                        numbers.push(i);
                    }
                }
            }

            if (next + 1 < totalpage) {
                numbers.push(-2);
            }

            numbers.push(totalpage);
        }

        numbers.push(-1);

        var items: INumberPagerItem[] = [];

        for (var i = 0; i < numbers.length; i++) {
            var num = numbers[i];
            if (num == -10) {
                items.push(new NumberPagerItemModel(this.props.url, '上一页', prepage, '上一页', null, false, prepage > 0));
            } else if (num == -1) {
                items.push(new NumberPagerItemModel(this.props.url, '下一页', nextpage, '下一页', null, false, nextpage <= totalpage));
            } else if (num == -9) {
                items.push(new NumberPagerItemModel(null, '...', num, '向前' + this.props.showcount.toString() + "页", null, false, false));
            } else if (num == -2) {
                items.push(new NumberPagerItemModel(null, '...', num, '向后' + this.props.showcount.toString() + "页", null, false, false));
            } else {
                items.push(new NumberPagerItemModel(this.props.url, num.toString(), num, num.toString(), null, this.state.current == num, this.state.current != num));
            }
        }

        this.setState({ current: this.state.current, items: items });
    }
    handleFastToPage(e) {
        window.location.href = this.props.url + e;
    }
    render() {
        var div = null;
        if (this.props.needfast) {
            div =
                (
                    <div>
                        <NumberPager items={this.state.items} total={this.props.total} current={this.state.current} showcount={this.props.showcount} />
                        <FastPager current={this.state.current} totalpages={Math.ceil(this.props.total / this.props.size)} fastToPage={e => this.handleFastToPage(e)} />
                    </div>
                );
        } else {
            div =
                (
                    <div>
                        <NumberPager items={this.state.items} total={this.props.total} current={this.state.current} showcount={this.props.showcount} />
                    </div>
                )
        }
        return (div);
    }
}

ReactDOM.render(
    <WsPager url="/pager/index/" needfast={true} current={1} showcount={9} size={10} total={124} />,
    document.getElementById('pager')
)

目前我就是最笨的判断,if else 嵌套,可有算法优化一下?
就是计算numbers数组的值的算法。

关注 3 回答 2

Arcthur 赞了文章 · 2017-01-10

一篇文章说清浏览器解析和CSS(GPU)动画优化

相信不少人在做移动端动画的时候遇到了卡顿的问题,这篇文章尝试从浏览器渲染的角度;一点一点告诉你动画优化的原理及其技巧,作为你工作中优化动画的参考。文末有优化技巧的总结。

因为GPU合成没有官方规范,每个浏览器的问题和解决方式也不同;所以文章内容仅供参考。

浏览器渲染

提高动画的优化不得不提及浏览器是如何渲染一个页面。在从服务器中拿到数据后,浏览器会先做解析三类东西:

  • 解析html,xhtml,svg这三类文档,形成dom树。

  • 解析css,产生css rule tree。

  • 解析js,js会通过api来操作dom tree和css rule tree。

解析完成之后,浏览器引擎会通过dom tree和css rule tree来构建rendering tree:

  • rendering tree和dom tree并不完全相同,例如:<head></head>或display:none的东西就不会放在渲染树中。

  • css rule tree主要是完成匹配,并把css rule附加给rendering tree的每个element。

在渲染树构建完成后,

  • 浏览器会对这些元素进行定位和布局,这一步也叫做reflow或者layout。

  • 浏览器绘制这些元素的样式,颜色,背景,大小及边框等,这一步也叫做repaint。

  • 然后浏览器会将各层的信息发送给GPU,GPU会将各层合成;显示在屏幕上。

渲染优化原理

如上所说,渲染树构建完成后;浏览器要做的步骤:

reflow——》repaint——》composite

reflow和repaint

reflow和repaint都是耗费浏览器性能的操作,这两者尤以reflow为甚;因为每次reflow,浏览器都要重新计算每个元素的形状和位置。

由于reflow和repaint都是非常消耗性能的,我们的浏览器为此做了一些优化。浏览器会将reflow和repaint的操作积攒一批,然后做一次reflow。但是有些时候,你的代码会强制浏览器做多次reflow。例如:

var content = document.getElementById('content');
content.style.width = 700px;
var contentWidth = content.offsetWidth;
content.style.backgound = 'red';

以上第三行代码,需要浏览器reflow后;再获取值,所以会导致浏览器多做一次reflow。

下面是一些针对reflow和repaint的最佳实践:

  • 不要一条一条地修改dom的样式,尽量使用className一次修改。

  • 将dom离线后修改

    • 使用documentFragment对象在内存里操作dom。

    • 先把dom节点display:none;(会触发一次reflow)。然后做大量的修改后,再把它显示出来。

    • clone一个dom节点在内存里,修改之后;与在线的节点相替换。

  • 不要使用table布局,一个小改动会造成整个table的重新布局。

  • transform和opacity只会引起合成,不会引起布局和重绘。

从上述的最佳实践中你可能发现,动画优化一般都是尽可能地减少reflow、repaint的发生。关于哪些属性会引起reflow、repaint及composite,你可以在这个网站找到https://csstriggers.com/

composite

在reflow和repaint之后,浏览器会将多个复合层传入GPU;进行合成工作,那么合成是如何工作的呢?

假设我们的页面中有A和B两个元素,它们有absolute和z-index属性;浏览器会重绘它们,然后将图像发送给GPU;然后GPU将会把多个图像合成展示在屏幕上。

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 30px;
 top: 30px;
 z-index: 2;
}

#b {
 z-index: 1;
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

clipboard.png

我们将A元素使用left属性,做一个移动动画:

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { left: 30px; }
 to { left: 100px; }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

在这个例子中,对于动画的每一帧;浏览器会计算元素的几何形状,渲染新状态的图像;并把它们发送给GPU。(你没看错,position也会引起浏览器重排的)尽管浏览器做了优化,在repaint时,只会repaint部分区域;但是我们的动画仍然不够流畅。

因为重排和重绘发生在动画的每一帧,一个有效避免reflow和repaint的方式是我们仅仅画两个图像;一个是a元素,一个是b元素及整个页面;我们将这两张图片发送给GPU,然后动画发生的时候;只做两张图片相对对方的平移。也就是说,仅仅合成缓存的图片将会很快;这也是GPU的优势——它能非常快地以亚像素精度地合成图片,并给动画带来平滑的曲线。

为了仅发生composite,我们做动画的css property必须满足以下三个条件:

  • 不影响文档流。

  • 不依赖文档流。

  • 不会造成重绘。

满足以上以上条件的css property只有transform和opacity。你可能以为position也满足以上条件,但事实不是这样,举个例子left属性可以使用百分比的值,依赖于它的offset parent。还有em、vh等其他单位也依赖于他们的环境。

我们使用translate来代替left

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { transform: translateX(0); }
 to { transform: translateX(70px); }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

浏览器在动画执行之前就知道动画如何开始和结束,因为浏览器没有看到需要reflow和repaint的操作;浏览器就会画两张图像作为复合层,并将它们传入GPU。

这样做有两个优势:

  • 动画将会非常流畅

  • 动画不在绑定到CPU,即使js执行大量的工作;动画依然流畅。

看起来性能问题好像已经解决了?在下文你会看到GPU动画的一些问题。

GPU是如何合成图像的

GPU实际上可以看作一个独立的计算机,它有自己的处理器和存储器及数据处理模型。当浏览器向GPU发送消息的时候,就像向一个外部设备发送消息。

你可以把浏览器向GPU发送数据的过程,与使用ajax向服务器发送消息非常类似。想一下,你用ajax向服务器发送数据,服务器是不会直接接受浏览器的存储的信息的。你需要收集页面上的数据,把它们放进一个载体里面(例如JSON),然后发送数据到远程服务器。

同样的,浏览器向GPU发送数据也需要先创建一个载体;只不过GPU距离CPU很近,不会像远程服务器那样可能几千里那么远。但是对于远程服务器,2秒的延迟是可以接受的;但是对于GPU,几毫秒的延迟都会造成动画的卡顿。

浏览器向GPU发送的数据载体是什么样?这里给出一个简单的制作载体,并把它们发送到GPU的过程。

  • 画每个复合层的图像

  • 准备图层的数据

  • 准备动画的着色器(如果需要)

  • 向GPU发送数据

所以你可以看到,每次当你添加transform:translateZ(0)will-change:transform给一个元素,你都会做同样的工作。重绘是非常消耗性能的,在这里它尤其缓慢。在大多数情况,浏览器不能增量重绘。它不得不重绘先前被复合层覆盖的区域。

隐式合成

还记得刚才a元素和b元素动画的例子吗?现在我们将b元素做动画,a元素静止不动。

clipboard.png

和刚才的例子不同,现在b元素将拥有一个独立复合层;然后它们将被GPU合成。但是因为a元素要在b元素的上面(因为a元素的z-index比b元素高),那么浏览器会做什么?浏览器会将a元素也单独做一个复合层!

所以我们现在有三个复合层a元素所在的复合层、b元素所在的复合层、其他内容及背景层。

一个或多个没有自己复合层的元素要出现在有复合层元素的上方,它就会拥有自己的复合层;这种情况被称为隐式合成。

浏览器将a元素提升为一个复合层有很多种原因,下面列举了一些:

  • 3d或透视变换css属性,例如translate3d,translateZ等等(js一般通过这种方式,使元素获得复合层)

  • <video><iframe><canvas><webgl>等元素。

  • 混合插件(如flash)。

  • 元素自身的 opacity和transform 做 CSS 动画。

  • 拥有css过滤器的元素。

  • 使用will-change属性。

  • position:fixed。

  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

这看起来css动画的性能瓶颈是在重绘上,但是真实的问题是在内存上:

内存占用

使用GPU动画需要发送多张渲染层的图像给GPU,GPU也需要缓存它们以便于后续动画的使用。

一个渲染层,需要多少内存占用?为了便于理解,举一个简单的例子;一个宽、高都是300px的纯色图像需要多少内存?

300 300 4 = 360000字节,即360kb。这里乘以4是因为,每个像素需要四个字节计算机内存来描述。

假设我们做一个轮播图组件,轮播图有10张图片;为了实现图片间平滑过渡的交互;为每个图像添加了will-change:transform。这将提升图像为复合层,它将多需要19mb的空间。800 600 4 * 10 = 1920000。

仅仅是一个轮播图组件就需要19m的额外空间!

在chrome的开发者工具中打开setting——》Experiments——》layers可以看到每个层的内存占用。如图所示:

clipboard.png

clipboard.png

GPU动画的优点和缺点

现在我们可以总结一下GPU动画的优点和缺点:

  • 每秒60帧,动画平滑、流畅。

  • 一个合适的动画工作在一个单独的线程,它不会被大量的js计算阻塞。

  • 3D“变换”是便宜的。

缺点:

  • 提升一个元素到复合层需要额外的重绘,有时这是慢的。(即我们得到的是一个全层重绘,而不是一个增量)

  • 绘图层必须传输到GPU。取决于层的数量和传输可能会非常缓慢。这可能让一个元素在中低档设备上闪烁。

  • 每个复合层都需要消耗额外的内存,过多的内存可能导致浏览器的崩溃。

  • 如果你不考虑隐式合成,而使用重绘;会导致额外的内存占用,并且浏览器崩溃的概率是非常高的。

  • 我们会有视觉假象,例如在Safari中的文本渲染,在某些情况下页面内容将消失或变形。

优化技巧

避免隐式合成

  • 保持动画的对象的z-index尽可能的高。理想的,这些元素应该是body元素的直接子元素。当然,这不是总可能的。所以你可以克隆一个元素,把它放在body元素下仅仅是为了做动画。

  • 将元素上设置will-change CSS属性,元素上有了这个属性,浏览器会提升这个元素成为一个复合层(不是总是)。这样动画就可以平滑的开始和结束。但是不要滥用这个属性,否则会大大增加内存消耗。

动画中只使用transform和opacity

如上所说,transform和opacity保证了元素属性的变化不影响文档流、也不受文档流影响;并且不会造成repaint。
有些时候你可能想要改变其他的css属性,作为动画。例如:你可能想使用background属性改变背景:

<div class="bg-change"></div>
.bg-change {
  width: 100px;
  height: 100px;
  background: red;
  transition: opacity 2s;
}
.bg-change:hover {
  background: blue;
}

在这个例子中,在动画的每一步;浏览器都会进行一次重绘。我们可以使用一个复层在这个元素上面,并且仅仅变换opacity属性:

<div class="bg-change"></div>
<style>
.bg-change {
  width: 100px;
  height: 100px;
  background: red;
}
.bg-change::before {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  background: blue;
  opacity: 0;
  transition: opacity 20s;
}
.bg-change:hover::before {
  opacity: 1;
}
</style>

减小复合层的尺寸

看一下两张图片,有什么不同吗?

clipboard.png

这两张图片视觉上是一样的,但是它们的尺寸一个是39kb;另外一个是400b。不同之处在于,第二个纯色层是通过scale放大10倍做到的。

<div id="a"></div>
<div id="b"></div>

<style>
#a, #b {
 will-change: transform;
}

#a {
 width: 100px;
 height: 100px;
}

#b {
 width: 10px;
 height: 10px;
 transform: scale(10);
}
</style>

对于图片,你要怎么做呢?你可以将图片的尺寸减少5%——10%,然后使用scale将它们放大;用户不会看到什么区别,但是你可以减少大量的存储空间。

用css动画而不是js动画

css动画有一个重要的特性,它是完全工作在GPU上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给GPU。而如果使用js动画,浏览器必须计算每一帧的状态;为了保证平滑的动画,我们必须在浏览器主线程计算新状态;把它们发送给GPU至少60次每秒。除了计算和发送数据比css动画要慢,主线程的负载也会影响动画; 当主线程的计算任务过多时,会造成动画的延迟、卡顿。

所以尽可能地使用基于css的动画,不仅仅更快;也不会被大量的js计算所阻塞。

优化技巧总结

  • 减少浏览器的重排和重绘的发生。

  • 不要使用table布局。

  • css动画中尽量只使用transform和opacity,这不会发生重排和重绘。

  • 尽可能地只使用css做动画。

  • 避免浏览器的隐式合成。

  • 改变复合层的尺寸。

参考

GPU合成主要参考:

https://www.smashingmagazine....

哪些属性会引起reflow、repaint及composite,你可以在这个网站找到:

https://csstriggers.com/

查看原文

赞 103 收藏 155 评论 9

Arcthur 赞了文章 · 2016-12-26

知乎pure render专栏创办人@流形:选择React这条路,很庆幸

本文仅用于学习和交流目的,不得用于商业目的。非商业转载请注明作译者、出处,并保留本文的原始链接:http://www.ituring.com.cn/Art...

图片描述

陈屹(流形)

前端架构师,就职于阿里巴巴。热衷开源事业,长年专注于前端架构、数据可视化、Node.js等领域,知乎专栏pure render的创办人。

segmentfault社区活跃地址:https://segmentfault.com/u/ar...

图片描述

专栏写作近一载,积累了 24 篇经典沉淀,大都关于 React 相关的原理与实践分享展开。《深入 React 技术栈》的部分内容就基于专栏文章,通过整理提炼、纠错与升级,内容更加科学、系统。为了照顾到深度,还有很多内容完全重新编写,系统讲述了 React 与其技术栈的使用方法及工作原理。

访谈内容

前端技术那么多,为什么选择了React?

当初选择 React 的理由很简单,只是为了解决业务上的痛点。前年,产品架构还是 jQuery 和 Backbone。但随着产品的业务复杂度不断增加,数据层的逻辑基本上还是链路很短的数据请求,而 View 层的交互逻辑却变得越来越复杂,难以维护。

选型时,Angular 和 React 是重点考虑的两个对象。其中,Angular 比较成熟,也积累了很多粉丝,虽然尝试过,但不符合我们的场景,需要对现有构架作出较大的调整。我们需要的是,更轻量级、不绑定某种架构的技术,而且最重要的是,方便组件化的选择。实践后,决定下注 React。用 React 封装了一套组件与Backbone model 配合。

选择核心技术或库时,需要对业务担责。高成本的重构会给业务带来不必要的影响。发展、可持续、承前启后的考虑是首要的。

随着业务的发展,团队也在不断成长,不断探索着最佳实践方案。现在重新回顾一路的发展,非常庆幸选择 了 React。

如何看待同样以“轻便、易上手”著称的Vue?跟Vue相比,React有哪些优势?

Vue 使用的是 web 开发者更熟悉的模板与特性,React 的特色在于函数式编程的理念和丰富的技术选型。Vue 比起 React 更容易被前端工程师接受,这是一个直观的感受;React 则更容易吸引在 FP 上持续走下去的开发者。我想更多还是口味的不同。

如果一定要说 React 的优势,就是它活跃的生态圈。在 npm 社区搜索 React 关键词,会出现 21k+ 的库,而开源时间更久的 Angular 却只有 9k+,足可见开发者对其追捧的程度。另外,React 还是 FB 技术布局上重要的一部分,包括已开源的 React Native,未来的 React VR。当然,Vue 也有 Weex。

React 和 Vue 两者发展速度都很快,对于产品技术选型来说,活跃程度与生态发展可能比库本身带来的优势更为重要。现在的框架之争太多,我的建议是,当你选定之后,没必要急着切换,因为它们都可以完成中大规模的应用。从学习的角度,两者都值得学习。

编写《深入React技术栈》的原因有哪些?它的独特之处在哪里?

写这本书的时候,国内只有一本 React 相关的的入门书籍,并没有深入细节与实践方面的内容。而在这方面,pure render 专栏沉淀了很多经验。同时,踩过很多坑的经验,让我相信自己有能力写一本书更好地回馈社区。在此,感谢编辑老师的信任,战友们、朋友们给予的支持和鼓励。

要说本书的独特之处,一定在于每一部分都会去分析源码。尽管源码并不是开发者所要关注的,我想传达的是,读源码是学习技术最方便的途径,尤其对于非常活跃的前端开源社区来说

《深入React技术栈》一书中,为什么会选择“组件化、Flux和Redux、server、可视化”四个维度来讲解React?

全书从 React 组件化的思想和用法讲起,再讲到其背后的原理。组件化是前端工程中非常重要的部分,自前端开发的早期,工程师就一直在尝试用面向对象的理念来封装组件,直到今天也没有停下来。

到富客户端应用的年代,只有组件化已经不够了,先驱们借来了分层思想,先是 Backbone 站在了高点,到后来的百花齐放。说到 React 技术栈,Flux 应用架构起了个头儿,到 Redux 的诞生,算是完成了工程化的最后一块拼图,这是一个渐进的发展过程。结合 server render 完整打通了今天前端架构 SPA 的所有部件。

可视化在前端圈的地位很独特,已经有越来越多的前端转向专职的可视化工程师。可视化在这个领域有着不同于传统前端的开发方式,书中的内容也只是结合 React 开发的实践而已。

说到能够写作的程序员或是能够编程的作家,这两种人都是相当稀少。写完《深入React技术栈》一书,可以给我们分享下你的切身感受吗?

其实在互联技术上并不少吧。在国内知乎,SegmentFault,还有各种社区博客上可以看到不少善于分享的大咖。

说说写书过程中的感受。其实,从一开始的思路到最后成型的布局,之间产生了不小的改变,即使到最后时刻目录都在变动,真是不断挑战着自己。况且,React 技术栈在半年里的变化也不小,我会担心内容过时,承受很快被淘汰的命运,也许每一位技术书的作者都会经历这种痛苦。

当然,看到很多读者给我发来私信,表达学习到很多知识的时候,我想付出的一切都是值得的吧。

在知乎上创办专栏pure render,在SegmentFault等技术社区分享知识,不会分散精力影响技术研究吗?

分享并不是任务,是技术研究的一部分,并不会分散太多精力。我曾经说过,写文章并不单是为了别人,它可以把自己的想法或成果记录下来,是一件比较纯粹的事。

写文章也是为了交流。交流,更确切地说是,思想上的碰撞,碰撞那些还不坚定的想法,在说服与被说服的过程中共同进步。你理解了他人的经验,也完善自己的经验世界。

创办pure render专栏也有带领前端团队在技术道路上作沉淀的考虑。每一篇文章我或团队都会作审核,期望量少而精。现在,我也会试着邀请一些优秀人士给更多关注的朋友分享技术。

我了解到,你热衷开源事业。有哪些React开源项目推荐给大家学习?

如之前所说,React 社区在前端社区是非常活跃的,这一点非常像过去的 jQuery 开源社区。有非常多的轮子可以选择,却也带来选择上的困扰。

我在书中基本上把应用所需要的库都有说到。组件库方面,antd 已经被大家熟悉,如果想要定制组件,在 antd 背后的 react-component 做得也是非常优秀。另外,material-ui 也是一个很好的选择,尤其对于喜欢这套 UI 的开发者。

早期, Flux 衍生框架非常多,直到社区出现了 Redux、React Router、Redux Saga,Immutablejs 等最佳实践后才算消停。当然,如果你还是一个新手,还是建议你坚持使用 Redux,理解 Redux。

可视化方面,还是要推荐一下 Recharts。这个可视化库,是基于 React 和 D3,非常符合 React 构建组件的思想。

React 优秀的开源项目每周每月都会有,关注社区的动态也是我们前端工程师必备的技能。

读者希望陈老师能分享下你自己“从刚接触前端到现在拥有如此的技术沉淀”一路上的经验。如果真要踏上React学习之路,有哪些“坑洼”是值得注意,哪些“美景”是不容错过的?

我了解到,很多刚开始学习前端的学生就想一头扎到 React 或其它体系中去,这是非常危险的想法。比如我在专栏中提过,去 jQuery 的决定是和应用本身的特质相关的。如果说只是很简单的页面,并没有太多和服务端交互的内容,我还是首推 jQuery。因此,在你踏上 React 学习之路前,还请打好基础尤其是 DOM。

对于“坑洼”或是“美景”,我就说两点。第一,关注组件的复用粒度,尽可能保持组件的可扩展性,支持可控与不可控。第二是数据层的抽象,不同的业务需要有不同级别的数据抽象,有些越简单越好,有些封装得越厚越好。最重要的是根据业务的需要,保持界面与数据抽象的平衡。

在研究React的道路上,未来你会专注哪些方向?

走在 React 这条路上,很容易思考函数式编程的各种特性对复杂应用带来的好处。但函数式编程在生产环境中会对业务抽象带来更高的难度。很多人都在尝试用 React 的理念创造小而美的轮子,如 inferno,也可能会自己实现一套去匹配业务的需要。

说到未来,我可能会关注 FRP,它简化了现有架构的概念,更适合于用户界面的开发。Mobx 就作出了很多努力,同样,我也会关注更纯粹的 elm、cyclejs 这些 FRP 框架。


——更多访谈


更多精彩,加入图灵访谈微信!

查看原文

赞 5 收藏 11 评论 0

Arcthur 发布了文章 · 2016-05-25

koa 实现 react-view 原理

在之前我们有过一篇『React 同构实践与思考』的专栏文章,给读者实践了用 React 怎么实现同构。今天,其实讲的是在实现同构过程中看到过,可能非常容易被忽视更小的一个点 —— React View。

React View

每一个 BS 架构的框架都会涉及到 View 层的展现,Koa 也不例外。我们在做 View 层的时候有两种做法,一种是做成插件形式,对于 View 来说就是模板引擎,另一种是做成中件间的形式。

再说到 React,常常有人说它是增强版的模板引擎。这种说法即对也不对。

从表象来看的确,React 可以替换变量,有条件判断,有循环判断,JSX 语法让渲染过程和 HTML 没什么两样,毕竟说到底 React 就是 JavaScript,而 React 所推崇的无状态函数,也彻彻底底把 React 变成了像是模板的样子。

从内在来看,React 它还是 JavaScript,它可以方便地做模块化管理,有内部状态,有自己的数据流。它可以做一部分 Controller,或者说,可以完全承担 Controller 的工作。

但是在服务端,我们需要模板是为了作 HTML 的同步请求,因此说地简单一些就只需要渲染成 HTML 的功能就可以了。当然,特殊的一点是,之所以让 React 作模板就是可以让服务端跑到客户端的渲染逻辑,并解决单页应用常常诟病的加载后白屏的问题。

言归正传,现在我们就带着 React View 怎么实现这个问题来解读源码。

React-View 源码解读

配置

配置是设计的源头之一,一切源码都可以从配置入手研究。

var defaultOptions = {
  doctype: '<!DOCTYPE html>',
  beautify: false,
  cache: process.env.NODE_ENV === 'production',
  extname: 'jsx',
  writeResp: true,
  views: path.join(__dirname, 'views'),
  internals: false
};

如果我们用过像 handlebars 或是 jade View,我们看到 React View 的配置与其它 View 的配置有几点不同。doctype、internals 这些配置都是其它模板引擎不会有的。

模板常用的配置应该是什么呢?

  1. viewPath,在上述配置指的是 view,就是 View 的目录在哪里,这是每一个模板插件或中间件都需要去配的。

  2. extname,后缀名是什么,一般来说模板引擎都有自己独有的后缀,当然不排除可以有喜好选择的情况。比如对 React 而言,就可以写成是 .jsx.js 两种不同的形式。

  3. cache,我想一般模板引擎都会带 cache 功能,因为模板的解析是需要耗费资源的,而模板本身的改动的频度是非常低的。每当发布的时候,我们去刷新一次模板即可。但上述配置中的 cache 并不是指这个,我们等读源码时再来看。

渲染

标准的渲染过程其实非常的简单。对于 React 来说就是读取目录下的文件,像前端加载一样,require 那个文件。最后利用 ReactDOMServer 中的方法来渲染。

var render = internals
  ? ReactDOMServer.renderToString
  : ReactDOMServer.renderToStaticMarkup;

...

var markup = options.doctype || '';
try {
  var component = require(filepath);
  // Transpiled ES6 may export components as { default: Component }
  component = component.default || component;
  markup += render(React.createElement(component, locals));
} catch (err) {
  err.code = 'REACT';
  throw err;
}

if (options.beautify) {
  // NOTE: This will screw up some things where whitespace is important, and be
  // subtly different than prod.
  markup = beautifyHTML(markup);
}

var writeResp = locals.writeResp === false
    ? false
    : (locals.writeResp || options.writeResp);
      
if (writeResp) {
  this.type = 'html';
  this.body = markup;
}

return markup

这里我们截取最关键的片段,正如我们预估的渲染过程一样。但我们看到,从流程上看有四个细节:

设置 doctype 的目的

在一般模板中我们很少看到将 doctype 放在配置中配置,但因为 React 的特殊性,让我们不得不这么做。原因很简单,React render 方法返回时一定需要一个包裹的元素,比如 div,ul,甚至 html,因此,我们需要手动去加 doctype。

渲染 React 组件

renderToStringrenderToStaticMarkup 都是 'react-dom/server' 下的方法,与 render 不同,render 方法需要指定具体渲染到 DOM 上的节点,但那两个方法都只返回一段 HTML 字符串。这一点让 React 成为模板语言而存在。它们两个方法的区别在于:

  • renderToString 方法渲染的时候带有 data-reactid 属性,意味着可以做 server render,React 在前端会认识服务端渲染的内容,不会重新渲染 DOM 节点,开始执行 componentDidMount 继续执行后续生命周期。

  • renderToStaticMarkup 方法渲染时没有 data-reactid,把 React 当做是纯模板来使用,这个时候只渲染 body 外的框架是比较合适的。

render 方法里,我们看到 React.createElement 方法。是因为在服务端 render 方法没有 babel 编译,因此写的其实是 <component {...locals} /> 编译后的代码。

美化 HTML

options.beautify 配置了我们是否要美化 HTML,默认时是关闭的。任何需要编译的模板引擎一般都会有类似的配置。在 Reat 中,因为 render 后的代码是一连串的字符串,返回到前台的时候都是无法阅读的代码。在有必要时,我们可以开启这个配置。

绑定到上下文

最后一步,尽管有一个开关控制,但我们看到最后是把内容绑定到 this.body 下的。 这里省略了整个实现过程是在 app.context.render 方法下,即是重写了 app.context 下的 render 方法,用于渲染 React。如果说 app.context.render 方法是 function*,那么我们的 react-view,就会变为中间件。

Cache

我们从一开始就看到了配置中就有 cache 配置,这个 cache 是不是我们所想呢?我们来看下源代码:

// match function for cache clean
var match = createMatchFunction(options.views);

...

if (!options.cache) {
  cleanCache(match);
}

这里的 cache 指的是模板缓存么。事实上不完全是,我们来看一下 cleanCache 方法就明白了:

function cleanCache(match) {
  Object.keys(require.cache).forEach(function(module) {
    if (match(require.cache[module].filename)) {
      delete require.cache[module];
    }
  });
}

因为我们读取 React 文件用的是 require 方法,而在 Node 中 require 方法是有缓存的,Node 在每个第一次 Load Module 时就会将该 Module 缓存,存入全局的 _cache 中,在一般情况下我们当然需要这么做。但在模板加载这个情景下就不同了。

在这里的确我们全局缓存了 React 模板文件,但这个文件是编译前的文件。而我们需要缓存的是编译后的文件,也就是说 markup 是我们需要缓存的值。

在这里我们想想怎么去实现,方便起见,我们可以新增一个 lru-cache,用它的好处是 lru 封装了很多关于 cache 时效与容量的开关。

var LRU = require("lru-cache");
var cache = LRU(this.options.cacheOptions);

...

if (options.cache && cache.get(filepath)) {
  markup = cache.get(filepath);
} else {
  var markup = options.doctype || '';
  try {
    var component = require(filepath);
  } else {
      // Transpiled ES6 may export components as { default: Component }
      component = component.default || component;
      markup += render(React.createElement(component, locals));
    }
  } catch (err) {
    err.code = 'REACT';
    throw err;
  }

  // beautify ...
  
  if (options.cache) {
    cache.set(filepath, markup);
  }
}

当然,我们现在这种情形下都需要清除 require 的 cache。

Babel

我想很多开发者在写 React 组件的时候用的是 ES6 Class 来写的,而且会用到很多 ES6/ES7 的方法,不巧的是 Node 还不支持有些高级特性。因此就引到了一个话题,服务端怎么引用 babel?

在业务有 babel-node 这类解决方案,但这毕竟是一个实验性的 Node,我们不会拿生产环境去冒险。

在 koa/react-view 中间件内,有一段说明,它建议开发者在使用的时候加入 babel-register 作实时编译。关于这个问题,当然也可以写在中间件内,在加载模板前引入。随着 Node 对 ES6 方法支持的完善,也许有一天也用不到了。

总结

其实,实现 View 非常简单,我们也从一些维度看到了设计一个 xx-view 的一般方法。在具体实现的时候,我们可以用一些更好的方法去做,比如用类来抽象 View,用 Promise 来描述过程。

查看原文

赞 5 收藏 6 评论 0

Arcthur 回答了问题 · 2016-05-11

解决react+redux中ShowTodoLists.js之中有一段代码不太清楚的意思,求解,谢谢

关注 3 回答 2

认证与成就

  • SegmentFault 讲师
  • 获得 22 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Recharts

    Redefined chart library built with React and D3

注册于 2014-04-16
个人主页被 1k 人浏览