【源码共读】Vue2源码 shared 模块中的36个实用工具函数分析

·  阅读 791

我正在参与掘金会员专属活动-源码共读第一期,点击参与

今天带来的是vue2源码中的shared模块,主要是针对util.ts文件下的几十个实用工具函数,这些函数在vue2源码中被广泛使用,我们可以通过这些函数来了解vue2源码的一些实现细节。

废话不多说,直接上仓库地址:

看到源码发现都是.ts结尾的,看不明白的小伙伴可以使用tsc xxx.ts命令编译成.js文件,然后再看。

我下面会将这些函数都装换为js的代码,方便大家理解和阅读。

如果遇到ts的代码看不懂,我这里教大家一个方法,简单直接粗暴,直接删除:后面的东西就好了,比如:

image.png

删掉就是下面的样子:

export function isFunction(value) {
    return typeof value === 'function'
}
复制代码

这种方式能应对大多数情况,还有一些情况,比如泛型,可以直接删除<xxx>,比如:

var a: <T>(a: T) => T = function<E>(a: E): E {
    return a
}
复制代码

删除<T>:后面的东西后:

var a = function(a) {
    return a
}
复制代码

还有很多关键字,如果这些你都了解我觉得你已经可以看懂ts,就不需要我这里的方法了。

所有的工具函数

我先将所有的工具函数都罗列出来,让大家先有个整体的印象,然后再逐个分析。

  • emptyObject: 被冻结的空对象
  • isArray: es6的Array.isArray方法
  • isUndef: 判断是否是undefined或者null
  • isDef: 判断是否不是undefined并且不是null
  • isTrue: 判断是否是true
  • isFalse: 判断是否是false
  • isPrimitive: 判断是否是原始类型
  • isFunction: 判断是否是函数
  • isObject: 判断是否是对象
  • toRawType: 获取对象的原始类型
  • isPlainObject: 判断是否是纯对象
  • isRegExp: 判断是否是正则
  • isValidArrayIndex: 判断是否是有效的数组索引
  • isPromise: 判断是否是Promise对象
  • toString: 将值转换为实际呈现的字符串
  • toNumber: 将值转换为数字
  • makeMap: 生成一个包含指定字符串的对象
  • isBuiltInTag: 判断是否是内置标签
  • isReservedAttribute: 判断是否是保留属性
  • remove: 从数组中移除指定项
  • hasOwn: 判断对象是否有指定属性
  • cached: 缓存函数
  • camelize: 将字符串转换为驼峰格式
  • capitalize: 将字符串首字母大写
  • hyphenate: 将字符串转换为连字符格式
  • bind: 将函数绑定到指定的上下文
  • toArray: 将类数组转换为数组
  • extend: 将源对象的属性拷贝到目标对象
  • toObject: 将数组转换为对象
  • noop: 空函数
  • no: 返回false
  • identity: 返回传入的值
  • genStaticKeys: 生成静态键
  • looseEqual: 比较两个值是否相等
  • looseIndexOf: 获取指定值在数组中的索引
  • once: 保证函数只执行一次
  • hasChanged: 判断两个值是否不相等

一共是36个函数,下面开始逐个分析。

emptyObject

export const emptyObject = Object.freeze({})
复制代码

使用Object.freeze冻结一个空对象,这样就可以保证这个对象不会被修改。

简单的提一下Object.freeze的用法,它可以冻结一个对象,冻结后的对象不可扩展,也就是说不能再添加新的属性,也不能删除已有属性,已有属性的值也不能被修改,同时也不能修改已有属性的gettersetter方法。

我这里就不详讲了,这个API的特性都可以写一篇文章了,大家可以自行查阅。

扩展阅读:Object.freeze()

isArray

export const isArray = Array.isArray
复制代码

这个函数就是es6Array.isArray方法,用来判断一个值是否是数组。

扩展阅读:Array.isArray()

isUndef

export function isUndef(v) {
    return v === undefined || v === null
}
复制代码

判断一个值是否是undefined或者null,这里使用了严格相等,在源码上面有注释,这样显示定义,有助于js引擎进行优化。

isDef

export function isDef(v) {
    return v !== undefined && v !== null
}
复制代码

