fx109138

fx109138 查看完整档案

哈尔滨编辑东北农业大学  |  软件工程 编辑  |  填写所在公司/组织 fengxu.ink 编辑
编辑

我挥舞着键盘和本子,发誓要把这世界写个明明白白!!!

个人动态

fx109138 收藏了文章 · 2018-12-01

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 表示:怪我咯)。
查看原文

fx109138 回答了问题 · 2018-11-03

react + typescript 的时候,使用修饰器,编译报错?

你好,这个解决了吗,怎么处理的

关注 4 回答 2

fx109138 关注了收藏夹 · 2018-10-01

前端进阶

关注 314

fx109138 评论了文章 · 2018-07-06

js学习之异步处理

学习js开发,无论是前端开发还是node.js,都避免不了要接触异步编程这个问题,就和其它大多数以多线程同步为主的编程语言不同,js的主要设计是单线程异步模型。正因为js天生的与众不同,才使得它拥有一种独特的魅力,也给学习者带来了很多探索的道路。本文就从js的最初设计开始,整理一下js异步编程的发展历程。

什么是异步

在研究js异步之前,先弄清楚异步是什么。异步是和同步相对的概念,同步,指的是一个调用发起后要等待结果返回,返回时候必须拿到返回结果。而异步的调用,发起之后直接返回,返回的时候还没有结果,也不用等待结果,而调用结果是产生结果后通过被调用者通知调用者来传递的。

举个例子,A想找C,但是不知道C的电话号码,但是他有B的电话号码,于是A给B打电话询问C的电话号码,B需要查找才能知道C的电话号码,之后会出现两种场景看下面两个场景:

  • A不挂电话,等到B找到号码之后直接告诉A
  • A挂电话,B找到后再给A打电话告诉A

能感受到这两种情况是不同的吧,前一种就是同步,后一种就是异步。

为什么是异步的

先来看js的诞生,JavaScript诞生于1995年,由Brendan Eich设计,最早是在Netscape公司的浏览器上实现,用来实现在浏览器中处理简单的表单验证等用户交互。至于后来提交到ECMA,形成规范,种种历史不是这篇文章的重点,提到这些就是想说一点,js的最初设计就是为了浏览器的GUI交互。对于图形化界面处理,引入多线程势必会带来各种各样的同步问题,因此浏览器中的js被设计成单线程,还是很容易理解的。但是单线程有一个问题:一旦这个唯一的线程被阻塞就没办法工作了--这肯定是不行的。由于异步编程可以实现“非阻塞”的调用效果,引入异步编程自然就是顺理成章的事情了。

现在,js的运行环境不限于浏览器,还有node.js,node.js设计的最初想法就是设计一个完全由事件驱动,非阻塞式IO实现的服务器运行环境,因为网络IO请求是一个非常大的性能瓶颈,前期使用其他编程语言都失败了,就是因为人们固有的同步编程思想,人们更倾向于使用同步设计的API。而js由于最初设计就是全异步的,人们不会有很多不适应,加上V8高性能引擎的出现,才造就了node.js技术的产生。node.js擅长处理IO密集型业务,就得益于事件驱动,非阻塞IO的设计,而这一切都与异步编程密不可分。

js异步原理

这是一张简化的浏览器js执行流程图,nodejs和它不太一样,但是都有一个队列

这个队列就是异步队列,它是处理异步事件的核心,整个js调用时候,同步任务和其他编程语言一样,在栈中调用,一旦遇上异步任务,不立刻执行,直接把它放到异步队列里面,这样就形成了两种不同的任务。由于主线程中没有阻塞,很快就完成,栈中任务边空之后,就会有一个事件循环,把队列里面的任务一个一个取出来执行。只要主线程空闲,异步队列有任务,事件循环就会从队列中取出任务执行。

说的比较简单,js执行引擎设计比这复杂的多得多,但是在js的异步实现原理中,事件循环和异步队列是核心的内容。

异步编程实现

异步编程的代码实现,随着时间的推移也在逐渐完善,不止是在js中,许多编程语言的使用者都在寻找一种优雅的异步编程代码书写方式,下面来看js中的曾出现的几种重要的实现方式。

最经典的异步编程方式--callback

提起异步编程,不能不提的就是回调(callback)的方式了,回调方式是最传统的异步编程解决方案。首先要知道回调能解决异步问题,但是不代表使用回调就是异步任务了。下面以最常见的网络请求为例来演示callback是如何处理异步任务的,首先来看一个错误的例子:

