源码共读,第24期 | vue2工具函数

161 阅读4分钟

前言

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

本篇是源码共读第33期 | arrify转数组,点击了解本期详情

代码

实不相瞒,这一期源码共读竟不知要如何下手,呵!思之令人发笑(笑我自己😬)想了好久最后官方的定位此篇笔记为阅读总结🧐

Object.freeze

Object.freeze()方法可以冻结一个对象。一个被冻结的对象再也不能被修改(第一层无法修改)。冻结了一个对象则不能向该对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。

但是我不大确定为什么要

export const emptyObject = Object.freeze({})

想了想原因可能是为了判断不需要赋其他值的情况😳

isUndef 判断值是否是未定义

我是先看了若川大佬的文章初学者也能看懂的 Vue2 源码中那些实用的基础工具函数然后根据列出来的各工具函数的具体功能自己先简单写了一下

function isUndef(value) {
    return value === undefined || value === null
}

与源码对比一致,不再赘述

isDef 判断值是否是已经定义

function isDef(value) {
    return value !== undefined && value !== null
}

与源码对比一致,不再赘述

isTrue 判断值是否为true

function isTrue(value) {
    return value === true
}

与源码对比一致,不再赘述

isFalse 判断值是否为false

function isFalse(value) {
    return value === false
}

与源码对比一致,不再赘述

isPrimitive 判断值是否是原始值

呃....这个我很惭愧一下子没反应过来啥是原始值😂看了下源码

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

应该是判断是否为基本数据类型,不过为啥没有undefinednull这两种呢,如果是误传了一个未定义的数据呢?

isObject 判断值是否是对象

function isObject (value) {
    Object.prototype.toString.call(value) === '[object Object]' // true 代表为对象
}

查看源码

/**
 * Quick object check - this is primarily used to tell
 * Objects from primitive values when we know the value
 * is a JSON-compliant type.
 */
function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

// 例子:
isObject([]) // true

源码此方法没有严格区分数组和对象,这块我觉着看业务需求决定用那种即可

toRawType 将值转换成原始类型

这个没看明白题目意思,源码如下

/**
 * Get the raw type string of a value, e.g., [object Object].
 */
var _toString = Object.prototype.toString;

function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}

// 例子:
toRawType('') // 'String'
toRawType() // 'Undefined'

了解Object.prototype.toString方法 这个应该就容易懂了。不过我忘记了slice第二个参数为负数代表啥了,又去补了一下,slice(start, end) end为负数,表示倒数第几个

isPlainObject 判断值是否是纯对象

和上面个人写的isObject方法相同,就不再敲一遍了

isRegExp 判断值是否是正则表达式

function isObject (value) {
    Object.prototype.toString.call(value) === '[object RegExp]'
}

和源码相同

 isValidArrayIndex 判断是否是可用的数组索引值

这个看题目没明白是传数组索引还是传数组😂,看源码

/**
 * Check if val is a valid array index.
 */
