Vue源码学习系列01——全局工具函数解析

俗话说,工欲善其事,必先利其器。在我们开始探究vue核心功能之前,先来学习一下vue源码中全局的工具函数,看看vue是如何“利其器”的。
注意,这里的工具函数对应的是src/shared/下的util.js,这些函数都是全局通用的,不涉及具体模块(响应式原理啊, 编译啊之类的)。所以介绍的时候仅从函数本身功能的角度出发来解析。阅读本篇文章的之前,你应该有良好的js基础,对于一些概念:原型,闭包,函数柯里化等都有一定的了解。
另一个建议是,你最好先了解一下flow的基本语法,它是vue2用来做代码静态检查的。由于vue3中即将使用typescript来重写,所以这里也不对flow做过多介绍了。去官网看看基本使用,会对你阅读vue2的源码有帮助。传送门: https://flow.org/

废话不多说,开始吧。

emptyObject
export  const  emptyObject  =  Object.freeze({})

作用: 创建一个不可修改的对象
拓展: 参考之前的博客,对象的三个安全级别
地址: https://blog.csdn.net/qq_25324335/article/details/79859407#t2
isUndef
export  function  isUndef (v:  any):  boolean %checks {
  return  v  ===  undefined  ||  v  ===  null
}

作用: 检查一个值是不是没有定义,这里检查了它是undefined或者null类型,满足其一就表示它已定义,返回true。
isUndef
export  function  isDef (v:  any):  boolean %checks {
  return  v  !==  undefined  &&  v  !==  null
}

作用: 检查一个值是不是定义了,必须同时满足它不是undefined类型且不是null类型。
isUndef
export  function  isTrue (v:  any):  boolean %checks {
  return  v  ===  true
}

作用: 检查一个值是不是true
isUndef
export  function  isFalse (v:  any):  boolean %checks {
  return  v  ===  false
}


作用: 检查一个值是不是false
isPrimitive
// 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'
  )
}

