vuejs源码解剖 — 工具方法篇

1,291 阅读4分钟

debug.js

正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。

该文件主要暴露了四个方法warntipgenerateComponentTraceformatComponentName,主要代码如下

//noop是从shared/util中引入的空方法
export function noop (a?: any, b?: any, c?: any) {}

//debug.js
export let warn = noop
export let tip = noop
export let generateComponentTrace = (noop: any) // work around flow check
export let formatComponentName = (noop: any)
if (process.env.NODE_ENV !== 'production') {
	//暂时忽略此部分
}

主要在调试中使用,生产环境下他们就仅仅是个空Function,不做任何处理,只有在开发环境下这四个变量才会被重新定义。

注:源码中以后会经常遇到process.env.NODE_ENV !== 'production'这样的写法,这是从性能角度考虑,一些检测、警告等非必要的提示只有在非生产环境才会做处理,以此达到最优性能。

//检测当前环境是否支持 console
const hasConsole = typeof console !== 'undefined'
//正则表达式
const classifyRE = /(?:^|[-_])(\w)/g
//转换字符串格式
const classify = str => str
    .replace(classifyRE, c => c.toUpperCase())
    .replace(/[-_]/g, '')

前面两个相对好理解,classify的目的是使用正则classifyRE将字符串中横岗写法转换成驼峰式。例如:

classify('fishing-wang');
//输出 FisingWang
//重写打印或警告内容到客户端
warn = (msg, vm) => {
  const trace = vm ? generateComponentTrace(vm) : ''
  if (config.warnHandler) {
    config.warnHandler.call(null, msg, vm, trace)
  } else if (hasConsole && (!config.silent)) {
    console.error(`[Vue warn]: ${msg}${trace}`)
  }
}

tip = (msg, vm) => {
  if (hasConsole && (!config.silent)) {
    console.warn(`[Vue tip]: ${msg}` + (
      vm ? generateComponentTrace(vm) : ''
    ))
  }
}

env.js

正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。

检测当前宿主环境对变量的支持情况。

//检测当前浏览器是否支持 '__proto__'
export const hasProto = '__proto__' in {}
//检测是否浏览器环境
export const inBrowser = typeof window !== 'undefined'
//检测当前是否是weex环境
export const inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform
//是否weex平台
export const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase()
//先确保当前环境是浏览器环境,然后将浏览器的use Agent信息保存在 UA 变量中 (非浏览器环境则直接返回false)
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
//是否IE浏览器
export const isIE = UA && /msie|trident/.test(UA)
//是否IE9浏览器
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
//是否Edge浏览器
export const isEdge = UA && UA.indexOf('edge/') > 0
//是否安卓环境
export const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
//是否IOS环境
export const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
//是否Chrome环境
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
//当前 window.navigator.userAgent 是否含有 phantomjs 关键字
export const isPhantomJS = UA && /phantomjs/.test(UA)
//是否火狐浏览器环境
export const isFF = UA && UA.match(/firefox\/(\d+)/)
export const nativeWatch = ({}).watch

在火狐浏览器中原生提供了Object.prototype.watch方法。所以只有当前宿主环境为火狐的情况下,nativeWatch才会返回正确的函数,否则直接返回undefined。也可以用于区分VUE实例中的watch,防止冲突产生意外情况。

//通过监听调用test-passive检测当前浏览器是否支持passive。
//其中代码用try catch 包裹起来,说明支持性较差,容易出错
export let supportsPassive = false
if (inBrowser) {
  try {
    const opts = {}
    Object.defineProperty(opts, 'passive', ({
      get () {
        supportsPassive = true
      }
    }: Object))
    window.addEventListener('test-passive', null, opts)
  } catch (e) {}
}

//检测是否服务端环境
let _isServer
export const isServerRendering = () => {
  if (_isServer === undefined) {
    if (!inBrowser && !inWeex && typeof global !== 'undefined') {
      _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'
    } else {
      _isServer = false
    }
  }
  return _isServer
}