判断一个值是否不是undefined或者null,正好和isUndef相反。

isTrue

export function isTrue(v) {
    return v === true
}
复制代码

判断一个值是否是true

isFalse

export function isFalse(v) {
    return v === false
}
复制代码

判断一个值是否是false

isPrimitive

export function isPrimitive(value) {
    return (
        typeof value === 'string' ||
        typeof value === 'number' ||
        // $flow-disable-line
        typeof value === 'symbol' ||
        typeof value === 'boolean'
    )
}
复制代码

判断一个值是否是原始类型,原始类型包括stringnumbersymbolboolean;

扩展阅读:JavaScript 原始类型

isFunction

export function isFunction(value) {
    return typeof value === 'function'
}
复制代码

判断一个值是否是函数。

isObject

export function isObject(obj) {
    return obj !== null && typeof obj === 'object'
}
复制代码

判断一个值是否是对象,这里先判断是否是null,因为null的类型是object

扩展阅读:typeof

toRawType

const _toString = Object.prototype.toString

export function toRawType(value) {
    return _toString.call(value).slice(8, -1)
}
复制代码

获取一个指的原生类型,这里使用了Object.prototype.toString,这个方法可以返回一个对象的原生类型;

比如[object Object],然后使用slice截取[objectObject]之间的字符串,就是原生类型。

扩展阅读:Object.prototype.toString()

isPlainObject

export function isPlainObject(obj) {
    return _toString.call(obj) === '[object Object]'
}
复制代码

严格检查一个值是否是纯JavaScript对象,这里还是使用上面获取的_toString方法。

isRegExp

export function isRegExp(v) {
    return _toString.call(v) === '[object RegExp]'
}
复制代码

判断一个值是否是正则表达式,逻辑和上面相同。

isValidArrayIndex

export function isValidArrayIndex(val) {
    const n = parseFloat(String(val))
    return n >= 0 && Math.floor(n) === n && isFinite(val)
}
复制代码

检查传入的值是否是一个有效的数组索引:

  1. 先将值做String转换
  2. 然后使用parseFloat转换为数字,这里使用parseFloat而不是parseInt,因为parseInt会将3.14转换为3,而parseFloat会保留小数点后面的值;
  3. 判断值是否大于等于0,因为数组索引不能为负数;
  4. 判断值是否是一个整数,这里使用Math.floor向下取整,然后和原值做严格相等,因为3.143是不相等的;
  5. 判断值是否是有限的

扩展阅读:

isPromise

export function isPromise(val) {
    return (
        isDef(val) &&
        typeof val.then === 'function' &&
        typeof val.catch === 'function'
    )
}
复制代码

判断一个值是否是Promise对象,首先使用上面定义的isDef方法判断是否是undefined或者null;

然后判断thencatch是否是函数,因为Promise对象必须有thencatch方法。

尝试了一下使用一个对象里面有thencatch方法的对象,返回结果是true,所以还是约定大于规范。

扩展阅读:Promise

toString

function toString(val) {
    return val == null
        ? ''
        : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
            ? JSON.stringify(val, null, 2)
            : String(val)
}
复制代码

将一个值转化为一个可阅读的字符串,拆分一下:

  1. 如果值是null或者undefined,返回空字符串;
  2. 如果值是数组或者是一个纯对象,且没有重写toString方法,使用JSON.stringify转换为字符串,这里使用JSON.stringify的第二个参数为null,第三个参数为2,表示缩进为2个空格;
  3. 如果都不是,使用String转换为字符串。

扩展阅读:JSON.stringify()

toNumber

function toNumber(val) {
    const n = parseFloat(val)
    return isNaN(n) ? val : n
}
复制代码

将一个值转换为数字,如果转换失败,返回原值。

扩展阅读:isNaN()

makeMap

export function makeMap(str, expectsLowerCase) {
    const map = Object.create(null)
    const list = str.split(',')
    for (let i = 0; i < list.length; i++) {
        map[list[i]] = true
    }
    return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val]
}
复制代码

将一个字符串转换为一个对象,这个对象的属性值都是true

最后返回一个函数,这个函数接收一个值,然后判断这个值是否是对象的属性。

