BUG来

BUG来 查看完整档案

广州编辑华南师范大学  |  软件工程 编辑BBGame  |  前端工程师 编辑填写个人主网站
编辑

前端攻城狮

个人动态

BUG来 收藏了文章 · 8月20日

border、outline、boxshadow那些事以及如何做内凹圆

border

边框是我们美化网页、增强样式最常用的手段之一。例如:

<div class="text"></div>
    .text {
    width: 254px;
    height: 254px;
    background-color: #33AAE1;
    border: 10px solid #03D766;
}

图片描述

但有些时候,我们的需求是给一个容器加上多重边框,最容易想到的是给它多加一层标签:

<div class="text-outborder">
    <div class="text"></div>
</div>

.text-outborder {
    width: 274px;
    height: 274px;
    border: 10px solid #03D766;
}

.text {
    margin: auto;
    width: 254px;
    height: 254px;
    background-color: #33AAE1;
    border: 10px solid #03D766;
}

图片描述

不过有些时候,我们可能无法修改结构,或者修改结构的成本很高,此时就需要我们在纯 CSS 层面解决这个问题。

outline

这时候可以通过 outline 属性来解决:

.text {
    width: 254px;
    height: 254px;
    background-color: #33AAE1;
    border: 10px solid #03D766;
    outline: 10px solid #BC9E9C;
}

图片描述

描边有一个好处在于,它跟边框类似,可以设置各种线型,比如虚线:

.text {
    width: 254px;
    height: 254px;
    background-color: #33AAE1;
    border: 10px solid #03D766;
    outline: 5px dashed #CE843B;
}

图片描述

有趣的是,还有一个 outline-offset 属性,可以控制描边的偏移量。

.text {
    width: 254px;
    height: 254px;
    background-color: #33AAE1;
    border: 20px solid #03D766;
    outline: 5px dashed #FFF;
    outline-offset: 10px;
}

我们可以把 outline 扩展出去:
图片描述

outline-offset

而且 outline-offset 还支持负值,可以将 outline 叠加在 border 之上:

.text {
    width: 254px;
    height: 254px;
    background-color: #33AAE1;
    border: 20px solid #03D766;
    outline: 5px dashed #FFF;
    outline-offset: -12px;
}

图片描述

利用这个特性可以玩出很多好玩的效果。

不过描边有一个缺陷——如果这个容器本身有圆角的话,描边并不能完全贴合圆角。目前所有浏览器的行为都是这样的:
图片描述

box-shadow

如果你需要使用圆角,那么你就得另寻它法了。接着,我们又想到了 box-shadow 属性:

我们通常是这样设置投影的:

box-shadow: 0 5px 5px #000;

图片描述

前面三个长度值,再加一个颜色值。

前两个长度值分别表示投影在水平和垂直方向上的偏移量,第三个长度值表示投影的模糊半径(也就是模糊的程度);颜色值就是投影的颜色。

如果我们把前三个值都设为零,实际上是没有任何效果的。因为如果投影即不偏移也不模糊,刚好会被这个元素自己严严实实地遮住。

box-shadow第四个长度值

很多人可能不知道的是,投影还可以有第四个长度值。这个值表示投影向外扩张的程度:

box-shadow: 0 0 0 10px #FF0000;

图片描述

这样,投影就会从元素的底下露出一圈了。

关于投影,另外一个不是每个人都知道的特性是,投影属性其实可以接受一个列表,我们可以一次赋予它多层投影,像这样:

.text {
    width: 254px;
    height: 254px;
    background-color: #33AAE1;
    border: 20px solid #03D766;
    border-radius: 50px;
    box-shadow: 
        0 0 0 10px #FB0000,
        0 0 0 20px #FBDD00, 
        0 0 0 30px #00BDFB;
}

图片描述

这样我们就得到了超过两层的 “边框” 效果了。

投影的另外一个好处是,它的扩张效果是根据元素自己的形状来的。如果元素是矩形,它扩张开来就是一个更大的矩形;如果元素有圆角,它也会扩张出圆角。

注意事项

由于描边和投影都是不影响布局的,所以如果这个元素和其它元素的相对位置关系很重要,就需要我们以外边距等方式来为这些多出来的 “边框” 腾出位置,以防被其它元素盖住。

因此,从这个意义上来说,使用内嵌投影似乎是更好的选择。因为内嵌投影让投影出现在元素内部,我们可以用内边距在元素的内部消化掉这些额外 “边框” 所需要的空间,处理起来更容易一些。

内凹圆

标签页我们都很熟悉了,它是一种常用的 UI 元素。
图片描述

我们把它拉近来看一看:
图片描述

这个标签还是比较美观的,我们用圆角让它看起来很接近真实的标签造型。不过我们也注意到,它底部的两个直角看起来似乎有点生硬。

所以设计师原本期望的效果可能是这样的:
![图片描述][14]

这样就自然多了。但这看起来似乎很难实现啊!

我们的难点主要在这里:
图片描述

这个特殊的形状如何实现?

我们把它放大来看一下:
图片描述

首先我们可能会想到用图片。这当然是可行的,但图片有种种局限,我们最好还是完全用 CSS 来实现它。

好,接下来我们来分析一下它的形状。它其实就是一个方形,再挖掉一个 90° 的扇形。于是我们试着创建一个方形,再用背景色做出一个扇形叠加上去:
图片描述

看起来好像可以了。但这是骗人的啊!

把它放在复杂背景下,立马就露馅了——扇形部分不是透明的:
图片描述

如何实现内凹圆角

所以,我们的问题就变成了如何用CSS实现内凹圆角。

对于普通外凸的圆角,我们都已经非常熟悉了,我们用圆角属性就可以得到:
图片描述

但我们需要的是一个内凹的圆角形状。

这是一个实实在在的需求,于是有开发者曾经提议,扩展圆角属性,让它支持负值。如果是负值,圆角就不再是外凸的,而是内凹的。这个提议听起来似乎很有道理,语法设计也很紧凑。
图片描述

但实际上它的语义不够准确。因此 CSS 工作组并没有接受这个提议,并未将它纳入标准。
图片描述

这条路走不通,我们还需要继续探索。

我们顺着这个方向,头脑中很自然地会迸出这个疑问:CSS中还有和圆形有关的属性吗?

答案当然是有!

径向渐变

“径向渐变” 特性就是跟圆形有关的。

线性渐变

不过它稍稍有些复杂。在讲解径向渐变之前,我们先来看一看比较简单的 “线性渐变”。
图片描述

说到渐变,那自然需要有一个容器。然后,还需要有两种颜色。渐变的颜色设置我们称之为 “色标”——每个色标不仅有颜色信息,还有位置信息。

我们给起点和终点的色标分别设置颜色,就可以得到一条渐变图案:图片描述

接下来,关于渐变,我们其实可以设置不止两个色标。比如我们可以在中央再增加一个色标。请注意我们特意选择了跟起点色标一样的颜色。我们得到的效果如下:
图片描述

我们发现,渐变只发生在颜色不同的色标之间。如果两个色标的颜色相同,则它们之间会显示为一块实色。

好的,我们继续增加色标。这次我们在渐变地带的中央增加一个色标,且让它的颜色和终点色标相同:
图片描述