作用: 检查一个值的数据类型是不是简单类型(字符串/数字/symbol/布尔)
拓展: js中共有7种数据类型: Number,Undefined,Null,String,Boolean,Object,Symbol
isObject
/**
 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'
}

作用: 对象的快速检查-这主要是用来将对象从简单值中区分出来
拓展: 为什么要检查一下obj !== null呢?因为虽然在js中Null与Object是两种数据类型,但是使用typeof操符号的结果是一样的,即 typeof null === 'object', 所以这里要先排除null值的干扰
上面几个isXXX名称的函数,源码中有这样的一行注释these helpers produces better vm code in JS engines due to their explicitness and function inlining,主要是说这几个函数是用来各司其职的检查数据类型的。我们也可以借鉴这种写法,写业务逻辑的时候,应该拆分成最小的单元,这样各单元功能明确,代码可读性也更高。

toRawType
const  _toString  =  Object.prototype.toString

// Get the raw type string of a value e.g. [object Object]
export  function  toRawType (value:  any):  string {
  return  _toString.call(value).slice(8, -1)
}


作用: 获取一个值的原始类型字符串

拓展: 在任何值上调用Object原生的toString方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]的内部属性,这个属性就指定了这个NativeConstructorName的名称。例如

Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(1) // "[object Number]"
...

所以上述 toRawType 函数实际上是把 [object Number]这个字符串做了截取,返回的是类型值,如 "Number", "Boolean", "Array"等

isPlainObject
// Strict object type check. Only returns true for plain JavaScript objects.
export  function  isPlainObject (obj:  any):  boolean {
  return  _toString.call(obj) ===  '[object Object]'
}

作用: 严格的类型检查,只在是简单js对象返回true

拓展: 为什么特意加一句 Only returns true for plain JavaScript objects.呢?因为有一些值,虽然也属于js中的对象,但是有着更精确的数据类型,比如:

  Object.prototype.toString.call([]) // "[object Array]"
  Object.prototype.toString.call(()=>{}) // "[object Function]"
  Object.prototype.toString.call(null) // "[object Null]" 
  ...

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

作用: 检查一个值是不是正则表达式

拓展: 正则表达式不是对象吗?为什么不能直接使用typeof操作符检查呢? 这主要是处于兼容不同浏览器的考虑:

typeof /s/ === 'function'; // Chrome 1-12 Non-conform to ECMAScript 5.1
typeof /s/ === 'object'; // Firefox 5+ Conform to ECMAScript 5.1

参考: typeof | MDN

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

作用: 检查一个值是不是合法的数组索引,要满足:非负数, 整数, 有限大
拓展: 这主要是检查外来的值作为数组的索引的情况。
toString
// Convert a value to a string that is actually rendered.
export  function  toString (val:  any):  string {
  return  val  ==  null
      ?  '' 
      :  typeof  val  ===  'object'
         ?  JSON.stringify(val, null, 2)
         :  String(val)
}

作用: 把一个值转换成可以渲染的字符串。
拓展:
第一个判断用的是 == 而不是 ===, 所以当值为undefined或者null时都会返回空串
JSON.stringify接收三个参数,分别是要序列化的值, 要序列化的属性, 序列化所需的空格
参考: JSON.stringify() | MDN
toNumber
// Convert a 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
}

作用: 把一个值转换成数字,转化成功,返回数字,否则原样返回。
makeMap
// 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 {
  // 先创建一个map,用来存放每一项的数据
  const  map  =  Object.create(null)
  // 获取元素的集合
  const  list:  Array<string> =  str.split(',')
  // 遍历元素数组,以 键=元素名, 值=true 的形式,存进map中
  // 即如果 str = 'hello, world', 那么这个循环后
  // map = { 'hello': true, 'world': true}
  for (let  i  =  0; i  <  list.length; i++) {
    map[list[i]] =  true
  }
  // 如果需要小写(expectsLowerCase = true)的话,就将 val 的值转换成小写再检查; 否则直接检查
  // 这里的检查,就是检查传入的 val(键), 是不是在map中,在的话会根据该键名,找到对应的值(true)
  // 这里返回的是一个函数,该函数接收一个待检查的键名称,返回查找结果(true/undefined)
  return  expectsLowerCase
      ?  val  =>  map[val.toLowerCase()]
      :  val  =>  map[val]
}

作用: 生成一个map,返回一个函数来检查一个键是不是在这个map中。详解见注释。
拓展:
函数柯里化
闭包
isBuiltInTag
// Check if a tag is a built-in tag.
export  const  isBuiltInTag  =  makeMap('slot,component', true)

作用: 检查一个标签是不是vue的内置标签

解析: 从上面的makeMap函数可以知道,这个isBuiltInTag是一个函数,接收一个值作为查找的键名,返回的是查找结果。通过这个makeMap('slot,component', true)处理后,map的值已经变成

  {
    "slot": true,
    "component": true
  }

而且是检查小写,也就是说"Slot", "Component"等格式的都会返回true, 如下:

isBuiltInTag('slot')  // true
isBuiltInTag('Slot')  // true
isBuiltInTag('CoMponeNt') //true
isBuiltInTag('kobe')  // undefined(注意它是一个falsy的值,但是不是false)

isReservedAttribute
// Check if a attribute is a reserved attribute.
export  const  isReservedAttribute  =  makeMap('key,ref,slot,slot-scope,is')

作用: 检查一个属性是不是vue的保留属性。同上。
remove
// 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)
    }
  }
}

作用: 移除数组的某一项。
hasOwn
// Check whether the object has the property.
const  hasOwnProperty  =  Object.prototype.hasOwnProperty
export  function  hasOwn (obj:  Object  |  Array<*>, key:  string):  boolean {
  return  hasOwnProperty.call(obj, key)
}

作用: 检查一个对象是否包含某属性, 这个属性不包括原型链上的属性(toString之类的)。
拓展: 这里要思考的是,为什么不直接调用对象上的hasOwnProperty方法,反而要找对象原型上的呢?原因其实很简单,因为对象上的hasOwnProperty是可以被改写的,万一被重写了方法就无法实现这种检查了。
也可以这么简写: ({}).hasOwnProperty.call(obj, key)

cached
// Create a cached version of a pure function.
export  function  cached<F:  Function> (fn:  F):  F {
  // const cache = {}
  // 这个const相当于一个容器,盛放着 key-value 们
  const  cache  =  Object.create(null)
  // 返回的是一个函数表达式cacheFn
  return (function  cachedFn (str:  string) {
    const  hit  =  cache[str]
    // 如果命中了,那么拿缓存值; 反之就是第一次执行计算,调用函数fn计算并且装入 cache 容器
    // cache 容器中的键值对, key是函数入参, value是函数执行结果
    return  hit  || (cache[str] =  fn(str))
  }: any)
}

作用: 为一个纯函数创建一个缓存的版本。
解析: 因为一个纯函数的返回值只跟它的参数有关,所以我们可以将入参作为key,返回值作为value缓存成key-value的键值对,这样如果之后还想获取之前同样参数的计算结果时,不需要再重新计算了,直接获取之前计算过的结果就行。
拓展: 纯函数:一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,则该函数可以称为纯函数。更多用法请百度。
camelize
// Camelize a hyphen-delimited string.
const  camelizeRE  = /-(\w)/g
export  const  camelize  =  cached((str:  string):  string  => {
  return  str.replace(camelizeRE, (_, c) =>  c  ?  c.toUpperCase() :  '')
})

作用: 将连字符-连接的字符串转化成驼峰标识的字符串

解析: 可以通过此例来感受下上面的cached函数的作用,因为cache的参数, 一个转换连字符字符串的函数,是一个很纯很纯的函数,所以可以把它缓存起来。

使用:

camelize('hello-world') // 'HelloWorld'

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

作用: 将一个字符串的首字母大写后返回,也是可以被缓存的函数。

使用:

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

作用: 将一个驼峰标识的字符串转换成连字符-连接的字符串

使用:

hyphenate('HelloWorld') // 'hello-world'

polyfillBind
/**
* 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 more performant in most browsers, but removing it would be breaking for
* code that was able to run in PhantomJS 1.x, so this must be kept for
* backwards compatibility.
**/
function  polyfillBind (fn:  Function, ctx:  Object):  Function {
  function  boundFn (a) {
    // 获取函数参数个数
    // 注意这个arguments是boundFn的,不是polyfillBind的
    const  l  =  arguments.length
    // 如果参数不存在,直接绑定作用域调用该函数
    // 如果存在且只有一个,那么调用fn.call(ctx, a), a是入参
    // 如果存在且不止一个,那么调用fn.apply(ctx, arguments)
    return  l
      ?  l  >  1 ? 
      fn.apply(ctx, arguments)
      :  fn.call(ctx, a)
      :  fn.call(ctx)
  }
  boundFn._length  =  fn.length
  return  boundFn
}


