【源码漫游】vue2-util | 超详细解析vue2函数工具库

323 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

hey, 我是小黄瓜没有刺。一枚菜鸟瓜🥒,期待关注➕ 点赞,共同成长~

本系列会精读一些小而美的开源作品或者框架的核心源码。

本文整理自vue.js(2x)版本中的工具函数。

emptyObject

返回一个冻结对象

 export const emptyObject = Object.freeze({})

freeze()方法用于执行一切不受seal()方法限制的属性值变更。 例如:

 let obj = Object.freeze({name: '小黄瓜'});
 
 obj.name = '小黑瓜'
 obj.age = 18
 
 console.log(obj.name) // '小黄瓜'
 console.log(obj.obj) // undefined

扩展

Object.freeze() 方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。也就是说,这个对象永远是不可变的。该方法返回被冻结的对象。

Object 中有一个内置的方法:Object.isFrozen() 用于判断一个对象是否为冻结对象

 Object.isFrozen(obj) // true

isUndef

判断一个值是否等于 undefined / null

 export function isUndef (v: any): boolean {
   return v === undefined || v === null
 }

isDef

判断一个值是否不等于 undefined / null

 export function isDef (v: any): boolean {
   return v !== undefined && v !== null
 }

isTrue

判断一个值是否为true

 export function isTrue (v: any): boolean {
   return v === true
 }

isFalse

判断一个值是否为false

 export function isFalse (v: any): boolean {
   return v === false
 }

isPrimitive

判断一个值是否为非 undefined / null 的基本类型

 export function isPrimitive (value: any): boolean {
   // 依次判断是否为 string/number/symbol/boolean 类型
   return (
     typeof value === 'string' ||
     typeof value === 'number' ||
     typeof value === 'symbol' ||
     typeof value === 'boolean'
   )
 }

isObject

判断一个值是否为引用类型

export function isObject (obj: mixed): boolean {
  // 首先排除obj为null的情况
  return obj !== null && typeof obj === 'object'
}

扩展

typeof 运算符并不能准确的辨别每一种类型,对于对于对象类型的值,typeof返回的都是object

typeof '小黄瓜'  // string
typeof 1 // number
typeof (()=>1) // function

typeof (new Date())   // object
typeof [1, 2, 3]   // object
typeof { a: 1, b: 2, c: 3 }  // object

可以看到 Array, Date, Object 类型的值全部都返回了 object

定义 _toString 方法

const _toString = Object.prototype.toString

Object.prototype.toString 返回的是一个用于描述目标对象的字符串。如果目标值为Number,还可以传递一个用于规定进制位的值

var o = { prop:1 };
o.toString(); // '[object Object]'

var n = new Number(255);
n.toString(); // '255'
n.toString(16); // 'ff'

toRawType

用于返回一个值的具体类型 前文说过 typeof 运算符不能准确的判断对象类型的属性,而 Object.prototype.toString 可以解决这个问题

export function toRawType (value: any): string {
  // slice可以传入负数作为参数,此处的-1指截取到最后一个值之前
  return _toString.call(value).slice(8, -1)
}

let obj = { name: '小黄瓜' }
let ary = [1, 2, 3]

toRawType(obj) // [object Object] -> Object
toRawType(ary) // [object Array] -> Array

isPlainObject

判断一个值是否为对象类型(此处的对象为具体的对象类型)

export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

isPlainObject({}) // true
isPlainObject([]) // false

isRegExp

判断一个值是否为正则对象

export function isRegExp (v: any): boolean {
  return _toString.call(v) === '[object RegExp]'
}

isRegExp(/w+/) // true

isValidArrayIndex

判断一个值是否为可用的数组索引值

