Vue.use
定义在src/core/global-api/use.js中
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
Vue.use传入的第一个参数可以是对象也可以是函数。首先获取或创建_installedPlugins数组,并判断传入的参数是否在这个数组中,防止多次注册。接下来将第一个参数以外的其他参数转换成数组;并将Vue构造函数添加到数组开头。如果传入的第一个参数是对象并且有install方法,通过apply方法调用install,参数为数组元素,this指向这个传入的对象。如果第一个参数是一个函数,通过apply调用参数为数组元素,this指向null。最后返回Vue
也就是说如果Vue.use第一个参数是一个对象并且有install方法,install方法第一个参数是Vue,并且内部this指向这个对象
const installTest = {
name: 'installTest',
install(Vue, ...args){
console.log(this.name, args)
}
}
Vue.use(installTest, 1, 2, 3)
// 打印 installTest, [1, 2, 3]
如果Vue.use第一个参数是函数,这个函数的第一个参数也是Vue,并且this指向null
const fnTest = function (Vue, ...args){
console.log(this, args)
}
Vue.use(fnTest, 1, 2, 3)
// 打印 null, [1, 2, 3]
Vue.mixin
定义在src/core/global-api/mixin.js中
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
调用mergeOptions将传入的对象根据某些合并策略添加到Vue.options中,并返回Vue
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (typeof child === 'function') {
child = child.options
}
// 规范化 props
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
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
}
首先是规范化props、inject、directives,这里就看下props
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
// props: ['name', 'nick-name']
if (Array.isArray(props)) { // 数组
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
// nick-name -> nickName
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) { //对象
for (const key in props) {
val = props[key]
name = camelize(key)
// val 可能是一个对象 也可能是一个构造函数(name: Boolean)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
- 如果
props是数组,比如['nick-name'],经过转换变成{ nickName: { type: null } } - 如果
props是一个对象,转换前后如下
// 转换前
{
nick-name: Boolean,
name: { type: String }
}
// 转换后
{
nickName: { type: Boolean },
name: { type: String }
}
回到mergeOptions,规范化完成后,如果child没有_base属性,将child.extends和child.mixins通过mergeOptions合并到parent中
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
在src/core/global-api/index.js中会往Vue.options上挂载_base属性,属性值为Vue, Vue.options._base = Vue。在_init方法中如果是根实例会调用mergeOptions合并Vue.options到根实例的options中;组件实例在创建组件VNode时,调用Vue.extend创建组件实例的构造函数,在Vue.extend中也会调用mergeOptions合并Vue.options到构造函数的options中;也就是说经mergeOptions合并后的child都会带有_base;只有原始child对象才没有;所有这里只将原始child的extends和mixins通过mergeOptions合并到parent中
接下来开始合并逻辑
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
遍历parent中所有的key和child剩余的key,分别调用mergeField函数;strats是一个对象,存储的各种缓存策略
strats = {
props: xxx,
methods: xxx,
computed: xxx,
生命周期: xxx,
...
}
如果strats对象中没有相应的key,则调用默认策略defaultStrat
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
默认策略就是child优先,根据上述合并策略后,将合并后的属性值赋值给options并返回
介绍下几种合并策略
生命周期合并策略
const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
...
];
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
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
}
根据parent和child的有无,将生命周期合并成数组;通过dedupeHooks对数组去重后返回这个数组
data合并策略
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)
}
不管有没有vm都会调用mergeDataOrFn,Vue.extend和Vue.mixin触发的mergeOptions函数没有vm属性,在开发环境下如果传入的data不是函数会报警告。子组件实例的构造函数通过Vue.extend创建,所以也不会传入vm属性
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
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 () {
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
}
}
}
}
如果没有传入vm,返回一个函数mergedDataFn,在创建实例过程中会调用这个函数,函数内部调用mergeData,并将childVal和parentVal的所有属性传入。
如果vm有值,返回mergedInstanceDataFn函数,在创建实例过程中会调用这个函数,首先获取childVal和parentVal的所有属性;如果childVal有值,调用mergeData,反之返回parentVal对象,其实有vm的情况只发生在根实例创建过程中,所以直接返回对象也没问题
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
if (key === '__ob__') continue
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
}
获取from的属性名数组,遍历这个数组,如果当前key没在to中,调用Vue.prototype.$set将值添加到to中。如果属性值都是对象并且不相等,递归调用mergeData,最终返回to。
Vue.$nextTick
代码定义在src/core/util/next-tick.js中,在定义nextTick方法之前,会初始化timerFunc变量
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {}
let timerFunc
// 如果当前环境支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 如果当前环境支持 MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 如果当前环境支持 setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 上述都不支持
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
timerFunc始终是一个函数,只不过函数内容会根据当前环境支持API的情况设置不同值
如果支持Promise,创建一个Promise实例,并将isUsingMicroTask设置为true,代表使用微任务。在timerFunc中设置回调函数为flushCallbacks的then函数
如果不支持Promise,但是支持MutationObserver的话,创建一个回调函数为flushCallbacks的监听器和一个文本节点,监听这个文本节点;也会将isUsingMicroTask设置为true,代表微任务。timerFunc函数内部修改文本节点的属性,从而触发回调。
如果上面两个微任务都不支持,但是支持setImmediate的话,timerFunc函数内部调用setImmediate方法,回调函数为flushCallbacks,这是一个宏任务
如果上面三种方式都不支持,则timerFunc内部创建一个定时器,回调函数是flushCallbacks,延时时间为0
当调用Vue.$nextTick时,代码如下
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
首先创建一个函数并将这个函数添加到callbacks中;如果pending为false,将pending设置为true,这样做的目的是在同一时刻,只有一个flushCallbacks函数等待执行。也就是说如果在同一时刻多次调用nextTick只会往callbacks中添加一个函数,而不会多次触发timerFunc。
接下来调用timerFunc函数,timerFunc函数就是将flushCallbacks函数推入队列中(微任务优先)。如果没有传入cb,则返回一个Promise,Promise内部将resolve赋值给_resolve ,也就是说调用_resolve()时,才会修改这个Promise的状态,而这个状态的修改也是发生在flushCallbacks中。
flushCallbacks函数如下
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
首先将pending置为false,表示当前队列没有flushCallbacks函数等待执行了。接下来就是遍历并执行callbacks数组中所有回调函数。
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
首先判断有没有cb,如果有则调用cb,并且this指向ctx;如果没有cb,则调用_resolve,并将ctx传入。
综上,Vue.$nextTick就是根据当前环境支持的API设置回调的执行时机,微任务优先。
最后来一个小测试,考虑下打印结果
new Vue({
el: '#app',
template: `<div @click="change">
{{title}}
</div>`,
data () {
return {
title: '我是标题'
}
},
methods: {
change () {
this.$nextTick(() => {
console.log(1, this.title)
})
this.title = 'test'
this.$nextTick(() => {
console.log(2, this.title)
})
}
}
})
上述打印结果为
1, 'test'
2, 'test'
其他API原理对应文章中详细分析过,就直接贴下链接啦