function isValidArrayIndex (val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

是传的索引值,将索引值转字符串,然后用parseFloat方法用于将该字符串转为浮点数,再看该浮点数是否大于0,向下取整是否为自身并且是一个有限数值

 isPromise 判断传入值是否是为promise

这块没想出来,看源码

function isPromise (val) {
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

// 例子:
// 判断是不是Promise对象 
const p1 = new Promise(function(resolve, reject){
    resolve('123');
});
isPromise(p1); // true

我个人觉得主要在thencatch这两个方法特性这块

toString 将数值转字符串

没想出来对象和数组怎么转,难道拿key一个个去转吗?(sorry,本菜鸡只能想到最简单粗暴的这种笨方法)索性直接看源码

/**
 * Convert a value to a string that is actually rendered.
 */
function toString (val) {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) // 是否为数组或对象,且prototype有toString方法
      ? JSON.stringify(val, null, 2) // 开始序列化,val原数据,null为替换,2为缩进值
      : String(val) // 序列化
}

JSON.stringify平时使用时一般只传第一个参数,特意去复习了下

JSON.stringify(value[, replacer [, space]])

value: 将要序列化成 一个 JSON 字符串的值。

replacer: 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。

space: 指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为 10。该值若小于 1,则意味着没有空格;如果该参数为字符串(当字符串长度超过 10 个字母,取其前 10 个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。

toNumber 将传入值转数字

唉,就记得有个parseInt方法了,试了下怎么有的参数解析出来和我预想的不大一样啊🤔,好吧,可以看出api忘的差不多了,再次去回顾

parseFloat:解析一个参数(必要时先转换为字符串)并返回一个浮点数。

  • 如果 parseFloat 在解析过程中遇到了正号(+)、负号(-)、数字(0-9)、小数点(.)、或者科学记数法中的指数(e 或 E)以外的字符,则它会忽略该字符以及之后的所有字符,返回当前已经解析到的浮点数。
  • 第二个小数点的出现也会使解析停止(在这之前的字符都会被解析)
  • 参数首位和末位的空白符会被忽略
  • 如果参数字符串的第一个字符不能被解析成为数字,则 parseFloat 返回 NaN

parseInt(stringradix)  解析一个字符串并返回指定基数的十进制整数,radix 是 2-36 之间的整数,表示被解析字符串的基数。

  • parseInt函数将其第一个参数转换为一个字符串,对该字符串进行解析,然后返回一个整数或 NaN
  • 如果不是 NaN,返回值将是以第一个参数作为指定基数 radix 的转换后的十进制整数
  • 如果 parseInt 遇到的字符不是指定 radix 参数中的数字,它将忽略该字符以及所有后续字符,并返回到该点为止已解析的整数值。parseInt 将数字截断为整数值。允许前导和尾随空格。

源码如下

/**
 * Convert an input value to a number for persistence.
 * If the conversion fails, return original string.
 */
function toNumber (val) {
  var n = parseFloat(val);
  return isNaN(n) ? val : n
}

toNumber('a') // 'a'
toNumber('1') // 1
toNumber('1a') // 1
toNumber('a1') // 'a1'

makeMap 生成一个 map (对象)

没看懂题,我以为要new一个Map呢,然而并不是。源码如下:

/**
 * 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]; }
}

// Object.create(null) 没有原型链的空对象

传入的是一个以逗号分隔的字符串,生成一个键值对对象,key就是分割出来的字符,value就是true。然后第二个参数是是否要转为小写,最后返回的是一个函数,检测 传入的key 值在不在这个对象中

isBuiltInTag 是否是内置的 tag

makeMap接口就好了

/**
 * Check if a tag is a built-in tag.
 */
var isBuiltInTag = makeMap('slot,component', true);

// 返回的函数,第二个参数不区分大小写
isBuiltInTag('slot') // true
isBuiltInTag('component') // true
isBuiltInTag('Slot') // true
isBuiltInTag('Component') // true

isReservedAttribute 是否是保留的属性

/**
 * Check if an attribute is a reserved attribute.
 */
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');

isReservedAttribute('key') // true
isReservedAttribute('ref') // true
isReservedAttribute('slot') // true
isReservedAttribute('slot-scope') // true
isReservedAttribute('is') // true
isReservedAttribute('IS') // undefined

remove 移除数组中的中一项

function remove(arr,target){
    const index = arr.findIndex((x) => x === target);
    const newArr = [...arr.slice(0, index), ...arr.slice(index + 1)];
    return newArr
}

我用的是slice方法,如果不介意改变原数组可以直接arr.splice(index, 1),我看源码是用的splice

hasOwn 检测是否是自己的属性

var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

cached 缓存

/**
 * Create a cached version of a pure function.
 */
function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}

在当前函数作用域定义了一个空对象,用来缓存数据,运用柯里化返回一个函数,这个函数可以访问到定义的cache,然后判断cache中是否已存在输入参数。如果已经存在,直接返回cache的内容,如果没有存在,使用函数func对输入参数求值,然后把结果存储在cache

camelize 连字符转小驼峰

这辈子看着就头大的有三个东西,gitwebpack正则😬

/**
 * Camelize a hyphen-delimited string.
 */
var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});