function getData(url) {
    const data = $.get(url);
    return data;
}

const data = getData('/api/data'); // 错误,data为undefined

由于函数getData内部需要执行网络请求,无法预知结果的返回时机,直接通过同步的方式返回结果是行不通的,正确的写法是像下面这样:

function getData(url, callback) {
    $.get(url, data => {
        if (data.status === 200) {
            callback(null, data);
        } else {
            callback(data);
        }
    });
}

getData('/api/data', (err, data) => {
    if (err) {
        console.log(err);
    } else {
        console.log(data);
    }
});

callback方式利用了函数式编程的特点,把要执行的函数作为参数传入,由被调用者控制执行时机,确保能够拿到正确的结果。这种方式初看可能会有点难懂,但是熟悉函数式编程其实很简单,很好地解决了最基本的异步问题,早期异步编程只能通过这种方式。

然而这种方式会有一个致命的问题,在实际开发中,模型总不会这样简单,下面的场景是常有的事:

fun1(data => {
    // ...
    fun2(data, result => {
        // ...
        fun3(result, () => {
            // ...
        });
    });
});

整个随着系统越来越复杂,整个回调函数的层次会逐渐加深,里面再加上复杂的逻辑,代码编写维护都将变得十分困难,可读性几乎没有。这被称为毁掉地狱,一度困扰着开发者,甚至是曾经异步编程最为人诟病的地方。

从地狱中走出来--promise

使用回调函数来编程很简单,但是回调地狱实在是太可怕了,嵌套层级足够深之后绝对是维护的噩梦,而promise的出现就是解决这一问题的。promise是按照规范实现的一个对象,ES6提供了原生的实现,早期的三方实现也有很多。在此不会去讨论promise规范和实现原理,重点来看promise是如何解决异步编程的问题的。

Promise对象代表一个未完成、但预计将来会完成的操作,有三种状态:

  • pending:初始值,不是fulfilled,也不是rejected
  • resolved(也叫fulfilled):代表操作成功
  • rejected:代表操作失败

整个promise的状态只支持两种转换:从pending转变为resolved,或从pending转变为rejected,一旦转化发生就会保持这种状态,不可以再发生变化,状态发生变化后会触发then方法。这里比较抽象,我们直接来改造上面的例子:

function getData(url) {
    return new Promise((resolve, reject) =>{
        $.get(url, data => {
            if (data.status === 200) {
                reject(data);
            } else {
                resolve(data);
            }
        });
    });
}

getData('/api/data').then(data => {
    console.log(data);
}).catch(err => {
    console.log(err);
});

Promise是一个构造函数,它创建一个promise对象,接收一个回调函数作为参数,而回调函数又接收两个函数做参数,分别代表promise的两种状态转化。resolve回调会使promise由pending转变为resolved,而reject 回调会使promise由pending转变为rejected。

当promise变为resolved时候,then方法就会被触发,在里面可以获取到resolve的内容,then方法。而一旦promise变为rejected,就会产生一个error。无论是resolve还是reject,都会返回一个新的Promise实例,返回值将作为参数传入这个新Promise的resolve函数,这样就可以实现链式调用,对于错误的处理,系统提供了catch方法,错误会一直向后传递,总是能被下一个catch捕获。用promise可以有效地避免回调嵌套的问题,代码会变成下面的样子:

fun1().then(data => {
    // ...
    return fun2(data);
}).then(result => {
    // ...
    return fun3(result);
}).then(() => {
    // ...
});

整个调用过程变的很清晰,可维护性可扩展性都会大大增强,promise是一种非常重要的异步编程方式,它改变了以往的思维方式,也是后面新方式产生的重要基础。

转换思维--generator

promise的写法是最好的吗,链式调用相比回调函数而言却是可维护性增加了不少,但是和同步编程相比,异步看起来不是那么和谐,而generator的出现带来了另一种思路。

generator是ES对协程的实现,协程指的是函数并不是整个执行下去的,一个函数执行到一半可以移交执行权,等到可以的时候再获得执行权,这种方式最大的特点就是同步的思维,除了控制执行的yield命令之外,整体看起来和同步编程感觉几乎一样,下面来看一下这种方式的写法:

function getDataPromise(url) {
    return new Promise((resolve, reject) =>{
        $.get(url, data => {
            if (data.status === 200) {
                reject(data);
            } else {
                resolve(data);
            }
        });
    });
}