作用: bind 函数的简单的 polyfill。因为有的环境不支持原生的 bind, 比如: PhantomJS 1.x。技术上来说我们不需要这么做,因为现在大多数浏览器都支持原生bind了,但是移除这个吧又会导致在PhantomJS 1.x 上的代码出错,所以为了向后兼容还是留住。

拓展: call与apply的区别,call接受参数是一个一个接收,apply是作为数组来接收。如:

fn.call(this, 1,2,3)
fn.apply(this, [1,2,3])

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

作用: 原生的bind。
bind
export  const  bind  =  Function.prototype.bind
  ?  nativeBind
  :  polyfillBind

作用: 导出的bind函数,如果浏览器支持原生的bind则用原生的,否则使用polyfill版的bind。
toArray
// Convert an Array-like object to a real Array.
export  function  toArray (list:  any, start?:  number):  Array<any> {
  // start为开始拷贝的索引,不传的话默认0,代表整个类数组元素的的转换
  start  =  start  ||  0
  let  i  =  list.length  -  start
  const  ret:  Array<any> =  new  Array(i)
  while (i--) {
    ret[i] =  list[i  +  start]
  }
  return  ret
}

作用: 将类数组的对象转换成一个真正的数组。实际上就是一个元素逐一复制到另一个数组的过程。
extend
// Mix properties into target object.
export  function  extend (to:  Object, _from:  ?Object):  Object {
  for (const  key  in  _from) {
    to[key] =  _from[key]
  }
  return  to
}

