深入剖析 Vue 公共方法模块原理
本人掘金号,欢迎点击关注:掘金号地址
本人公众号,欢迎点击关注:公众号地址
一、引言
Vue.js 作为一款流行的前端框架,以其简洁易用、高效灵活的特点受到广泛开发者的青睐。在 Vue 的源码实现中,存在着诸多公共方法模块,这些模块为整个框架的稳定运行和功能拓展提供了基础支撑。深入理解这些公共方法模块的原理,不仅有助于开发者更好地运用 Vue 进行项目开发,还能在面试和技术交流中展现扎实的技术功底。本文将从源码级别深入分析 Vue 的公共方法模块,带大家领略其背后的精妙设计。
二、Vue 公共方法模块概述
2.1 公共方法模块的重要性
Vue 的公共方法模块包含了一系列通用的工具函数和辅助方法,它们在 Vue 源码的各个部分被广泛使用。这些方法的设计遵循了高内聚、低耦合的原则,使得代码的复用性和可维护性得到了极大提升。通过对这些公共方法的封装,Vue 能够更加高效地处理各种复杂的逻辑,例如数据的响应式处理、虚拟 DOM 的创建和更新、生命周期钩子的调用等。
2.2 公共方法模块的分类
Vue 的公共方法模块可以大致分为以下几类:
- 数据处理相关方法:用于处理数据的响应式、深拷贝、浅拷贝等操作。
- 类型判断相关方法:用于判断数据的类型,如是否为对象、数组、函数等。
- 数组操作相关方法:对数组进行一些特殊操作,如插入、删除元素等。
- 对象操作相关方法:对对象进行合并、扩展等操作。
- 事件处理相关方法:处理事件的绑定、触发和取消等操作。
- 其他辅助方法:包括一些工具函数,如日志输出、错误处理等。
三、数据处理相关公共方法
3.1 响应式数据处理方法
3.1.1 observe
方法
javascript
javascript
// src/core/observer/index.js
import Dep from './dep'
import VNode from '../vdom/vnode'
import { arrayMethods } from './array'
import {
def,
warn,
hasOwn,
hasProto,
isObject,
isPlainObject,
isPrimitive,
isUndef,
isValidArrayIndex,
isServerRendering
} from '../util/index'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
* In some cases we may want to disable observation inside a component's
* update computation.
*/
export let shouldObserve = true
export function toggleObserving(value) {
shouldObserve = value
}
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
// 如果支持原型链,将数组的原型指向重写后的数组方法
value.__proto__ = arrayMethods
} else {
// 不支持原型链,直接将重写后的数组方法赋值给数组
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
// 对数组中的每个元素进行响应式处理
this.observeArray(value)
} else {
// 对对象进行响应式处理
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 对对象的每个属性进行响应式处理
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 对数组中的每个元素进行响应式处理
observe(items[i])
}
}
}
// Attempt to create an observer instance for a value,
// returns the new observer if successfully observed,
// or the existing observer if the value already has one.
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
// 如果不是对象或者是虚拟节点,直接返回
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 如果对象已经有观察者,直接使用该观察者
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建一个新的观察者实例
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
// ... 后续代码继续分析
observe
方法是 Vue 实现响应式数据的核心方法之一。它的主要作用是对传入的数据对象进行观察,将其转换为响应式数据。在方法内部,首先会检查传入的值是否为对象,如果不是对象或者是虚拟节点,则直接返回。接着会检查对象是否已经有观察者,如果有则直接使用该观察者。如果没有,则创建一个新的 Observer
实例来处理该对象。对于数组,会对数组的原型进行处理,重写数组的一些方法,以便在数组元素发生变化时能够触发更新。对于对象,会遍历其所有属性,使用 defineReactive
方法将其转换为响应式属性。
3.1.2 defineReactive
方法
javascript
javascript
// src/core/observer/index.js
import Dep from './dep'
import { arrayMethods } from './array'
import {
def,
warn,
hasOwn,
hasProto,
isObject,
isPlainObject,
isPrimitive,
isUndef,
isValidArrayIndex,
isServerRendering
} from '../util/index'
// ... 前面的代码
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() // 创建一个依赖收集器
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
// 如果属性不可配置,直接返回
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
// 如果没有 getter 或者有 setter 且参数长度为 2,获取对象的原始值
val = obj[key]
}
let childOb =!shallow && observe(val) // 递归观察子对象
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter? getter.call(obj) : val
if (Dep.target) {
// 如果存在当前依赖目标,进行依赖收集
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
// 对数组元素进行依赖收集
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal!== newVal && value!== value)) {
// 如果新值和旧值相同,直接返回
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV!== 'production' && customSetter) {
customSetter()
}
if (setter) {
// 如果有 setter,调用 setter 设置新值
setter.call(obj, newVal)
} else {
val = newVal
}
childOb =!shallow && observe(newVal) // 递归观察新值
dep.notify() // 通知依赖更新
}
})
}
// ... 后续代码继续分析
defineReactive
方法用于将对象的属性转换为响应式属性。它接受一个对象、属性名、属性值等参数。在方法内部,首先创建一个 Dep
实例用于依赖收集。然后获取对象属性的原始描述符,包括 getter
和 setter
。接着递归观察属性值,如果属性值是对象或数组,会对其进行响应式处理。在 getter
中,会进行依赖收集,将当前依赖目标添加到 Dep
的依赖列表中。在 setter
中,会检查新值是否与旧值相同,如果不同则更新值,并递归观察新值,最后通知所有依赖进行更新。
3.2 数据拷贝相关方法
3.2.1 clone
方法(浅拷贝)
javascript
javascript
// src/shared/util.js
/**
* 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() : '')
})
/**
* 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, faster than native
*/
export function bind (fn: Function, ctx: Object): Function {
return function (a) {
const l = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
}
/**
* 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
/**
* Return same value
*/
export const identity = (_: any) => _
/**
* Check if value is primitive
*/
export function isPrimitive (value: any): boolean {
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: any): boolean {
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]'
}
/**
* Check if a value is a function
*/
export function isFunction (obj: any): boolean {
return typeof obj === 'function'
}
/**
* Check if a value is a Promise
*/
export function isPromise (val: any): boolean {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
/**
* 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)
}
/**
* 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 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
}
/**
* 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 the object has the property.
*/
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<any>, 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() : '')
})
/**
* 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, faster than native
*/
export function bind (fn: Function, ctx: Object): Function {
return function (a) {
const l = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
}
/**
* 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
}
/**
* 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
/**
* Return same value
*/
export const identity = (_: any) => _
/**
* Check if value is primitive
*/
export function isPrimitive (value: any): boolean {
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: any): boolean {
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]'
}
/**
* Check if a value is a function
*/
export function isFunction (obj: any): boolean {
return typeof obj === 'function'
}
/**
* Check if a value is a Promise
*/
export function isPromise (val: any): boolean {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
/**
* 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)
}
/**
* 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 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
}
/**
* 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 the object has the property.
*/
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<any>, 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() : '')
})
/**
* 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, faster than native
*/
export function bind (fn: Function, ctx: Object): Function {
return function (a) {
const l = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
}
/**
* 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
}
/**
* Extend an object with the properties from another object.
*/
export function extend (to: Object, _from:? Object): Object {
for (const key in _from) {
// 将源对象的属性复制到目标对象中
to[key] = _from[key]
}
return to
}
/**
* Shallow copy an object.
*/
export function clone (obj: any): any {
if (Array.isArray(obj)) {
// 如果是数组,创建一个新数组并复制元素
return obj.slice()
} else if (isPlainObject(obj)) {
// 如果是普通对象,创建一个新对象并复制属性
const res = {}
for (const key in obj) {
res[key] = obj[key]
}
return res
}
return obj
}
clone
方法用于实现浅拷贝。它首先判断传入的值是否为数组,如果是数组,则使用 slice
方法创建一个新的数组并复制元素。如果是普通对象,则创建一个新对象,并将原对象的属性复制到新对象中。对于其他类型的值,直接返回该值。浅拷贝只复制对象的一层属性,如果对象的属性是引用类型,新对象和原对象的这些属性会指向同一个引用。
3.2.2 deepClone
方法(深拷贝)
javascript
javascript
// src/shared/util.js
// 继续在 util.js 文件中添加深拷贝方法
/**
* Deep clone an object.
*/
export function deepClone (obj: any): any {
if (isPrimitive(obj)) {
// 如果是原始类型,直接返回
return obj
}
if (Array.isArray(obj)) {
// 如果是数组,递归深拷贝每个元素
const newArr = []
for (let i = 0; i < obj.length; i++) {
newArr[i] = deepClone(obj[i])
}
return newArr
}
if (isPlainObject(obj)) {
// 如果是普通对象,递归深拷贝每个属性
const newObj = {}
for (const key in obj) {
newObj[key] = deepClone(obj[key])
}
return newObj
}
return obj
}
deepClone
方法用于实现深拷贝。它首先判断传入的值是否为原始类型,如果是则直接返回。如果是数组,会创建一个新数组,并递归地对数组中的每个元素进行深拷贝。如果是普通对象,会创建一个新对象,并递归地对对象的每个属性进行深拷贝。深拷贝会创建一个完全独立的对象,新对象和原对象的所有属性都指向不同的引用。
四、类型判断相关公共方法
4.1 isObject
方法
javascript
javascript
// src/shared/util.js
/**
* 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: any): boolean {
return obj!== null && typeof obj === 'object'
}
isObject
方法用于判断一个值是否为对象。它通过检查值是否不为 null
且类型为 object
来进行判断。在 Vue 中,很多地方需要判断一个值是否为对象,例如在响应式处理中,只有对象类型的值才会进行递归观察。
4.2 isPlainObject
方法
javascript
javascript
// src/shared/util.js
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
export function isPlainObject (obj: any): boolean {
return Object.prototype.toString.call(obj) === '[object Object]'
}
isPlainObject
方法用于判断一个值是否为普通的 JavaScript 对象。它通过 Object.prototype.toString.call
方法来获取值的类型字符串,并与 [object Object]
进行比较。普通的 JavaScript 对象是指通过 {}
或 new Object()
创建的对象,不包括数组、函数等其他类型的对象。
4.3 isFunction
方法
javascript
javascript
// src/shared/util.js
/**
* Check if a value is a function
*/
export function isFunction (obj: any): boolean {
return typeof obj === 'function'
}
isFunction
方法用于判断一个值是否为函数。它通过检查值的类型是否为 function
来进行判断。在 Vue 中,很多地方会传入函数作为回调,因此需要对传入的值进行类型判断。
五、数组操作相关公共方法
5.1 remove
方法
javascript
javascript
// src/shared/util.js
/**
* 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)
}
}
}
remove
方法用于从数组中移除指定的元素。它首先检查数组的长度,如果数组不为空,则使用 indexOf
方法查找元素的索引。如果找到元素,则使用 splice
方法从数组中移除该元素。
5.2 insert
方法(自定义实现)
javascript
javascript
// 可以在 util.js 中添加自定义的 insert 方法
/**
* Insert an item into an array at a specified index.
*/
export function insert (arr: Array<any>, index: number, item: any): Array<any> {
if (index < 0 || index > arr.length) {
// 如果索引超出范围,抛出错误
throw new Error('Index out of range')
}
// 在指定索引位置插入元素
arr.splice(index, 0, item)
return arr
}
insert
方法用于在数组的指定索引位置插入一个元素。它首先检查索引是否合法,如果索引超出范围,则抛出错误。然后使用 splice
方法在指定索引位置插入元素。
六、对象操作相关公共方法
6.1 extend
方法
javascript
javascript
// src/shared/util.js
/**
* Extend an object with the properties from another object.
*/
export function extend (to: Object, _from:? Object): Object {
for (const key in _from) {
// 将源对象的属性复制到目标对象中
to[key] = _from[key]
}
return to
}
extend
方法用于将一个对象的属性复制到另一个对象中。它遍历源对象的所有属性,并将这些属性复制到目标对象中。如果目标对象已经存在相同的属性,则会被覆盖。
6.2 mergeOptions
方法
javascript
javascript
// src/core/util/options.js
import {
warn,
extend,
hasOwn,
camelize,
toRawType,
capitalize,
isBuiltInTag,
isPlainObject
} from '../util/index'
/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
*/
const strats = config.optionMergeStrategies
/**
* Options with restrictions
*/
if (process.env.NODE_ENV!== 'production') {
strats.el = strats.propsData = function (parent, child, vm, key) {
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
}
/**
* Helper that recursively merges two data objects together.
*/
function mergeData (to: Object, from:? Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
// 如果目标对象没有该属性,直接复制
set(to, key, fromVal)
} else if (toVal!== fromVal && isPlainObject(toVal) && isPlainObject(fromVal)) {
// 如果属性值都是对象,递归合并
mergeData(toVal, fromVal)
}
}
return to
}
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
):?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function'? childVal.call(this, this) : childVal,
typeof parentVal === 'function'? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
):?Function {
if (!vm) {
if (childVal && typeof childVal!== 'function') {
process.env.NODE_ENV!== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal:?Array<Function>,
childVal:?Function |?Array<Function>
):?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)? childVal : [childVal]
: parentVal
return res? dedupeHooks(res) : res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
strats.beforeCreate = strats.created = mergeHook
strats.beforeMount = strats.mounted = mergeHook
strats.beforeUpdate = strats.updated = mergeHook
strats.beforeDestroy = strats.destroyed = mergeHook
strats.activated = strats.deactivated = mergeHook
strats.errorCaptured = mergeHook
/**
* Assets
*
* When a vm is present (instance creation), we need to do
* a three-way merge between constructor options, instance
* options and parent options.
javascript
javascript
// 继续分析 mergeOptions 方法,在 src/core/util/options.js 中
// 前面代码已定义了部分策略函数和辅助函数
strats.components = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component
): ?Object {
const res = Object.create(null);
if (parentVal) {
for (const key in parentVal) {
res[key] = parentVal[key];
}
}
if (childVal) {
for (const key in childVal) {
res[key] = childVal[key];
}
}
return res;
};
strats.directives = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component
): ?Object {
const res = Object.create(null);
if (parentVal) {
for (const key in parentVal) {
res[key] = parentVal[key];
}
}
if (childVal) {
for (const key in childVal) {
res[key] = childVal[key];
}
}
return res;
};
strats._parentVnode = function () {
return null;
};
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component
): ?Object {
if (!childVal) return parentVal;
if (!parentVal) return childVal;
const ret = {};
extend(ret, parentVal);
for (const key in childVal) {
let parent = ret[key];
const child = childVal[key];
if (!parent) {
ret[key] = child;
} else if (Array.isArray(parent)) {
if (Array.isArray(child)) {
ret[key] = parent.concat(child);
} else {
ret[key] = parent.concat([child]);
}
} else {
ret[key] = [parent, child];
}
}
return ret;
};
// 默认的合并策略
const defaultStrat = function (parentVal, childVal) {
return childVal === undefined? parentVal : childVal;
};
// 最终的 mergeOptions 函数
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
const options = {};
let key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
}
mergeOptions
方法在 Vue 中用于合并两个选项对象,这在组件继承、插件安装等场景中频繁使用。它的核心逻辑是遍历父选项和子选项,根据不同的属性名(如data
、components
、directives
等)应用对应的合并策略函数。
比如对于data
属性,strats.data
函数会处理其合并逻辑。如果childVal
存在且不是函数,在非生产环境会发出警告,因为data
选项在组件定义中应该是一个返回实例特定值的函数。之后会调用mergeDataOrFn
函数,该函数根据是否是 Vue.extend 合并(无vm
实例)还是实例合并(有vm
实例)来返回不同的合并结果。在实例合并时,会分别调用childVal
和parentVal
函数获取实例数据和默认数据,然后通过mergeData
函数递归合并这两个数据对象。
对于components
和directives
属性,合并策略是创建一个新对象,先将父选项中的属性复制进去,再复制子选项中的属性,这样子选项的属性会覆盖父选项中同名的属性。
对于watch
属性,合并逻辑更为复杂。如果childVal
不存在则返回parentVal
,反之亦然。如果两者都存在,会创建一个新对象ret
,先将parentVal
扩展到ret
中,然后遍历childVal
。对于每个childVal
中的键,如果ret
中不存在该键对应的属性,则直接将childVal
中的属性值赋给ret
;如果ret
中已存在该键且其值是数组,若childVal
中的值也是数组,则将两者拼接,否则将childVal
的值作为新元素添加到数组中;若ret
中已存在该键且其值不是数组,则将ret
中的值和childVal
的值组成一个新数组。
最后,mergeOptions
函数遍历父选项和子选项,对每个属性调用相应的合并策略函数(若没有特定策略则使用defaultStrat
),将合并结果存入新的options
对象并返回。
6.3 def
方法
javascript
javascript
// src/shared/util.js
/**
* Define a property.
*/
export function def (
obj: Object,
key: string,
val: any,
enumerable?: boolean
) {
Object.defineProperty(obj, key, {
value: val,
enumerable:!!enumerable,
writable: true,
configurable: true
});
}
def
方法是一个简单但实用的工具函数,用于在对象上定义属性。它接受四个参数:要定义属性的对象obj
、属性名key
、属性值val
以及一个可选的布尔值enumerable
,用于指定该属性是否可枚举。在 Vue 源码中,这个方法被广泛用于一些内部属性的定义,比如在响应式数据处理中,给观察的对象添加__ob__
属性来标记该对象已被观察,使用的就是def
方法。通过Object.defineProperty
方法,它精确地控制了属性的特性,使得这些内部属性在 Vue 的运行机制中能按照预期工作,同时避免了这些属性在不必要的情况下被意外访问或修改,保证了框架内部数据结构的完整性和稳定性。
七、事件处理相关公共方法
7.1 on
方法(简化模拟,实际 Vue 事件处理更复杂)
javascript
javascript
// 假设在一个自定义的 event-util.js 文件中模拟简单的 on 方法
const eventMap = {};
/**
* 模拟为元素或对象绑定事件
* @param {Object} target - 绑定事件的目标对象
* @param {string} eventName - 事件名称
* @param {Function} handler - 事件处理函数
*/
export function on (target, eventName, handler) {
if (!eventMap[target]) {
eventMap[target] = {};
}
if (!eventMap[target][eventName]) {
eventMap[target][eventName] = [];
}
eventMap[target][eventName].push(handler);
}
在 Vue 实际的源码中,事件处理是一个复杂的体系,涉及到模板编译时对事件绑定语法的解析、在不同环境(如浏览器、Node.js)下的事件绑定机制等。这里简单模拟的on
方法用于说明事件绑定的基本原理。它维护了一个全局的eventMap
对象,以目标对象target
作为键。当调用on
方法时,首先检查eventMap
中是否有针对该target
的记录,如果没有则创建一个空对象。接着检查该target
下是否有指定eventName
的事件处理函数数组,如果没有则创建一个新数组。最后将传入的handler
事件处理函数添加到对应的数组中。在 Vue 中,类似的机制用于将用户在模板中定义的事件绑定(如@click
)和对应的处理函数关联起来,只不过 Vue 还会处理诸如事件修饰符(.prevent
、.stop
等)、组件间通信的自定义事件等复杂情况。
7.2 off
方法(简化模拟)
javascript
javascript
// 在 event-util.js 中继续添加 off 方法
/**
* 模拟移除元素或对象上的事件绑定
* @param {Object} target - 移除事件绑定的目标对象
* @param {string} eventName - 事件名称
* @param {Function} handler - 要移除的事件处理函数,如果未传入则移除该事件名下所有处理函数
*/
export function off (target, eventName, handler) {
if (!eventMap[target]) {
return;
}
if (!eventMap[target][eventName]) {
return;
}
if (!handler) {
delete eventMap[target][eventName];
} else {
const handlers = eventMap[target][eventName];
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
off
方法用于移除之前绑定的事件处理函数。同样,这是一个简化模拟,Vue 实际的事件移除机制更为复杂且全面。该方法首先检查eventMap
中是否有针对该target
的记录以及是否存在指定eventName
的事件处理函数数组,如果不存在则直接返回。如果没有传入handler
参数,说明要移除该eventName
下所有的处理函数,于是直接删除eventMap
中对应的属性。如果传入了handler
,则在对应的事件处理函数数组中查找该函数的索引,若找到则使用splice
方法将其从数组中移除。在 Vue 中,当组件销毁或者需要动态取消某些事件绑定时,类似的逻辑会被用于确保不再有无效的事件处理函数残留,从而避免内存泄漏和潜在的错误。
7.3 emit
方法(简化模拟)
javascript
javascript
// 在 event-util.js 中继续添加 emit 方法
/**
* 模拟触发元素或对象上绑定的事件
* @param {Object} target - 触发事件的目标对象
* @param {string} eventName - 事件名称
* @param {...any} args - 传递给事件处理函数的参数
*/
export function emit (target, eventName, ...args) {
if (!eventMap[target]) {
return;
}
const handlers = eventMap[target][eventName];
if (!handlers) {
return;
}
handlers.forEach(handler => {
handler(...args);
});
}
emit
方法用于触发绑定在target
上的指定eventName
的事件。它首先检查eventMap
中是否有针对该target
的记录以及是否存在指定eventName
的事件处理函数数组,如果不存在则直接返回。若存在对应的事件处理函数数组handlers
,则遍历该数组,依次调用每个事件处理函数,并将传入的参数args
传递给它们。在 Vue 中,组件之间通过自定义事件进行通信时,就会使用类似的机制。例如,子组件通过$emit
方法触发自定义事件,父组件在模板中监听该事件并定义处理函数,当子组件调用$emit
时,就如同这里的emit
方法一样,会执行父组件中对应的事件处理逻辑,实现了组件间的数据传递和交互。
八、其他辅助公共方法
8.1 noop
方法
javascript
javascript
// src/shared/util.js
/**
* 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) {}
noop
方法是一个空操作函数,它不执行任何实际的逻辑。在 Vue 源码中,它常用于一些需要占位或者默认行为的场景。比如在一些事件处理函数的初始化中,如果暂时没有具体的逻辑需要执行,就可以使用noop
函数作为占位,这样可以保证代码结构的完整性,同时在后续需要添加实际逻辑时,也能方便地进行替换,而不需要大幅修改代码结构。
8.2 warn
方法
javascript
javascript
// src/core/util/env.js
import { inBrowser, inWeex, weexPlatform } from './env'
const hasConsole = typeof console!== 'undefined'
function logError (msg, vm, trace) {
const tip = vm? ` in component <${vm.$options.name}>` : ''
if (hasConsole) {
console.error(`[Vue warn]: ${msg}${tip}`)
if (trace) {
console.error(trace)
}
}
/* istanbul ignore else */
if (process.env.NODE_ENV!== 'production' && inBrowser) {
debugger
}
}
// 简化的 warn 方法,实际在Vue中还有更多复杂逻辑用于不同环境和情况的处理
export function warn (msg, vm, trace) {
logError(msg, vm, trace)
}
warn
方法用于在开发环境中输出警告信息。它接收三个参数:警告消息msg
、Vue 实例vm
(用于标识在哪个组件中出现的问题)以及可选的错误堆栈跟踪trace
。在方法内部,首先根据是否有vm
实例来生成一个提示字符串tip
,然后通过console.error
输出格式化后的警告信息,格式为[Vue warn]: <msg><tip>
。如果提供了trace
,也会一并输出错误堆栈信息。在非生产环境且在浏览器环境下,还会调用debugger
语句,方便开发者在调试工具中定位问题。在 Vue 开发过程中,当出现一些潜在的错误或者不符合预期的情况时,就会调用warn
方法,帮助开发者快速发现和解决问题,提升开发体验和代码质量。
8.3 isDef
方法
javascript
javascript
// src/shared/util.js
/**
* Check if a value is not undefined or null.
*/
export function isDef (v: any): boolean {
return v!== undefined && v!== null
}
isDef
方法用于检查一个值是否既不是undefined
也不是null
。在 Vue 源码中,很多地方需要确保传入的值是有效的,避免因使用undefined
或null
值而导致的运行时错误。通过这个方法,可以方便地进行这样的检查。例如在一些函数参数校验、对象属性值判断等场景中,先使用isDef
方法检查值的有效性,再进行后续的逻辑处理,能有效增强代码的健壮性和稳定性。
九、总结与展望
9.1 总结
通过对 Vue 公共方法模块的深入剖析,我们清晰地看到了这些方法在整个框架中的关键作用。从数据处理的响应式机制,到类型判断、数组与对象操作、事件处理以及各种辅助方法,它们相互协作,构建起了 Vue 高效运行的基础。
在数据处理方面,observe
和defineReactive
方法是响应式系统的核心,通过巧妙地利用 JavaScript 的Object.defineProperty
来劫持对象属性的访问和修改,实现了数据变化的自动追踪和更新通知。数据拷贝方法clone
和deepClone
为数据的复制提供了便捷,开发者可根据实际需求选择浅拷贝或深拷贝,以满足不同场景下对数据独立性和性能的要求。
类型判断方法如isObject
、isPlainObject
和isFunction
等,帮助 Vue 在运行时准确判断数据类型,从而进行正确的逻辑处理,避免了因类型错误而引发的潜在问题。数组和对象操作方法,像remove
、insert
、extend
和mergeOptions
等,极大地简化了对数组和对象的常见操作,尤其是mergeOptions
方法,在组件选项合并等场景中发挥了重要作用,确保了组件继承和配置的灵活性与正确性。
事件处理相关的模拟方法on
、off
和emit
虽然是简化版,但反映了 Vue 事件处理机制的基本原理,即事件的绑定、移除和触发过程,实际的 Vue 事件处理体系在此基础上进一步扩展,支持了丰富的事件修饰符和组件间通信的自定义事件。而其他辅助方法,如noop
提供了占位功能,warn
用于开发环境的错误提示,isDef
用于值的有效性检查,它们从不同方面提升了代码的可读性、可维护性和健壮性。
深入理解这些公共方法的原理,不仅有助于开发者更好地编写 Vue 应用,优化代码性能,还能在遇到问题时更快速地定位和解决。无论是在日常开发中避免踩坑,还是在面试中展示对 Vue 底层原理的掌握,对公共方法模块的深入研究都具有重要意义。
9.2 展望
随着前端技术的不断演进,Vue 也在持续发展。对于公共方法模块,未来可能会有以下几个方向的改进和拓展。
在性能优化方面,随着 JavaScript 引擎的不断升级,Vue 可能会进一步利用新的语言特性来优化公共方法的执行效率。例如,对于数据处理方法,可能会探索更高效的响应式追踪算法,减少依赖收集和更新通知过程中的性能开销。在数组和对象操作方法中,可能会借助 JavaScript 的新数组和对象方法(如Object.fromEntries
、Array.flatMap
等)来简化代码实现并提升性能。
在功能拓展上,随着 Vue 在大型项目和复杂业务场景中的应用越来越广泛,公共方法模块可能会增加更多实用的功能。比如,可能会出现更强大的数据校验方法,以满足复杂业务规则下对数据合法性的严格要求;或者增强事件处理相关