highlight: tomorrow-night-eighties
Vue.use
定义在src/core/global-api/use.js
中
```javascript 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
指向这个对象
```javascript 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
javascript 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
中
javascript Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this }
调用mergeOptions
将传入的对象根据某些合并策略添加到Vue.options
中,并返回Vue
```javascript 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
javascript 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
是一个对象,转换前后如下
```javascript // 转换前 { nick-name: Boolean, name: { type: String } }
// 转换后 { nickName: { type: Boolean }, name: { type: String } } ```
回到mergeOptions
,规范化完成后,如果child
没有_base
属性,将child.extends
和child.mixins
通过mergeOptions
合并到parent
中
javascript 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
中
接下来开始合并逻辑
javascript 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
是一个对象,存储的各种缓存策略
javascript strats = { props: xxx, methods: xxx, computed: xxx, 生命周期: xxx, ... }
如果strats
对象中没有相应的key
,则调用默认策略defaultStrat
javascript const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal }
默认策略就是child
优先,根据上述合并策略后,将合并后的属性值赋值给options
并返回
介绍下几种合并策略
生命周期合并策略
```javascript const LIFECYCLEHOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', ... ]; LIFECYCLEHOOKS.forEach(hook => { strats[hook] = mergeHook })
function mergeHook ( parentVal: ?Array , childVal: ?Function | ?Array ): ?Array { const res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal return res ? dedupeHooks(res) : res } ```
根据parent
和child
的有无,将生命周期合并成数组;通过dedupeHooks
对数组去重后返回这个数组
data合并策略
```javascript 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
属性
javascript 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
的情况只发生在根实例创建过程中,所以直接返回对象也没问题
```javascript 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
变量
```javascript 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
时,代码如下
javascript 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
函数如下
javascript 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
数组中所有回调函数。
javascript 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设置回调的执行时机,微任务优先。
最后来一个小测试,考虑下打印结果
javascript 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) }) } } })
上述打印结果为
bash 1, 'test' 2, 'test'
其他API原理对应文章中详细分析过,就直接贴下链接啦