根据上面的经验,这个结果正是我们所预料的——渐变只发生在颜色不同的色标之间。

接下来,我们玩点更特别的,我们把中间的两个色标相互靠近直至重合,会发生什么?
图片描述

实际上这个渐变也会趋向于零。也就是说,虽然这本质上仍然是一个 “渐变” 图案,但经过我们的精心设计之后,我们最终得到了两个纯色的色块条纹。

如果我们把终点颜色换为透明色……
图片描述

我们甚至还会得到实色和透明色间隔的条纹。

再来看径向渐变

好的,接下来我们来看径向渐变。它稍稍有些复杂,但原理是一样的。

同样,我们需要有一个容器。但对径向渐变来说,顾名思议,所有色标是排布在一条半径上的。也就是说,我们还需要有一个圆心。默认情况下,圆心就是这个容器的正中心:
图片描述

而这条半径就是圆心指向容器最远端的一条假想的线:
图片描述

接下来,我们要设置一些色标:
图片描述

说到这里,就要讲解一下径向渐变的特别之处。所有色标的颜色变化推进不是像线性渐变那样平行推进的,而是以同心圆的方式向外扩散的——就像水池里被石子激起的涟漪那样。

看到这个色标的分布,我们应该可以想像出线性渐变的结果是什么;但这里我们把它按照径向渐变的特征来推演一下,实际上最终的效果是这样的:
d4477682-427b-11e5-87fd-8e0ad91113d2.png

我们把所有辅助性的标记都去掉,只留下渐变图案:
图片描述

这是一个穿了个窟窿的实色背景。很好玩是吧?不过不要忘了我们是为什么来到这儿的——我们是为了得到一个内凹圆角的形状。

细心的朋友可能已经发现了,我们需要的东西已经出现了:
图片描述

接下来,我们调整一下圆心的位置和容器的尺寸,就可以得到这个内凹圆角的造型了。
图片描述

内凹圆具体代码例子

具体代码例子如下:

.text {
    width: 254px;
    height: 254px;
    background:
      -moz-radial-gradient(
          100% 0%, 
          rgba(255, 255, 255, 0) 0%,
          rgba(255, 255, 255, 0) 71%,
          #0059FF 0%;
      );
}

内凹圆完成

利用这个技巧,我们用纯 CSS 最终实现了这个看似不可能的 “圆润的标签页” 效果!
图片描述

原文链接:https://github.com/cssmagic/blog/issues/54#rd

查看原文

BUG来 赞了回答 · 2019-12-05

解决ant design getFieldDecorator 无法获取自定义组件的值

https://ant.design/components...

在自定义组件里调用this.props.onChange

关注 2 回答 1

BUG来 赞了回答 · 2019-07-09

axios创建实例作用是啥 可以不用吗

默认会导出实例axios,通常你只需使用这个axios就可以了。

但是有时候你需要创建多个实例,比如你需要访问多个服务地址,而这些服务请求和响应的结构都完全不同,那么你可以通过axios.create创建不同的实例来处理。

比如axios1是用http状态码确定响应是否正常,而axios2是服务器自己定义的状态码,又或者他们请求头不同,支持的content-type不同,那么我可以单独为axios1axios2写拦截器。

关注 2 回答 1

BUG来 赞了文章 · 2019-04-23

WebAssembly 实践:如何写代码

本文不讨论 WebAssembly 的发展,只是一步一步地教你怎么写 WebAssembly 的各种 demo。文中给出的例子我都放在 GitHub 中了(仓库地址),包含了编译脚本和编译好的可执行文件,只需再有一个支持 WebAssembly 的浏览器就可以直接运行。

配置开发调试环境

安装编译工具

略。 参考官方 Developer’s GuideAdvanced Tools,需要安装的工具有:

安装过程挺繁琐的,得本地 clone 代码再编译。

安装浏览器

作为一个新技术,之所以说 WebAssembly 前途明媚,不仅是因为 W3C 成立了专门的 Webassembly Community Group,被标准认可;也是因为这次各大主流浏览器厂商(难得的)达成了一致,共同参与规范的讨论,在自家的浏览器里都实现了。

体验新技术,建议使用激进版浏览器,最新版本中都已经支持了 WebAssembly。

除了上边几个激进的浏览器,在主流版本里开启 flag 也是可以使用 WebAssembly 的:

  • Chrome: 打开 chrome://flags/#enable-webassembly,选择 enable

  • Firefox: 打开 about:configjavascript.options.wasm 设置为 true

快速体验 WebAssembly

想快速体验 WebAssembly ?最简单的办法就是找个支持 WebAssembly 的浏览器,打开控制台,把下列代码粘贴进去。

WebAssembly.compile(new Uint8Array(`
  00 61 73 6d  01 00 00 00  01 0c 02 60  02 7f 7f 01
  7f 60 01 7f  01 7f 03 03  02 00 01 07  10 02 03 61
  64 64 00 00  06 73 71 75  61 72 65 00  01 0a 13 02
  08 00 20 00  20 01 6a 0f  0b 08 00 20  00 20 00 6c
  0f 0b`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
)).then(module => {
  const instance = new WebAssembly.Instance(module)
  const { add, square } = instance.exports

  console.log('2 + 4 =', add(2, 4))
  console.log('3^2 =', square(3))
  console.log('(2 + 5)^2 =', square(add(2 + 5)))

})

里边这一坨奇怪的数字,就是 WebAssembly 的二进制源码。

运行结果

如果报错,说明你的浏览器不支持 WebAssembly ;如果没报错,代码的运行结果如下(还会返回一个 Promise):

2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49

其中 addsquare 虽然做的事情很简单,就是计算加法和平方,但那毕竟是由 WebAssembly 编译出来的接口,是硬生生地用二进制写出来的!

解释代码

上边的二进制源码一行 16 个数,有 4 行零两个,一共有 66 个数;每个数都是 8 位无符号十六进制整数,一共占 66 Byte。

WebAssembly 提供了 JS API,其中 WebAssembly.compile 可以用来编译 wasm 的二进制源码,它接受 BufferSource 格式的参数,返回一个 Promise。

那些代码里的前几行,目的就是把字符串转成 ArrayBuffer。先将字符串分割成普通数组,然后将普通数组转成 8 位无符号整数的数组;里的数字是十六进制的,所有用了 parseInt(str, 16)

new Uint8Array(
  `...`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
)

如果浏览器支持通过 <script type="module"> 的方式引入 wasm 文件,这些步骤都是多余的(他们有这个计划)。

然后,如果 WebAssembly.compile 返回的 Promise fulfilled 了,resolve 方法的第一个参数就是 WebAssembly 的模块对象,是 WebAssembly.Module 的实例。

然后使用 WebAssembly.Instance 将模块对象转成 WebAssembly 实例(第二个参数可以用来导入变量)。

通过 instance.exports 可以拿到 wasm 代码输出的接口,剩下的代码就和和普通 javascript 一样了。

注意数据类型

WebAssembly 是有明确的数据类型的,我那个例子里用的都是 32 位整型数(是不是看不出来…… 二进制里那些 7f 表示 i32 指令,意思就是32位整数),所以用 WebAssembly 编译出来的时候要注意数据类型。