//检测是否开发工具环境
export const devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__
//检测当前方法是否是原生js提供的
export function isNative (Ctor: any): boolean {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

//检测当前宿主环境是否支持原生 symbol Reflect.ownKeys, 返回布尔值
export const hasSymbol =
  typeof Symbol !== 'undefined' && isNative(Symbol) &&
  typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)

lang.js

正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。

本文件只暴露一个变量,三个方法

export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/

unicode正则表达式,用于解析template模板

export function isReserved (str: string): boolean {
  const c = (str + '').charCodeAt(0)
  return c === 0x24 || c === 0x5F
}

检测字符串是否是以_或者美元符号$开头,主要用来检测VUE组件实例中变量的命名是否符合规范(VUE不建议以_$为开头的字符串命名变量,这是框架保留命的名方式)。

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

应该是为了方便调用,作者将Object.defineProperty做了一个简单的封装。def函数支持四个参数,分别是源对象本身、key值、value值、是否可枚举,最后一个参数enumerable非必填,默认不可枚举。

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

对路径进行一个简单的解析,单看这个方法其实比较简单:若path符合正则(例如含有~/*)则返回(其实是边界处理,确保不传入异常值而已),否则用.切割path成一个数组,保存在变量segments中,随后返回一个函数,函数内遍历访问segments得到一个对象。此方法主要用于初始化watcher的时候触发值的读取。

perf.js

正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。

性能监控专用

定义非生产环境用于监控性能的两个变量markmeasure,生产环境无需监控,直接返回undefined。源代码如下:

import { inBrowser } from './env'

export let mark
export let measure

if (process.env.NODE_ENV !== 'production') {
  const perf = inBrowser && window.performance
  /* istanbul ignore if */
  if (
    perf &&
    perf.mark &&
    perf.measure &&
    perf.clearMarks &&
    perf.clearMeasures
  ) {
    mark = tag => perf.mark(tag)
    measure = (name, startTag, endTag) => {
      perf.measure(name, startTag, endTag)
      perf.clearMarks(startTag)
      perf.clearMarks(endTag)
      // perf.clearMeasures(name)
    }
  }
}

当且仅当宿主是浏览器且在非生产环境下,perf就是window.performance。if语句中做了多个判断,就是为了确保当前环境支持window.performance。之后重新定义markmeasure,以上代码在非生产环境下简化后的结果就是:

const mark = tag => window.performance.mark(tag);
const measure = (name,startTag,endTag) => {
	window.performance.measure(name,startTag,endTag)
	window.performance.clearMarks(startTag)
	window.performance.clearMarks(endTag)
}

window.performance是什么?

Web Performance API允许网页访问某些函数来测量网页和Web应用程序的性能。详细介绍请移步window.performance

shared/util.js

正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。

emptyObject

export const emptyObject = Object.freeze({})

创建一个冻结的空对象,这就意味着emptyObject不可扩展,也就是不能添加新的属性

isUndef

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

判断变量是否未定义,也就是判断是否是定义了未赋值,或者赋值null的情况

isDef

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

判断变量是否定义

isTrue

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

判断变量是否为true

isFalse

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

判断变量是否为false

isPrimitive

