vue源码中的柯里化

2,977 阅读3分钟
先学习一波基础知识

柯里化概念:在计算机科学中,柯里化(英語:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

通俗的解释:柯里化就是一个函数原本有多个参数,只传入一个参数,生成一个新函数,由新函数接收剩下的参数来运行得到结果,这里重点是一个参数。

柯里化有什么作用?

参数复用,或者说是固定参数,避免重复传参;
提前返回,或者说是提前确认,避免重复判断;
延迟执行。
而这三点怎么通俗的解释呢,只能通过例子。
1.假如我们有个需求,如何判断一个标签是否为原生的html标签,第一反应就是暴力循环一波,可以写出以下代码
  var str = 'html,body,base,head,link,meta,style,title,' +
      'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
      'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
      'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
      's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
      'embed,object,param,source,canvas,script,noscript,del,ins,' +
      'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
      'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
      'output,progress,select,textarea,' +
      'details,dialog,menu,menuitem,summary,' +
      'content,element,shadow,template,blockquote,iframe,tfoot';
  function isHTMLTag ( tag, str ) {
      var list = str.split(',');
      var flag =false;
      for (var i = 0; i < list.length; i++) {
          if(list[i] == tag){
              flag = true;
              break;
          }
      }
      return flag;
  }
  var isHTMLTag = isHTMLTag('div',str);

这样写本来没有问题,但是假如我们有100个标签需要判断,那这个函数就要调100次,本来函数里面都是for循环,我们循环就是(100*原生标签的个数)次

接下来我们看vue中是如果巧妙的实现这个功能的

  /**
   * Make a map and return a function for checking if a key
   * is in that map.
    */
    function makeMap ( str, expectsLowerCase ) {
        var map = Object.create(null);
        var list = str.split(',');
        for (var i = 0; i < list.length; i++) {
            map[list[i]] = true;
        }
        return expectsLowerCase
            ? function (val) { return map[val.toLowerCase()]; }
            : function (val) { return map[val]; }
    }
    var isHTMLTag = makeMap(
        'html,body,base,head,link,meta,style,title,' +
        'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
        'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
        'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
        's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
        'embed,object,param,source,canvas,script,noscript,del,ins,' +
        'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
        'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
        'output,progress,select,textarea,' +
        'details,dialog,menu,menuitem,summary,' +
        'content,element,shadow,template,blockquote,iframe,tfoot'
    );
    var isHTMLTag = isHTMLTag('div');

这样做的好处是这下我们假如要判断100个标签,只需要执行makemap时循环执行一次,后面再执行isHTMLTag就不会去再走循环,这里map中存了我们需要的结果,我们只需要根据map的键去检索,多余的开销就是创建了map这一个对象,典型的空间换时间理念。 这里就是提前确认,避免重复判断这个作用

2.vue在生命周期的声明为如下代码
  const createHook = function (lifecycle) {
        return function (hook, target = currentInstance) {
          injectHook(lifecycle, hook, target)
        }
      }
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)

这里的createhook函数无非就是把injecthook函数返回出来,为什么要多此一举呢?那我们可以像下面这么写吗?

 export const onBeforeMount = function (hook, target = currentInstance) {
        injectHook(LifecycleHooks.BEFORE_MOUNT, hook, target)
 }
 export const onMounted = function (hook, target = currentInstance) {
        injectHook(LifecycleHooks.MOUNTED, hook, target)
 }

当然可以,但是这时我们这时发现我们这样定义的函数只是injectHook的第一个参数不同,我们这么写每次都要要多写 hook, target这两个重复参数,就显得很冗余,而createHook函数就巧妙的解决了我们的问题。这里就是参数复用,或者说是固定参数,避免重复传参的作用。

3.如果写过vue3的话都应该写过以下代码
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

这结构一看,我立马盲猜createApp函数的源码

const createApp = function(root){
 return function mount(container){
     ////balabla
 }
}

然后跑去扒源码

const createApp = ((...args) => {
  // 创建 app 对象
  const app = ensureRenderer().createApp(...args)
  const { mount } = app
  // 重写 mount 方法,为了跨平台
  app.mount = (containerOrSelector) => {
    // ...
  }
  return app
})

这,这不太像我想的啊,别急继续往下看,那我继续把想的我的代码改改

const createApp = function(root){
 const app={
     mount(){//}
 }
 app.mount=(containerOrSelector) => {
    // ...
 }
  return app
}

只不过源码这里的app不是这么一个简单只有一个方法的对象

// 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
const rendererOptions = {
  patchProp,
  ...nodeOps
}
let renderer
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
function ensureRenderer() {
  return renderer || (renderer = createRenderer(rendererOptions))
}
function createRenderer(options) {
  return baseCreateRenderer(options)
}
function baseCreateRenderer(options) {
  function render(vnode, container) {
    // 组件渲染的核心逻辑
  }

  return {
    render,
    createApp: createAppAPI(render)
  }
}
function createAppAPI(render) {
  // createApp createApp 方法接受的两个参数:根组件的对象和 prop
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _component: rootComponent,
      _props: rootProps,
      //基础的mount方法
      mount(rootContainer) {
        // 创建根组件的 vnode
        const vnode = createVNode(rootComponent, rootProps)
        // 利用渲染器渲染 vnode
        render(vnode, rootContainer)
        app._container = rootContainer
        return vnode.component.proxy
      }
    }
    return app
  }
}

这一通操作下来其实就是我们开始想的 ensureRenderer().createApp(...args)返回一个对象,对象中有mount方法,就是最后这段代码

    const app = {
      _component: rootComponent,
      _props: rootProps,
      mount(rootContainer) {
        // 创建根组件的 vnode
        const vnode = createVNode(rootComponent, rootProps)
        // 利用渲染器渲染 vnode
        render(vnode, rootContainer)
        app._container = rootContainer
        return vnode.component.proxy
      }
    }

这段代码可谓是把柯里化用到了极致,函数套函数,return一层又一层,目的无非是为了tree-shaking,有的为了跨平台,有的是为了参数复用。做到不同的判断逐渐传入参数,而不是一次性把参数传完,又不断地if else.