如果你乱传数据,WebAssembly 程序也不会报错,因为在执行时会被动态转换(dynamic_cast),它支持传递模糊类型的数据引用。但是你如果给函数传了个字符串或者超大的数,具体会被转成什么就说不清了,通常是转成 0。

console.log(square('Tom')) // 0
console.log(add(2e+66, 3e+66)) // 0
console.log(2e+66 + 3e+66) // 5e+66

想了解更多关于数据类型的细节,可以参考:Data Types

把 C/C++ 编译成 WebAssembly

有一个在线 C++ 转 wasm 的工具: WasmExplorer

二进制代码简直不是人写的😂,还有其他方式能写 WebAssembly 吗?

有,那就是把其他语言编译成 WebAssembly 的二进制。想实现这个效果,不得不用到各种编译工具了。其中一个比较关键的工具是 Emscripten,它基于 LLVM ,可以将 C/C++ 编译成 asm.js,使用 WASM 标志也可以直接生成 WebAssembly 二进制文件(后缀是 .wasm)。

         Emscripten
source.c   ----->  target.js

     Emscripten (with flag)
source.c   ----->  target.wasm

工具如何安装就不讲了,在此只提醒一点:emcc 在 1.37 以上版本才支持直接生成 wasm 文件。

编写 C 代码

项目代码地址

首先新建一个 C 语言文件,假设叫 math.c 吧,在里边实现 addsquare 方法:

// math.c

int add (int x, int y) {
  return x + y;
}

int square (int x) {
  return x * x;
}

然后执行 emcc math.c -Os -s WASM=1 -s SIDE_MODULE=1 -o math.wasm 就可以生成 wasm 文件了。

代码解释

C 语言代码一目了然,就是写了两个函数,由于 C 语言里的函数都是全局的,这两个函数默认都会被模块导出。

不知道你有没有注意到,这个文件里没写 main 函数!没写入口函数,它自身什么也执行不了,但是可以把它当成一个库文件使用,所以我在也是用模块的方式编译生成的 wasm 文件。

在 WebAssembly 官方给出的例子中,是写了 main 函数,而且是直接把 C 文件编译生成了 html + js + wasm 文件,实际上是生成了一个可以运行 demo,简单粗暴。生成的代码体积比较大,很难看懂里边具体做了什么。为了代码简洁,我这里只是生成 wasm 模块,没有其他多余文件,要想把它运行起来还需要自己写 html 和 js 读取并执行 wasm 文件。(完整代码

如果你也想直接生成可用的 demo,你可以再写个 main 函数,然后执行 emcc math.c -s WASM=1 -o math.html 就可以了。

如何运行 WebAssembly 二进制文件?

现在有了 wasm 文件,也有了支持 WebAssembly 的浏览器,怎么把它运行起来呢?

目前只有一种方式能调用 wasm 里的提供接口,那就是:用 javascript !

官方网站中有一篇 Understanding the JS API 介绍了如何用 JS API 加载并执行 wasm 文件,写的比较粗略。

WebAssembly 目前只设计也只实现了 javascript API,就像我刚开始提供的那个例子一样,只有通过 js 代码来编译、实例化才可以调用其中的接口。这也很好的说明了 WebAssembly 并不是要替代 javascript ,而是用来增强 javascript 和 Web 平台的能力的。我觉得 WebAssembly 更适合用于写模块,承接各种复杂的计算,如图像处理、3D运算、语音识别、视音频编码解码这种工作,主体程序还是要用 javascript 来写的。

编写加载函数 (loader)

在最开始的例子里,已经很简化的将执行 WebAssembly 的步骤写出来了,其实就是 【加载文件】->【转成 buffer】->【编译】->【实例化】。

function loadWebAssembly (path) {
  return fetch(path)                   // 加载文件        
    .then(res => res.arrayBuffer())    // 转成 ArrayBuffer
    .then(WebAssembly.instantiate)     // 编译 + 实例化
    .then(mod => mod.instance)         // 提取生成都模块
}

代码其实很简单,使用了 Fetch API 来获取 wasm 文件,然后将其转换成 ArrayBuffer,然后使用 WebAssembly.instantiate 这个一步到位的方法来编译并初始化一个 WebAssembly 的实例。最后一步是从生成的模块中提取出真正的实例对象。

完成了上边的操作,就可以直接使用 loadWebAssembly 这个方法加载 wasm 文件了,它相当于是一个 wasm-loader ;返回值是一个 Promise,使用起来和普通的 js 函数没什么区别。从 instance.exports 中可以找到 wasm 文件输出的接口。

loadWebAssembly('path/to/math.wasm')
  .then(instance => {
    const { add, square } = instance.exports
    // ...
  })

返回 Promise 不只是因为 fetch 函数,即使像最开始的例子那样把二进制硬编码,也必须要用 Promise 。因为 WebAssembly.compileWebAssembly.instantiate 这些接口都是异步的,本身就返回 Promise 。

更完整的加载函数

如果你直接使用上边那个 loadWebAssembly 函数,有可能会执行失败,因为在 wasm 文件里,可能还会引入一些环境变量,在实例化的同时还需要初始化内存空间和变量映射表,也就是 WebAssembly.MemoryWebAssembly.Table

/**
 * @param {String} path wasm 文件路径
 * @param {Object} imports 传递到 wasm 代码中的变量
 */
function loadWebAssembly (path, imports = {}) {
  return fetch(path)
    .then(response => response.arrayBuffer())
    .then(buffer => WebAssembly.compile(buffer))
    .then(module => {
      imports.env = imports.env || {}

      // 开辟内存空间
      imports.env.memoryBase = imports.env.memoryBase || 0
      if (!imports.env.memory) {
        imports.env.memory = new WebAssembly.Memory({ initial: 256 })
      }

      // 创建变量映射表
      imports.env.tableBase = imports.env.tableBase || 0
      if (!imports.env.table) {
        // 在 MVP 版本中 element 只能是 "anyfunc"
        imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
      }

      // 创建 WebAssembly 实例
      return new WebAssembly.Instance(module, imports)
    })
}

这个 loadWebAssembly 函数还接受第二个参数,表示要传递给 wasm 的变量,在初始化 WebAssembly 实例的时候,可以把一些接口传递给 wasm 代码。

调用 wasm 导出的接口

有了 loadWebAssembly 就可以调用 wasm 代码导出的接口了。

loadWebAssembly('./math.wasm')
  .then(instance => {
    const add = instance.exports._add
    const square = instance.exports._square

    console.log('2 + 4 =', add(2, 4))
    console.log('3^2 =', square(3))
    console.log('(2 + 5)^2 =', square(add(2 + 5)))
  })

比较奇怪的一点是,用 C/C++ 导出的模块,属性名上默认都带了 _ 前缀,asm.js 转成了 wasm 模块就不带。

在浏览器中的运行效果

参考刚才用 C 语言写出来的项目(代码地址),直接用浏览器打开 index.html 即可。能看到这样的输出(我使用的是 Chrome Canany 浏览器):

Preview in Browser

如果你打开开发者工具的 Source 面板,能够看到 wasm 的源代码,浏览器已经将二进制转换成了对等的文本指令)。

View Source