这里第二个值影响返回的函数的行为,如果为true,则会将传入的值转换为小写,然后返回map对象的属性值。

扩展阅读:toLowerCase()

isBuiltInTag

export const isBuiltInTag = makeMap('slot,component', true)
复制代码

判断一个标签是否是内置标签,这里使用makeMap方法将slot,component转换为一个对象,然后返回一个函数;

这个函数将接收一个值,然后然后就可以通过返回值判断这个值是否是内置标签。

可以看到源码中一共有4处用到了这个方法:

image.png

isReservedAttribute

export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
复制代码

判断一个属性是否是保留属性,同上面的isBuiltInTag方法相同。

这个只有两处使用:

image.png

remove

export function remove(arr, item) {
    const len = arr.length
    if (len) {
        // fast path for the only / last item
        if (item === arr[len - 1]) {
            arr.length = len - 1
            return
        }
        const index = arr.indexOf(item)
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}
复制代码

从数组中移除一个元素,这里先将数组的长度赋值给len,如果频繁使用obj.field的形式访问对象的属性,会比较耗性能;

然后使用隐式类型装换的方式做判断,0 == falsetrue

然后对比数组的最后一个元素和传入的元素是否相等,如果相等,直接将数组的长度减一,源码中有注释,这是一个快速路径;

最后使用indexOf方法找到元素的索引,然后使用splice方法移除这个元素。

扩展阅读:splice()

hasOwn

export const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn(obj, key) {
    return hasOwnProperty.call(obj, key)
}
复制代码

判断一个对象是否有某个属性,这里使用hasOwnProperty方法,这个方法是Object的原型方法;

然后使用call方法改变this指向,将obj作为this传入,判断obj是否有key属性。

扩展阅读:hasOwnProperty()

cached

export function cached(fn) {
    const cache = Object.create(null)
    return function cachedFn(str: string) {
        const hit = cache[str]
        return hit || (cache[str] = fn(str))
    }
}
复制代码

缓存函数,这里使用Object.create(null)创建一个空对象,然后返回一个函数;

这个函数接收一个字符串,然后判断这个字符串是否在缓存对象中;

这里使用管道符||,如果hit存在,直接返回hit,否则将fn(str)的结果赋值给cache[str],然后返回cache[str]

小知识variable = value是有返回值的,返回值就是value

camelize

const camelizeRE = /-(\w)/g
export const camelize = cached((str) => {
    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})
复制代码

将字符串转换为驼峰命名,这里使用了正则表达式,-(\w)匹配-后面的一个字符,然后使用replace方法替换;

同时这里还使用到了上面的cached方法,用于性能优化。

扩展阅读:replace()

capitalize

export const capitalize = cached((str) => {
    return str.charAt(0).toUpperCase() + str.slice(1)
})
复制代码

将字符串的首字母大写,这里使用charAt方法获取字符串的第一个字符,然后使用toUpperCase方法将其转换为大写,然后使用slice方法截取字符串的第二个字符到最后一个字符,然后拼接起来。

这里也使用了cached方法,用于性能优化。

扩展阅读:charAt()

hyphenate

const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str) => {
    return str.replace(hyphenateRE, '-$1').toLowerCase()
})
复制代码

将驼峰命名转换为连字符命名,这里使用了正则表达式,\B匹配非单词边界,([A-Z])匹配大写字母,然后使用replace方法替换;

这里也使用到了上面的cached方法;

bind

/**
 * Simple bind polyfill for environments that do not support it,
 * e.g., PhantomJS 1.x. Technically, we don't need this anymore
 * since native bind is now performant enough in most browsers.
 * But removing it would mean breaking code that was able to run in
 * PhantomJS 1.x, so this must be kept for backward compatibility.
 */

/* istanbul ignore next */
function polyfillBind(fn, ctx) {
    function boundFn(a) {
        const l = arguments.length
        return l
            ? l > 1
                ? fn.apply(ctx, arguments)
                : fn.call(ctx, a)
            : fn.call(ctx)
    }

    boundFn._length = fn.length
    return boundFn
}

function nativeBind(fn, ctx) {
    return fn.bind(ctx)
}

