携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情
前言
源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大
指令生效,其实就是在合适的时机执行定义指令时所设置的钩子函数
v-for
在对template
进行解析时会将相关指令收集,在编译时会做统一的处理,会将v-for
生成_l
函数(类似于 forEach)。处理时先处理v-for
,再处理v-if
,所以**v-for
的优先级比v-if
高,如果同时写v-for
和v-if
,假如v-if
值为false
,那么先v-for
渲染,再v-if
隐藏,多出了不必要的渲染,所以不推荐v-for
和v-if
同时使用,而是利用计算属性代替**
// src/compiler/codegen/index.js
export function genElement (el: ASTElement, state: CodegenState): string {
...
else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
}
...
}
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
// v-for='a in arr'
const exp = el.for // arr
const alias = el.alias // a
const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' // 第一个参数
const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' // 第二个参数
el.forProcessed = true // avoid recursion生成循环函数,防止死循环
// 字符串拼接 -l((arr),function(a){return _c('div'), {}})
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
v-if
v-if
在编译时会产生类似三元表达式的写法,如果不显示就会走之后的逻辑(比如 v-else),将当前编译成_e
函数(空虚拟节点)
export function genIf(
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string,
): string {
el.ifProcessed = true; // el.ifConditions.slice()可能会有多个条件 v-if v-else v-else-if
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
}
function genIfConditions(
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string,
): string {
if (!conditions.length) {
return altEmpty || '_e()';
}
// 取出第一个条件
const condition = conditions.shift();
// 三元表达式
if (condition.exp) {
// 如果有表达式
return `(${condition.exp})?${
// 将表达式拼接起来
genTernaryExp(condition.block)
}:${
// v-else-if
genIfConditions(conditions, state, altGen, altEmpty)
}`;
} else {
return `${genTernaryExp(condition.block)}`; // 没有表达式就直接生成元素 像v-else
}
function genTernaryExp(el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state);
}
}
v-show
v-show
指令类似于自定义指令,会在不同时机去调用设置的钩子函数,在创建虚拟节点的时候会设置style
的display
属性
// /src/platforms/directives/show.js
export default {
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
vnode = locateNode(vnode)
const transition = vnode.data && vnode.data.transition
const originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display
if (value && transition) {
vnode.data.show = true
enter(vnode, () => {
el.style.display = originalDisplay
})
} else {
el.style.display = value ? originalDisplay : 'none'
}
},
update (el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {
...
},
unbind (el,binding,vnode,oldVnode,isDestroy) {
if (!isDestroy) {
el.style.display = el.__vOriginalDisplay
}
}
}
v-model
v-model
使用场景有两种方式,一种是表单控件绑定,一种是组件上使用
解析属性
// src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
if (bindRE.test(name)) { // v-bind
...
} else if (onRE.test(name)) { // v-on
...
} else { // normal directives
// 替换v-
name = name.replace(dirRE, '')
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
...
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
}
} else {
...
}
}
}
// src/compiler/helpers.js
export function addDirective (
el: ASTElement,
name: string,
rawName: string,
value: string,
arg: ?string,
isDynamicArg: boolean,
modifiers: ?ASTModifiers,
range?: Range
) {
// 添加到el.directives数组中
(el.directives || (el.directives = [])).push(rangeSetItem({
name,
rawName,
value,
arg,
isDynamicArg,
modifiers
}, range))
el.plain = false
}
生成代码
// src/compiler/codegen/index.js
let data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state);
}
export function genData (el: ASTElement, state: CodegenState): string {
const dirs = genDirectives(el, state)
...
}
function genDirectives (el: ASTElement, state: CodegenState): string | void {
// gen此时为model
const gen: DirectiveFunction = state.directives[dir.name]
}
// src/platforms/web/compiler/directives/model.js
export default function model (el,dir,_warn){
warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
// 动态组件
if (el.component) {
genComponentModel(el, value, modifiers)
return false
// 判断tag类型
} 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)
return false
}
return true
}
function genDefaultModel (el,value,modifiers) {
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}`
}
// 为el(input)添加value的prop
addProp(el, 'value', `(${value})`)
// 为el添加事件
addHandler(el, event, code, null, true)
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()')
}
}
v-model
事实上是input + :value
的语法糖(两者还是有些许差别),编译阶段会在v-model
的元素上定义一个value
的prop
,并生成一个inputevent
事件,所以在使用v-model
时不能定义value的prop,会冲突
// 示例
// 此种写法不能和v-model等价,当输入中文时,输入一个字母时,此种写法会实时更新,而v-model不会,会监听compositionstart和compositionend事件,当监听到输入结束时会手动调用input方法
let vm = new Vue({
el: '#app',
template: '<div>' +
'<p>{{message}}</p>' +
'<input' + ':value="message"' +
'@input="message=$event.target.value"' +
'placeholder="edit me"' + '</div>',
data(){
return {
message: ''
}
}
})
在组件上使用
let chile = {
template: '<div>' +
'<input : value="value" @input="updateValue"' + '</div>',
props: ['value'],
methods: {
updateValue(e) {
this.$emit('input', e.target.value)
}
}
}
let vm = new Vue({
el: '#app',
template: '<div>'+
'<child v-model="message"></child>' +
'<p>{{message}}</p>' +
'</div>',
data(){
return {
message: ''
}
},
components: {child}
})
与表单控件绑定不一样的是,在生成代码阶段会走不同的处理函数,它会创建model属性,定义值和回调函数,为data扩展model对象,在创建组件时会转化为props和events,所以再子组件内需要定义model
对象,存放event(事件)
和prop
// src/compiler/codegen/index.js
if (el.component) {
genComponentModel(el, value, modifiers)
return false
// 判断tag类型
} 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)
return false
}
// core/compiler/directives/model.js
export function genComponentModel (el,value,modifiers) {
const { number, trim } = modifiers || {}
const baseValueExpression = '$$v'
...
// 定义表达式
const assignment = genAssignmentCode(value, valueExpression)
// 创建model属性,定义值和回调函数,为data扩展model对象,在创建组件时会转化为props和events
el.model = {
value: `(${value})`,
expression: JSON.stringify(value),
callback: `function (${baseValueExpression}) {${assignment}}`
}
}
export function genAssignmentCode (value,assignment) {
const res = parseModel(value)
if (res.key === null) {
return `${value}=${assignment}`
} else {
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
v-slot
插槽有普通插槽和作用域插槽,两者区别是渲染位置不同
- 普通插槽是父组件编译完毕后替换子组件的内容
<div id ="app">
<home>
<h1 v-slot:title>标题</h1>
<div #content>内容</div>
</home>
</div>
<script>
Vue.component('home', {
template: `<div>
<slot name="title"></slot>
<slot name="content"></slot>
</div>`
})
</script>
首先编译父组件如果遇到子组件有slot
会给对应的ast元素节点的data上存放slot属性,值为插槽名字,父组件编译完成后,在codegen
阶段开始编译子组件,在编译子组件时在parser
阶段,遇到slot
标签时候会给对应的ast元素节点添加slotName
属性,在codegen
阶段会判断如果当前是slot
标签,则执行genSlot
函数得到_t
函数,_t
函数会拿插槽名字去$slot
属性上找对应的vnode,在编译子组件时候父组件已经编译完成,$slots
属性的生成是在子组件init
过程中会执行initRender
函数,会执行resolveSlot
方法,遍历父vnode的children,拿到每一个child的data,通过data.slot
拿到插槽名称,接着以插槽名称为key
把child添加到slots
中,如果data.slot
不存在,则是默认插槽的内容,则把对应的child添加到slots.defaults
中,$slots
就是slots
,它是一个对象,key
是插槽名称,value
是一个vnode类型
的数组,因为他可以有多个同名插槽
- 作用域插槽是在子组件里边渲染插槽的内容
<div id ="app">
<home>
<template slot-scope="{article}">
<h1>{{article.title}}</h1>
</template>
</home>
</div>
<script>
Vue.component('home', {
template: `<div>
<slot :article="{title: '标题',content: '内容'}"></slot>
</div>`
})
</script>
解析插槽
// src/compiler/parser/index.js
function processSlotContent (el) {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
el.slotScope = slotScope
}
// 取当前属性绑定的slot
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {// 增加slotTarget属性
//如果没有给名字会默认是default
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
if (el.tag !== 'template' && !el.slotScope) {
// 给el添加slot属性 {slot: xxx}
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
// 2.6.x处理v-slot
if (process.env.NEW_SLOT_SYNTAX) {
...
}
}
生成代码
// src/compiler/codegen/index.js
// slot 为data扩展一个slot属性
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// <slot></slot>
function genSlot (el, state) {
const slotName = el.slotName || '"default"'
const children = genChildren(el, state)
// 生成_t函数(去$slots属性上找对应的name的vnode)
let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
const attrs = el.attrs || el.dynamicAttrs
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
name: camelize(attr.name),
value: attr.value,
dynamic: attr.dynamic
})))
: null
...
}
首先是编译父组件,读取slot-scope
属性并赋值给当前ast元素节点的slotScope
属性,构造ast树的时候,对于拥有slotScope
属性的元素而言,是不会作为children
添加到ast中,而是存在了父元素节点的scopedSlots
(对象,key为插槽名字)属性上,在codegen
生成代码阶段会对scopedSlots
对象遍历,执行genScopedSlot
函数,genScopedSlot
会生成一段函数代码,参数时scoped-slot
对应的值,返回一个对象,key
是插槽名称,fn
是生成的函数代码,此时,scopedSlots
为一个_u
函数(遍历传入的数组,生成一个对象,对象的key
是插槽名称,value是函数)。在编译子组件时,与普通插槽过程基本相同,唯一区别在于codegen
时,会对attrs
和v-bind
做处理,也会得到_t
函数,会去$scopedSlots
去找插槽名字的fn
,然后把相关数据扩展到slot-scope
属性上,作为函数的参数传入,执行函数返回生成的vnode
,后续渲染。$scopedSlots
是在子组件渲染前执行时得到
普通插槽和作用域插槽区别:
- 普通插槽在父组件编译和渲染阶段生成vnode,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的vnode
- 作用域插槽父组件在编译和渲染阶段并不会直接生成vnode,而是在父节点vnode的data中保留一个scopedSlots对象,储存着不同名称的插槽以及他们对应的渲染函数,只有在渲染子组件阶段才会执行这个渲染哈数生成vnode