Vue源码(十二)常用API


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

首先是规范化propsinjectdirectives,这里就看下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.extendschild.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属性,属性值为VueVue.options._base = Vue。在_init方法中如果是根实例会调用mergeOptions合并Vue.options到根实例的options中;组件实例在创建组件VNode时,调用Vue.extend创建组件实例的构造函数,在Vue.extend中也会调用mergeOptions合并Vue.options到构造函数的options中;也就是说经mergeOptions合并后的child都会带有_base;只有原始child对象才没有;所有这里只将原始childextendsmixins通过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中所有的keychild剩余的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 } ```

根据parentchild的有无,将生命周期合并成数组;通过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都会调用mergeDataOrFnVue.extendVue.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,并将childValparentVal的所有属性传入。

如果vm有值,返回mergedInstanceDataFn函数,在创建实例过程中会调用这个函数,首先获取childValparentVal的所有属性;如果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中设置回调函数为flushCallbacksthen函数

如果不支持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中;如果pendingfalse,将pending设置为true,这样做的目的是在同一时刻,只有一个flushCallbacks函数等待执行。也就是说如果在同一时刻多次调用nextTick只会往callbacks中添加一个函数,而不会多次触发timerFunc

接下来调用timerFunc函数,timerFunc函数就是将flushCallbacks函数推入队列中(微任务优先)。如果没有传入cb,则返回一个PromisePromise内部将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原理对应文章中详细分析过,就直接贴下链接啦

Vue.prototype.$set

Vue 源码(二)响应式原理

Vue.extend

Vue 源码(一)如何创建VNode

Vue.component

Vue源码(八)异步组件原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值