Vue源码的工具库|shared

1,354 阅读3分钟

Vue.js源码中定义了一些工具方法,浏览器端、服务端的Vue.js都可共享这些方法

Vue 2.x

基于2.6.14

  • 具体路径:src\shared\util.js

  • 背景:基于flow构建类型系统

  • 目的:帮助程序在JS引擎中生成更好的VM代码,利于性能优化,提高效率

类型判断

isUndef & isDef

判断是否为undefined或null

function isUndef (v) {
  return v === undefined || v === null
}
function isDef (v) {
  return v !== undefined && v !== null
}

isTrue & isFalse

判断真假与否

function isTrue (v) {
  return v === true
}
function isFalse (v) {
  return v === false
}

isPrimitive

数据类型是否为基础类型(部分)

function isPrimitive (value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

eg:src\core\vdom\create-element.js 的 createElement,进行判断

 if (Array.isArray(data) || isPrimitive(data)) {//……}

isObject

是否为引用类型或通过由new构造的类型

此方法在Vue中用于快速对象检查:当已知值是符合JSON的类型时,用于从该值中识别对象

function isObject (obj) {
  return obj !== null && typeof obj === 'object'  // 注意 null 在 typeof 的坑
}

isPromise

是否为 Promise 函数

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

eg:src\core\util\error.js 的 invokeWithErrorHandling,用于处理异步异常

if (res && !res._isVue && isPromise(res) && !res._handled) {//……}

toRawType

获取值的原始类型字符串

基于Object.prototype.toString.call, 除自定义类型外,几乎通用。返回:`[object, 类型]

var _toString = Object.prototype.toString;

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

eg:src\core\util\options.js 的 normalizeProps,规范化props属性

// props属性类型不符合要求,常见的错误
warn(
    `Invalid value for option "props": expected an Array or an Object, ` +
    `but got ${toRawType(props)}.`,
    vm
) 

hasOwnProperty

判断对象是否存在指定属性

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

数据转换

toNumber

转换为数字

//  将值转换为数字。若转换失败,返回原始字符串
function toNumber (val) {
  var n = parseFloat(val);
  return isNaN(n) ? val : n
}

toString

转换为字符串

function toString (val) {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)  // 针对数组或对象
      : String(val)
}

其中,JSON.stringify(val, null, 2),所代表的语句为:JSON.stringify(value[, replacer [, space]])

toArray

将类数组对象转换为数组

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
}

eg: src\core\global-api\use.js 的 initUse

 const args = toArray(arguments, 1) // arguments可理解传递给函数的参数的类数组对象

将类数组对象转换为数组,还有以下方法:

(1)Array.prototype.slice.call

(2)Array.from()

toObject

将对象数组合并到对象中

// 将属性赋予目标对象
function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}

function toObject (arr) {
  var res = {};
  for (var i = 0; i < arr.length; i++) {
    if (arr[i]) {
      extend(res, arr[i]);
    }
  }
  return res
}

eg:src\platforms\weex\runtime\modules\style.js 的 updateStyle

if (Array.isArray(style)) {
    style = vnode.data.style = toObject(style)
}

已知,v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上

capitalize

首字母大写

// cached方法见下
var capitalize = cached(function (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
});

hyphenate

驼峰式变短横线分隔

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

camelize

短横线分隔变驼峰式

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

缓存数据

cached

传参是函数,返回也是函数,用于缓存创建的函数,上述的capitalize、hyphenate均使用了cached方法

为什么要这样做?尤大大的巧思:利于性能优化。

详见说明:传送门1 传送门2

function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];  // 进行缓存
    return hit || (cache[str] = fn(str))
  })
}

makeMap

思路:定义一个数据集合,可用于判断是否为html内置标签

/**
 * @description: 
 * @param str 待判断字符串
 * @param expectsLowerCase 是否需要小写
 * @return {*}
 */
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]; }
}

eg:src\platforms\web\util\element.js 的 isHTMLTag

var isHTMLTag = makeMap(
  'html,body,base,head,link,meta,style,title,' +
  '......' +
  'content,element,shadow,template,blockquote,iframe,tfoot'
)

isHTMLTag('iframe')   // true

判断对象是否相等

looseEqual

整体思路:先进行类型判断,再递归调用

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) {
        // 若两者为数组,当长度一致时再递归判断每个数据项,不一致时返回false
        return a.length === b.length && a.every(function (e, i) {
          return looseEqual(e, b[i])
        })
      } else if (a instanceof Date && b instanceof Date) {
        // 若两者为Date类型,利用时间戳判断
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) {
        // 若两者为对象,当长度一致时再递归判断每个属性值是否相同,不一致时返回false
        var keysA = Object.keys(a);
        var keysB = Object.keys(b);
        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
  }
}

其他

remove

删除数组项

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

once

函数只执行一次

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

Vue 3.x

基于3.2.6

  • 源码链接

  • 背景: 类型检验基于TypeScript,舍弃flow,不得不提到Vue2.x 引人注目的注释

// src\core\instance\lifecycle.js
const propOptions: any = vm.$options.props // wtf flow?

// src\platforms\web\server\modules\dom-props.js
// $flow-disable-line (WTF?)
const attr = propsToAttrMap[key] || key.toLowerCase()

TypeScript vs Flow,尤大大后续也真香了,传送门

保留 & 补充

  • cacheStringFunction:保留,即原cached

  • makeMap:保留

  • looseEqual:保留,改写优化,补充 looseCompareArrays 方法,比较两数组是否相等

// 源码:packages\shared\src\looseEqual.ts
function looseCompareArrays(a, b) {
    // 若长度不一致,返回false
    if (a.length !== b.length)
        return false;
    let equal = true;
    // 若长度一致,再逐个数组项进行对比
    for (let i = 0; equal && i < a.length; i++) {
        equal = looseEqual(a[i], b[i]);
    }
    return equal;
}
  • 数据转换

    • 移除toArray、toObject

      其中toArray用Array.prototype.slice.call和Array.from代替

    • 补充toTypeString,对象转字符串

    const objectToString = Object.prototype.toString;
    const toTypeString = (value) => objectToString.call(value);
    
    toTypeString({num: 0})  // "[object Object]"
    
  • 类型判断:补充调整,更为细化

const isArray = Array.isArray;
const isMap = (val) => toTypeString(val) === '[object Map]';
const isSet = (val) => toTypeString(val) === '[object Set]';
const isDate = (val) => val instanceof Date;
const isFunction = (val) => typeof val === 'function';
const isString = (val) => typeof val === 'string';
const isSymbol = (val) => typeof val === 'symbol';
const isObject = (val) => val !== null && typeof val === 'object';
const isPromise = (val) => {
    return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};
// ……

新增

getGlobalThis

获取全局this指向对象

let _globalThis;
const getGlobalThis = () => {
    // 若_globalThis已被定义(已执行方法),则直接返回,无需再次判断
    return (_globalThis ||
        (_globalThis =
            typeof globalThis !== 'undefined'
                ? globalThis
                : typeof self !== 'undefined'
                    ? self
                    : typeof window !== 'undefined'
                        ? window
                        : typeof global !== 'undefined'
                            ? global
                            : {}));
};

def

定义对象属性

const def = (obj, key, value) => {
    Object.defineProperty(obj, key, {
        configurable: true,  // 可配置的
        enumerable: false,  // 不可枚举的
        value
    });
};

isModelListener

判断当前字符串是否以onUpdate:开头

const isModelListener = (key) => key.startsWith('onUpdate:');

String.prototype.startsWith(searchString[, position]):返回布尔值,表示参数字符串是否在原字符串的头部。

isIntegerKey

判断是否为整数键值

const isIntegerKey = (key) => isString(key) &&
    key !== 'NaN' &&
    key[0] !== '-' &&
    '' + parseInt(key, 10) === key;

invokeArrayFns

fns是数组,各元素均为函数,遍历数组执行,利于一次性执行多个函数

const invokeArrayFns = (fns, arg) => {
    for (let i = 0; i < fns.length; i++) {
        fns[i](arg);
    }
};

hasChanged

比较两个值是否相等,利用Object.is,与严格比较运算符(===)的行为基本一致,但注意:

  • +0不等于-0
  • NaN等于自身
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

hasChanged(+0, -0)  // true
hasChanged(NaN, NaN) // false

链接传动门

JSON.stringify

Class 与 Style 绑定

Object.is

Last but not least

如有不妥,请多指教~