function *getDataGen(url) {
    yield getDataPromise(url);
}

const g = getDataGen('/api/data');
g.next();

generator与普通函数的区别就是前面多一个*,不过这不是重点,重点是generator里面可以使用yield关键字来表示暂停,它接收一个promise对象,返回promise的结果并且停在此处等待,不是一次性执行完。generator执行后会返回一个iterator,iterator里面有一个next方法,每次调用next方法,generator都会向下执行,直到遇上yield,返回结果是一个对象,里面有一个value属性,值为当前yield返回结果,done属性代表整个generator是否执行完毕。generator的出现使得像同步一样编写异步代码成为可能,下面是使用generator改造后的结果:

* fun() {
    const data = yield fun1();
    // ...
    const result = yield fun2(data);
    // ...
    yield fun3(result);
    // ...
}

const g = fun();
g.next();
g.next();
g.next();
g.next();

在generator的编写过程中,我们还需要手动控制执行过程,而实际上这是可以自动实现的,接下来的一种新语法的产生使得异步编程真的和同步一样容易了。

新时代的写法--async,await

异步编程的最高境界,就是根本不用关心它是不是异步。在最新的ES中,终于有了这种激动人心的语法了。async函数的写法和generator几乎相同,把*换成async关键字,把yield换成await即可。async函数内部自带generator执行器,我们不再需要手动控制执行了,现在来看最终的写法:

function getDataPromise(url) {
    return new Promise((resolve, reject) =>{
        $.get(url, data => {
            if (data.status === 200) {
                reject(data);
            } else {
                resolve(data);
            }
        });
    });
}

async function getData(url) {
    return await getDataPromise(url);
}

const data = await getData(url);

除了多了关键字,剩下的和同步的编码方式完全相同,对于异常捕获也可以采取同步的try-catch方式,对于再复杂的场景也不会逻辑混乱了:

* fun() {
    const data = await fun1();
    // ...
    const result = await fun2(data);
    // ...
    return await fun3(result);
    // ...
}
fun()

现在回去看回调函数的写法,感觉好像换了一个世界。这种语法比较新,在不支持的环境要使用babel转译。

写在最后

在js中,异步编程是一个长久的话题,很庆幸现在有这么好用的async和await,不过promise原理,回调函数都是要懂的,很重要的内容,弄清楚异步编程模式,算是扫清了学习js尤其是node.js路上最大的障碍了。


尊重原创,转载分享前请先知悉作者,也欢迎指出错误不足共同交流,更多内容欢迎关注作者博客点击这里

查看原文

fx109138 发布了文章 · 2018-03-10

vue.js原理初探

vue.js是一个非常优秀的前端开发框架,不是我说的,大家都知道。本人也使用过vue.js开发过移动端SPA应用,还是学习阶段,经验尚浅,能力有限。不过我也懂得只会使用轮子不知所以然是远远不够的,凭自己浅薄的见识,斗胆写一篇略微深入的一点文章。

首先我现在的能力,独立阅读源码还是有很大压力的,所幸vue写的很规范,通过方法名基本可以略知一二,里面的原理不懂的地方多方面查找资料,本文中不规范不正确的地方欢迎指正,学生非常愿意接受各位前辈提出宝贵的建议和指导。

写这篇文章时GitHub上vue最新版是v2.5.13,采用了flow作为类型管理工具,关于flow相关内容选择性忽略了,不考虑类型系统,只考虑实现原理,写下这篇文章。

本文大概涉及到vue几个核心的地方:vue实例化,虚拟DOM,模板编译过程,数据绑定。

下图为最新版本vue的生命周期

vue实例化

首先从创建vue实例开始,vue的构造函数在src/core/instance/index.js文件中,不过在src/core/index.js中对其进行了一系列处理,其中关于服务器环境渲染等相关内容在此不做讨论。这里有initGlobalAPI方法在src/core/global-api/index.js中,此方法初始化了一些vue提供的的全局方法,set,delete,nextTick等等,并初始化了和处理mixins,extends等相关功能的方法。现在回过来从全局来看src/core/instance/index.js,在其中还包括几个方法,它们初始化了vue原型上面提供的一些方法,而vue的构造函数中调用的就是原型上面的_init方法。

