fwon

fwon 查看完整档案

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

个人动态

fwon 赞了文章 · 10月28日

全面了解 React 新功能: Suspense 和 Hooks

悄悄的, React v16.7 发布了。 React v16.7: No, This Is Not The One With Hooks.

clipboard.png

最近我也一直在关注这两个功能,也听了程墨大佬的React讲座,十分受用,就花些时间就整理了一下, 在此分享给大家, 希望对大家有所帮助。


引子

为什么不推荐在 componentwillmount 里最获取数据的操作呢?

这个问题被过问很多遍了, 前几天又讨论到这个问题, 就以这个作为切入点吧。

有些朋友可能会想, 数据早点获取回来,页面就能快点渲染出来呀, 提升用户体验, 何乐而为不为?

这个问题, 简单回答起来就是, 因为是可能会调用多次

要深入回答这个问题, 就不得不提到一个React 的核心概念: React Fiber.

一些必须要先了解的背景

React Fiber

React Fiber 是在 v16 的时候引入的一个全新架构, 旨在解决异步渲染问题。

新的架构使得使得 React 用异步渲染成为可能,但要注意,这个改变只是让异步渲染成为可能

但是React 却并没有在 v16 发布的时候立刻开启,也就是说,React 在 v16 发布之后依然使用的是同步渲染

不过,虽然异步渲染没有立刻采用,Fiber 架构还是打开了通向新世界的大门,React v16 一系列新功能几乎都是基于 Fiber 架构。

说到这, 也要说一下 同步渲染异步渲染.

同步渲染 和 异步渲染

同步渲染

我们都知道React 是facebook 推出的, 他们内部也在大量使用这个框架,(个人感觉是很良心了, 内部推动, 而不是丢出去拿用户当小白鼠), 然后就发现了很多问题, 比较突出的就是渲染问题

他们的应用是比较复杂的, 组件树也是非常庞大, 假设有一千个组件要渲染, 每个耗费1ms, 一千个就是1000ms, 由于javascript 是单线程的, 这 1000ms 里 CPU 都在努力的干活, 一旦开始,中间就不会停。 如果这时候用户去操作, 比如输入, 点击按钮, 此时页面是没有响应的。 等更新完了, 你之前的那些输入就会啪啪啪一下子出来了。

这就是我们说的页面卡顿, 用起来很不爽, 体验不好。

这个问题和设备性能没有多大关系, 归根结底还是同步渲染机制的问题。

目前的React 版本(v16.7), 当组件树很大的时候,也会出现这个问题, 逐层渲染, 逐渐深入,不更新完就不会停

函数调用栈如图所示:

clipboard.png

因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。

异步渲染

Fiber 的做法是:分片。

把一个很耗时的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。 而维护每一个分片的数据结构, 就是Fiber

用一张图来展示Fiber 的碎片化更新过程:

clipboard.png

中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。

更详细的信息可以看: Lin Clark - A Cartoon Intro to Fiber - React Conf 2017

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来

因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段: render phase and commit phase.

两个重要概念: render phase and commit phase

有了Fiber 之后, react 的渲染过程不再是一旦开始就不能终止的模式了, 而是划分成为了两个过程: 第一阶段和第二阶段, 也就是官网所谓的 render phase and commit phase

在 Render phase 中, React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的, 而到了第二阶段commit phase, 就一鼓作气把DOM更新完,绝不会被打断。

两个阶段的分界点

这两个阶段, 分界点是什么呢?

其实是 render 函数。 而且, render 函数 也是属于 第一阶段 render phase 的

那这两个 phase 包含的的生命周期函数有哪些呢?

render phase:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

commit phase:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

clipboard.png

因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!

这里也可以回答文行开头的那个问题了, 当然, 在异步渲染模式没有开启之前, 你可以在 willMount 里做ajax (不建议)。 首先,一个组件的 componentWillMount 比 componentDidMount 也早调用不了几微秒,性能没啥提高,而且如果开启了异步渲染, 这就难受了。 React 官方也意识到了这个问题,觉得有必要去劝告(威胁, 阻止)开发者不要在render phase 里写有副作用的代码了(副作用:简单说就是做本函数之外的事情,比如改一个全局变量, ajax之类)。

static getDerivedStateFromProps(nextProps, prevState) {
  //根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
}

新的静态方法

为了减少(避免?)一些开发者的骚操作,React v16.3,干脆引入了一个新的生命周期函数 getDerivedStateFromProps, 这个函数是一个 static 函数,也是一个纯函数,里面不能通过 this 访问到当前组件(强制避免一些有副作用的操作),输入只能通过参数,对组件渲染的影响只能通过返回值。目的大概也是让开发者逐步去适应异步渲染。

我们再看一下 React v16.3 之前的的生命周期函数 示意图:

clipboard.png

再看看16.3的示意图:

clipboard.png

上图中并包含全部React生命周期函数,另外在React v16发布时,还增加了一个componentDidCatch,当异常发生时,一个可以捕捉到异常的componentDidCatch就排上用场了。不过,很快React觉着这还不够,在v16.6.0又推出了一个新的捕捉异常的生命周期函数getDerivedStateFromError

如果异常发生在render阶段,React就会调用getDerivedStateFromError,如果异常发生在第commit阶段,React会调用componentDidCatch。 这个异常可以是任何类型的异常, 捕捉到这个异常之后呢, 可以做一些补救之类的事情。

componentDidCatchgetDerivedStateFromError 的 区别

componentDidCatch 和 getDerivedStateFromError 都是能捕捉异常的,那他们有什么区别呢?

我们之前说了两个阶段, render phasecommit phase.

render phase 里产生异常的时候, 会调用 getDerivedStateFromError;

在 commit phase 里产生异常大的时候, 会调用 componentDidCatch

严格来说, 其实还有一点区别:

componentDidCatch 是不会在服务器端渲染的时候被调用的 而 getDerivedStateFromError 会。

背景小结

啰里八嗦一大堆, 关于背景的东西就说到这, 大家只需要了解什么是Fiber: ‘ 哦, 这个这个东西是支持异步渲染的, 虽然这个东西还没开启’。

然后就是渲染的两个阶段:renderphasecommit phase.

  • render phase 可以被打断, 大家不要在此阶段做一些有副作用的操作,可以放心在commit phase 里做。
  • 然后就是生命周期的调整, react 把你有可能在render phase 里做的有副作用的函数都改成了static 函数, 强迫开发者做一些纯函数的操作。

现在我们进入正题: SuspenseHooks

正题


suspense

Suspense要解决的两个问题:

  1. 代码分片;
  2. 异步获取数据。

刚开始的时候, React 觉得自己只是管视图的, 代码打包的事不归我管, 怎么拿数据也不归我管。 代码都打到一起, 比如十几M, 下载就要半天,体验显然不会好到哪里去。

可是后来呢,这两个事情越来越重要, React 又觉得, 嗯,还是要掺和一下,是时候站出来展现真正的技术了。

Suspense 在v16.6的时候 已经解决了代码分片的问题,异步获取数据还没有正式发布。

先看一个简单的例子:

import React from "react";
import moment from "moment";
 
const Clock = () => <h1>{moment().format("MMMM Do YYYY, h:mm:ss a")}</h1>;

export default Clock;

假设我们有一个组件, 是看当前时间的, 它用了一个很大的第三方插件, 而我想只在用的时候再加载资源,不打在总包里。