虽然是一个 wasm 文件,浏览器将它解析成了两个(也有可能更多),是因为我们输出了两个接口,每个文件都对应了一个接口的定义。可以理解为 Canary 浏览器为了方便看源码实现的 sourcemap 功能。

把 asm.js 编译成 WebAssembly

项目代码地址

刚才也介绍了 Emscripten 可以将 C/C++ 编译成 asm.js ,这是它的默认功能,加上 flag 才能生成 wasm 。

asm.js 是 javascript 的子集,是一种语法(不是一个前端工具库!),用了很多底层语法来标注数据类型,目的是提高 javascript 的运行效率,本身就是作为 C/C++ 编译的目标设计的(不是给人写的),可以理解为一种中间表示层语法 (IR, Intermediate Representation)。asm.js 出生于 WebAssembly 之前, WebAssembly 借鉴了这个思路,做的更彻底一些,直接跳过 javascript ,设计了一套新的平台指令。

编写 asm.js 代码

// math.js

function () {
  "use asm";

  function add (x, y) {
    x = x | 0;
    y = y | 0;
    return x + y | 0;
  }

  function square (x) {
    x = x | 0;
    return x * x | 0;
  }

  return {
    add: add,
    square: square
  };
}

上边定义了一个函数,并且声明了 "use asm",这样一来,这个函数就会被视为 asm.js 的模块,里边可以添加方法,通过 return 暴露给外部使用。

不过,目前只有 asm.js 才能转成 wasm,普通 javascript 是不行的! 因为 javascript 是弱类型语言,用法也比较灵活,本身就很难编译成强类型的指令。

使用 Binaryen 和 WABT

虽然 Emscripten 能生成 asm.js 和 wasm ,但是却不能把 asm.js 转成 wasm 。因为它是基于 LLVM 的,然而 asm.js 没法编译成 LLVM IR (Intermediate Representation)。想要把 asm.js 编译成 WebAssembly,就要用到他们官方提供的 BinaryenWABT (WebAssembly Binary Toolkit) 工具了。

原理和编译方法参考官方文档,整个过程大概是这样的:

        Binaryen             WABT
math.js   --->   math.wast   --->   math.wasm

用脚本描述大概是这样:

asm2wasm math.js -o math.wast
wast2wasm math.wast -o math.wasm

wast 是什么格式?

WebAssembly 除了定义了二进制格式以外,还定义了一份对等的文本描述。官方给出的是线性表示的例子,而 wast 是用 S-表达式(s-expressions) 描述的另一种文本格式。

上边的 asm.js 代码编译生成的 wast 文件是这样的:

(module
  (export "add" (func $add))
  (export "square" (func $square))

  (func $add (param $x i32) (param $y i32) (result i32)
    (return
      (i32.add
        (get_local $x)
        (get_local $y)
      )
    )
  )

  (func $square (param $x i32) (result i32)
    (return
      (i32.mul
        (get_local $x)
        (get_local $x)
      )
    )
  )
)

和 lisp 挺像的,反正比二进制宜读多了😂。能看出来最外层声明了是一个模块,然后导出了两个函数,下边紧接着是两个函数的定义,包含了参数列表和返回值的类型声明。如果对这种类似 lisp 的语法比较熟悉,完全可以手写 wast 嘛,只要装个 wast2wasm 小工具就可以生成 wasm 了。或者在这个在线 wast -> wasm 转换工具 里写 wast 代码,可以实时预览编译的结果,也可以下载生成的 wasm 文件。

在 WebAssembly 中调用 Web API

在 js 里能调用 wasm 里定义的方法,反过来,wasm 里能不能调用 javascript 写的方法呢?能不能调用平台提供的方法(Web API)呢?

当然是可以的。不过在 MVP (Minimum Viable Product) 版本里实现的功能有限。要想在 wasm 里调用 Web API,需要在创建 WebAssembly 实例的时候把 Web API 传递过去才可以,具体做法可以参考上边写的那个比较复杂的 loader。(通过 WebAssembly.Table 传变量相当麻烦)。

向 wasm 中传递 js 变量

在有了 loadWebAssembly 这个方法之后,就可以给 wasm 代码传递 js 变量和函数了。

const imports = {
  Math,
  objects: {
    count: 2333
  },
  methods: {
    output (message) {
      console.log(`-----> ${message} <-----`)
    }
  }
}

loadWebAssembly('path/to/source.wasm', imports)
  .then(instance => {
    // ...
  })

上边的代码里给 wasm 模块传递了三个对象: Mathobjectsmethods,分别对应了 Web API 、普通 js 对象、使用了 Web API 的 js 函数。属性名和变量名都并没什么限制,是可以随便起的,把它传递给 loadWebAssembly 方法的第二个参数就可以传递到 wasm 模块中了。

真正实现传递的是 loadWebAssembly 的这行代码:

new WebAssembly.Instance(module, imports)

获取并使用从 js 传递的变量

既然 wasm 的代码最外层声明的是一个模块,我们能向外 export 接口,当然也可以 import 接口。完整代码如下:

(module
  (import "objects" "count" (global $count f32))
  (import "methods" "output" (func $output (param f32)))
  (import "Math" "sin" (func $sin (param f32) (result f32)))

  (export "test" (func $test))
  (func $test (param $x f32)
    (call $output (f32.const 42))
    (call $output (get_global $count))
    (call $output (get_local $x))
    (call $output
      (call $sin
        (get_local $x)
      )
    )
  )
)

这段代码也是在最外层声明了一个 module,然后前三行是 import 语句。首先从 objects 中导入 count 属性,并且在代码里声明为全局的 $count 变量,格式是 32 位浮点数。

(import "objects" "count" (global $count f32))

然后从 methods 中导入 output 方法,声明为一个接受 32 位浮点数作为参数的函数 $output

(import "methods" "output" (func $output (param f32)))

最后从 Math 中导入 sin 方法,声明为一个接受 32 位浮点数作为参数的函数 $sin,返回值也是 32 位浮点数。这样一来就把 js 传递的对象转成了自身模块中可以使用变量。

(import "Math" "sin" (func $sin (param f32) (result f32)))

调用 js 中定义的接口

接下来是定义并且导出了一个 test 函数,接受一个 32 位浮点数作为参数。在 wast 的语法里 call 指令用来调用函数,get_global 用来获取全局变量的值,get_local 用来获取局部变量的值,只能在函数定义中使用。这样来看,test 函数 里执行了四条命令,首先调用 $output 输出了一个常量 42;然后调用 $output 输出全局变量 $count ,这个值是通过 import 获取来的;接着又输出了函数的参数 $x;最后输出了函数参数 $x 调用 Web API $sin 计算后的结果。

  (func $test (param $x f32)
    (call $output (f32.const 42))
    (call $output (get_global $count))
    (call $output (get_local $x))
    (call $output
      (call $sin
        (get_local $x)
      )
    )
  )

编译执行

通过 west2wasm source.wast -o source.wasm 可以生成 wasm 文件,然后使用 loadWebAssembly 编译 wasm 文件。

loadWebAssembly('path/to/source.wasm', imports)
  .then(instance => {
    const { test } = instance.exports
    test(2333)
  })

会得到如下结果:

-----> 42 <-----
-----> 666 <-----
-----> 2333 <-----
-----> 0.9332447648048401 <-----

代码虽然简单,但是实现了向 wasm 中传递变量,并且能在 wasm 中调用 Mathconsole 这种平台接口。如果想要绕过 javascript 直接给 wasm 传参,或者在 wasm 里直接引用 DOM API,就得看他们下一步的计划了。参考 GC / DOM / Web API Integration

结语

根据这篇《如何画马》的教程,相信你很快就能用 WebAssembly 写出来 Angry Bots 这样的游戏啦~ 💪

如何画马

查看原文

赞 81 收藏 136 评论 37

BUG来 赞了回答 · 2019-04-09

解决当在Webpack里配置了别名之后,Webstorm里可以设置路径提示吗

可以的
windows:

File > Settings > Languages and Frameworks >JavaScript | Webpack 

macOS:

WebStorm | Preferences | Languages and Frameworks | JavaScript | Webpack

配置指定的webpack配置

图片描述

参考:

https://www.jetbrains.com/hel...
https://blog.jetbrains.com/we...

关注 5 回答 3

BUG来 收藏了文章 · 2019-04-03

webpack4升级完全指南

webpack4官方已经于近日升级到了V4.5的稳定版本,对应的一些必备插件(webpack-contrib)也陆续完成了更新支持,笔者在第一时间完成了项目由V3到V4的迁移,在此记录一下升级过程中遇到的种种问题和对应的解决手段,方便后续入坑者及时查阅,减少重复工作,如果觉得本篇文章对你有帮助,欢迎点赞😁

一、Node版本依赖重新调整

官方不再支持node4以下的版本,依赖node的环境版本>=6.11.5,当然考虑到最佳的es6特性实现,建议node版本可以升级到V8.9.4或以上版本,具体更新说明部分可以见:webpack4更新日志

"engines": {
    "node": ">=6.11.5" // >=8.9.4 (recommendation version) 
  },

二、用更加快捷的mode模式来优化配置文件

webpack4中提供的mode有两个值:development和production,默认值是 production。mode是我们为减小生产环境构建体积以及节约开发环境的构建时间提供的一种优化方案,提供对应的构建参数项的默认开启或关闭,降低配置成本。

开启方式 1:直接在启动命令后加入参数

"scripts": {
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
}

开启方式 2:可以在配置文件中加入一个mode属性:

module.exports = {
  mode: 'production' // development
};

development模式下,将侧重于功能调试和优化开发体验,包含如下内容:

  1. 浏览器调试工具
  2. 开发阶段的详细错误日志和提示
  3. 快速和优化的增量构建机制

production模式下,将侧重于模块体积优化和线上部署,包含如下内容:

  1. 开启所有的优化代码
  2. 更小的bundle大小
  3. 去除掉只在开发阶段运行的代码
  4. Scope hoisting和Tree-shaking
  5. 自动启用uglifyjs对代码进行压缩

webpack一直以来最饱受诟病的就是其配置门槛极高,配置内容复杂而繁琐,容易让人从入门到放弃,而它的后起之秀如rollup,parcel等均在配置流程上做了极大的优化,做到开箱即用,webpack在V4中应该也从中借鉴了不少经验来提升自身的配置效率,详见内容可以参考这篇文章《webpack 4: mode and optimization》

三、再见commonchunk,你好optimization

从webpack4开始官方移除了commonchunk插件,改用了optimization属性进行更加灵活的配置,这也应该是从V3升级到V4的代码修改过程中最为复杂的一部分,下面的代码即是optimize.splitChunks 中的一些配置参考,

module.exports = {
  optimization: {
    runtimeChunk: {
      name: 'manifest'
    },
    minimizer: true, // [new UglifyJsPlugin({...})]
    splitChunks:{
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      name: false,
      cacheGroups: {
        vendor: {
          name: 'vendor',
          chunks: 'initial',
          priority: -10,
          reuseExistingChunk: false,
          test: /node_modules\/(.*)\.js/
        },
        styles: {
          name: 'styles',
          test: /\.(scss|css)$/,
          chunks: 'all',
          minChunks: 1,
          reuseExistingChunk: true,
          enforce: true
        }
      }
    }
  }
}

从中我们不难发现,其主要变化有如下几个方面:

  1. commonchunk配置项被彻底去掉,之前需要通过配置两次new webpack.optimize.CommonsChunkPlugin来分别获取vendor和manifest的通用chunk方式已经做了整合, 直接在optimization中配置runtimeChunk和splitChunks即可 ,提取功能也更为强大,具体配置见:splitChunks
  2. runtimeChunk可以配置成true,single或者对象,用自动计算当前构建的一些基础chunk信息,类似之前版本中的manifest信息获取方式。
  3. webpack.optimize.UglifyJsPlugin现在也不需要了,只需要使用optimization.minimize为true就行,production mode下面自动为true,当然如果想使用第三方的压缩插件也可以在optimization.minimizer的数组列表中进行配置

四、ExtractTextWebpackPlugin调整,建议选用新的CSS文件提取插件mini-css-extract-plugin

由于webpack4以后对css模块支持的逐步完善和commonchunk插件的移除,在处理css文件提取的计算方式上也做了些调整,之前我们首选使用的extract-text-webpack-plugin也完成了其历史使命,将让位于mini-css-extract-plugin

基本配置如下:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,  // replace ExtractTextPlugin.extract({..})
          "css-loader"
        ]
      }
    ]
  }
}

生产环境下的配置优化:

const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: true 
      }),
      new OptimizeCSSAssetsPlugin({})  // use OptimizeCSSAssetsPlugin
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/app.[name].css',
      chunkFilename: 'css/app.[contenthash:12].css'  // use contenthash *
    })
  ]
  ....
}

将多个css chunk合并成一个css文件

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {            
          name: 'styles',
          test: /\.scss|css$/,
          chunks: 'all',    // merge all the css chunk to one file
          enforce: true
        }
      }
    }
  }
}

五、其他调整项备忘

  1. NoEmitOnErrorsPlugin- > optimization.noEmitOnErrors(默认情况下处于生产模式)
  2. ModuleConcatenationPlugin- > optimization.concatenateModules(默认情况下处于生产模式)
  3. NamedModulesPlugin- > optimization.namedModules(在开发模式下默认开启)
  4. webpack命令优化 -> 发布了独立的 webpack-cli 命令行工具包
  5. webpack-dev-server -> 建议升级到最新版本
  6. html-webpack-plugin -> 建议升级到的最新版本
  7. file-loader -> 建议升级到最新版本
  8. url-loader -> 建议升级到最新版本

六、参考工程

webpack4配置工程实例

七、参阅资料

  1. webpack4
  2. webpack4发布概览
  3. webpack 4: mode and optimization
  4. webpack4新特性介绍
  5. webpack4升级指北
  6. webpack4升级指南以及从webpack3.x迁移
查看原文

BUG来 赞了文章 · 2019-04-03

webpack4升级完全指南

webpack4官方已经于近日升级到了V4.5的稳定版本,对应的一些必备插件(webpack-contrib)也陆续完成了更新支持,笔者在第一时间完成了项目由V3到V4的迁移,在此记录一下升级过程中遇到的种种问题和对应的解决手段,方便后续入坑者及时查阅,减少重复工作,如果觉得本篇文章对你有帮助,欢迎点赞😁