作用: 将属性混入到目标对象中,返回被增强的目标对象。
toObject
// 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]) {
      // 遍历这个对象,将属性都拷贝到res中
      extend(res, arr[i])
    }
  }
  return  res
}


作用: 将一个对象数组合并到一个单一对象中去。

解析: 函数的输入和输出是这样的:

const arr = [{age: 12}, {name: 'jerry', age: 24}, {major: 'js'}]
const res = toObject(arr)
console.info(res) // {age: 24, name: 'jerry', major: 'js'}

noop
// 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) {}

作用: 一个空函数。在这里插入的参数,是为了避免 Flow 使用 rest操作符(…) 产生无用的转换代码。
no
// Always return false.
export  const  no  = (a?:  any, b?:  any, c?:  any) =>  false

作用: 总是返回false
identity
//  Return same value
export  const  identity  = (_:  any) =>  _

作用: 返回自身。
genStaticKeys
// Generate a static keys string from compiler modules.
export  function  genStaticKeys (modules:  Array<ModuleOptions>):  string {
  return  modules.reduce((keys, m) => {
    return  keys.concat(m.staticKeys  || [])
  }, []).join(',')
}

作用: 从编译器模块中生成一个静态的 键 字符串。
这个函数的作用,我目前不是很清楚编译器模块是个什么东西…就函数本身功能而言,接收一个对象数组,然后取出其中的 staticKeys 的值(是个数组), 拼成一个keys的数组,再返回这个数组的字符串形式,用,连接的,如:'hello,wolrd,vue'

looseEqual
// 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 {
  // 如果全等,返回true
  if (a  ===  b) return  true
  const  isObjectA  =  isObject(a)
  const  isObjectB  =  isObject(b)
  if (isObjectA  &&  isObjectB) {
  // 如果 a 和 b 都是对象的话
    try {
      const  isArrayA  =  Array.isArray(a)  
      const  isArrayB  =  Array.isArray(b)
      // 如果 a 和 b 都是数组
      if (isArrayA  &&  isArrayB) {
          // 长度相等 且 每一项都相等(递归)
          return  a.length  ===  b.length  &&  a.every((e, i) => {
            return  looseEqual(e, b[i])
          })
      } else  if (!isArrayA  &&  !isArrayB) {
          // 如果 a 和 b 都不是数组
          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 {
          // 不满足上面的两种都返回 false
          return  false
      }
    } catch (e) {
        // 发生异常了也返回 false
        return  false
    }
  } else  if (!isObjectA  &&  !isObjectB) {
    // 如果 a 和 b 都不是对象,则转成String来比较字符串来比较
    return  String(a) ===  String(b)
  } else {
    // 其余返回 false
    return  false
  }
}

作用: 判断两个值是否相等。是对象吗?结构相同吗?
解析: 为什么叫loosely equal呢?因为两个对象是不相等的,这里比较的只是内部结构和数据。代码详解见注释。
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表示没找到。方法很简单,就是上面looseEqual的一个使用场景。
once
// 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)
    }
  }
}

作用: 确保一个函数只执行一次。
解析: 这里是闭包的一个运用,该函数返回的是一个函数,不过这个函数是由called这个变量决定的。第一次调用后这个值就被设置成true了,之后再调用,由于闭包的存在,called这个标记变量会被访问到,这时已经是true了,就不会在调用了。
--------------------- 
作者:袁杰Jerry 
来源:CSDN 
原文:https://blog.csdn.net/qq_25324335/article/details/84948363 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值