再看一段代码:

// Usage of Clock
const Clock = React.lazy(() => {
  console.log("start importing Clock");
  return import("./Clock");
});

这里我们使用了React.lazy, 这样就能实现代码的懒加载。 React.lazy 的参数是一个function, 返回的是一个promise. 这里返回的是一个import 函数, webpack build 的时候, 看到这个东西, 就知道这是个分界点。 import 里面的东西可以打包到另外一个包里。

真正要用的话, 代码大概是这个样子的:

<Suspense fallback={<Loading />}>
  { showClock ? <Clock/> : null}
</Suspense>

showClock 为 true, 就尝试render clock, 这时候, 就触发另一个事件: 去加载clock.js 和它里面的 lib momment。

看到这你可能觉得奇怪, 怎么还需要用个<Suspense> 包起来, 有啥用, 不包行不行。

哎嗨, 不包还真是不行。 为什么呢?

前面我们说到, 目前react 的渲染模式还是同步的, 一口气走到黑, 那我现在画到clock 这里, 但是这clock 在另外一个文件里, 服务器就需要去下载, 什么时候能下载完呢, 不知道。 假设你要花十分钟去下载, 那这十分钟你让react 去干啥, 总不能一直等你吧。 Suspens 就是来解决这个问题的, 你要画clock, 现在没有,那就会抛一个异常出来,我们之前说
componentDidCatch 和 getDerivedStateFromError, 这两个函数就是来抓子组件 或者 子子组件抛出的异常的。

子组件有异常的时候就会往上抛,直到某个组件的 getDerivedStateFromError 抓住这个异常,抓住之后干嘛呢, 还能干嘛呀, 忍着。

下载资源的时候会抛出一个promise, 会有地方(这里是suspense)捕捉这个promise, suspense 实现了getDerivedStateFromError,捕获到异常的时候, 一看, 哎, 小老弟,你来啦,还是个promise, 然后就等这个promise resolve, 完成之后,它会尝试重新画一下子组件。

这时候资源已经到本地了,也就能画成功了。

用伪代码 大致实现一下:

getDerivedStateFromError(error) {
   if (isPromise(error)) {
      error.then(reRender);
   }
}

以上大概就是Suspense 的原理, 其实也不是很复杂,就是利用了 componentDidCatch 和 getDerivedStateFromError, 其实刚开始在v16的时候, 是要用componentDidCatch 的, 但它毕竟是commit phase 里的东西, 还是分出来吧, 所以又加了个getDerivedStateFromError来实现 Suspense 的功能。

这里需要注意的是 reRender 会渲染suspense 下面的所有子组件。

异步渲染什么时候开启呢, 根据介绍说是在19年的第二个季度随着一个小版本的升级开启, 让我们提前做好准备。

做些什么准备呢?

  • render 函数之前的代码都检查一边, 避免一些有副作用的操作

到这, 我们说完了Suspense 的一半功能, 还有另一半: 异步获取数据。

目前这一部分功能还没正式发布。 那我们获取数据还是只能在commit phase 做, 也就是在componentDidMount 里 或者 didUpdate 里做。

就目前来说, 如果一个组件要自己获取数据, 就必须实现为一个类组件, 而且会画两次, 第一次没有数据, 是空的, 你可以画个loading, didMount 之后发请求, 数据回来之后, 把数据setState 到组件里, 这时候有数据了, 再画一次,就画出来了。

虽然是一个很简答的功能, 我就想请求个数据, 还要写一堆东西, 很麻烦, 但在目前的正式版里, 不得不这么做。

但以后这种情况会得到改善, 看一段示例:

import {unstable_createResource as createResource} from 'react-cache';

const resource = createResource(fetchDataApi);

const Foo = () => {
  const result = resource.read();
  return (
    <div>{result}</div>
  );

// ...

<Suspense>
   <Foo />
</Suskpense>};

代码里我们看不到任何譬如 async await 之类的操作, 看起来完全是同步的操作, 这是什么原理呢。

上面的例子里, 有个 resource.read(), 这里就会调api, 返回一个promise, 上面会有suspense 抓住, 等resolve 的时候,再画一下, 就达到目的了。

到这,细心的同学可能就发现了一个问题, resource.read(); 明显是一个有副作用的操作, 而且 render 函数又属于render phase, 之前又说, 不建议在 render phase 里做有副作用的操作, 这么矛盾, 不是自己打脸了吗。

这里也能看出来React 团队现在还没完全想好, 目前放出来测试api 也是以unstable_开头的, 不用用意还是跟明显的: 让大家不要写class的组件,Suspense 能很好的支持函数式组件。

hooks

React v16.7.0-alpha 中第一次引入了 Hooks 的概念, 为什么要引入这个东西呢?

有两个原因:

  1. React 官方觉得 class组件太难以理解,OO(面向对象)太难懂了
  2. React 官方觉得 , React 生命周期太难理解。

最终目的就是, 开发者不用去理解class, 也不用操心生命周期方法。

但是React 官方又说, Hooks的目的并不是消灭类组件。此处应手动滑稽。

回归正题, 我们继续看Hooks, 首先看一下官方的API

clipboard.png

乍一看还是挺多的, 其实有很多的Hook 还处在实验阶段,很可能有一部分要被砍掉, 目前大家只需要熟悉的, 三个就够了:

  • useState
  • useEffect
  • useContext

useState

举个例子来看下, 一个简单的counter :

// 有状态类组件
class Counter extends React.Component {
   state = {
      count: 0
   }
   
   increment = () => {
       this.setState({count: this.state.count + 1});
   }
   
   minus = () => {
       this.setState({count: this.state.count - 1});
   }
   
   render() {
       return (
           <div>
               <h1>{this.state.count}</h1>
               <button onClick={this.increment}>+</button>
               <button onClick={this.minus}>-</button>
           </div>
       );
   }
}
// 使用useState Hook
const Counter = () => {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  
  return (
    <div>
        <h1>{count}</h1>
        <button onClick={increment}>+</button>
    </div>
  );
};

这里的Counter 不是一个类了, 而是一个函数。

进去就调用了useState, 传入 0,对state 进行初始化,此时count 就是0, 返回一个数组, 第一个元素就是 state 的值,第二个元素是更新 state 的函数。

// 下面代码等同于: const [count, setCount] = useState(0);
  const result = useState(0);
  const count = result[0];
  const setCount = result[1];

利用 count 可以读取到这个 state,利用 setCount 可以更新这个 state,而且我们完全可以控制这两个变量的命名,只要高兴,你完全可以这么写:

 const [theCount, updateCount] = useState(0);

因为 useState 在 Counter 这个函数体中,每次 Counter 被渲染的时候,这个 useState 调用都会被执行,useState 自己肯定不是一个纯函数,因为它要区分第一次调用(组件被 mount 时)和后续调用(重复渲染时),只有第一次才用得上参数的初始值,而后续的调用就返回“记住”的 state 值。

读者看到这里,心里可能会有这样的疑问:如果组件中多次使用 useState 怎么办?React 如何“记住”哪个状态对应哪个变量?

React 是完全根据 useState 的调用顺序来“记住”状态归属的,假设组件代码如下:

const Counter = () => {
  const [count, setCount] = useState(0);
  const [foo, updateFoo] = useState('foo');
  
  // ...
}