export function isValidArrayIndex (val: any): boolean {
  // 格式化参数 parseFloat将一个string类型的值转为浮点数
  const n = parseFloat(String(val))
  // 判断n 必须大于等于0(数组下标从0开始)  
  // Math.floor(n) === n 判断为小数的情况
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

扩展

isFinite() 是js内置的函数,用来判断被传入的参数值是否为一个有限数值,在必要情况下,参数会首先转为一个数值

可以用这个方法来判定一个数字是否是有限数字。isFinite 方法检测它参数的数值。如果参数是 NaN,正无穷大或者负无穷大,会返回false,其他返回 true

isFinite(Infinity);  // false
isFinite(NaN);       // false
isFinite(-Infinity); // false

isFinite(0);         // true
isFinite(2e64);      // true,在更强壮的 Number.isFinite(null) 中将会得到 false


isFinite("0");       // true,在更强壮的 Number.isFinite('0') 中将会得到 false

isPromise

判断一个值是否为promise对象

export function isPromise (val: any): boolean {
  // Promise对象中存在then和catch两个属性,并且为函数
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

toString

将一个值转化为字符串

有以下几种情况:

  • 如果值为 null,返回 ''
  • 如果值为 数组或者对象,返回JSON序列化后的值
  • 如果以上情况都没有命中,则返回通过String()后的值
export function toString (val: any): string {
  return val == null
    ? ''
    // 判断val是否为数组或者对象 
    // _toString 为上文中的 Object.prototype.toString
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

toNumber

将一个值转化为数字

export function toNumber (val: string): number | string {
  // 将val转化为数字浮点数
  const n = parseFloat(val)
  // 判断是否为NaN,如果是的话返回val,不是的话返回转化后的值
  return isNaN(n) ? val : n
}

makeMap

makeMap用于生成一个对象,返回一个函数,用于检查key是否存在于这个对象中

export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {

  // 创建一个对象
  const map = Object.create(null)
  // 将传入的字符串转化为数组
  const list: Array<string> = str.split(',')
  // 遍历数组,将每个值作为key放入对象中
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }

  // 返回箭头函数
  // expectsLowerCase 是否将key转化为小写
  return expectsLowerCase 
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

扩展

Object.create() 方法创建一个新对象,使用传入的参数新创建的对象的 __proto__

Object.create(null){…} 的区别:

// {…} 的方式
let obj = { a: 1 };
console.log(obj);

WechatIMG485.png

新创建的对象和new Object()的方式是一样的,都会继承Object对象的所有属性

var obj = Object.create(null);
console.log(obj)

WechatIMG486.png

通过 Object.create(null) 创建的对象是不继承Object原型链上的属性,如tostring()方法这些

isBuiltInTag

该函数返回是否是内置的tag

// 使用slot,component对函数进行初始化
export const isBuiltInTag = makeMap('slot,component', true)

isBuiltInTag('slot')  // true
isBuiltInTag('Component') // true

将被转化为

 {
   slot: true,
   component: true
 }

isReservedAttribute

判断是否为保留的属性,依然是利用makeMap函数来实现 ​

export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')

isReservedAttribute('key') // true
isReservedAttribute('ref') // true
isReservedAttribute('IS') // undefined

remove

用于删除指定元素

export function remove (arr: Array<any>, item: any): Array<any> | void {
  // 判断数组是否有值
  if (arr.length) {
    // 查找元素下标
    const index = arr.indexOf(item)
    if (index > -1) {
      // 使用数组的splice方法进行删除
      return arr.splice(index, 1)
    }
  }
}

扩展

数组中的splice对性能的损耗很大,因为数组的存储结构的原因,任何对某一个任意位置的移动都会造成数组后续所有位置的移动。

引申:在 axios InterceptorManager 拦截器源码 中,拦截器也用数组存储的。但实际移除拦截器时,只是把拦截器置为 null 。而不是用splice移除。最后执行时为 null 的不执行。因为理论上有可能用户设置的拦截有很多个。

axios代码实例:

// 伪代码
// 声明
this.handlers = [];

// 移除
if (this.handlers[id]) {
    this.handlers[id] = null;
}

// 执行
if (h !== null) {
    fn(h);
}

hasOwn

判断属性是否为对象自身的属性

// 定义hasOwnProperty
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
  // 直接使用定义hasOwnProperty进行判断,第一个参数为对象,第二个参数为需要判断的属性
  return hasOwnProperty.call(obj, key)
}

扩展

Object.prototype.hasOwnProperty 可以进行判断属性是否为某一个对象自身的属性 js对象中的属性分为两种,一种是通过原型链的机制访问得到的,另一种是对象自身的属性。 因为函数中存在一块独有的区域prototype,可以存放一些公共的属性和方法,当我们通过new进行调用时,创造一个构造函数的实例,实例可以通过__proto__来访问构造函数的prototype,可以访问和使用prototype中的属性和方法。

 let obj = { 
  a: 1, 
  add() {
    console.log('小黄瓜')
  } 
}

obj.toString() // [object Object]
obj.add() // '小黄瓜'
console.log(obj.hasOwnProperty("toString")) // false
console.log(obj.hasOwnProperty("a"))  // true

可以看到我们并没有定义toString方法,但是依然可以使用,正是由于对象继承了Object中的方法

cached

返回一个缓存函数,利用闭包的特性,创建一个对象保存函数执行的值,再次调用时会首先在对象中查找是否有相同参数的执行结果

export function cached<F: Function> (fn: F): F {
  // 创建一个空对象
  const cache = Object.create(null)
  // 返回一个函数,该函数接受一个参数
  return (function cachedFn (str: string) {
    // 根据参数名当作key来查找
    const hit = cache[str]
    // 如果在对象中可以查找到,直接返回值,
    // 如果没有查找到,直接执行,然后将参数值当作key,执行结果当作value,保存到对象中
    return hit || (cache[str] = fn(str))
  }: any)
}

camelize

-字符转小驼峰

例如:on-change --> onChange

// 正则匹配 - 和 -后面的第一个字符
const camelizeRE = /-(\w)/g
// 利用cached函数缓存执行结果
export const camelize = cached((str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})

camelize('on-click')  // onClick
camelize('on-change')  // onChange
// 再次传入on-click,会直接在对象中获取结果,不会再次执行转换
camelize('on-click')  // onClick

扩展

replace方法的第二个参数

平时在使用replace的时候可能大多数时间第二个参数都会传递一个字符串或者其他值使用,其实replace还可以传递一个函数

'get/data?id=$18&t=info'.replace(/$(\d+)/g, function (a, b, c, d) {
    console.log(a);
    console.log(b);
    console.log(c);
    console.log(d);
});

得到的值为

$18 // 匹配项
18 // 捕获组
12 // 在字符串中的位置
get/data?id=$18&t=info // 原始字符串

一般情况下,如果传递了一个函数,那么会向这个函数传递3个参数:模式的匹配项、模式匹配项在字符串中的位置和原始字符串, 但是上面多定义了一个捕获组(\d+),在正则表达式中定义了多个捕获组的情况下,传递给函数的参数依次是模式的匹配项、第一个捕获组的匹配项、第二个捕获组的匹配项……,但最后两个参数仍然分别是模式的匹配项在字符串中的位置和原始字符串。

capitalize

字符串首字母大写

// 利用cached函数缓存执行结果
export const capitalize = cached((str: string): string => {
  // 字符串拼接,截取第一个字符转大写+字符串第一个字符位置以后的字符串
  return str.charAt(0).toUpperCase() + str.slice(1)
})

capitalize('change') // Change
capitalize('click') // Click

hyphenate

小驼峰转换为-字符

例如:onClick --> on-click

// 正则获取大写字母
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
  // 利用replace拼接并转换
  return str.replace(hyphenateRE, '-$1').toLowerCase()
})