研究vue的实例化就要研究_init方法,此方法定义在src/core/instance/init.js下的initMixin中,里面是对vue实例即vm的处理。其中包括开发环境下的代理配置等一些列处理,并处理了传递给构造函数的参数等,重点在一系列方法

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

其实从名字就能看出这些方法都是做什么的:初始化生命周期,初始化事件,初始化渲染,触发执行beforeCreate生命周期方法,初始化data/props数据监听,触发执行created生命周期方法。

此时,对应到生命周期示例图,created方法执行结束,接下来判断是否传入挂载的el节点,如果传入的话此时就会通过$mount函数把组件挂载到DOM上面,整个vue构造函数就执行完成了。以上是vue对象创建的基本流程,其中有几个重要的关键点也是vue的核心所在,下面来重点探讨一下。

模板编译

上面提到了挂载的$mount函数,此函数的实现与运行环境有关,在此只看web中的实现。该方法在src/platforms/web/runtime/index.js中定义,挂载在vue的原型上。实现只有简单的两行,判断运行环境为浏览器,调用工具方法查找到el对应的DOM节点,再调用位于src/core/instance/lifecycle.js下的mountComponent方法来实现挂载,这里就涉及到了挂载之前的处理问题。对于拥有render(JSX)函数的情况,组件可以直接挂载,如果使用的是template,需要从中提取AST渲染方法(注意如果使用构建工具,最终会为我们编译成render(JSX)形式,所以无需担心性能问题),AST即抽象语法树,它是对真实DOM结构的映射,可执行,可编译,能够把每个节点部分都编译成vnode,组成一个有对应层次结构的vnode对象。有了渲染方法,下一步就是更新DOM,注意并不是直接更新,而是通过vnode,于是涉及到了一个非常重要的概念。

虚拟DOM

虚拟DOM技术是一个很流行的东西,现代前端开发框架vue和react都是基于虚拟DOM来实现的。虚拟DOM技术是为了解决一个很重要的问题:浏览器进行DOM操作会带来较大的开销。

操作DOM是不可避免的,常规的操作也不会有任何问题,但是经验不足的开发者往往很容易写出大量的多余或重复的DOM操作,成为前端性能优化中重要的问题。想提升效率,我们就要尽可能减少DOM操作,只修改需要修改的地方。要知道js本身运行速度是很快的,而js对象又可以很准确地描述出类似DOM的树形结构,基于这一前提,人们研究出一种方式,通过使用js描述出一个假的DOM结构,每次数据变化时候,在假的DOM上分析数据变化前后结构差别,找出这个最小差别并且在真实DOM上只更新这个最小的变化内容,这样就极大程度上降低了对DOM的操作带来的性能开销。

上面的假的DOM结构就是虚拟DOM,比对的算法成为diff算法,这是实现虚拟DOM技术的关键,在vue初始化时,首先用JS对象描述出DOM树的结构,用这个描述树去构建真实DOM,并实际展现到页面中,一旦有数据状态变更,需要重新构建一个新的JS的DOM树,对比两棵树差别,找出最小更新内容,并将最小差异内容更新到真实DOM上。

有了虚拟DOM,下面一个问题就是,什么时候会触发更新,接下来要介绍的,就是vue中最具特色的功能--数据响应系统及实现。

数据绑定

记得vue.js的作者尤雨溪老师在知乎上一个回答中提到过自己创作vue的过程,最初就是尝试实现一个类似angular1的东西,发现里面对于数据处理非常不优雅,于是创造性的尝试利用ES5中的Object.defineProperty来实现数据绑定,于是就有了最初的vue。vue中响应式的数据处理方式是一项很有价值的东西。

关于响应式的实现原理,vue官网上面其实有具体介绍,下面是一张官方图片:

vue会遍历此data中对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter,而每个组件实例都有watcher对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。这就是响应实现的基本原理,Object.defineProperty无法shim,所以vue不支持IE8及以下不支持ES5的浏览器。

一个简单的demo:

  <input type="text" id="inputName">
  <br>
  <span id="showName"></span>
    // 传统方式处理数据
    // document.getElementById('inputName').addEventListener('keyup', function (e) {
    //   document.getElementById('showName').innerText = e.target.value;
    // });

    // 利用Object.defineProperty自动响应数据
    var obj = {};
    Object.defineProperty(obj, 'name', {
      get: function () {

      },
      set: function (val) {
        document.getElementById('showName').innerText = val;
      }
    });
    document.getElementById('inputName').addEventListener('keyup', function (e) {
      obj.name = e.target.value;
    });