// @ts-expect-error bind cannot be `undefined`
export const bind = Function.prototype.bind ? nativeBind : polyfillBind
复制代码

这里是先定义了一个polyfillBind函数,然后定义了一个nativeBind函数,看名字就知道,polyfillBind是用来兼容不支持bind方法的浏览器,nativeBind是直接使用bind方法;

这里的重点是polyfillBind函数,里面定义了一个boundFn函数,这个函数会依据参数的个数来调用fnapply或者call方法;

扩展阅读:

toArray

export function toArray(list, start) {
    start = start || 0
    let i = list.length - start
    const ret = new Array(i)
    while (i--) {
        ret[i] = list[i + start]
    }
    return ret
}
复制代码

将类数组转换为数组,这里使用了while循环,将list中的元素依次添加到ret中;

es6中有Array.from方法,可以将类数组转换为数组,扩展阅读:Array.from()

extend

export function extend(to, _from) {
    for (const key in _from) {
        to[key] = _from[key]
    }
    return to
}
复制代码

将属性合并到目标对象中,也就是将_from中的属性合并到to中;

这里使用了for...in循环,扩展阅读:for...in

toObject

export function toObject(arr) {
    const res = {}
    for (let i = 0; i < arr.length; i++) {
        if (arr[i]) {
            extend(res, arr[i])
        }
    }
    return res
}
复制代码

将对象数组合并为一个对象,这里使用了for循环,然后调用了上面的extend方法;

es6中有Object.assign方法,可以将对象合并为一个对象,扩展阅读:Object.assign()

noop

export function noop(a, b, c) {}
复制代码

空函数,什么都不做,通常用于兜底函数;

no

export const no = (a, b, c) => false
复制代码

返回false的函数,通常用于兜底函数;

identity

export const identity = _ => _
复制代码

返回传入的参数的函数,通常用于兜底函数;

genStaticKeys

/**
 * @param modules Array<{ staticKeys?: string[] }
 * @return {*}
 */
export function genStaticKeys(modules) {
    return modules.reduce((keys, m) => {
        return keys.concat(m.staticKeys || [])
    }, []).join(',')
}
复制代码

这里写上函数签名方便理解,modules是一个对象数组,每个对象都有一个staticKeys属性,这个属性是一个字符串数组;

这个函数的作用是将modules中的每个对象的staticKeys属性的值合并为一个字符串,然后用逗号分隔;

扩展阅读:Array.prototype.reduce()

looseEqual

export function looseEqual(a, b) {
  if (a === b) return true
  const isObjectA = isObject(a)
  const isObjectB = isObject(b)
  if (isObjectA && isObjectB) {
    try {
      const isArrayA = Array.isArray(a)
      const isArrayB = Array.isArray(b)
      if (isArrayA && isArrayB) {
        return (
          a.length === b.length &&
          a.every((e, i) => {
            return looseEqual(e, b[i])
          })
        )
      } else if (a instanceof Date && b instanceof Date) {
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) {
        const keysA = Object.keys(a)
        const keysB = Object.keys(b)
        return (
          keysA.length === keysB.length &&
          keysA.every(key => {
            return looseEqual(a[key], b[key])
          })
        )
      } else {
        /* istanbul ignore next */
        return false
      }
    } catch (e) {
      /* istanbul ignore next */
      return false
    }
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  } else {
    return false
  }
}
复制代码

好长,不要慌,来看看这个函数的作用,这个函数的作用是判断两个值是否相等;

看到太长就拆分:

function looseEqual(a, b) {
    if (a === b) return true
    return equal(a, b)
}

function equal(obj1, obj2) {
    const isObjectA = isObject(a)
    const isObjectB = isObject(b)
    if (isObjectA && isObjectB) {
        try {
            return arrayEqual(obj1, obj2) || dateEqual(obj1, obj2) || objectEqual(obj1, obj2)
        } catch (e) {
            /* istanbul ignore next */
            return false
        }
    } else if (!isObjectA && !isObjectB) {
        return String(a) === String(b)
    } else {
        return false
    }
}

function arrayEqual(arr1, arr2) {
    const isArrayA = Array.isArray(a)
    const isArrayB = Array.isArray(b)
    if (isArrayA && isArrayB) {
        return (
            a.length === b.length &&
            a.every((e, i) => {
                return looseEqual(e, b[i])
            })
        )
    }
    return false
}