bind

优化了bind方法,处理兼容和参数个数

function polyfillBind (fn: Function, ctx: Object): Function {
  function boundFn (a) {
    const l = arguments.length
    // 判断参数个数,大于一个使用apply,否则使用call
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length
  return boundFn
}

// 使用bind
function nativeBind (fn: Function, ctx: Object): Function {
  return fn.bind(ctx)
}

// 判断是否存在bind函数,如果存在,直接使用,不存在的话使用call/apply来代替
export const bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind

扩展

call,apply,'bind'的区别?

  • call的第二个参数依次进行传递
  • apply的第二个参数是通过数组进行传递
  • bind只是绑定this绑定时并不会执行,而callapply绑定时会执行一次

call的实现,通过自定义对象方法的方式实现:

Function.prototype.myCall = function(context,...args) {
    // 定义对象
    context =  (context ?? window) || new Object(context)
    // 定义对象下的函数
    context.fn = this
    // 执行
    const result = context.fn(args)
    // 删除临时创建的属性函数
    delete context.fn
    return result
}

apply函数的实现,与call的区别是第二个参数换成了数组传递:

Function.prototype.myApply = function(context) {
    context =  (context ?? window) || new Object(context)
    // 收集参数
    const args = arguments[1]
    context.fn = this
    // 调用,判断参数是否存在
    const result= args ? context.fn(...args) : context.fn()
    delete context.fn
    return result
}

bind函数简单实现:

Function.prototype.myBind = function (context) {
  // this  指的是需要绑定的函数
  let self = this
  return function () {
    // context 指需要绑定的this
    return self.apply(context, arguments)
   }
}

toArray

将类数组转换为数组,支持从任意位置开始转换

export function toArray (list: any, start?: number): Array<any> {
  // 初始化值
  start = start || 0
  // 计算需要转换的长度
  let i = list.length - start
  // 定义一个指定长度的数组
  const ret: Array<any> = new Array(i)
  // 循环,从后往前添加值
  while (i--) {
    // 例如 传入4,长度为10
    // res[6] = list[6 + 4]
    ret[i] = list[i + start]
  }
  return ret
}


toArray("123456789", 4) // [5, 6, 7, 8, 9]

extend

用于将两个对象合并

export function extend (to: Object, _from: ?Object): Object {
  // 遍历原数组的key
  for (const key in _from) {
    // 使用key复制对象下面的值
    to[key] = _from[key]
  }
  return to
}


let o1 = { name: '小黄瓜' } 
let o2 = extend(o1, { age: 18, isHappy: true })
console.log(o2) // { name: '小黄瓜', age: 18, isHappy: true }

toObject

用于将数组转换为对象结构

export function toObject (arr: Array<any>): Object {
  const res = {}
  // 遍历数组中的每一项
  for (let i = 0; i < arr.length; i++) {
    // 调用extend方法进行合并对象
    if (arr[i]) {
      extend(res, arr[i])
    }
  }
  return res
}

toObject(['小黄瓜没有刺']) // {0: '小', 1: '黄', 2: '瓜', 3: '没', 4: '有', 5: '刺'}

noop

返回一个空函数

export function noop (a?: any, b?: any, c?: any) {}

no

一直返回一个false

export const no = (a?: any, b?: any, c?: any) => false

identity

返回参数本身

export const identity = (_: any) => _

genStaticKeys

生成静态属性

export function genStaticKeys (modules: Array<ModuleOptions>): string {
  // 生成一个静态属性列表并转换成字符串
  return modules.reduce((keys, m) => {
    return keys.concat(m.staticKeys || [])
  }, []).join(',')
}

looseEqual

对数组、日期、对象进行递归比对。如果内容完全相等则宽松相等

export function looseEqual (a: any, b: any): boolean {
  // 如果a和b完全相等,则直接返回true
  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)

      // 如果都为数组,则先比较两个数组的长度,然后使用every递归调用looseEqual函数
      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()
      // 判断为对象的情况,取出key进行比较
      } else if (!isArrayA && !isArrayB) {
        const keysA = Object.keys(a)
        const keysB = Object.keys(b)

        // 首先判断长度,然后按照数组的every方式递归调用
        return keysA.length === keysB.length && keysA.(key => {
          // 取value值
          return looseEqual(a[key], b[key])
        })
      } else {
        return false
      }
    } catch (e) {
      return false
    }
  // 如果两个参数都不为对象,则转换为字符串进行对比
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  // 其他情况返回false
  } else {
    return false
  }
}