这个例子并不是什么复杂的实现,但是却体现了vue最核心的东西,我们可以发现,Object.defineProperty下的get和set是可以自动相应的,基于此vue实现了一套基于数据驱动视图的自动响应系统,使得开发模型得到了极大的简化。


至此,本文就暂时结束了,水平一般能力有限,后面随着理解的加深会更深入去学习。更多文章欢迎访问个人网站

查看原文

赞 1 收藏 5 评论 0

fx109138 发布了文章 · 2018-02-23

ajax请求相关

上一篇单独写的是ajax跨域,这一篇就来详细说一说ajax,ajax是现代web开发中必不可少的一部分内容,非常基础也非常重要,这篇总结一下到目前为止我对ajax的理解。

什么是ajax

ajax是web开发中的一种交互技术,全称为Asynchronous JavaScript And XMLHttpRequest,使用ajax可以实现页面局部更新,每次变化不再需要请求整个页面,之前在我web开发历史的文章中也提到过,从前的web页面每次需要更新时都必须要刷新整个页面,整体体验非常不好。ajax的出现并大量使用在web开发中绝对是颠覆性的变化,它使得开发出优秀的web应用成为现实,从此各种各样的前端技术才得以兴起。时至今日,ajax已经成为web开发中难以或缺的一部分。

ajax的核心自然就是XMLHttpRequest对象了,它存在于所有现代浏览器中(IE5 和 IE6 使用 ActiveXObject),它使得浏览器可以发出HTTP请求与接收HTTP响应。有了这一基础,剩下的就是js交互了,整个过程浏览器就可以处理,而交换数据的文档也不限于xml(现在常用json)。

ajax交互流程

一次ajax交互是浏览器向服务器请求一次数据的过程,整个过程可分为4步:

  1. 请求发起:在此阶段,由XMLHttpRequest发起一个http请求,GET、POST、PUT、DELETE、UPDATE等等都可以。
  2. 数据传送:发起请求之后就要传递数据,不同的请求方式传递数据的方式细节不同,但都是浏览器向服务器方向的,因为交互是双方的,数据传递自然很重要。
  3. 监听状态:整个请求过程结束后浏览器的任务就是等,等待服务器的响应,这个过程不会阻塞用户,只是在后台监听连接状态,这里就体现出异步的优势了。
  4. 接收响应:服务器处理完数据之后,后返回结果给浏览器,浏览器就可以接收整个请求返回的响应信息,然后本次请求结束。

以上就是一次完整的ajax交互,下面来通过代码展示一下简单的ajax流程。

代码演示

先来看代码

var xhr = new XMLHttpRequest();          
xhr.open('GET', url, true); // url 是一个URL
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304){        
        // 获得 xhr.responseText 为相应数据
    }
};
xhr.send();

我们来一点点看其中涉及到的方法和相关概念,首先创建了一个XMLHttpRequest对象,然后接下来是一个open方法,第一个参数是请求方法,第二个参数是一个URL,默认情况要求同源(关于同源策略和跨域可以看我上一篇文章),第三个参数指的是是否为异步请求,默认是true可以省略。open方法结束会初始化HTTP请求参数,但是并不发送请求。

做好请求发送准备了,不过现在还不能发送请求。因为请求是异步的,我们无法获知请求的进度和响应状态,XMLHttpRequest给我们提供了一个事件onreadystatechange,我们可以通过监听这个时间来关注这种变化,所以下一步是注册onreadystatechange事件。

先了解一下readyState,当一个XMLHttpRequest初次创建时,这个readyState的值从0开始,直到接收到完整的HTTP响应,这个值增加到4,具体情况如下:

状态名称描述
0Uninitialized初始化状态。XMLHttpRequest 对象已创建或已被 abort() 方法重置。
1Openopen()方法已调用,但是 send() 方法未调用。请求还没有被发送。
2SentSend()方法已调用,HTTP 请求已发送到 Web 服务器。未接收到响应。
3Receiving所有响应头部都已经接收到。响应体开始接收但未完成。
4LoadedHTTP 响应已经完全接收。

在这里我们只要判断这个值是不是4就可以知道响应是否接收完成了。

