柯里化的应用

  • 源码地址: src\platforms\web**(****weex****)**\runtime\patch.js
  • 描述:使用柯里化技巧缓存平台方法

源码

[web 平台]


1
2
3
4
5
6
7
8
9
import * as nodeOps from 'web/runtime/node-ops'  
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patchFunction = createPatchFunction({ nodeOps, modules })

[weex 平台]


1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as nodeOps from 'weex/runtime/node-ops'  
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'weex/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patchFunction = createPatchFunction({
  nodeOps,
  modules,
  LONG_LIST_THRESHOLD10
})

解析

[解决了什么问题]


createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.__patch__

由于 patch 是平台相关的,在 Web 和 Weex 环境,它们把虚拟 DOM 映射到 “平台 DOM”的方法是不同的,并且对“DOM”包括的属性和模块创建和更新也不完全相同。因此每个平台都有各自的 nodeOpsmodules, 它们的代码需要托管在 src/platforms 这个大目录下。

而不同平台的 patch 的主要逻辑部分是相同的,所以这部分公共代码托管在 core 这个大目录下。差异化部分只需要通过参数来区别,这里就用到了函数柯里化的技巧,通过 createPatchFunction 把差异化参数提前固化,这样不用每次都调用 patch 的时候都传递 nodeOpsmodules 了。

在这里,nodeOps 表示调 “平台 DOM”的一些操作方法,modules 表示平台的一些模块,他们会在整个 patch 过程的不同阶段执行响应的钩子函数。

柯里化(Currying)

柯里化是一种关于函数的高阶技术,他不仅被用于 Javascript,还被用于其他编程语言。

柯里化一种函数的转换,他是指讲一个函数从可调用的 f(a, b, c) 转换成可调用的 f(a)(b)(c)

柯里化不会调用函数,他只是对函数进行转换。

[示例]


柯里化的简单实现

创建一个辅助函数 curry(f) ,该函数将对两个参数的函数 f 执行柯里化。

换句话说,对于两个参数的函数 f(a, b) 执行 curry(f) 会将其转换为 f(a)(b) 形式的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// curry(f) 执行柯里化转换  
function curry(f) { 
    return function(a) {
        return function(b) {
            return f(a,b);
        }
    }
}

// 使用
function sum(a, b){
  return a + b
}

const curriedSum = curry(sum);

console.log(curriedSum(1)(2)); //-> 3

如上面代码所示,柯里化的实现非常简单: 只有两个包装器(wrapper)

  • curry(func) 的结果就是一个包装器 function(a)

  • 当他被向 curriedSum(1) 这样调用时,他的参数会被保存在词法环境中,然后返回一个新的包装器 function(b)。

  • 然后这个包装器被以 2 为参数调用,并且,它将该调用传递给原始的 sum 函数。

柯里化更高级的实现

如 loadsh 库的 _.curry, 会返回一个包装器,改包装器允许函数被正常调用或者以偏函数(partial)的方式调用

1
2
3
4
5
6
7
8
9
function sum(a,b){  
    return a + b;
}

// loadsh 库的 _.curry
let curriedSum = _.curry(sum);

console.log(curriedSum(1,2)); //-> 3, 仍可正常使用
console.log(curriedSum(1)(2)); //-> 3, 以偏函数的方式调用

[柯里化的目的]


例如,我们有一个用于格式化和输出信息的日志(logging)函数 log(date, importance, message)。

在实际项目中,此类函数具有很多有用的功能, 例如通过网络发送日志(log)。

1
2
3
4
5
function log(date, importance, message){  
  const dateTime = `${date.getHours()}:${date.getMinutes()}`;
  // 此处用 alert 代替发送日志请求
  alert(`[${dateTime}] [${importance}${message}`);
}

将 log 方法进行柯里化:

1
log = _.curry(log);

柯里化后,log 仍正常允许:

1
log(new Date(), "DEBUG", "some debug"); //-> log(a, b, c)

同时也可以以柯里化的形式运行:

1
log(new Date())("DEBUG")("some debug"); //-> log(a)(b)(c)

现在可以轻松为当前日志创建便捷函数:

1
2
3
4
5
// logNow 会是带有固定第一个参数的日志偏函数  
let logNow = log(new Date());

// 使用它
logNow("INFO","message"); // [HH:mm] INFO message

现在,logNow 是具有一个固定第一个参数的 log。

换句话说,就是更简短的 “偏应用函数(partially applied function)”或 “偏函数(partial)”

我们可以更进一步,为当前的调试日志(debug log)提供便捷函数:

1
2
3
let debugNow = logNow("DEBUG");  

debugNow("message"); //-> [HH:mm] DEBUG message

所以:

  1. 柯里化之后,我们没有丢失任何定西:log 依然可以被正常调用
  2. 我们可以轻松地生成偏函数,例如用于生成今天的日志的偏函数。

[高级柯里化实现]


高级实现

1
2
3
4
5
6
7
8
9
10
11
function curry(func) {  
 return function curried(...args){
     if(args.length >= func.length){
      return func.apply(this, args);
    } else {
      return function(...args2){
          return curried.apply(this,args.concat(args2));
      }
    }
 }
}

用例

下面是用于多参数函数的“高级”柯里化实现:

1
2
3
4
5
6
7
8
9
function sum(a, b, c){  
    return a + b + c;
}

let curriedSum = curry(sum);

alertcurriedSum(1,2,3) ); //-> 6,仍可以被正常调用
alertcurriedSum(1)(2,3) ); //-> 6,对第一个参数的柯里化
alertcurriedSum(1)(2)(3) ); //-> 6,全柯里化

解析

curry(func) 调用的结果是如下所示的包装器 curried:

1
2
3
4
5
6
7
8
9
10
11
// func 是要转换的函数  
function curried(...args){
  // func.length 可以获取形参个数
  if(args.length >= func.length) { // (1)
    return func.apply(this, args);
  } else {
      return function(...args2){
      return curried.apply(this, args.concat(args2)); 
    }
  }
}

当我们运行它时, 这里有两个 if 执行分支:

  1. 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,那么只需要使用 func.apply 将调用传递给他即可。

  2. 否则,获取一个偏函数:我们目前还没调用 func。取而代之的是返回一个包装器 pass,它将重新应用 curried, 将之传入的参数与新的参数一起传入

  3. 最后,我们再次调用它,我们将得到一个新的偏函数(如何没有足够的参数),或者最终的结果

注:

  1. 只允许确定参数长度的函数

    1. 柯里化要求函数具有固定数量的参数

    2. 使用 rest 参数的函数,例如 f(…args), 不能以这种方式进行柯里化。

  2. 比柯里化多一点

    1. 根据定义,柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)。

    2. 但是如前所述,JavaScript 中大多数的柯里化实现都是高级版:他们使得函数可以被多参变体

[总结]


柯里化 是一种转换,将 f(a, b, c) 转换为可以被 f(a)(b)(c) 的形式进行调用。

JavaScript 实现通常都改保持该函数可以被正常调用,并且如果参数不足,则返回偏函数。

柯里化 可以让我们和能够耿荣的获取偏函数。

如日志记录示例:普通函数 log(date, importance, message)在被柯里化之后,当我们调用它的时候传入一个参数(如 log(date))或两个参数(log(date, importance))时,他会返回偏函数。..


柯里化的应用
https://blog.pangcy.cn/2022/04/28/前端编程相关/前端框架与库/Vue/Vue2 源码分析/柯里化的应用/
作者
子洋
发布于
2022年4月28日
许可协议