capitalize 首字母转大写

报告老师🙋这个我会

function capitalize (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
});

咦,咋和源码有点不一样嘞....哦源码进行了一步缓存,有时候可能需要多次操作相同的字符串,我们处理一次将这个字符以及结果缓存起来,下次再遇到直接读取即可。下面是源码:

/**
 * Capitalize a string.
 */
var capitalize = cached(function (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
});

hyphenate 小驼峰转连字符

还是那个样子,正则我都抓瞎

/**
 * Hyphenate a camelCase string.
 */
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
});

polyfillBind 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) {
    var 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)
}

var bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind;

这块没大明白,作用点在哪

toArray 把类数组转成真正的数组

什么是类数组?

  1. 可以通过索引属性访问元素并且拥有 length 属性的对象。
  2. 不具有数组所具有的方法

我原本想的是Array.from()或者扩展运算符,但是源码好像有它自己的思想

/**
 * Convert an Array-like object to a real Array.
 */
function toArray (list, start) {
  start = start || 0;
  var i = list.length - start;
  var ret = new Array(i);
  while (i--) {
    ret[i] = list[i + start];
  }
  return ret
}

// 例子:
function fn(){
  var arr1 = toArray(arguments);
  console.log(arr1); // [1, 2, 3, 4, 5]
  var arr2 = toArray(arguments, 2);
  console.log(arr2); // [3, 4, 5]
}
fn(1,2,3,4,5);

源码是支持从任意位置开始转换

extend 合并

老师,我!我!我会🙋

function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}

toObject 转对象

那个...是啥要转对象啊?看了看源码。哦数组啊

/**
 * Merge an Array of Objects into a single Object.
 */
function toObject (arr) {
  var res = {};
  for (var i = 0; i < arr.length; i++) {
    if (arr[i]) {
      extend(res, arr[i]);
    }
  }
  return res
}

noop 空函数

function noop (a, b, c) {}

no 一直返回 false

function no(a, b, c) { 
    return false
};

identity 返回参数本身

function identity(value) { 
    return value
};

genStaticKeys 生成静态属性

/**
 * Generate a string containing static keys from compiler modules.
 */
function genStaticKeys (modules) {
  return modules.reduce(function (keys, m) {
    return keys.concat(m.staticKeys || [])
  }, []).join(',')
}

 looseEqual 宽松相等

/**
 * Check if two values are loosely equal - that is,
 * if they are plain objects, do they have the same shape?
 */
function looseEqual (a, b) {
  // 如果a和b完全相等,返回true
  if (a === b) { return true }
  var isObjectA = isObject(a);
  var isObjectB = isObject(b);
  // 判断两个参数是否都为对象
  if (isObjectA && isObjectB) {
    try {
      var isArrayA = Array.isArray(a);
      var isArrayB = Array.isArray(b);
      // 如果两个对象都为对象,再看是否都为数组
      if (isArrayA && isArrayB) {
        // 都为数组,再比较两个数组的长度,然后使用every递归调用looseEqual函数
        return a.length === b.length && a.every(function (e, i) {
          return looseEqual(e, b[i])
        })
      } else if (a instanceof Date && b instanceof Date) { // 判断是否为时间对象
        // 是,转换为时间戳进行比较
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) { // 为对象的,取出key进行比较
        var keysA = Object.keys(a);
        var keysB = Object.keys(b);
        // 判断长度,然后按照数组的every方式递归调用
        return keysA.length === keysB.length && keysA.every(function (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
  }
}

 looseIndexOf 宽松的 indexOf

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

once 确保函数只执行一次

function once (fn) {
  var called = false;
  return function () {
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}

完结,撒花🎉