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

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


今天带来的是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]之间的字符串,就是原生类型。


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转换为字符串。


toNumber

function toNumber(val) {
    const n = parseFloat(val)
    return isNaN(n) ? val : n
}

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

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对象的属性值。


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方法移除这个元素。


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属性。


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方法,用于性能优化。


capitalize

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

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


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


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属性的值合并为一个字符串,然后用逗号分隔;


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函数中;


流程如下:

image.png

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


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


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函数,我们可以用一个变量来记录函数是否执行过,如果执行过了,那么就不再执行,这样就可以将一个函数转换为只能执行一次的函数;


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


目录
相关文章
|
18天前
|
前端开发 JavaScript 容器
在 vite+vue 中使用@originjs/vite-plugin-federation 模块联邦
【10月更文挑战第25天】模块联邦是一种强大的技术,它允许将不同的微前端模块组合在一起,形成一个统一的应用。在 vite+vue 项目中,使用@originjs/vite-plugin-federation 模块联邦可以实现高效的模块共享和组合。通过本文的介绍,相信你已经了解了如何在 vite+vue 项目中使用@originjs/vite-plugin-federation 模块联邦,包括安装、配置和使用等方面。在实际开发中,你可以根据自己的需求和项目的特点,灵活地使用模块联邦,提高项目的可维护性和扩展性。
|
23天前
|
JavaScript
如何在 Vue 项目中选择合适的模块格式
【10月更文挑战第20天】选择合适的模块格式需要综合考虑多个因素,没有一种绝对正确的选择。需要根据项目的具体情况进行权衡和分析。在实际选择过程中,要保持灵活性,根据项目的发展和变化适时调整模块格式。
20 7
|
23天前
|
JavaScript 前端开发 编译器
在 Vue 项目中使用 ES 模块格式的优点
【10月更文挑战第20天】在 Vue 项目中使用 ES 模块格式具有众多优点,这些优点共同作用,使得项目能够更高效、更可靠地开发和运行。当然,在实际应用中,还需要根据项目的具体情况和需求进行合理的选择和配置。
25 6
|
19天前
|
缓存 JavaScript UED
Vue 中异步加载模块的方式
【10月更文挑战第23天】这些异步加载模块的方式各有特点和适用场景,可以根据项目的需求和架构选择合适的方法来实现模块的异步加载,以提高应用的性能和用户体验
|
19天前
|
JavaScript 测试技术 UED
解决 Vue 项目中 Tree shaking 无法去除某些模块
【10月更文挑战第23天】解决 Vue 项目中 Tree shaking 无法去除某些模块的问题需要综合考虑多种因素,通过仔细分析、排查和优化,逐步提高 Tree shaking 的效果,为项目带来更好的性能和用户体验。同时,持续关注和学习相关技术的发展,不断探索新的解决方案,以适应不断变化的项目需求。
|
1月前
|
JavaScript
vue尚品汇商城项目-day03【16.开发Search组件模块中的TypeNav商品分类菜单(过渡动画效果)+17.(优化)针对三级菜单联动进行优化,优化方向为减少查询】
vue尚品汇商城项目-day03【16.开发Search组件模块中的TypeNav商品分类菜单(过渡动画效果)+17.(优化)针对三级菜单联动进行优化,优化方向为减少查询】
35 0
|
4月前
|
开发框架 JavaScript 前端开发
基于Vue的工作流项目模块中,使用动态组件的方式统一呈现不同表单数据的处理方式
基于Vue的工作流项目模块中,使用动态组件的方式统一呈现不同表单数据的处理方式
|
6月前
|
JavaScript 前端开发
vue的论坛管理模块-文章评论02
vue的论坛管理模块-文章评论02
|
6月前
|
JavaScript Java
vue的论坛管理模块-文章查看-01
vue的论坛管理模块-文章查看-01
|
6月前
|
JavaScript 前端开发
除了ES6的模块导出语法,还有哪些其他的方式可以在Vue项目中进行模块化开发?
除了ES6的模块导出语法,还有哪些其他的方式可以在Vue项目中进行模块化开发?
35 2