一、Node版本依赖重新调整

官方不再支持node4以下的版本,依赖node的环境版本>=6.11.5,当然考虑到最佳的es6特性实现,建议node版本可以升级到V8.9.4或以上版本,具体更新说明部分可以见:webpack4更新日志

"engines": {
    "node": ">=6.11.5" // >=8.9.4 (recommendation version) 
  },

二、用更加快捷的mode模式来优化配置文件

webpack4中提供的mode有两个值:development和production,默认值是 production。mode是我们为减小生产环境构建体积以及节约开发环境的构建时间提供的一种优化方案,提供对应的构建参数项的默认开启或关闭,降低配置成本。

开启方式 1:直接在启动命令后加入参数

"scripts": {
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
}

开启方式 2:可以在配置文件中加入一个mode属性:

module.exports = {
  mode: 'production' // development
};

development模式下,将侧重于功能调试和优化开发体验,包含如下内容:

  1. 浏览器调试工具
  2. 开发阶段的详细错误日志和提示
  3. 快速和优化的增量构建机制

production模式下,将侧重于模块体积优化和线上部署,包含如下内容:

  1. 开启所有的优化代码
  2. 更小的bundle大小
  3. 去除掉只在开发阶段运行的代码
  4. Scope hoisting和Tree-shaking
  5. 自动启用uglifyjs对代码进行压缩

webpack一直以来最饱受诟病的就是其配置门槛极高,配置内容复杂而繁琐,容易让人从入门到放弃,而它的后起之秀如rollup,parcel等均在配置流程上做了极大的优化,做到开箱即用,webpack在V4中应该也从中借鉴了不少经验来提升自身的配置效率,详见内容可以参考这篇文章《webpack 4: mode and optimization》

三、再见commonchunk,你好optimization

从webpack4开始官方移除了commonchunk插件,改用了optimization属性进行更加灵活的配置,这也应该是从V3升级到V4的代码修改过程中最为复杂的一部分,下面的代码即是optimize.splitChunks 中的一些配置参考,

module.exports = {
  optimization: {
    runtimeChunk: {
      name: 'manifest'
    },
    minimizer: true, // [new UglifyJsPlugin({...})]
    splitChunks:{
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      name: false,
      cacheGroups: {
        vendor: {
          name: 'vendor',
          chunks: 'initial',
          priority: -10,
          reuseExistingChunk: false,
          test: /node_modules\/(.*)\.js/
        },
        styles: {
          name: 'styles',
          test: /\.(scss|css)$/,
          chunks: 'all',
          minChunks: 1,
          reuseExistingChunk: true,
          enforce: true
        }
      }
    }
  }
}

从中我们不难发现,其主要变化有如下几个方面:

  1. commonchunk配置项被彻底去掉,之前需要通过配置两次new webpack.optimize.CommonsChunkPlugin来分别获取vendor和manifest的通用chunk方式已经做了整合, 直接在optimization中配置runtimeChunk和splitChunks即可 ,提取功能也更为强大,具体配置见:splitChunks
  2. runtimeChunk可以配置成true,single或者对象,用自动计算当前构建的一些基础chunk信息,类似之前版本中的manifest信息获取方式。
  3. webpack.optimize.UglifyJsPlugin现在也不需要了,只需要使用optimization.minimize为true就行,production mode下面自动为true,当然如果想使用第三方的压缩插件也可以在optimization.minimizer的数组列表中进行配置

四、ExtractTextWebpackPlugin调整,建议选用新的CSS文件提取插件mini-css-extract-plugin

由于webpack4以后对css模块支持的逐步完善和commonchunk插件的移除,在处理css文件提取的计算方式上也做了些调整,之前我们首选使用的extract-text-webpack-plugin也完成了其历史使命,将让位于mini-css-extract-plugin

基本配置如下:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,  // replace ExtractTextPlugin.extract({..})
          "css-loader"
        ]
      }
    ]
  }
}

生产环境下的配置优化:

const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: true 
      }),
      new OptimizeCSSAssetsPlugin({})  // use OptimizeCSSAssetsPlugin
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/app.[name].css',
      chunkFilename: 'css/app.[contenthash:12].css'  // use contenthash *
    })
  ]
  ....
}

将多个css chunk合并成一个css文件

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {            
          name: 'styles',
          test: /\.scss|css$/,
          chunks: 'all',    // merge all the css chunk to one file
          enforce: true
        }
      }
    }
  }
}

五、其他调整项备忘

  1. NoEmitOnErrorsPlugin- > optimization.noEmitOnErrors(默认情况下处于生产模式)
  2. ModuleConcatenationPlugin- > optimization.concatenateModules(默认情况下处于生产模式)
  3. NamedModulesPlugin- > optimization.namedModules(在开发模式下默认开启)
  4. webpack命令优化 -> 发布了独立的 webpack-cli 命令行工具包
  5. webpack-dev-server -> 建议升级到最新版本
  6. html-webpack-plugin -> 建议升级到的最新版本
  7. file-loader -> 建议升级到最新版本
  8. url-loader -> 建议升级到最新版本

六、参考工程

webpack4配置工程实例

七、参阅资料

  1. webpack4
  2. webpack4发布概览
  3. webpack 4: mode and optimization
  4. webpack4新特性介绍
  5. webpack4升级指北
  6. webpack4升级指南以及从webpack3.x迁移
查看原文

赞 235 收藏 321 评论 18

BUG来 收藏了文章 · 2019-03-12

React优化-记忆化技术-使用闭包提升你的React性能

为什么要使用记忆性技术?

使用React开发的时候,我们请求服务器拿回来一个复杂的数据,我们在render里去处理这个数据,但是state和props频繁修改会触发render,每次触发render,数据都要去处理一次,每次处理都是对性能的损耗

举个例子:把大于18岁的人列出来

class Example extends Component {
    ...
    render() {
        const { dataList } = this.props;
        const newDataList = dataList.filter((item) => item.age > 18);
        return (
            <div>
                {newDataList.map((item, i) =>
                    <p key={i}>{item.name}:{item.age}岁</p>
                )}
            </div>
        )
    }
    ...
}

从例子中我们看到render中我们处理数据,但是每次state和props的修改都会触发render,都会去处理数据dataList,生成新的数据newDataList,每次处理都是对性能的损耗!

什么叫记忆性技术?

每次调用函数把你的传参和结果记录下来,遇到相同的传参,就直接返回记录缓存的结果,不用再去调用函数处理数据!

memoize-one官方案例

import memoizeOne from 'memoize-one';

const add = (a, b) => a + b;
const memoizedAdd = memoizeOne(add);

memoizedAdd(1, 2); // 3

memoizedAdd(1, 2); // 3
// Add 函数并没有执行: 前一次执行的结果被返回

memoizedAdd(2, 3); // 5
// Add 函数再次被调用,返回一个新的结果

memoizedAdd(2, 3); // 5
// Add 函数并没有执行: 前一次执行的结果被返回

memoizedAdd(1, 2); // 3
// Add 函数再次被调用,返回一个新的结果