另一个要关注的就是status,它指的就是HTTP状态码,这个大家都很熟悉了,只要是200(OK)或304(Not Modified)就是成功的请求(这里也可以关注statusText,它指的是状态码对应的名称,不常用)。此时就可以获取到响应数据了,responseText即为响应体内容(还有一个responseXML,它对请求的响应解析为XML并作为Document对象返回,不常用)。到此,请求准备完全完成。

接下来调用send方法,发送请求,其中如果是POST或PUT请求可以把请求体作为参数传入。整个请求到此就发送完成了。

XMLHttpRequest还有几个这里没涉及到的方法abort,getAllResponseHeaders,getResponseHeader,setRequestHeader,暂时用不到这里不过多介绍了。

对于ie5、6,创建xhr对象要使用new ActiveXObject("Microsoft.XMLHTTP"),不过以后应该没用了。

以上就是原生js实现的ajax,在实际开发中我们几乎永远都不会去写ajax,封装好的ajax库有很多,比较熟悉的jquery中的$.ajax,$get,$post等等。到此,传统的基于XMLHttpRequest 实现的ajax的内容就结束了,不过现在还有一个东西需要认识一下。

fetch

XMLHttpRequest的api上面已经看到了,可以说的上很复杂了,它复杂到我们平时几乎都用不上原生api,于是,一种新的更优雅的解决方案--fetch诞生了。

首先fetch是新东西,先来看浏览器支持率:

可以看出其实不是很乐观,不过不要紧,我们可以使用polyfill来实现,所以可以直接来看fetch的例子:

fetch(url, { 
    method: 'GET',
    headers: new Headers({
      'Accept': 'application/json'
    })
  }).then(res=>{
    return res.json() 
  }).then(res=>{
    console.log(res) 
  }).catch(err=>{
    // 处理异常
  })

可以看出fetch是基于promise的(关于promise相关内容在这篇文章中提到过),所以可以链式调用,整个过程不难理解,请求结果如果是json还支持直接处理,fetch的api非常实用,适合现代前端开发使用,使用React开发时候通常我们都选fetch作为数据请求工具。


至此,这篇文章内容就结束了,最后还是版权信息:尊重原创,转载分享前请先知悉作者,也欢迎指出错误不足共同交流,更多内容欢迎关注作者博客点击这里

查看原文

赞 1 收藏 3 评论 0

fx109138 回答了问题 · 2018-02-20

解决react写的spa应用如何才能打包成一个移动端可以下载的app

可以使用cordova打包一下

关注 7 回答 5

fx109138 赞了回答 · 2018-02-10

JavaScript如何屏蔽页面的滚动?

最近刚好研究了一下这个问题,这里是我写的文章,也许可以给你提供一些思路。
禁止蒙层底部页面跟随滚动

关注 4 回答 7

fx109138 回答了问题 · 2018-02-02

解决关于前端工程师的书单?

you don't know javascript 一定要看

关注 3 回答 3

fx109138 关注了用户 · 2018-02-02

yszou @yszou

阿里,杭州,招人招人,欢迎联系。

有偿承接各类可视化编辑前端项目。

关注 2262

fx109138 关注了用户 · 2018-02-02

差不多先生 @1023

自信、自洽、自在

微信公众号: 一步前端

关注 1591

fx109138 关注了用户 · 2018-02-02

徐小武 @codedeer

关注 889

fx109138 关注了用户 · 2018-02-02

某熊猫桑 @marsgt

承认自己做不到,坦诚自己不够好。

关注 19693

fx109138 关注了用户 · 2018-02-02

缘自世界 @birenyangguangcanlan

心态很重要,我始终相信没有不会做的,只有不想做的,在这个人人都聪明的今天,你不凭智慧,只需努力就能打败90%的对手,如果你再展现出你50%的智慧,我想没有什么问题可以难倒你。

关注 7720

fx109138 关注了用户 · 2018-02-02

keke @keke233

好好学习,天天向上~

关注 8403

fx109138 关注了用户 · 2018-02-02

剑心无痕 @jianxinwuhen

关注 8097

fx109138 关注了用户 · 2018-02-02

李十三 @lishisan

知道的越多,不知道的越多。

青团社招聘:前端技术专家

简历发至邮箱:lishixuan@qtshe.com

关注 16686

fx109138 关注了用户 · 2018-02-02

厦冰 @xiabingli

君子终日乾乾,夕惕若,厉无咎

关注 15510

fx109138 关注了用户 · 2018-02-02

咚子 @zi_597d64ce14187

一个前端

关注 12542