looseIndexOf

获取两个值相等的下标值

export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
  // 依次遍历,调用looseEqual函数进行对比,如果相等,直接返回下标
  for (let i = 0; i < arr.length; i++) {
    if (looseEqual(arr[i], val)) return i
  }
  return -1
}

once

一个只执行一次的函数

利用闭包的特性,设置一个开关,执行过后设置为false,不再执行

export function once (fn: Function): Function {
  // 定义开关
  let called = false
  return function () {
    // 如果被关闭,则不会执行函数
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

const fn = once(function(){
  console.log('执行~');
});

fn(); // '执行~'
fn(); // 不输出
fn(); // 不输出
fn(); // 不输出

完整代码

 /* @flow */
 ​
 export const emptyObject = Object.freeze({})
 ​
 // These helpers produce better VM code in JS engines due to their
 // explicitness and function inlining.
 export function isUndef (v: any): boolean %checks {
   return v === undefined || v === null
 }
 ​
 export function isDef (v: any): boolean %checks {
   return v !== undefined && v !== null
 }
 ​
 export function isTrue (v: any): boolean %checks {
   return v === true
 }
 ​
 export function isFalse (v: any): boolean %checks {
   return v === false
 }
 ​
 /**
  * Check if value is primitive.
  */
 export function isPrimitive (value: any): boolean %checks {
   return (
     typeof value === 'string' ||
     typeof value === 'number' ||
     // $flow-disable-line
     typeof value === 'symbol' ||
     typeof value === 'boolean'
   )
 }
 ​
 /**
  * Quick object check - this is primarily used to tell
  * objects from primitive values when we know the value
  * is a JSON-compliant type.
  */
 export function isObject (obj: mixed): boolean %checks {
   return obj !== null && typeof obj === 'object'
 }
 ​
 /**
  * Get the raw type string of a value, e.g., [object Object].
  */
 const _toString = Object.prototype.toString
 ​
 export function toRawType (value: any): string {
   return _toString.call(value).slice(8, -1)
 }
 ​
 /**
  * Strict object type check. Only returns true
  * for plain JavaScript objects.
  */
 export function isPlainObject (obj: any): boolean {
   return _toString.call(obj) === '[object Object]'
 }
 ​
 export function isRegExp (v: any): boolean {
   return _toString.call(v) === '[object RegExp]'
 }
 ​
 /**
  * Check if val is a valid array index.
  */
 export function isValidArrayIndex (val: any): boolean {
   const n = parseFloat(String(val))
   return n >= 0 && Math.floor(n) === n && isFinite(val)
 }
 ​
 export function isPromise (val: any): boolean {
   return (
     isDef(val) &&
     typeof val.then === 'function' &&
     typeof val.catch === 'function'
   )
 }
 ​
 /**
  * Convert a value to a string that is actually rendered.
  */
 export function toString (val: any): string {
   return val == null
     ? ''
     : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
       ? JSON.stringify(val, null, 2)
       : String(val)
 }
 ​
 /**
  * Convert an input value to a number for persistence.
  * If the conversion fails, return original string.
  */
 export function toNumber (val: string): number | string {
   const n = parseFloat(val)
   return isNaN(n) ? val : n
 }
 ​
 /**
  * Make a map and return a function for checking if a key
  * is in that map.
  */
 export function makeMap (
   str: string,
   expectsLowerCase?: boolean
 ): (key: string) => true | void {
   const map = Object.create(null)
   const list: Array<string> = str.split(',')
   for (let i = 0; i < list.length; i++) {
     map[list[i]] = true
   }
   return expectsLowerCase
     ? val => map[val.toLowerCase()]
     : val => map[val]
 }
 ​
 /**
  * Check if a tag is a built-in tag.
  */
 export const isBuiltInTag = makeMap('slot,component', true)
 ​
 /**
  * Check if an attribute is a reserved attribute.
  */
 export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
 ​
 /**
  * Remove an item from an array.
  */
 export function remove (arr: Array<any>, item: any): Array<any> | void {
   if (arr.length) {
     const index = arr.indexOf(item)
     if (index > -1) {
       return arr.splice(index, 1)
     }
   }
 }
 ​
 /**
  * Check whether an object has the property.
  */
 const hasOwnProperty = Object.prototype.hasOwnProperty
 export function hasOwn (obj: Object | Array<*>, key: string): boolean {
   return hasOwnProperty.call(obj, key)
 }
 ​
 /**
  * Create a cached version of a pure function.
  */
 export function cached<F: Function> (fn: F): F {
   const cache = Object.create(null)
   return (function cachedFn (str: string) {
     const hit = cache[str]
     return hit || (cache[str] = fn(str))
   }: any)
 }
 ​
 /**
  * Camelize a hyphen-delimited string.
  */
 const camelizeRE = /-(\w)/g
 export const camelize = cached((str: string): string => {
   return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
 })
 ​
 /**
  * Capitalize a string.
  */
 export const capitalize = cached((str: string): string => {
   return str.charAt(0).toUpperCase() + str.slice(1)
 })
 ​
 /**
  * Hyphenate a camelCase string.
  */
 const hyphenateRE = /\B([A-Z])/g
 export const hyphenate = cached((str: string): string => {
   return str.replace(hyphenateRE, '-$1').toLowerCase()
 })
 ​
 /**
  * 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: Function, ctx: Object): Function {
   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: Function, ctx: Object): Function {
   return fn.bind(ctx)
 }
 ​
 export const bind = Function.prototype.bind
   ? nativeBind
   : polyfillBind
 ​
 /**
  * Convert an Array-like object to a real Array.
  */
 export function toArray (list: any, start?: number): Array<any> {
   start = start || 0
   let i = list.length - start
   const ret: Array<any> = new Array(i)
   while (i--) {
     ret[i] = list[i + start]
   }
   return ret
 }
 ​
 /**
  * Mix properties into target object.
  */
 export function extend (to: Object, _from: ?Object): Object {
   for (const key in _from) {
     to[key] = _from[key]
   }
   return to
 }
 ​
 /**
  * Merge an Array of Objects into a single Object.
  */
 export function toObject (arr: Array<any>): Object {
   const res = {}
   for (let i = 0; i < arr.length; i++) {
     if (arr[i]) {
       extend(res, arr[i])
     }
   }
   return res
 }
 ​
 /* eslint-disable no-unused-vars */
 ​
 /**
  * Perform no operation.
  * Stubbing args to make Flow happy without leaving useless transpiled code
  * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
  */
 export function noop (a?: any, b?: any, c?: any) {}
 ​
 /**
  * Always return false.
  */
 export const no = (a?: any, b?: any, c?: any) => false
 ​
 /* eslint-enable no-unused-vars */
 ​
 /**
  * Return the same value.
  */
 export const identity = (_: any) => _
 ​
 /**
  * Generate a string containing static keys from compiler modules.
  */
 export function genStaticKeys (modules: Array<ModuleOptions>): string {
   return modules.reduce((keys, m) => {
     return keys.concat(m.staticKeys || [])
   }, []).join(',')
 }
 ​
 /**
  * Check if two values are loosely equal - that is,
  * if they are plain objects, do they have the same shape?
  */
 export function looseEqual (a: any, b: any): boolean {
   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
   }
 }
 ​
 /**
  * Return the first index at which a loosely equal value can be
  * found in the array (if value is a plain object, the array must
  * contain an object of the same shape), or -1 if it is not present.
  */
 export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
   for (let i = 0; i < arr.length; i++) {
     if (looseEqual(arr[i], val)) return i
   }
   return -1
 }
 ​
 /**
  * Ensure a function is called only once.
  */
 export function once (fn: Function): Function {
   let called = false
   return function () {
     if (!called) {
       called = true
       fn.apply(this, arguments)
     }
   }
 }
 ​

本文参考

若川老师的博客

juejin.cn/post/702427…

MDN

写在最后 ⛳

源码系列第二篇!未来可能会更新实现mini-vue3javascript基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