我们可以发现连续两次相同传参,第二次会直接返回上次的结果,每次传参不一样,就直接调用函数返回新的结果,会丢失之前的记录,并不是完全记忆,这也是个不足点!

在React中使用memoize-one

根据上的例子,我们对那个例子进行修改,使用memoize-one提升React的性能

import memoize from "memoize-one";

class Example extends Component {
    ...
    filter = memoize((dataList, age) => dataList.filter((item) => item.age > age))
    render() {
        const { dataList } = this.props;
        const newDataList = this.filter(dataList, 18)
        return (
            <div>
                ...
                {newDataList.map((item, i) =>
                    <p key={i}>{item.name}:{item.age}岁</p>
                )}
                ...
            </div>
        )
    }
    ...
}

memoize-one源码解析

memoize-one是采用闭包来缓存数据的

type EqualityFn = (a: mixed, b: mixed) => boolean;

const simpleIsEqual: EqualityFn = (a: mixed, b: mixed): boolean => a === b;

export default function <ResultFn: (...Array<any>) => mixed>(resultFn: ResultFn, isEqual?: EqualityFn = simpleIsEqual): ResultFn {
  let lastThis: mixed; // 用来缓存上一次result函数对象
  let lastArgs: Array<mixed> = []; // 用来缓存上一次的传参
  let lastResult: mixed; // 用来缓存上一次的结果
  let calledOnce: boolean = false; // 是否之前调用过
  // 判断两次调用的时候的参数是否相等
  // 这里的 `isEqual` 是一个抽象函数,用来判断两个值是否相等
  const isNewArgEqualToLast = (newArg: mixed, index: number): boolean => isEqual(newArg, lastArgs[index]);

  const result = function (...newArgs: Array<mixed>) {
    if (calledOnce &&
      lastThis === this &&
      newArgs.length === lastArgs.length &&
      newArgs.every(isNewArgEqualToLast)) {
      // 返回之前的结果
      return lastResult;
    }

    calledOnce = true; // 标记已经调用过
    lastThis = this; // 重新缓存result对象
    lastArgs = newArgs; // 重新缓存参数
    lastResult = resultFn.apply(this, newArgs); // 重新缓存结果
    return lastResult; // 返回新的结果
  };

  // 返回闭包函数
  return (result: any);
}

关于isEqual函数(memoize-one推荐使用loadsh.isEqual)

一般两个对象比较是否相等,我们不能用===或者==来处理,memoize-one允许用户自定义传入判断是否相等的函数,比如我们可以使用lodash的isEqual来判断两次参数是否相等

import memoizeOne from 'memoize-one';
import deepEqual from 'lodash.isEqual';

const identity = x => x;

const defaultMemoization = memoizeOne(identity);
const customMemoization = memoizeOne(identity, deepEqual);

const result1 = defaultMemoization({foo: 'bar'});
const result2 = defaultMemoization({foo: 'bar'});

result1 === result2 // false - 索引不同

const result3 = customMemoization({foo: 'bar'});
const result4 = customMemoization({foo: 'bar'});

result3 === result4 // true - 参数通过lodash.isEqual判断是相等的

参考

https://github.com/alexreardo...

查看原文

BUG来 关注了专栏 · 2019-01-24

前端进击的巨人

前端进阶系列,从小白进阶大神

关注 3959

BUG来 赞了文章 · 2019-01-24

前端进击的巨人(五):学会函数柯里化(curry)

前端进击的巨人(五):学会函数柯里化(curry)

柯里化(Curring, 以逻辑学家Haskell Curry命名)

写在开头

柯里化理解的基础来源于我们前几篇文章构建的知识,如果还未能掌握闭包,建议回阅前文。

代码例子会用到 apply/call ,一般用来实现对象冒充,例如字符串冒充数组对象,让字符串拥有数组的方法。待对象讲解篇会细分解析。在此先了解,两者功能相同,区别在于参数传递方式的不同, apply 参数以数组方式传递,call 多个参数则是逗号隔开。

apply(context, [arguments]);
call(context, arg1, arg2, arg3, ....);

代码例子中使用到了ES6语法,对ES6还不熟悉的话,可学习社区这篇文章:《30分钟掌握ES6/ES2015核心内容(上)》


函数柯里化

函数柯里化在JavaScript中其实是高阶函数的一种应用,上篇文章我们简略介绍了高阶函数(可以作为参数传递,或作为返回值)。

理论知识太枯燥,来个生活小例子,"存款买房"(富二代绕道)。假设买房是我们存钱的终极目标。那么在买房前,存在卡里的钱(老婆本)就不能动。等到够钱买房了,钱从银行卡取出来,开始买买买。。。

函数柯里化就像我们往卡里存钱,存够了,才能执行买房操作,存不够,接着存。

函数柯里化公式

先上几个公式(左边是普通函数,右边就是转化后柯里化函数支持的调用方式):

// 公式类型一
fn(a,b,c,d) => fn(a)(b)(c)(d);
fn(a,b,c,d) => fn(a, b)(c)(d);
fn(a,b,c,d) => fn(a)(b,c,d);

// 公式类型二
fn(a,b,c,d) => fn(a)(b)(c)(d)();
fn(a,b,c,d) => fn(a);fn(b);fn(c);fn(d);fn();

两种公式类型的区别 —— 函数触发执行的机制不同:

  • 公式一当传入参数等于函数参数数量时开始执行
  • 公式二当没有参数传入时(且参数数量满足)开始执行

通过公式,我们先来理解这行代码 fn(a)(b)(c)(d), 执行 fn(a) 时返回的是一个函数,并且支持传参。何时返回目标函数结果值而不是函数的触发机制,控制权在我们手里,我们可以为函数制定不同的触发机制。

普通的函数调用,一次性传入参数就执行。而通过柯里化,它可以帮我们实现函数部分参数传入执行(并未立即执行原始函数,钱没存够接着存),这就是函数柯里化的特点:"延迟执行和部分求值"

"函数柯里化:指封装一个函数,接收原始函数作为参数传入,并返回一个能够接收并处理剩余参数的函数"

函数柯里化的例子

// 等待我们柯里化实现的方法add
function add(a, b, c, d) {
    return a + b + c + d;
};
// 最简单地实现函数add的柯里化
// 有点low,有助于理解
function add(a, b, c, d) {
    return function(a) {
        return function(b) {
            return function(c) {
                return a + b + c + d;
            }
        }
    }
}

分析代码知识点:

  1. 函数作为返回值返回,闭包形成,外部环境可访问函数内部作用域
  2. 子函数可访问父函数的作用域,作用域由内而外的作用域链查找规则,作用域嵌套形成
  3. 在函数参数数量不满足时,返回一个函数(该函数可接收并处理剩余参数)
  4. 当函数数量满足我们的触发机制(可自由制定),触发原始函数执行

前几篇文章的知识点此时刚好。可见基础知识的重要性,高阶的东西始终要靠小砖头堆砌出来。

弄清原理后,接下来就是将代码写得更通用些(高大上些)。

// 公式类型一: 参数数量满足函数参数要求,触发执行
// fn(a,b,c,d) => fn(a)(b)(c)(d);