export function isPrimitive (value: any): boolean %checks {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

判断变量是否是原始值,也就是判断变量是否未非复合型数据

isObject

export function isObject (obj: mixed): boolean %checks {
  return obj !== null && typeof obj === 'object'
}

判断变量是否是 “对象” 或者 “数组”

toRawType

const _toString = Object.prototype.toString
export function toRawType (value: any): string {
  return _toString.call(value).slice(8, -1)
}

判断类型后返回原始字符串类型。例如判断一个数组:Object.prototype.toString.call([])得到的结果是[object Array],再使用slice(8,-1)后得到的结果就是Array字符串

isPlainObject

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

判断变量是否是纯对象

isRegExp

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

判断变量是否是正则对象

isValidArrayIndex

export function isValidArrayIndex (val: any): boolean {
  const n = parseFloat(String(val))
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

判断变量是否是有效的数组索引。

  • 正常的数组索引格式应该是大于等于0的整数
  • 先将变量解析并返回一个浮点数,保存在变量n中
  • 当且仅当变量大于等于0,且是整数,并且是一个有限数值的情况下才会返回true

isPromise

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

判断一个变量是否是Promise

toString

export function toString (val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

将变量转换成字符串格式

  • 若是null,则返回空字符串
  • 若是数组或纯对象,则使用JSON.stringify处理
  • 其他情况直接String函数处理

toNumber

export function toNumber (val: string): number | string {
  const n = parseFloat(val)
  return isNaN(n) ? val : n
}

将变量转换成数字类型,若转换失败(比如转换字符串"aaa"得到的结果是NaN)则返回初始值

makeMap

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]
}

返回一个函数,判断给定的字符串中是否包含指定内容(映射)。

str 数据格式: 以,间隔的字符串,例如:a,b,c,d,e
expectsLowerCase:是否将map的值key小写

使用示例(检测是否是小写的元音字母):

let isLower = makeMap('a,b,c,d,e');
isLower('b');  // true
isLower('f');  // undefined

remove

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)
    }
  }
}

从数组中删除给定元素,并返回被删除项组成的数组:[item]

在数组不为空的情况下,获取当前元素序号,当元素存在的前提下(大于-1),则使用splice方法删除,并返回被删除的元素组成的新数组,否则返回undefined

使用示例:

let arr = ['a','b','c'];
remove(arr,'a');  // ['a']
//arr变成 ['b','c']

hasOwn

// 封装`Object.prototype.hasOwnProperty`,用于检测给定对象中是否含有给定`key`值
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
  return hasOwnProperty.call(obj, key)
}

cached

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)
}

为一个纯函数创建创建一个缓存版本的函数。输入的参数必须是一个纯函数,得到的返回也是一个纯函数。

  • 首先我们要确认为何一定要传一个纯函数,因为纯函数的输出结果只与输入有关,所运行的环境不能改变输出值。
  • 创建一个原型链为空的闭包对象cache用以缓存结果。
  • 随后返回一个函数 cachedFn,优先读取缓存,如果有则直接返回返回的值,否则使用原函数fn计算一次并缓存结果。

camelize

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

将中横线连字符转换成驼峰式命名

capitalize

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

将字符串首字母改成大写

hyphenate

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

camelize方法相反,将驼峰式命名改成连字符方式。例如:hyphenateRE('aaBb') => aa-bb

polyfillBind

nativeBind

bind

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

以上其实就是一个bind函数,绑定this指向后返回的一个新函数。

  • polyfillBind为手动写的一个绑定函数
  • nativeBind Function原型链上的原生绑定函数
  • bind 优先判断是否支持原生绑定函数,若支持则优先使用原生,否则使用手动实现的函数

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--) {
    ret[i] = list[i + start]
  }
  return ret
}

从指定位置(默认从0开始)开始将类数组转换成数组的方法。

  • list 类数组
  • start可选的开始索引位置

extend

export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

将一个对象(_from)的数据拷贝到另一个对象(to)上。若key值有重复,则_from中的会替换to中的数据。注意:若value值为复合型数据,则是浅拷贝。

toObject

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
}

遍历数组每个对象元素,将内容拷贝到一个新对象上,并返回新对象。for循环遍历数组,与extend函数一起使用,拷贝到新对象res中,逻辑很简单。

noop

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

一个空函数,什么都不做。

no

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

始终返回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(',')
}

从编译器模块生成包含静态键的字符串。

  • modules是一个数组,是编译器的一个选项,其中每个元素都是一个可能包含staticKeys属性的对象。此方法的目的就是遍历数组元素,收集staticKeys,隔开组成的拼接字符串。

looseEqual

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
  }
}

判断两个值是否全等。

looseIndexOf

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
}

查找给定元素是否在指定数组中,若存在则返回当前元素所在数组中的索引,否则返回-1。

once

export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

利用闭包的特性实现一个只调用一次的函数。