function dateEqual(date1, date2) {
    if (a instanceof Date && b instanceof Date) {
        return a.getTime() === b.getTime()
    } 
    return false
}

function objectEqual(obj1, obj2) {
    const keysA = Object.keys(obj1)
    const keysB = Object.keys(obj2)
    return (
        keysA.length === keysB.length &&
        keysA.every(key => {
            return looseEqual(a[key], b[key])
        })
    )
}
复制代码

拆分开来可以看到,主要分为四种情况:

  1. 如果两个值是相等的,直接返回true,这里的相等是指===,也就是说,如果两个值是引用类型,那么只有当两个值的引用地址相同时才会返回true
  2. 如果两个值都是Array,那么判断两个数组的长度是否相等,如果相等,那么再判断两个数组中的每个元素是否相等,如果相等,递归到looseEqual函数中;
  3. 如果两个值都是Date,那么判断两个Date对象的时间戳是否相等;
  4. 如果两个值都是Object,那么判断两个对象的键值对个数是否相等,如果相等,递归到looseEqual函数中;

流程如下:

graph TB
A[looseEqual] --> B[a==b]
B -->|是| C[return true] 
B -->|否| D[a b 都是数组]
D -->|是| E[判断两个数组的长度是否相等]
E -->|否| G[a b 都是日期]
E -->|是| F[判断两个数组中的每个元素是否相等]
F --> A
G -->|否| H[a b 都是对象]
G -->|是| I[判断两个日期的时间戳是否相等]
I -->|否| J[return false]
I -->|是| K[return true]
H -->|否| L[a b 都不是对象]
L -->|否| M[return false]
L -->|是| N[判断两个对象的键值对个数是否相等]
N -->|否| O[return false]
N -->|是| P[判断两个对象中的每个键值对是否相等]
P --> A

流程图不能用===,所以用==代替

流程图看着也挺复杂的,但是代码还是很好理解的。

looseIndexOf

function looseIndexOf(arr, val) {
  for (let i = 0; i < arr.length; i++) {
    if (looseEqual(arr[i], val)) return i
  }
  return -1
}
复制代码

这个函数就是在数组中查找某个值的索引,如果找到了,就返回索引,否则返回-1

这里就用到了looseEqual函数,如果两个值相等,那么就返回索引,否则继续循环。

once

function once(fn) {
  let called = false
  return function() {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
复制代码

这个函数的作用是将一个函数转换为只能执行一次的函数。

这里用到了一个闭包,called变量是在函数外部定义的,所以它的作用域是全局的,当once函数执行时,called变量会被赋值为false,然后返回一个函数,这个函数就是once函数的返回值,这个函数会判断called变量是否为false,如果是,那么就将called变量赋值为true,然后执行fn函数,如果called变量不是false,那么就不执行fn函数。

hasChanged

export function hasChanged(x, y) {
    if (x === y) {
        return x === 0 && 1 / x !== 1 / y
    } else {
        return x === x || y === y
    }
}
复制代码

这个函数是Object.ispolyfillObject.is是用来判断两个值是否相等的;

扩展阅读:Object.is()

结束

到这里,我们就完成了Vueutils模块的源码分析;

这个模块的代码量不大,但是里面的函数还是很有用的,这里最多的就是类型判断,还有一些常用的函数,对于我们日常开发可能大多数都用不到,但是里面一些思想都值得我们学习;

例如类型判断,我们可以用typeof来判断,但是typeof有一些缺陷,比如typeof null的结果是object,所以我们可以用Object.prototype.toString.call来判断,这样就可以准确的判断出类型;

例如缓存函数,我们可以用一个对象来缓存函数的执行结果,这样就可以减少函数的执行次数,提高性能;

例如once函数,我们可以用一个变量来记录函数是否执行过,如果执行过了,那么就不再执行,这样就可以将一个函数转换为只能执行一次的函数;

这些都是很有用的思想,我们可以在平时的开发中多多使用,提高我们的开发效率。

分类:
前端
收藏成功!
已添加到「」, 点击更改