const createCurry = (fn, ...args) => {
    let _args = args || [];
    let length = fn.length; // fn.length代码函数参数数量

    return (...rest) => {
        let _allArgs = _args.slice(0);  
        // 深拷贝闭包共用对象_args,避免后续操作影响(引用类型)
        _allArgs.push(...rest);
        if (_allArgs.length < length) {
            // 参数数量不满足原始函数数量,返回curry函数
            return createCurry.call(this, fn, ..._allArgs);
        } else {
            // 参数数量满足原始函数数量,触发执行
            return fn.apply(this, _allArgs);
        }
    }
}

const curryAdd = createCurry(add, 2);
let sum = curryAdd(3)(4)(5);    // 14

// ES5写法
function createCurry() {
    var fn = arguments[0];
    var _args = [].slice.call(arguments, 1);
    var length = fn.length;
    
    return function() {
        var _allArgs = _args.slice(0);
        _allArgs = _allArgs.concat([].slice.call(arguments));
        if (_allArgs.length < length) {
            _allArgs.unshift(fn);
            return createCurry.apply(this, _allArgs);
        } else {
            return fn.apply(this, _allArgs);
        }
    }
}
// 公式类型二: 无参数传入时并且参数数量已经满足函数要求
// fn(a, b, c, d) => fn(a)(b)(c)(d)();
// fn(a, b, c, d) => fn(a);fn(b);fn(c);fn(d);fn();

const createCurry = (fn, ...args) => {
    let all = args || [];
    let length = fn.length;

    return (...rest) => {
        let _allArgs = all.slice(0);
        _allArgs.push(...rest);
        if (rest.length > 0 || _allArgs.length < length) {
            // 调用时参数不为空或存储的参数不满足原始函数参数数量时,返回curry函数
            return createCurry.call(this, fn, ..._allArgs);
        } else {
            // 调用参数为空(),且参数数量满足时,触发执行
            return fn.apply(this, _allArgs);
        }
    }
}
const curryAdd = createCurry(add, 2);
let sum = curryAdd(3)(4)(5)();  // 14

// ES5写法
function createCurry() {
    var fn = arguments[0];
    var _args = [].slice.call(arguments, 1);
    var length = fn.length;
    
    return function() {
        var _allArgs = _args.slice(0);
        _allArgs = _allArgs.concat([].slice.call(arguments));
        if (arguments.length > 0 || _allArgs.length < length) {
            _allArgs.unshift(fn);
            return createCurry.apply(this, _allArgs);
        } else {
            return fn.apply(this, _allArgs);
        }
    }
}

为实现公式中不同的两种调用公式,两个createCurry方法制定了两种不同的触发机制。记住一个点,函数触发机制可根据需求自行制定。

偏函数与柯里化的区别

先上个公式看对比:

// 函数柯里化:参数数量完整
fn(a,b,c,d) => fn(a)(b)(c)(d);
fn(a,b,c,d) => fn(a,b)(c)(d);

// 偏函数:只执行了部分参数
fn(a,b,c,d) => fn(a);
fn(a,b,c,d) => fn(a, b);

"函数柯里化中,当你传入部分参数时,返回的并不是原始函数的执行结果,而是一个可以继续支持后续参数的函数。而偏函数的调用方式更像是普通函数的调用方式,只调用一次,它通过原始函数内部来实现不定参数的支持。"

如果已经看懂上述柯里化的代码例子,那么改写支持偏函数的代码,并不难。

// 公式:
// fn(a, b, c, d) => fn(a);
// fn(a, b, c, d) => fn(a,b,c);

const partialAdd = (a = 0, b = 0, c = 0, d = 0) => {
    return a + b + c +d;
}

partialAdd(6);      // 6
partialAdd(2, 3);   // 5

使用ES6函数参数默认值,为没有传入参数,指定默认值为0,支持无参数或不定参数传入。

柯里化的特点:

  1. 参数复用(固定易变因素)
  2. 延迟执行
  3. 提前返回

柯里化的缺点

柯里化是牺牲了部分性能来实现的,可能带来的性能损耗:

  1. 存取 arguments 对象要比存取命名参数要慢一些
  2. 老版本浏览器在 arguments.lengths 的实现相当慢(新版本浏览器忽略)
  3. fn.apply()fn.call() 要比直接调用 fn()
  4. 大量嵌套的作用域和闭包会带来开销,影响内存占用和作用域链查找速度

柯里化的应用

  • 利用柯里化制定约束条件,管控触发机制
  • 处理浏览器兼容(参数复用实现一次性判断)
  • 函数节流防抖(延迟执行)
  • ES5前bind方法的实现

一个应用例子:浏览器事件绑定的兼容处理

// 普通事件绑定函数
var addEvent = function(ele, type, fn, isCapture) {
    if(window.addEventListener) {
        ele.addEventListener(type, fn, isCapture)
    } else if(window.attachEvent) {

        ele.attachEvent("on" + type, fn)
    }
}
// 弊端:每次调用addEvent都会进行判断

// 柯里化事件绑定函数
var addEvent = (function() {
    if(window.addEventListener) {
        return function(ele, type, fn, isCapture) {
            ele.addEventListener(type, fn, isCapture)
        }
    } else if(window.attachEvent) {
        return function(ele, type, fn) {
             ele.attachEvent("on" + type, fn)
        }
    }
})()
// 优势:判断只执行一次,通过闭包保留了父级作用域的判断结果

秒懂反柯里化

先上公式,从来没有这么喜欢写公式,简明易懂。

// 反柯里化公式:
curryFn(a)(b)(c)(d) = fn(a, b, c, d);
curryFn(a) = fn(a);

看完公式,是不是似曾相识,这不就是我们日常敲码的普通函数么?没错的,函数柯里化就是把普通函数变成成一个复杂的函数,而反柯里化其就是柯里化的逆反,把复杂变得简单。

函数柯里化是把支持多个参数的函数变成接收单一参数的函数,并返回一个函数能接收处理剩余参数:fn(a,b,c,d) => fn(a)(b)(c)(d),而反柯里化就是把参数全部释放出来:fn(a)(b)(c)(d) => fn(a,b,c,d)

// 反柯里化:最简单的反柯里化(普通函数)
function add(a, b, c, d) {
    return a + b + c + d;
}

反思:为何要使用柯里化

函数柯里化是函数编程中的一个重要的基础,它为我们提供了一种编程的思维方式。显然,它让我们的函数处理变得复杂,代码调用方式并不直观,还加入了闭包,多层作用域嵌套,会有一些性能上的影响。

但在一些复杂的业务逻辑封装中,函数柯里化能够为我们提供更好的应对方案,让我们的函数更具自由度和灵活性。

实际开发中,如果你的逻辑处理相对复杂,不妨换个思维,用函数柯里化来实现,技能包不嫌多。
说到底,程序员就是解决问题的那群人。


写在结尾

本篇函数柯里化知识点的理解确实存在难度,暂时跳过这章也无妨,可以先了解再深入。耐得主寂寞的小伙伴回头多啃几遍,没准春季面试就遇到了。


参考文档:

系列更文请关注专栏:《前端进击的巨人》,不断更新中。。。

本文首发Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。
查看原文

赞 75 收藏 51 评论 8

认证与成就

  • 获得 32 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-09
个人主页被 402 人浏览