每一次 Counter 被渲染,都是第一次 useState 调用获得 count 和 setCount,第二次 useState 调用获得 foo 和 updateFoo(这里我故意让命名不用 set 前缀,可见函数名可以随意)。

React 是渲染过程中的“上帝”,每一次渲染 Counter 都要由 React 发起,所以它有机会准备好一个内存记录,当开始执行的时候,每一次 useState 调用对应内存记录上一个位置,而且是按照顺序来记录的。React 不知道你把 useState 等 Hooks API 返回的结果赋值给什么变量,但是它也不需要知道,它只需要按照 useState 调用顺序记录就好了。

你可以理解为会有一个槽去记录状态。

正因为这个原因,Hooks,千万不要在 if 语句或者 for 循环语句中使用!

像下面的代码,肯定会出乱子的:

const Counter = () => {
    const [count, setCount] = useState(0);
    if (count % 2 === 0) {
        const [foo, updateFoo] = useState('foo');
    }
    const [bar, updateBar] = useState('bar');
 // ...
}

因为条件判断,让每次渲染中 useState 的调用次序不一致了,于是 React 就错乱了。

useEffect

除了 useState,React 还提供 useEffect,用于支持组件中增加副作用的支持。

在 React 组件生命周期中如果要做有副作用的操作,代码放在哪里?

当然是放在 componentDidMount 或者 componentDidUpdate 里,但是这意味着组件必须是一个 class。

在 Counter 组件,如果我们想要在用户点击“+”或者“-”按钮之后把计数值体现在网页标题上,这就是一个修改 DOM 的副作用操作,所以必须把 Counter 写成 class,而且添加下面的代码:

componentDidMount() {
  document.title = `Count: ${this.state.count}`;
}

componentDidUpdate() {
  document.title = `Count: ${this.state.count}`;
}

而有了 useEffect,我们就不用写一个 class 了,对应代码如下:

import { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${this.state.count}`;
  });

  return (
    <div>
       <div>{count}</div>
       <button onClick={() => setCount(count + 1)}>+</button>
       <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
};

useEffect 的参数是一个函数,组件每次渲染之后,都会调用这个函数参数,这样就达到了 componentDidMount 和 componentDidUpdate 一样的效果。

虽然本质上,依然是 componentDidMountcomponentDidUpdate 两个生命周期被调用,但是现在我们关心的不是 mount 或者 update 过程,而是“after render”事件,useEffect 就是告诉组件在“渲染完”之后做点什么事。

读者可能会问,现在把 componentDidMountcomponentDidUpdate 混在了一起,那假如某个场景下我只在 mount 时做事但 update 不做事,用 useEffect 不就不行了吗?

其实,用一点小技巧就可以解决。useEffect 还支持第二个可选参数,只有同一 useEffect 的两次调用第二个参数不同时,第一个函数参数才会被调用. 所以,如果想模拟 componentDidMount,只需要这样写:

  useEffect(() => {
    // 这里只有mount时才被调用,相当于componentDidMount
  }, [123]);

在上面的代码中,useEffect 的第二个参数是 [123],其实也可以是任何一个常数,因为它永远不变,所以 useEffect 只在 mount 时调用第一个函数参数一次,达到了 componentDidMount 一样的效果。

useContext

在前面介绍“提供者模式”章节我们介绍过 React 新的 Context API,这个 API 不是完美的,在多个 Context 嵌套的时候尤其麻烦。

比如,一段 JSX 如果既依赖于 ThemeContext 又依赖于 LanguageContext,那么按照 React Context API 应该这么写:

<ThemeContext.Consumer>
    {
        theme => (
            <LanguageContext.Cosumer>
                language => {
                    //可以使用theme和lanugage了
                }
            </LanguageContext.Cosumer>
        )
    }
</ThemeContext.Consumer>

因为 Context API 要用 render props,所以用两个 Context 就要用两次 render props,也就用了两个函数嵌套,这样的缩格看起来也的确过分了一点点。

使用 Hooks 的 useContext,上面的代码可以缩略为下面这样:

const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
// 这里就可以用theme和language了

这个useContext把一个需要很费劲才能理解的 Context API 使用大大简化,不需要理解render props,直接一个函数调用就搞定。

但是,useContext也并不是完美的,它会造成意想不到的重新渲染,我们看一个完整的使用useContext的组件。

const ThemedPage = () => {
    const theme = useContext(ThemeContext);
    
    return (
       <div>
            <Header color={theme.color} />
            <Content color={theme.color}/>
            <Footer color={theme.color}/>
       </div>
    );
};

因为这个组件ThemedPage使用了useContext,它很自然成为了Context的一个消费者,所以,只要Context的值发生了变化,ThemedPage就会被重新渲染,这很自然,因为不重新渲染也就没办法重新获得theme值,但现在有一个大问题,对于ThemedPage来说,实际上只依赖于theme中的color属性,如果只是theme中的size发生了变化但是color属性没有变化,ThemedPage依然会被重新渲染,当然,我们通过给Header、Content和Footer这些组件添加shouldComponentUpdate实现可以减少没有必要的重新渲染,但是上一层的ThemedPage中的JSX重新渲染是躲不过去了。

说到底,useContext 需要一种表达方式告诉React:“我没有改变,重用上次内容好了。”

希望Hooks正式发布的时候能够弥补这一缺陷。

Hooks 带来的代码模式改变

上面我们介绍了 useStateuseEffectuseContext 三个最基本的 Hooks,可以感受到,Hooks 将大大简化使用 React 的代码。

首先我们可能不再需要 class了,虽然 React 官方表示 class 类型的组件将继续支持,但是,业界已经普遍表示会迁移到 Hooks 写法上,也就是放弃 class,只用函数形式来编写组件。

对于 useContext,它并没有为消除 class 做贡献,却为消除 render props 模式做了贡献。很长一段时间,高阶组件和 render props 是组件之间共享逻辑的两个武器,但如同我前面章节介绍的那样,这两个武器都不是十全十美的,现在 Hooks 的出现,也预示着高阶组件和 render props 可能要被逐步取代。

但读者朋友,不要觉得之前学习高阶组件和 render props 是浪费时间,相反,你只有明白 React 的使用历史,才能更好地理解 Hooks 的意义。

可以预测,在 Hooks 兴起之后,共享代码之间逻辑会用函数形式,而且这些函数会以 use- 前缀为约定,重用这些逻辑的方式,就是在函数形式组件中调用这些 useXXX 函数。

例如,我们可以写这样一个共享 Hook useMountLog,用于在 mount 时记录一个日志,代码如下:

const useMountLog = (name) => {
    useEffect(() => {
        console.log(`${name} mounted`);    
    }, [123]);
}

任何一个函数形式组件都可以直接调用这个 useMountLog 获得这个功能,如下:

const Counter = () => {
    useMountLog('Counter');
    
    ...
}

对了,所有的 Hooks API 都只能在函数类型组件中调用,class 类型的组件不能用,从这点看,很显然,class 类型组件将会走向消亡。

如何用Hooks 模拟旧版本的生命周期函数

Hooks 未来正式发布后, 我们自然而然的会遇到这个问题, 如何把写在旧生命周期内的逻辑迁移到Hooks里面来。下面我们就简单说一下,

模拟整个生命周期中只运行一次的方法

useMemo(() => {
  // execute only once
}, []);

我们可以看到useMemo 接收两个参数, 第一个参数是一个函数, 第二个参数是一个数组。

这里有个地方要注意, 就是, 第二个参数的数组里的元素和上一次执行useMemo的第二个参数的数组的元素 完全一样的话,那就表示没有变化, 就不用执行第一个参数里的函数了。 如果有不同, 说明有变化, 就执行。

上面的例子里, 我们只传入了一个空数组, 不会有变化, 也就是只会执行一次。

模拟shouldComponentUpdate

const areEqual = (prevProps, nextProps) => {
   // 返回结果和shouldComponentUpdate正好相反
   // 访问不了state
}; 
React.memo(Foo, areEqual);

模拟componentDidMount

useEffect(() => {
    // 这里在mount时执行一次
}, []);

模拟componentDidUpdate

const mounted = useRef();
useEffect(() => {
  if (!mounted.current) {
    mounted.current = true;
  } else {
    // 这里只在update是执行
  }
});

模拟componentDidUnmount

useEffect(() => {
    // 这里在mount时执行一次
    return () => {
       // 这里在unmount时执行一次
    }
}, []);

未来的代码形势

Hooks 未来发布之后, 我们的代码会写成什么样子呢? 简单设想一下:

// Hooks之后的组件逻辑重用形态

const XXXX = () => {
  const [xx, xxx, xxxx] = useX();
  
  useY();
  
  const {a, b} = useZ();
  

  return (
    <>
     //JSX
    </>
  );
};

内部可能用各种Hooks, 也可能包含第三方的Hooks。 分享Hooks 就是实现代码重用的一种形势。 其实现在已经有人在做这方面的工作了: useHooks.com, 有兴趣的朋友可以去看下。

Suspense 和 Hooks 带来的改变

Suspense 和 Hooks 发布后, 会带来什么样的改变呢? 毫无疑问, 未来的组件, 更多的将会是函数式组件。

原因很简单, 以后大家分享出来的都是Hooks,这东西只能在函数组件里用啊, 其他地方用不了,后面就会自然而然的发生了。

但函数式组件和函数式编程还不是同一个概念。 函数式编程必须是纯的, 没有副作用的, 函数式组件里, 不能保证, 比如那个resource.read(), 明显是有副作用的。

关于好坏

既然这两个东西是趋势, 那这两个东西到底好不好呢 ?

个人理解, 任何东西都不是十全十美。 既然大势所趋, 我们就努力去了解它,学会它, 努力用它好的地方, 避免用不好的地方。

React 发布路线图

最新的消息: https://reactjs.org/blog/2018...

  • React 16.6 with Suspense for Code Splitting (already shipped)
  • A minor 16.x release with React Hooks (~Q1 2019)
  • A minor 16.x release with Concurrent Mode (~Q2 2019)
  • A minor 16.x release with Suspense for Data Fetching (~mid 2019)

明显能够看到资源在往 Suspense 和 Hooks 倾斜。

结语

看到这, 相信大家都Suspense 和 Hooks 都有了一个大概的了解了。

收集各种资料花费了挺长时间,大概用了两三天写出来,中间参考了很多资料, 一部分是摘录到了上面的内容里。

在这里整理分享一下, 希望对大家有所帮助。

才疏学浅, 难免会有纰漏, 欢迎指正:)。

最后

觉得内容有帮助可以关注下我的公众号 「 前端e进阶 」,一起学习成长

clipboard.png

参考资料

查看原文

赞 161 收藏 94 评论 7

fwon 发布了文章 · 2019-11-08

一工具让你的网站支持iOS13 Darkmode 模式

最近iOS13 发布了darkmode模式。虽然本人觉得次此功能呼声大于实际,但作为一个以用户体验为己任的前端,当然不能坐视不管,我们总该做点什么。

在进行了一番调研了解后,我们有几种支持darkmode模式的方法。

首先了解到一个叫 Darkmode.js 的工具,该工具通过配置一些通用的颜色,能让网站实现一键切换darkmode模式,简单方便。但缺点就是没办法实现比较细微的界面定制,只能在一些颜色上做处理。

如果你的设计师在设计稿上另外做了一版暗黑模式的,并且细节上除了颜色还有一些图片的修改,这时候可以用到css的媒体查询,对正常模式下的一些样式做覆盖。举个例子

.body {
    color: #000;
    background: url('normal.png');
}

在媒体查询中,我们覆盖需要修改的属性

/* b.css */
.body {
    color: #000;
    background: url('normal.png');
}

@media (prefers-color-scheme: dark) {
    .body {
        color: #fff;
        background: url('dark.png');
    }
}

在支持暗黑模式媒体查询的浏览器中,会自动渲染出media中的样式,实现样式覆盖。

该方法需要我们每次写一个样式都要在媒体查询中输入同样的选择器,如果选择器嵌套比较深或者当样式表比较长的时候,需要来回切换位置或文件,比较麻烦。基于这点,我开发了一个小工具,支持在正常样式的旁边,以注释的方式写上暗黑模式的样式,后续工具会自动编译成media的格式,无需用户手动编写。

举个例子,我们的代码可以这样写:

.body {
    color: #000;
    background: url('normal.png');
    /* dm[color: #fff;background:url('dark.png');] */
}

该文件最后会被编译成b.css中的格式。
如果每次都需要输入/* dm[] */会比较繁琐,你可以在IDE中创建一个snippet. 方便输入。

更多详细的用法,请移步至 css-plugin-darkmode

查看原文

赞 5 收藏 4 评论 0

fwon 发布了文章 · 2018-10-16

一条命令解决接口Mock

背景

在前端或客户端开发中,经常需要依赖后端同学开发的API。为了能够前后端并行开发,前端通常会采用Mock数据的方法,通过模拟api假数据进行页面渲染。等到后端API开发完毕再接入真实API进行调试。

API数据Mock的方法有很多,常用的有以下几种:

前端打包工具安装Mock插件

比如Grunt, Gulp, Webpack等等都有相应的mock插件,开发者需要根据不同的项目类型来做配置。其原理是启动一个本地服务器实现接口请求。由于是模拟网络请求,所以有时还需要根据需求对一些打包脚本进行修改。该方法的缺点是针对不同的工具需要有不同的方案,有一定的安装和适配成本,并且只针对使用打包工具的项目。

外部Mock服务平台

业界有一些Mock的解决方案,提供一个第三方平台,开发者在平台上配置api接口,然后直接调用平台接口进行调试。比较出名的如 easy-mock

这种方法的优点是无任何插件依赖,所有项目都可以直接访问使用,无需安装,所以对客户端也是通用的。但是带来另外一个问题是数据的泄露,一些隐私度比较高的项目可能会觉得把模拟数据放到其他平台上也是不安全的。当然easy-mock也考虑到这个问题,开源并支持用户直接部署项目到公司内网。但这个需要主机,数据库等资源的支持,落实难度较大。

写死数据

第三种是比较直接的,就是在前端写死数据,接口不发出请求而是直接返回。这种方法对于一些小的活动项目也许是不错的选择。但是构造的数据模式有限,也无法模拟真实的网络环境

在以前的开发中使用的基本是这三类方法,但是这些方法都有上述提到的明显的局限性。

另一种方式

为了避免以上方案中的缺点,本文介绍另外一种mock的思路

对于Mock数据,我们希望它在项目中的存在只是一个文件夹,里面定义了每个接口的返回数据。开发者不需要配置项目插件,每个项目都可以简单的通过一个命令启动mock服务,并监听这个目录。这个Mock服务需要具备以下功能:

  1. 模拟接口请求,灵活配置返回数据
  2. 在开发人员间共享模拟数据,而不是只存在本地,也不是放在外网
  3. 不要繁琐的安装配置,最好一个命令开启即用
  4. 对于所有类型的前端项目都是通用的
  5. 提供一些复杂需求的拓展功能

基于以上思路开发了一个工具 l-mock

实现数据mock只需三步:
1.全局安装

npm i l-mock -g

2.初始化mock目录, init命令在project根目录下生成mock目录,并放置demo接口

cd path/to/project
lmock init

3.运行, 进入生成的mock目录,运行start命令,直接访问localhost:3000/a 则可看到/a接口返回

cd mock
lmock start

第一次初始化后,后面的开发只需要在mock目录中运行lmock start就可以开启接口模拟。

对于有package.json的前端项目,可以直接配置在npm命令中,往后就运行 npm run mock

"scripts": {
  "mock": "cd mock && lmock start",
}

怎么来编写一个模拟接口呢?举一个例子:

path/to/project/mock/a.js 模拟了一个get请求

module.exports = {
  url: '/a',
  method: 'get',
  result: {
    'status|1': ["no_login", "OK", "error", "not_registered", "account_reviewing"],
    'msg': '@csentence()',
    'data': {
      a: 2
    }
  }
}

以上js返回的json内容如下

roadmap.path

paramsdescription
url配置API地址, 支持正则匹配模式
method配置请求方法,支持RESTFUL
result定义返回内容

result 可以直接返回json内容,通过Mockjs,可以生成动态的数据内容,比如上面的@csnetence()会随机生成一个段落的文字

Mockjs的语法很多,可以参考其文档

该工具还有一些拓展功能,需要用到复杂接口逻辑的,可以在result返回方法中配置。

详细的使用方法可以参考工具文档 l-mock

如果你的项目中正好需要用到数据Mock,不妨试一试。

查看原文

赞 4 收藏 4 评论 0

fwon 发布了文章 · 2018-10-15

一条命令解决接口Mock

背景

在前端或客户端开发中,经常需要依赖后端同学开发的API。为了能够前后端并行开发,前端通常会采用Mock数据的方法,通过模拟api假数据进行页面渲染。等到后端API开发完毕再接入真实API进行调试。

API数据Mock的方法有很多,常用的有以下几种:

前端打包工具安装Mock插件

比如Grunt, Gulp, Webpack等等都有相应的mock插件,开发者需要根据不同的项目类型来做配置。其原理是启动一个本地服务器实现接口请求。由于是模拟网络请求,所以有时还需要根据需求对一些打包脚本进行修改。该方法的缺点是针对不同的工具需要有不同的方案,有一定的安装和适配成本,并且只针对使用打包工具的项目。

外部Mock服务平台

业界有一些Mock的解决方案,提供一个第三方平台,开发者在平台上配置api接口,然后直接调用平台接口进行调试。比较出名的如 easy-mock

这种方法的优点是无任何插件依赖,所有项目都可以直接访问使用,无需安装,所以对客户端也是通用的。但是带来另外一个问题是数据的泄露,一些隐私度比较高的项目可能会觉得把模拟数据放到其他平台上也是不安全的。当然easy-mock也考虑到这个问题,开源并支持用户直接部署项目到公司内网。但这个需要主机,数据库等资源的支持,落实难度较大。

写死数据

第三种是比较直接的,就是在前端写死数据,接口不发出请求而是直接返回。这种方法对于一些小的活动项目也许是不错的选择。但是构造的数据模式有限,也无法模拟真实的网络环境

在以前的开发中使用的基本是这三类方法,但是这些方法都有上述提到的明显的局限性。

另一种方式

为了避免以上方案中的缺点,本文介绍另外一种mock的思路

对于Mock数据,我们希望它在项目中的存在只是一个文件夹,里面定义了每个接口的返回数据。开发者不需要配置项目插件,每个项目都可以简单的通过一个命令启动mock服务,并监听这个目录。这个Mock服务需要具备以下功能:

  1. 模拟接口请求,灵活配置返回数据
  2. 在开发人员间共享模拟数据,而不是只存在本地,也不是放在外网
  3. 不要繁琐的安装配置,最好一个命令开启即用
  4. 对于所有类型的前端项目都是通用的
  5. 提供一些复杂需求的拓展功能

基于以上思路开发了一个工具 l-mock

实现数据mock只需三步:
1.全局安装

npm i l-mock -g

2.初始化mock目录, init命令在project根目录下生成mock目录,并放置demo接口

cd path/to/project
lmock init

3.运行, 进入生成的mock目录,运行start命令,直接访问localhost:3000/a 则可看到/a接口返回

cd mock
lmock start

第一次初始化后,后面的开发只需要在mock目录中运行lmock start就可以开启接口模拟。

对于有package.json的前端项目,可以直接配置在npm命令中,往后就运行 npm run mock

"scripts": {
  "mock": "cd mock && lmock start",
}

怎么来编写一个模拟接口呢?举一个例子:

path/to/project/mock/a.js 模拟了一个get请求

module.exports = {
  url: '/a',
  method: 'get',
  result: {
    'status|1': ["no_login", "OK", "error", "not_registered", "account_reviewing"],
    'msg': '@csentence()',
    'data': {
      a: 2
    }
  }
}

以上js返回的json内容如下

roadmap.path

paramsdescription
url配置API地址, 支持正则匹配模式
method配置请求方法,支持RESTFUL
result定义返回内容

result 可以直接返回json内容,通过Mockjs,可以生成动态的数据内容,比如上面的@csnetence()会随机生成一个段落的文字

Mockjs的语法很多,可以参考其文档

该工具还有一些拓展功能,需要用到复杂接口逻辑的,可以在result返回方法中配置。

详细的使用方法可以参考工具文档 l-mock

如果你的项目中正好需要用到数据Mock,不妨试一试。

查看原文

赞 4 收藏 4 评论 0

fwon 评论了文章 · 2018-06-13

Electron + Vue 实现一个代理客户端

开源项目地址:戳我

原理

作为一个合格的前端工程师,你一定用过Fiddler或Charles之类的抓包工具。但是在Mac上做开发时,相关的抓包工具很多是收费的。当你费劲心思下载到了破解版,却还是难以忍受其丑陋的win风格界面和令人悲伤的闪退问题。有没有想过自己来实现一个代理客户端呢?其实这个真的可以有。

中间人

一个http代理服务器的原理很简单。有了Nodejs作为武器,创建一个代理服务器就是分分钟的事。具体可参见jerryQu写的两篇文章 《HTTP 代理原理及实现(一)》《HTTP 代理原理及实现(二)》 文章对HTTP代理的原理和实践讲得比较清楚。

简单来讲就是要实现一个中间人,用户通过设置代理,网络请求就会通过中间人代理,再发往正式服务器。

这种中间人的实现方式有两种。

一种为普通的HTTP代理,通过Node.js开启一个HTTP服务,并将我们的浏览器或手机设置到该服务所在的ip和端口,那么HTTP流量就会经过该代理,从而实现数据的拦截。

pic

对于非HTTP请求,比如HTTPS, 或其他应用层请求。可以通过在Node.js 中开启一个TCP服务,监听CONNECT请求,因为应用层也是基于传输层的,所以数据在到达应用层之前会首先经过传输层,从而我们能实现传输层数据监听。

pic

但是对于CONNECT捕抓到的请求,无法获取到HTTP相关的信息,包括头信息等,这对一般的前端分析作用不大,那么想要真正监听HTTPS,还需要支持证书相关的验证。

证书

假设我们是通过浏览器设置代理进行抓包实验(或全局代理),在这个过程中我们主要关注的是浏览器和代理服务器之间的交互,这个过程大概如下:

  1. 浏览器客户端发出了一个请求,该请求会首先经过代理服务器。

  2. 代理服务器获取到客户端请求,知道了真实服务器的地址,它可能会做一些手脚,比如对请求数据进行修改,再发往真实服务器,获取到数据再返回给浏览器(利用这一点能实现跨域支持等)。或者代理服务器压根就不会请求真实服务器,而是直接伪造一份假数据给浏览器(利用这一点能实现接口mock)。

  3. 浏览器接收到数据,并返回给用户,显示在页面。

上面这三步在HTTP中会无比流畅,然而如果请求是HTTPS,浏览器会验证代理服务器的安全性。这里会涉及到TLS握手的过程,其中也包括了证书的验证。

代理服务器返回HTTPS请求时,需要将对应请求域名的证书发给浏览器,浏览器再向本地的CA根证书验证域名证书的安全性。如果验证通过,则继续后续请求,验证失败浏览器会返回安全警告。

pic

这里提到了两个证书,一个是域名证书,一个是CA根证书。

域名证书 是每个支持HTTPS网站都需要有的一份证书,用于客户端验证该网站的安全性,而该证书通常是通过安全机构申请的,这个机构就是 CA(Certificate Authority,证书颁发机构)。在每台用户计算机的操作系统或浏览器中,都会保存一份CA列表,也就是有多个根证书,不同CA分别包含了不同的域名证书,浏览器在获取到域名证书之后,会向CA根证书进行验证,如果验证通过则能正常收发请求。

对于代理服务器来说,我们并没有合法的域名证书(证书只存在真实目标服务器,无法获取到),怎么让浏览器相信我们是个安全的代理(服务器)呢?答案是————伪造!

没错,我们既要伪造域名证书,也要伪造根证书。其实根证书是可以自己签发的。下面两条命令首先生成了一个私钥,然后利用私钥生成crt证书,我们只要双击crt文件进行安装,并设置为信任,就成功建立了一个本地根证书。

openssl genrsa -out private.pem 2048
openssl req -new -x509 -key private.pem -out public.crt -days 99999

利用根证书,我们能够签发更多的域名证书。证书是链式验证的,验证域名证书的时候,会往上验证CA根证书,由于CA根证书已经被我们本地信任了,所以浏览器也会信任该域名证书,成功返回代理服务器的数据。

pic

具体的操作流程是这样的,首先利用Node.js 生成AnyProxy CA证书,并手动信任。浏览器往cn.vuejs.org发出请求,代理服务器拦截到请求,知道请求是发往cn.vuejs.org,在返回数据之前,利用AnyProxy证书动态签发cn.vuejs.org域名证书,放于本地(用于下次请求,这里省去了中间证书的步骤),同时将该域名证书返回给浏览器。那么浏览器在接受到cn.vuejs.org域名证书后,往证书链上寻找到根证书AnyProxy,并通过AnyProxy证书验证域名证书是否受信任,同时还要检查域名证书的有效性,包括过期时间等。由于域名证书是我们通过AnyProxy动态创建的,所以保证了其受信任和有效性。最后浏览器返回代理服务器结果。从而实现了HTTPS请求的抓取。

具体的证书签发实现可参考 forge 库,现在广泛使用的证书是X.509格式。

Electron

解决了证书问题,可以说已经完成了一大半的工作,那如何快速实现一个代理客户端呢?对于一个JSer来说,能利用Node.js来写是最好不过了。

简介

Electron大家应该不陌生了,它提供了一种解决方案,让我们能够利用Node.js 和 前端三宝 HTML + JS + CSS 来实现客户端软件。咋一听感觉像NW.js。经过一番了解,才知道其实NW.js可以算是Electron的前身了,都是出自同个作者之手,只不过该作者现在维护Electron去了,这其中涉及到一些产权的问题,感兴趣的可以围观一下知乎上原作者的回答。关于Electron和NW.js的区别官网上是这么说的。简单讲就是Electron优化了NW.js中的一些不足。 秉着与时俱进的态度,我们当然要使用Electron。

有了Electron作为容器,我们小前端就可以用HTML+JS+CSS来开发客户端了。就像开发前端页面一样柔顺。Electron的使用比较简单,提供的API也比较清晰。核心概念就是Main Process 和 Render Process。

顾名思义Main Process是主进程,用于运行Electron的基本操作,如创建窗口,创建菜单等。Render Process是渲染进程,我们需要在渲染进程中创建软件界面,每个渲染进程对应的是一个窗口,主进程开启了多个窗口就会有多个渲染进程。

Electron提供了IPC用于进程间通信。分别是ipcMain和ipcRender。该通信机制允许ipcRender向ipcMain发送信号请求,并通过ipcMain返回数据。反回来ipcMain无法向特定的ipcRender发起请求。而且通信间传递的消息会被格式化为JSON字符串,所以并不支持在两个进程间传递句柄方法等,也就是不支持上下文传递。

Arguments will be serialized in JSON internally and hence no functions or prototype chain will be included.

假如要实现在渲染进程中点击一个按钮,则关闭客户端窗口,可以通过ipcRender发送一个信号给ipcMain, ipcMain接收到该信号后调用Electron的API关闭窗口。对于类似这种比较简单的指令操作,运用IPC实现就可以了,但是如果操作比较复杂,并且需要传递复杂数据类型,则用IPC就行不通了。

Electron提供了另一个API remote,用于在Render Process中直接操作主进程的方法。这样就不需要移交Main Process处理,直接在前端页面中调用Electron的API。

打包

由于Electron本身包含了chromium和Node.js的代码, 所以不考虑项目本身体积,打包后的软件最小仍然有100M+, 这也是Electron最为显著的缺点之一。所以基本体积是无法避免的,我们只能尽量减小其他开发文件的大小,避免将一些无关包文件也打包进去。

为什么要强调这点呢?因为基于Node.js开发的项目往往会有一个庞大的node_modules文件夹,里面包含了一些开发和生产所用的包,也即对应package.json中的dependencies和devDependencies。而devDependencies中的包是不需要打包到软件的。这里推荐使用 electron-packager, 能自动排除dev依赖包,并支持自定义排除包文件夹。也可以打包出支持不同系统格式的软件。

界面开发

界面开发采用传统前端页面开发方式,意味着你可以使用任何前端框架,利用Angular,Vue,React等框架来提升开发效率。

这些框架都支持模块化,利用webpack等打包工具,webpack本身会提供require等模块加载的方法,在前端开发的时候能实现类似后端的模块动态加载。

但是,当我们在Render Process中使用webpack进行开发,用require引入模块的时候就会出现冲突。因为require此时是webpack提供的一个引用本地文件的API,而不是Node.js的require, 导致我们无法通过require来引用Node.js的API,或者Electron的API。这有什么解决方案吗?

这里提供一个简单的方法,我们将需要用到Node.js API和Electron API 的方法抽象到renderer.js, 从HTML中单独引入,也就避免了webpack对renderer.js进行处理。然后通过插件的方式引入到前端框架中,以Vue为例:

index.html

<script data-original="renderer.js"></script><!--提供Render Process 方法 -->
<script data-original="./dist/build.js"></script><!--webpack 打包文件-->

renderer.js

const electron = require('electron');
const remote = electron.remote;
const remoteApi = remote.require('./api.js');

global.remoteApi = remoteApi;

Vue入口文件main.js

Vue.use({
    install (Vue, options) {
        //添加实例方法
        Vue.prototype.$remoteApi = global.remoteApi;
    }
});

在Vue组件中就可以直接通过this.$remoteApi调用基于Nodejs或Electron的接口了。这样就有效地分离了前端界面和客户端的代码,只要剥离了$remoteApi, 前端界面也可作为一个独立的项目进行开发。

方案优缺点

Electron的这种实现方式也不是什么新鲜套路了,对于NW.js 有的大多数缺点Electron也有。

其中一个通病就是性能问题,主要是渲染性能方面。基于webkit引擎来渲染UI界面,跟原生的系统UI还是有一定的差距。毕竟是基于DOM节点的渲染,每次节点的重排都是一次大的开销。这点只能通过在前端框架中来优化,比如利用Virtual DOM等相关技术。而视觉上的缺点则可以通过CSS做到竟可能接近原生控件。

而对于JS的执行性能,v8表示hold得住。

优点当然也比较明显,对比于Cocoa,Qt等传统桌面客户端技术,基于前端技术的实现成本较低(C++牛请忽略)跨平台支持更好(框架都帮你做好了),且天然支持热更新。

更重要的是,有这么多优秀软件帮你背书啊.....以下都是基于Electron开发。

pic

当然,我并不是在安利Electron。毕竟别人能开发得这么原生态,你不一定行...

关键还是看技术,Electron是完全能够开发出中大型产品级的软件的。

说了这么多,代码呢?

pic

能读到这里,感谢你的坚持!

下面基于以上理论实现的代理客户端。目前支持以下功能:

  1. 支持HTTP/HTTPS请求抓取。

  2. 支持网速模拟。

  3. 支持请求拦截修改,实现跨域等功能。

  4. 实现接口Mock,用于本地开发调试。

pic

pic

pic

pic

项目地址 欢迎试玩。

查看原文

fwon 赞了回答 · 2017-11-28

解决为什么 Socket.IO 可以让 WebSockets 和 HTTP 监听在同一端口?

按理说TCP与HTTP之间是不能用「或者」的。
在OSI模型中,TCP是第四层传输层的协议,HTTP是第七层应用层的协议。

你的问题可以这样理解,socket.io 和 Express 都在使用node正在监听的端口来分别处理请求,而不是分别监听端口。

关注 2 回答 3

fwon 发布了文章 · 2017-11-22

认识 WebAssembly

自从Brendan Eich用十天时间创造了JavaScript,人们对它的吐槽就从未间断过。众所周知JavaScript是一门动态语言。运行于JavaScript引擎中,我们熟悉的有Mozilla的SpiderMonkey,Safari的JavaScriptCore,Edge的Chakra还有大名鼎鼎的V8。V8引擎将JavaScript的运行效率提升到一个新的level。所以后来的Nodejs也采用V8作为引擎,实现了用js进行后端开发的愿景。

然而JavaScript发展到今天,其语言基因中存在的缺陷并不能得到根本性的改变。比如常见的加法操作

function add(a, b) {
    return a + b;
}

这段代码在浏览器中的运行过程比你想象的复杂。
add在被调用前,js引擎并不能提前预判传入参数的类型,需要在运行时对参数进行如下一连串的类型判断和转换操作。

pic

对js加法运算的详细操作(keng)有兴趣的可以看这篇文章

V8再快也难以逾越语言本身的瓶颈。这种问题是动态语言的弊端,对于此类问题,业界已经出现了非常多的解决方案。

而本文要讲的正是目前最为前沿的一种 ------ WebAssembly

WebAssembly这个概念其实2015年就提出来了,而就在不久之前,四大浏览器厂商,Chrome, Firefox, Edge, Safari 在新版的浏览器中才全部默认支持Webassembly(Chrome, Firefox早于后两者),这种技术很快将在前端高性能开发领域中大放异彩。

WebAssembly是什么?

下面是来自官方的定义:

WebAssembly or wasm is a new portable, size- and load-time-efficient format suitable for compilation to the web.

关键词:”format",WebAssembly 是一种编码格式,适合编译到web上运行。

事实上,WebAssembly可以看做是对JavaScript的加强,弥补JavaScript在执行效率上的缺陷。

  • 它是一个新的语言,它定义了一种AST,并可以用字节码的格式表示。

  • 它是对浏览器的加强,浏览器能够直接理解WebAssembly并将其转化为机器码。

  • 它是一种目标语言,任何其他语言都可以编译成WebAssembly在浏览器上运行。

想象一下,在计算机视觉,游戏动画,视频编解码,数据加密等需要需要高计算量的领域,如果想在浏览器上实现,并跨浏览器支持,唯一能做的就是用JavaScript来运行,这是一件吃力不讨好的事情。而WebAssembly可以将现有的用C,C++编写的库直接编译成WebAssembly运行到浏览器上, 并且可以作为库被JavaScript引用。那就意味着我们可以将很多后端的工作转移到前端,减轻服务器的压力。这是WebAssembly最为吸引人的特性。并且WebAssembly是运行于沙箱中,保证了其安全性。

为什么要有WebAssembly?

如果只是想让C,C++,Java等原生语言编写的模块运行在浏览器上。我们只需要一个转换器,将源语言转换为目标语言JavaScript,而这种技术其实很早就有了。

例如将Java转换成JavaScript的Google Web Toolkit (GWT)

将python转换成JavaScript的pyjamas 等等。

但是这并没有解决JavaScript执行慢的问题,这跟直接用JavaScript来重写代码库是一样的作用。这就是为什么Electron能直接运行Node.js但对比传统桌面应用依然弱鸡的原因。

要理解JavaScript为什么运行慢,就要理解它在引擎中的处理过程。
传统JavaScript在V8引擎中的编译过程是这样的:首先JavaScript会被编译成AST,然后引擎再将AST, 转化为机器语言交给底层执行。

V8的pipeline结构会进一步先将AST转化为一种中间代码,再对中间代码再次生成优化后的机器码,从而实现更快的执行速度。

pic

对于WebAssembly来说,前面的parser, optimize 全部省了,直接编译到机器码。

pic

浏览器通过增加一种语言格式的编译支持,来实现执行效率的突破。

WebAssembly除了运行快之外,其特殊的二进制表示法也大大减小了代码包的大小。同时提升了浏览器的加载速度。

如何使用WebAssembly?

现在你已经能在这些浏览器中使用WebAssembly了。

WebAssembly这么快,但并不意味着JavaScript这门语言要从此绝迹了。

如前面所说,WebAssembly和JavaScript之间是可以相互调用的。

假设我们用C写了这段代码

include <math.h>
int add(int a, int b) {
    return a + b;
}

首先将其转化为wasm文件, 这里运用一个线上的工具 WasmFiddle

将转化的add.wasm下载下来。

由于目前还没支持 <script data-original=“abc.wasm" type="module" />的引入方式。所以不能直接在html引入,我们可以通过JS fetch来请求文件。

先封装一个fetch方法:

function fetchAndInstantiateWasm (url, imports) {
    return fetch(url)
    .then(res => {
        if (res.ok)
            return res.arrayBuffer();
        throw new Error(`Unable to fetch Web Assembly file ${url}.`);
    })
    .then(bytes => WebAssembly.compile(bytes))
    .then(module => WebAssembly.instantiate(module, imports || {}))
    .then(instance => instance.exports);
}

用定义好的fetchAndInstantiateWasm方法请求add.wasm文件,并在回调中调用C中定义的add方法,成功输出结果15。

fetchAndInstantiateWasm('add.wasm', {})
.then(m => {
    console.log(m.add(5, 10)); // 15
});

同样通过js import,也能够在C中调用js的方法。

fetchAndInstantiateWasm('program.wasm', {
    env: {
      consoleLog: num => console.log(num)
    }
})
.then(m => {
    console.log(m.add(5, 10)); // 15
});

上面在js代码中定义了consoleLog, 并传入了wasm文件,在C中就可以调用consoleLog方法往控制台输出信息,你也可以执行一些你想要的其他操作。

#include<stdio.h>
void consoleLog(int num);
int add(int num1, int num2) {
    int result = num1 + num2;
    consoleLog(result);
    return result;
}

可直接下载demo代码执行查看效果 demo

运行python -m SimpleHTTPServer后访问localhost:8000, 查看log中输出信息。

前面说WebAssembly是一门新的语言,但上面引入的wasm只是一种字节码,是作为其他语言编译的目标语言,完全没有可读性。其实WebAssembly是有自己的语法的,文件格式为wast。下面是add方法编译成的WebAssembly版本。

(module
  (type $FUNCSIG$vi (func (param i32)))
  (import "env" "consoleLog" (func $consoleLog (param i32)))
  (table 0 anyfunc)
  (memory $0 1)
  (export "memory" (memory $0))
  (export "add" (func $add))
  (func $add (param $0 i32) (param $1 i32) (result i32)
    (call $consoleLog
      (tee_local $1
        (i32.add
          (get_local $1)
          (get_local $0)
        )
      )
    )
    (get_local $1)
  )
)

wast是可编辑的,它同样可以直接转化为wasm, 用于浏览器引入。

上面只是一个最简单的例子,实际上利用WebAssembly实现的应用已经可以相当酷炫。

官方展示的demo游戏

还有一个运用webassembly实现的浏览器视频编辑器

和其他类似技术的区别?

asm.js

可能对前端比较关注的同学有听说过asm.js。它是Mozilla开发的一个JavaScript的子集。就是在JavaScript的基础上,加入了静态类型的支持。
asm.js是Mozilla开发的,所以只支持自家浏览器Firefox。当然代码也可以兼容运行于其他浏览器,但是就没有了优化效果。

asm.js 提供一种语法来表示变量类型

var first = 5;
var second = first;

对于上面这段JavaScript代码,在asm.js里是这样写的

var first = 5;
var second = first | 0;

在first后面加上|0,我们就将first标记为32位整数,而被赋值的second也为被定义为32位整数。
在Mozilla引擎编译代码的时候,遇到这些标志就会提前知道变量的类型,提前优化代码。而这些标记也不影响其他引擎的运算结果。

然而说到底它还是JavaScript,只不过我们提前为优化做了准备。代码还是要经过JavaScript Code ->AST->Optimize的过程。
另外asm.js也是支持将C,C++转化为asm.js的,有兴趣的可以参考这里

TypeScript

大家应该也知道微软的TypeScript,TypeScript做的工作其实跟asm.js有点类似,只不过TypeScript是更加High-Level的。他是JavaScript的一个超集,就是在JavaScript的基础上支持了类型和类等语法。并且能直接编译为JavaScript。TypeScript在于能在开发阶段就进行类型检查,保证代码开发效率和安全性。但是从浏览器运行效率上来看并没有优化效果,因为浏览器并不原生支持。

相同功能的还有facabook的Flow,也是在开发阶段加入类型的支持。

结语

目前WebAssembly由W3C WebAssembly Community Group负责开发与标准定制,而该组织的成员正是来自Google, Microsoft, Mozilla等浏览器开发人员。几个大厂同时投入到WebAssembly的开发中,相信不久WebAssembly就会成为一种浏览器网站&应用的通用优化技术。

参考资料

  1. https://medium.com/javascript...

  2. https://medium.com/javascript...

  3. https://www.youtube.com/watch...

  4. http://blog.techbridge.cc/201...

  5. https://github.com/WebAssembl...

查看原文

赞 9 收藏 11 评论 0

fwon 赞了回答 · 2017-09-19

解决git merge --no-ff是什么意思

--no-ff指的是强行关闭fast-forward方式。

fast-forward方式就是当条件允许的时候,git直接把HEAD指针指向合并分支的头,完成合并。属于“快进方式”,不过这种情况如果删除分支,则会丢失分支信息。因为在这个过程中没有创建commit

git merge --squash 是用来把一些不必要commit进行压缩,比如说,你的feature在开发的时候写的commit很乱,那么我们合并的时候不希望把这些历史commit带过来,于是使用--squash进行合并,此时文件已经同合并后一样了,但不移动HEAD,不提交。需要进行一次额外的commit来“总结”一下,然后完成最终的合并。

总结:
--no-ff:不使用fast-forward方式合并,保留分支的commit历史
--squash:使用squash方式合并,把多次分支commit历史压缩为一次

图片描述

关注 13 回答 4

fwon 赞了回答 · 2017-08-07

解决PATCH和PUT方法的区别?

patch方法用来更新局部资源,这句话我们该如何理解?

假设我们有一个UserInfo,里面有userIduserNameuserGender等10个字段。可你的编辑功能因为需求,在某个特别的页面里只能修改userName,这时候的更新怎么做?

人们通常(为徒省事)把一个包含了修改后userName的完整userInfo对象传给后端,做完整更新。但仔细想想,这种做法感觉有点二,而且真心浪费带宽(纯技术上讲,你不关心带宽那是你土豪)。

于是patch诞生,只传一个userName到指定资源去,表示该请求是一个局部更新,后端仅更新接收到的字段。

put虽然也是更新资源,但要求前端提供的一定是一个完整的资源对象,理论上说,如果你用了put,但却没有提供完整的UserInfo,那么缺了的那些字段应该被清空

补充:

最后再补充一句,restful只是标准,标准的意思是如果在大家都依此行事的话,沟通成本会很低,开发效率就高。但并非强制(也没人强制得了),所以你说在你的程序里把方法名从put改成patch没有任何影响,那是自然,因为你的后端程序并没有按照标准对两个方法做不同处理,她的表现自然是一样的

关注 19 回答 3

fwon 回答了问题 · 2017-06-23

vue弹层遇到的问题,求大神解决

@click="goPay"

关注 2 回答 1

认证与成就

  • 获得 295 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-07-11
个人主页被 2.4k 人浏览