Vue
的核心以及编译过程,除此之外,Vue
还提供了很多好用的 feature
, 如 event
、v-model
、slot
、keep-alive
、transition
等等。对他们的理解有助于我们在平时开发中更好地应用这些 feature
,即使出现 bug
我们也可以很从容地应对。DOM
事件,还可以绑定自定义事件,非常灵活和方便。那么接下来我们从源码角度来看看它的实现原理。为了更加直观,我们通过一个例子来分析它的实现:let Child = {
template: ' +
'click me' +
'',
methods: {
clickHandler(e) {
console.log('Button clicked!', e)
this.$emit('select')
}
}
}
let vm = new Vue({
el: '#app',
template: '' +
' ' +
'',
methods: {
clickHandler() {
console.log('Child clicked!')
},
selectHandler() {
console.log('Child select!')
}
},
components: {
Child
}
})
parse
阶段,会执行 processAttrs
方法,它的定义在 src/compiler/parser/index.js
中:export const onRE = /^@|^v-on:/
export const dirRE = /^v-|^@|^:/
export const bindRE = /^:|^v-bind:/
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
el.hasBindings = true
modifiers = parseModifiers(name)
if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) {
// ..
} else if (onRE.test(name)) {
name = name.replace(onRE, '')
addHandler(el, name, value, modifiers, false, warn)
} else {
// ...
}
} else {
// ...
}
}
}
function parseModifiers (name: string): Object | void {
const match = name.match(modifierRE)
if (match) {
const ret = {}
match.forEach(m => { ret[m.slice(1)] = true })
return ret
}
}
parseModifiers
解析出修饰符,然后判断如果事件的指令,则执行 addHandler(el, name, value, modifiers, false, warn)
方法,它的定义在 src/compiler/helpers.js
中:export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: Function
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.'
)
}
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = '!' + name // mark the event as captured
}
if (modifiers.once) {
delete modifiers.once
name = '~' + name // mark the event as once
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
name = '&' + name // mark the event as passive
}
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
if (name === 'click') {
if (modifiers.right) {
name = 'contextmenu'
delete modifiers.right
} else if (modifiers.middle) {
name = 'mouseup'
}
}
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = {
value: value.trim()
}
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
addHandler
函数看起来长,实际上就做了三件事情,首先根据 modifier
修饰符对事件名 name
做处理,接着根据 modifier.native
判断是一个纯原生事件还是普通事件,分别对应 el.nativeEvents
和 el.events
,最后按照 name
对事件做归类,并把回调函数的字符串保留到对应的事件中。在我们的例子中,父组件的 child
节点生成的 el.events
和 el.nativeEvents
如下:el.events = {
select: {
value: 'selectHandler'
}
}
el.nativeEvents = {
click: {
value: 'clickHandler',
modifiers: {
prevent: true
}
}
}
子组件的
button
节点生成的el.events
如下:
el.events = {
click: {
value: 'clickHandler($event)'
}
}
然后在
codegen
的阶段,会在genData
函数中根据 AST 元素节点上的events
和nativeEvents
生成data
数据,它的定义在src/compiler/codegen/index.js
中:
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// ...
if (el.events) {
data += `${genHandlers(el.events, false, state.warn)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true, state.warn)},`
}
// ...
return data
}
对于这两个属性,会调用
genHandlers
函数,定义在src/compiler/codegen/events.js
中:
export function genHandlers (
events: ASTElementHandlers,
isNative: boolean,
warn: Function
): string {
let res = isNative ? 'nativeOn:{' : 'on:{'
for (const name in events) {
res += `"${name}":${genHandler(name, events[name])},`
}
return res.slice(0, -1) + '}'
}
const fnExpRE = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/
const simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/
function genHandler (
name: string,
handler: ASTElementHandler | Array<ASTElementHandler>
): string {
if (!handler) {
return 'function(){}'
}
if (Array.isArray(handler)) {
return `[${handler.map(handler => genHandler(name, handler)).join(',')}]`
}
const isMethodPath = simplePathRE.test(handler.value)
const isFunctionExpression = fnExpRE.test(handler.value)
if (!handler.modifiers) {
if (isMethodPath || isFunctionExpression) {
return handler.value
}
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, handler.value)
}
return `function($event){${handler.value}}` // inline statement
} else {
let code = ''
let genModifierCode = ''
const keys = []
for (const key in handler.modifiers) {
if (modifierCode[key]) {
genModifierCode += modifierCode[key]
// left/right
if (keyCodes[key]) {
keys.push(key)
}
} else if (key === 'exact') {
const modifiers: ASTModifiers = (handler.modifiers: any)
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(keyModifier => !modifiers[keyModifier])
.map(keyModifier => `$event.${keyModifier}Key`)
.join('||')
)
} else {
keys.push(key)
}
}
if (keys.length) {
code += genKeyFilter(keys)
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode
}
const handlerCode = isMethodPath
? `return ${handler.value}($event)`
: isFunctionExpression
? `return (${handler.value})($event)`
: handler.value
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`
}
}
genHandlers
方法遍历事件对象 events
,对同一个事件名称的事件调用 genHandler(name, events[name])
方法,它的内容看起来多,但实际上逻辑很简单,首先先判断如果 handler
是一个数组,就遍历它然后递归调用 genHandler
方法并拼接结果,然后判断 hanlder.value
是一个函数的调用路径还是一个函数表达式, 接着对 modifiers
做判断,对于没有 modifiers
的情况,就根据 handler.value
不同情况处理,要么直接返回,要么返回一个函数包裹的表达式;对于有 modifiers
的情况,则对各种不同的 modifer
情况做不同处理,添加相应的代码串。那么对于我们的例子而言,父组件生成的 data
串为:{
on: {"select": selectHandler},
nativeOn: {"click": function($event) {
$event.preventDefault();
return clickHandler($event)
}
}
}
子组件生成的
data
串为:
{
on: {"click": function($event) {
clickHandler($event)
}
}
}
其实 Vue 的事件有两种,一种是原生 DOM 事件,一种是用户自定义事件,我们分别来看。
DOM
事件,之前在 patch
的时候执行各种 module
的钩子函数,只分析了 DOM
是如何渲染的,而 DOM
元素相关的属性、样式、事件等都是通过这些 module
的钩子函数完成设置的。所有和 web
相关的 module
都定义在 src/platforms/web/runtime/modules
目录下,我们这次只关注目录下的 events.js
即可。在 patch
过程中的创建阶段和更新阶段都会执行 updateDOMListeners
:let target: any
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm
normalizeEvents(on)
updateListeners(on, oldOn, add, remove, vnode.context)
target = undefined
}
vnode.data.on
,这就是我们之前的生成的 data
中对应的事件对象,target
是当前 vnode
对于的 DOM
对象,normalizeEvents
主要是对 v-model
相关的处理,我们之后分析 v-model
的时候会介绍,接着调用 updateListeners(on, oldOn, add, remove, vnode.context)
方法,它的定义在 src/core/vdom/helpers/update-listeners.js
中:export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
}
add(event.name, cur, event.once, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
updateListeners
的逻辑很简单,遍历 on
去添加事件监听,遍历 oldOn
去移除事件监听,关于监听和移除事件的方法都是外部传入的,因为它既处理原生 DOM
事件的添加删除,也处理自定义事件的添加删除。对于 on
的遍历,首先获得每一个事件名,然后做 normalizeEvent
的处理:const normalizeEvent = cached((name: string): {
name: string,
once: boolean,
capture: boolean,
passive: boolean,
handler?: Function,
params?: Array<any>
} => {
const passive = name.charAt(0) === '&'
name = passive ? name.slice(1) : name
const once = name.charAt(0) === '~' // Prefixed last, checked first
name = once ? name.slice(1) : name
const capture = name.charAt(0) === '!'
name = capture ? name.slice(1) : name
return {
name,
once,
capture,
passive
}
})
addHandler
的时候添加上的)区分出这个事件是否有 once
、capture
、passive
等修饰符。处理完事件名后,又对事件回调函数做处理,对于第一次,满足 isUndef(old)
并且 isUndef(cur.fns)
,会执行 cur = on[name] = createFnInvoker(cur)
方法去创建一个回调函数,然后在执行 add(event.name, cur, event.once, event.capture, event.passive, event.params)
完成一次事件绑定。我们先看一下 createFnInvoker
的实现:export function createFnInvoker (fns: Function | Array<Function>): Function {
function invoker () {
const fns = invoker.fns
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
cloned[i].apply(null, arguments)
}
} else {
return fns.apply(null, arguments)
}
}
invoker.fns = fns
return invoker
}
这里定义了 invoker
方法并返回,由于一个事件可能会对应多个回调函数,所以这里做了数组的判断,多个回调函数就依次调用。注意最后的赋值逻辑, invoker.fns = fns
,每一次执行 invoker
函数都是从 invoker.fns
里取执行的回调函数,回到 updateListeners
,当我们第二次执行该函数的时候,判断如果 cur !== old
,那么只需要更改 old.fns = cur
把之前绑定的 involer.fns
赋值为新的回调函数即可,并且 通过 on[name] = old
保留引用关系,这样就保证了事件回调只添加一次,之后仅仅去修改它的回调函数的引用。updateListeners
函数的最后遍历 oldOn
拿到事件名称,判断如果满足 isUndef(on[name])
,则执行 remove(event.name, oldOn[name], event.capture)
去移除事件回调。
了解了 updateListeners
的实现后,我们来看一下在原生 DOM
事件中真正添加回调和移除回调函数的实现,它们的定义都在 src/platforms/web/runtime/modules/event.js
中:
function add (
event: string,
handler: Function,
once: boolean,
capture: boolean,
passive: boolean
) {
handler = withMacroTask(handler)
if (once) handler = createOnceHandler(handler, event, capture)
target.addEventListener(
event,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
function remove (
event: string,
handler: Function,
capture: boolean,
_target?: HTMLElement
) {
(_target || target).removeEventListener(
event,
handler._withTask || handler,
capture
)
}
add
和 remove
的逻辑很简单,就是实际上调用原生 addEventListener
和 removeEventListener
,并根据参数传递一些配置,注意这里的 hanlder
会用 withMacroTask(hanlder)
包裹一下,它的定义在 src/core/util/next-tick.js
中:export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
实际上就是强制在 DOM 事件的回调函数执行期间如果修改了数据,那么这些数据更改推入的队列会被当做
macroTask
在nextTick
后执行。
自定义事件,除了原生 DOM
事件,Vue
还支持了自定义事件,并且自定义事件只能作用在组件上,如果在组件上使用原生事件,需要加 .native
修饰符,普通元素上使用 .native
修饰符无效,接下来我们就来分析它的实现。
在 render
阶段,如果是一个组件节点,则通过 createComponent
创建一个组件 vnode
,我们再来回顾这个方法,定义在 src/core/vdom/create-component.js
中:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
const listeners = data.on
data.on = data.nativeOn
// ...
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
data.on
赋值给了 listeners
,把 data.nativeOn
赋值给了 data.on
,这样所有的原生 DOM
事件处理跟我们刚才介绍的一样,它是在当前组件环境中处理的。而对于自定义事件,我们把 listeners
作为 vnode
的 componentOptions
传入,它是在子组件初始化阶段中处理的,所以它的处理环境是子组件。然后在子组件的初始化的时候,会执行 initInternalComponent
方法,它的定义在 src/core/instance/init.js
中:export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// ....
const vnodeComponentOptions = parentVnode.componentOptions
opts._parentListeners = vnodeComponentOptions.listeners
// ...
}
这里拿到了父组件传入的
listeners
,然后在执行initEvents
的过程中,会处理这个listeners
,定义在src/core/instance/events.js
中:
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
拿到
listeners
后,执行updateComponentListeners(vm, listeners)
方法:
let target: any
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, vm)
target = undefined
}
updateListeners
我们之前介绍过,所以对于自定义事件和原生 DOM
事件处理的差异就在事件添加和删除的实现上,来看一下自定义事件 add
和 remove
的实现:function add (event, fn, once) {
if (once) {
target.$once(event, fn)
} else {
target.$on(event, fn)
}
}
function remove (event, fn) {
target.$off(event, fn)
}
实际上是利用 Vue 定义的事件中心,简单分析一下它的实现:
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
if (fn) {
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args)
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
}
非常经典的事件中心的实现,把所有的事件用 vm._events
存储起来,当执行 vm.$on(event,fn)
的时候,按事件的名称 event
把回调函数 fn
存储起来 vm._events[event].push(fn)
。当执行 vm.$emit(event)
的时候,根据事件名 event
找到所有的回调函数 let cbs = vm._events[event]
,然后遍历执行所有的回调函数。当执行 vm.$off(event,fn)
的时候会移除指定事件名 event
和指定的 fn
当执行 vm.$once(event,fn)
的时候,内部就是执行 vm.$on
,并且当回调函数执行一次后再通过 vm.$off
移除事件的回调,这样就确保了回调函数只执行一次。
所以对于用户自定义的事件添加和删除就是利用了这几个事件中心的 API
。需要注意的事一点,vm.$emit
是给当前的 vm
上派发的实例,之所以我们常用它做父子组件通讯,是因为它的回调函数的定义是在父组件中,对于我们这个例子而言,当子组件的 button
被点击了,它通过 this.$emit('select')
派发事件,那么子组件的实例就监听到了这个 select
事件,并执行它的回调函数——定义在父组件中的 selectHandler
方法,这样就相当于完成了一次父子组件的通讯。
总结:对 Vue
的事件实现有了进一步的了解,Vue
支持两种事件类型,原生 DOM
事件和自定义事件,它们主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。另外要注意一点,只有组件节点才可以添加自定义事件,并且添加原生 DOM
事件需要使用 native
修饰符;而普通元素使用 .native
修饰符是没有作用的,也只能添加原生 DOM
事件。
Vue
的数据响应原理理解为双向绑定,但实际上这是不准确的,我们之前提到的数据响应,都是通过数据的改变去驱动 DOM
视图的变化,而双向绑定除了数据驱动 DOM
外, DOM
的变化反过来影响数据,是一个双向关系,在 Vue
中,我们可以通过 v-model
来实现双向绑定。v-model
即可以作用在普通表单元素上,又可以作用在组件上,它其实是一个语法糖,接下来我们就来分析 v-model
的实现原理。
表单元素,结合示例来分析:
let vm = new Vue({
el: '#app',
template: ''
+ '' +
'Message is: {{ message }}
' +
'',
data() {
return {
message: ''
}
}
})
这是一个非常简单 demo,我们在
input
元素上设置了v-model
属性,绑定了message
,当我们在input
上输入了内容,message
也会同步变化。接下来我们就来分析 Vue 是如何实现这一效果的,其实非常简单。
parse
阶段, v-model
被当做普通的指令解析到 el.directives
中,然后在 codegen
阶段,执行 genData
的时候,会执行 const dirs = genDirectives(el, state)
,它的定义在 src/compiler/codegen/index.js
中:function genDirectives (el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn)
}
if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:"${dir.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
genDrirectives
方法就是遍历 el.directives
,然后获取每一个指令对应的方法 const gen: DirectiveFunction = state.directives[dir.name]
,这个指令方法实际上是在实例化 CodegenState
的时候通过 option
option
就是编译相关的配置,它在不同的平台下配置不同,在 web
环境下的定义在 src/platforms/web/compiler/options.js
下:export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
directives
定义在src/platforms/web/compiler/directives/index.js
中:
export default {
model,
text,
html
}
v-model
而言,对应的 directive
函数是在 src/platforms/web/compiler/directives/model.js
中定义的 model
函数:export default function model (
el: ASTElement,
dir: ASTDirective,
_warn: Function
): ?boolean {
warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
if (process.env.NODE_ENV !== 'production') {
// inputs with type="file" are read only and setting the input's
// value will throw an error.
if (tag === 'input' && type === 'file') {
warn(
`<${el.tag} v-model="${value}" type="file">:\n` +
`File inputs are read only. Use a v-on:change listener instead.`
)
}
}
if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (process.env.NODE_ENV !== 'production') {
warn(
`<${el.tag} v-model="${value}">: ` +
`v-model is not supported on this element type. ` +
'If you are working with contenteditable, it\'s recommended to ' +
'wrap a library dedicated for that purpose inside a custom component.'
)
}
// ensure runtime directive metadata
return true
}
needRuntime = !!gen(el, dir, state.warn)
就是在执行 model
函数,它会根据 AST
元素节点的不同情况去执行不同的逻辑,对于我们这个 case
而言,它会命中 genDefaultModel(el, value, modifiers)
的逻辑,稍后我们也会介绍组件的处理。我们来看一下 genDefaultModel
的实现:function genDefaultModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const type = el.attrsMap.type
// warn if v-bind:value conflicts with v-model
// except for inputs with v-bind:type
if (process.env.NODE_ENV !== 'production') {
const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
if (value && !typeBinding) {
const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
warn(
`${binding}="${value}" conflicts with v-model on the same element ` +
'because the latter already expands to a value binding internally'
)
}
}
const { lazy, number, trim } = modifiers || {}
const needCompositionGuard = !lazy && type !== 'range'
const event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'
let valueExpression = '$event.target.value'
if (trim) {
valueExpression = `$event.target.value.trim()`
}
if (number) {
valueExpression = `_n(${valueExpression})`
}
let code = genAssignmentCode(value, valueExpression)
if (needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()')
}
}
genDefaultModel
函数先处理了 modifiers
,它的不同主要影响的是 event
和 valueExpression
的值,对于我们的例子,event
为 input
,valueExpression
为 $event.target.value
。然后去执行 genAssignmentCode
去生成代码,它的定义在 src/compiler/directives/model.js
中:/**
* Cross-platform codegen helper for generating v-model value assignment code.
*/
export function genAssignmentCode (
value: string,
assignment: string
): string {
const res = parseModel(value)
if (res.key === null) {
return `${value}=${assignment}`
} else {
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
该方法首先对 v-model
对应的 value
做了解析,它处理了非常多的情况,对我们的例子,value
就是 messgae
,所以返回的 res.key
为 null
,然后我们就得到 ${value}=${assignment}
,也就是 message=$event.target.value
。然后我们又命中了 needCompositionGuard
为 true 的逻辑,所以最终的 code
为 if($event.target.composing)return;message=$event.target.value
。
code
生成完后,又执行了两句非常关键的代码:
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)
这实际上就是
input
实现v-model
的精髓,通过修改 AST 元素,给el
添加一个prop
,相当于我们在input
上动态绑定了value
,又给el
添加了事件处理,相当于在input
上绑定了input
事件,其实转换成模板如下:
<input
v-bind:value="message"
v-on:input="message=$event.target.value">
input
的 value
指向了 messgae
变量,并且在触发 input
事件的时候去动态把 message
设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model
实际上就是语法糖。再回到 genDirectives
,它接下来的逻辑就是根据指令生成一些 data
的代码:if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:"${dir.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
对我们的例子而言,最终生成的
render
代码如下:
with(this) {
return _c('div',[_c('input',{
directives:[{
name:"model",
rawName:"v-model",
value:(message),
expression:"message"
}],
attrs:{"placeholder":"edit me"},
domProps:{"value":(message)},
on:{"input":function($event){
if($event.target.composing)
return;
message=$event.target.value
}}}),_c('p',[_v("Message is: "+_s(message))])
])
}
关于事件的处理之前已经分析过了,所以对于 input
的 v-model
而言,完全就是语法糖,并且对于其它表单元素套路都是一样,区别在于生成的事件代码会略有不同。v-model
除了作用在表单元素上,新版的 Vue
还把这一语法糖用在了组件上,接下来我们来分析它的实现。
组件,为了更加直观,我们也是通过一个例子分析:
let Child = {
template: ''
+ '' +
'',
props: ['value'],
methods: {
updateValue(e) {
this.$emit('input', e.target.value)
}
}
}
let vm = new Vue({
el: '#app',
template: '' +
' ' +
'Message is: {{ message }}
' +
'',
data() {
return {
message: ''
}
},
components: {
Child
}
})
可以看到,父组件引用 child
子组件的地方使用了 v-model
关联了数据 message
;而子组件定义了一个 value
的 prop
,并且在 input
事件的回调函数中,通过 this.$emit('input', e.target.value)
派发了一个事件,为了让 v-model
生效,这两点是必须的。
接着我们从源码角度分析实现原理,还是从编译阶段说起,对于父组件而言,在编译阶段会解析 v-modle
指令,依然会执行 genData
函数中的 genDirectives
函数,接着执行 src/platforms/web/compiler/directives/model.js
中定义的 model
函数,并命中如下逻辑:
else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
return false
}
genComponentModel
函数定义在src/compiler/directives/model.js
中:
export function genComponentModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const { number, trim } = modifiers || {}
const baseValueExpression = '$$v'
let valueExpression = baseValueExpression
if (trim) {
valueExpression =
`(typeof ${baseValueExpression} === 'string'` +
`? ${baseValueExpression}.trim()` +
`: ${baseValueExpression})`
}
if (number) {
valueExpression = `_n(${valueExpression})`
}
const assignment = genAssignmentCode(value, valueExpression)
el.model = {
value: `(${value})`,
expression: `"${value}"`,
callback: `function (${baseValueExpression}) {${assignment}}`
}
}
genComponentModel
的逻辑很简单,对我们的例子而言,生成的el.model
的值为:
el.model = {
callback:'function ($$v) {message=$$v}',
expression:'"message"',
value:'(message)'
}
那么在
genDirectives
之后,genData
函数中有一段逻辑如下:
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
那么父组件最终生成的
render
代码如下:
with(this){
return _c('div',[_c('child',{
model:{
value:(message),
callback:function ($$v) {
message=$$v
},
expression:"message"
}
}),
_c('p',[_v("Message is: "+_s(message))])],1)
}
然后在创建子组件
vnode
阶段,会执行createComponent
函数,它的定义在src/core/vdom/create-component.js
中:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ...
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
其中会对
data.model
的情况做处理,执行transformModel(Ctor.options, data)
方法:
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.props || (data.props = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
if (isDef(on[event])) {
on[event] = [data.model.callback].concat(on[event])
} else {
on[event] = data.model.callback
}
}
transformModel
逻辑很简单,给 data.props
添加 data.model.value
,并且给data.on
添加 data.model.callback
,对我们的例子而言,扩展结果如下:data.props = {
value: (message),
}
data.on = {
input: function ($$v) {
message=$$v
}
}
其实就相当于我们在这样编写父组件:
let vm = new Vue({
el: '#app',
template: '' +
' ' +
'Message is: {{ message }}
' +
'',
data() {
return {
message: ''
}
},
components: {
Child
}
})
子组件传递的 value
绑定到当前父组件的 message
,同时监听自定义 input
事件,当子组件派发 input
事件的时候,父组件会在事件回调函数中修改 message
的值,同时 value
也会发生变化,子组件的 input
值被更新。
这就是典型的 Vue
的父子组件通讯模式,父组件通过 prop
把数据传递到子组件,子组件修改了数据后把改变通过 $emit
事件的方式通知父组件,所以说组件上的 v-model
也是一种语法糖。另外我们注意到组件 v-model
的实现,子组件的 value prop
以及派发的 input
事件名是可配的,可以看到 transformModel
中对这部分的处理:
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
// ...
}
也就是说可以在定义子组件的时候通过
model
选项配置子组件接收的prop
名以及派发的事件名,举个例子:
let Child = {
template: ''
+ '' +
'',
props: ['msg'],
model: {
prop: 'msg',
event: 'change'
},
methods: {
updateValue(e) {
this.$emit('change', e.target.value)
}
}
}
let vm = new Vue({
el: '#app',
template: '' +
' ' +
'Message is: {{ message }}
' +
'',
data() {
return {
message: ''
}
},
components: {
Child
}
})
子组件修改了接收的
prop
名以及派发的事件名,然而这一切父组件作为调用方是不用关心的,这样做的好处是我们可以把value
这个prop
作为其它的用途。
v-model
的实现就分析完了,我们了解到它是 Vue
双向绑定的真正实现,但本质上就是一种语法糖,它即可以支持原生表单元素,也可以支持自定义组件。在组件的实现中,我们是可以配置子组件接收的 prop
名称,以及派发的事件名称。Vue
的组件提供了一个非常有用的特性 —— slot
插槽,它让组件的实现变的更加灵活。我们平时在开发组件库的时候,为了让组件更加灵活可定制,经常用插槽的方式让用户可以自定义内容。插槽分为普通插槽和作用域插槽,它们可以解决不同的场景,但它是怎么实现的呢,下面我们就从源码的角度来分析插槽的实现原理。
普通插槽,为了更加直观,我们还是通过一个例子来分析插槽的实现:
let AppLayout = {
template: '' +
' ' +
'默认内容 ' +
'' +
''
}
let vm = new Vue({
el: '#app',
template: '' +
'' +
'{{title}}
' +
'{{msg}}
' +
'{{desc}}
' +
'' +
'',
data() {
return {
title: '我是标题',
msg: '我是内容',
desc: '其它信息'
}
},
components: {
AppLayout
}
})
AppLayout
子组件,它内部定义了三个插槽,两个为具名插槽,一个 name
为 header
,一个 name
为 footer
,还有一个没有定义 name
的是默认插槽。
和
之前填写的内容为默认内容。我们的父组件注册和引用了 AppLayout
的组件,并在组件内部定义了一些元素,用来替换插槽,那么它最终生成的 DOM
如下:<div>
<div class="container">
<header><h1>我是标题h1>header>
<main><p>我是内容p>main>
<footer><p>其它信息p>footer>
div>
div>
vm.$mount
的时候,所以编译的顺序是先编译父组件,再编译子组件。首先编译父组件,在 parse
阶段,会执行 processSlot
处理 slot
,它的定义在 src/compiler/parser/index.js
中:function processSlot (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
if (process.env.NODE_ENV !== 'production' && el.key) {
warn(
`\`key\` does not work on because slots are abstract outlets ` +
`and can possibly expand into multiple elements. ` +
`Use the key on a wrapping element instead.`
)
}
} else {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to to ` +
`denote scoped slots.`,
true
)
}
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper for the ` +
`scoped slot to make it clearer.`,
true
)
}
el.slotScope = slotScope
}
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget)
}
}
}
}
slot
属性的时候,会给对应的 AST
slotTarget
属性,然后在 codegen
阶段,在 genData
中会处理 slotTarget
,相关代码在 src/compiler/codegen/index.js
中:if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
会给
data
添加一个slot
属性,并指向slotTarget
,之后会用到。在我们的例子中,父组件最终生成的代码如下:
with(this){
return _c('div',
[_c('app-layout',
[_c('h1',{attrs:{"slot":"header"},slot:"header"},
[_v(_s(title))]),
_c('p',[_v(_s(msg))]),
_c('p',{attrs:{"slot":"footer"},slot:"footer"},
[_v(_s(desc))]
)
])
],
1)}
接下来编译子组件,同样在
parser
阶段会执行processSlot
处理函数,它的定义在src/compiler/parser/index.js
中:
function processSlot (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
}
// ...
}
slot
标签的时候会给对应的 AST
元素节点添加 slotName
属性,然后在 codegen
阶段,会判断如果当前 AST
元素节点是 slot
标签,则执行 genSlot
函数,它的定义在 src/compiler/codegen/index.js
中:function genSlot (el: ASTElement, state: CodegenState): string {
const slotName = el.slotName || '"default"'
const children = genChildren(el, state)
let res = `_t(${slotName}${children ? `,${children}` : ''}`
const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}`
const bind = el.attrsMap['v-bind']
if ((attrs || bind) && !children) {
res += `,null`
}
if (attrs) {
res += `,${attrs}`
}
if (bind) {
res += `${attrs ? '' : ',null'},${bind}`
}
return res + ')'
}
我们先不考虑
slot
标签上有attrs
以及v-bind
的情况,那么它生成的代码实际上就只有:
const slotName = el.slotName || '"default"'
const children = genChildren(el, state)
let res = `_t(${slotName}${children ? `,${children}` : ''}`
slotName
从 AST
元素节点对应的属性上取,默认是 default
,而 children
对应的就是 slot
开始和闭合标签包裹的内容。来看一下我们例子的子组件最终生成的代码,如下:with(this) {
return _c('div',{
staticClass:"container"
},[
_c('header',[_t("header")],2),
_c('main',[_t("default",[_v("默认内容")])],2),
_c('footer',[_t("footer")],2)
]
)
}
在编译章节我们了解到,
_t
函数对应的就是renderSlot
方法,它的定义在src/core/instance/render-heplpers/render-slot.js
中:
/**
* Runtime helper for rendering
*/
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) { // scoped slot
props = props || {}
if (bindObject) {
if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
)
}
props = extend(extend({}, bindObject), props)
}
nodes = scopedSlotFn(props) || fallback
} else {
const slotNodes = this.$slots[name]
// warn duplicate slot usage
if (slotNodes) {
if (process.env.NODE_ENV !== 'production' && slotNodes._rendered) {
warn(
`Duplicate presence of slot "${name}" found in the same render tree ` +
`- this will likely cause render errors.`,
this
)
}
slotNodes._rendered = true
}
nodes = slotNodes || fallback
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
render-slot
的参数 name
代表插槽名称 slotName
,fallback
代表插槽的默认内容生成的 vnode
数组。先忽略 scoped-slot
,只看默认插槽逻辑。如果 this.$slot[name]
有值,就返回它对应的 vnode
数组,否则返回 fallback
。那么这个 this.$slot
是哪里来的呢?我们知道子组件的 init
时机是在父组件执行 patch
过程的时候,那这个时候父组件已经编译完成了。并且子组件在 init
过程中会执行 initRender
函数,initRender
的时候获取到 vm.$slot
,相关代码在 src/core/instance/render.js
中:export function initRender (vm: Component) {
// ...
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
}
vm.$slots
是通过执行resolveSlots(options._renderChildren, renderContext)
返回的,它的定义在src/core/instance/render-helpers/resolve-slots.js
中:
/**
* Runtime helper for resolving raw children VNodes into a slot object.
*/
export function resolveSlots (
children: ?Array<VNode>,
context: ?Component
): { [key: string]: Array<VNode> } {
const slots = {}
if (!children) {
return slots
}
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
const data = child.data
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
const name = data.slot
const slot = (slots[name] || (slots[name] = []))
if (child.tag === 'template') {
slot.push.apply(slot, child.children || [])
} else {
slot.push(child)
}
} else {
(slots.default || (slots.default = [])).push(child)
}
}
// ignore slots that contains only whitespace
for (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}
resolveSlots
方法接收 2 个参数,第一个参数 chilren
对应的是父 vnode
的 children
,在我们的例子中就是
和 包裹的内容。第二个参数
context
是父 vnode
的上下文,也就是父组件的 vm
实例。
resolveSlots
函数的逻辑就是遍历 chilren
,拿到每一个 child
的 data
,然后通过 data.slot
获取到插槽名称,这个 slot
就是我们之前编译父组件在 codegen
阶段设置的 data.slot
。接着以插槽名称为 key
把 child
添加到 slots
中,如果 data.slot
不存在,则是默认插槽的内容,则把对应的 child
添加到 slots.defaults
中。这样就获取到整个 slots
,它是一个对象,key
是插槽名称,value
是一个 vnode
类型的数组,因为它可以有多个同名插槽。
这样我们就拿到了 vm.$slots
了,回到 renderSlot
函数,const slotNodes = this.$slots[name]
,我们也就能根据插槽名称获取到对应的 vnode
数组了,这个数组里的 vnode
都是在父组件创建的,这样就实现了在父组替换子组件插槽的内容了。对应的 slot
渲染成 vnodes
,作为当前组件渲染 vnode
的 children
。我们知道在普通插槽中,父组件应用到子组件插槽里的数据都是绑定到父组件的,因为它渲染成 vnode
的时机的上下文是父组件的实例。但是在一些实际开发中,我们想通过子组件的一些数据来决定父组件实现插槽的逻辑,Vue
提供了另一种插槽——作用域插槽,接下来我们就来分析一下它的实现原理。
作用域插槽,为了更加直观,我们也是通过一个例子来分析作用域插槽的实现:
let Child = {
template: '' +
' ' +
'',
data() {
return {
msg: 'Vue'
}
}
}
let vm = new Vue({
el: '#app',
template: '' +
'' +
'' +
'Hello from parent
' +
'{{ props.text + props.msg}}
' +
'' +
'' +
'',
components: {
Child
}
})
最终生成的 DOM 结构如下:
<div>
<div class="child">
<p>Hello from parentp>
<p>Hello Vuep>
div>
div>
slot
标签多了 text
属性,以及 :msg
属性。父组件实现插槽的部分多了一个 template
标签,以及 scope-slot
属性,其实在 Vue 2.5+
版本,scoped-slot
可以作用在普通元素上。这些就是作用域插槽和普通插槽在写法上的差别。在编译阶段,仍然是先编译父组件,同样是通过 processSlot
函数去处理 scoped-slot
,它的定义在在 src/compiler/parser/index.js
中:function processSlot (el) {
// ...
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to to ` +
`denote scoped slots.`,
true
)
}
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper for the ` +
`scoped slot to make it clearer.`,
true
)
}
el.slotScope = slotScope
}
// ...
}
这块逻辑很简单,读取
scoped-slot
属性并赋值给当前 AST 元素节点的slotScope
属性,接下来在构造 AST 树的时候,会执行以下逻辑:
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) {
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
currentParent.children.push(element)
element.parent = currentParent
}
scopedSlot
属性的 AST
元素节点而言,是不会作为 children
添加到当前 AST
树中,而是存到父 AST
元素节点的 scopedSlots
属性上,它是一个对象,以插槽名称 name
为 key
。然后在 genData
的过程,会对 scopedSlots
做处理:if (el.scopedSlots) {
data += `${genScopedSlots(el.scopedSlots, state)},`
}
function genScopedSlots (
slots: { [key: string]: ASTElement },
state: CodegenState
): string {
return `scopedSlots:_u([${
Object.keys(slots).map(key => {
return genScopedSlot(key, slots[key], state)
}).join(',')
}])`
}
function genScopedSlot (
key: string,
el: ASTElement,
state: CodegenState
): string {
if (el.for && !el.forProcessed) {
return genForScopedSlot(key, el, state)
}
const fn = `function(${String(el.slotScope)}){` +
`return ${el.tag === 'template'
? el.if
? `${el.if}?${genChildren(el, state) || 'undefined'}:undefined`
: genChildren(el, state) || 'undefined'
: genElement(el, state)
}}`
return `{key:${key},fn:${fn}}`
}
genScopedSlots
就是对 scopedSlots
对象遍历,执行 genScopedSlot
,并把结果用逗号拼接,而 genScopedSlot
是先生成一段函数代码,并且函数的参数就是我们的 slotScope
,也就是写在标签属性上的 scoped-slot
对应的值,然后再返回一个对象,key
为插槽名称,fn
为生成的函数代码。对于我们这个例子而言,父组件最终生成的代码如下:with(this){
return _c('div',
[_c('child',
{scopedSlots:_u([
{
key: "default",
fn: function(props) {
return [
_c('p',[_v("Hello from parent")]),
_c('p',[_v(_s(props.text + props.msg))])
]
}
}])
}
)],
1)
}
children
了,data
部分多了一个对象,并且执行了 _u
方法,在编译章节我们了解到,_u
函数对的就是 resolveScopedSlots
方法,它的定义在 src/core/instance/render-heplpers/resolve-slots.js
中:export function resolveScopedSlots (
fns: ScopedSlotsData, // see flow/vnode
res?: Object
): { [key: string]: Function } {
res = res || {}
for (let i = 0; i < fns.length; i++) {
if (Array.isArray(fns[i])) {
resolveScopedSlots(fns[i], res)
} else {
res[fns[i].key] = fns[i].fn
}
}
return res
}
fns
是一个数组,每一个数组元素都有一个 key
和一个 fn
,key
对应的是插槽的名称,fn
对应一个函数。整个逻辑就是遍历这个 fns
数组,生成一个对象,对象的 key
就是插槽名称,value
就是函数。这个函数的执行时机稍后我们会介绍。接着我们再来看一下子组件的编译,和普通插槽的过程基本相同,唯一一点区别是在 genSlot
的时候:function genSlot (el: ASTElement, state: CodegenState): string {
const slotName = el.slotName || '"default"'
const children = genChildren(el, state)
let res = `_t(${slotName}${children ? `,${children}` : ''}`
const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}`
const bind = el.attrsMap['v-bind']
if ((attrs || bind) && !children) {
res += `,null`
}
if (attrs) {
res += `,${attrs}`
}
if (bind) {
res += `${attrs ? '' : ',null'},${bind}`
}
return res + ')'
}
它会对
attrs
和v-bind
做处理,对应到我们的例子,最终生成的代码如下:
with(this){
return _c('div',
{staticClass:"child"},
[_t("default",null,
{text:"Hello ",msg:msg}
)],
2)}
_t
方法我们之前介绍过,对应的是renderSlot
方法:
export function renderSlot (
name: string,
fallback: ?Array<VNode>,
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) {
props = props || {}
if (bindObject) {
if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
)
}
props = extend(extend({}, bindObject), props)
}
nodes = scopedSlotFn(props) || fallback
} else {
// ...
}
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
this.$scopedSlots
又是在什么地方定义的呢,原来在子组件的渲染函数执行前,在 vm_render
方法内,有这么一段逻辑,定义在 src/core/instance/render.js
中: if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
这个 _parentVNode.data.scopedSlots
对应的就是我们在父组件通过执行 resolveScopedSlots
返回的对象。所以回到 genSlot
函数,我们就可以通过插槽的名称拿到对应的 scopedSlotFn
,然后把相关的数据扩展到 props
上,作为函数的参数传入,原来之前我们提到的函数这个时候执行,然后返回生成的 vnodes
,为后续渲染节点用。
总结:了解了普通插槽和作用域插槽的实现,它们有一个很大的差别是数据作用域,普通插槽是在父组件编译和渲染阶段生成 vnodes
,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes
。而对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes
,而是在父节点 vnode
的 data
中保留一个 scopedSlots
对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes
,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。简单地说,两种插槽的目的都是让子组件 slot
占位符生成的内容由父组件来决定,但数据的作用域会根据它们 vnodes
渲染时机不同而不同。