v-if
<template>
<div id="app">
<span v-if="isShow">hello2</span>
</div>
</template>
上面这段模板会被 Vue 编译器编译为下面的渲染函数
var render = function render() {
var _vm = this,
_c = _vm._self._c;
return _c('div', {
attrs: {
"id": "app"
}
}, [_vm.isShow ? _c('span', [_vm._v("hello2")]) : _vm._e()]);
};
笔者采用的 Vue2 版本为 2.7.15
由于 Vue2 的官方文档中没有提供在线的演练场,Vue2 要得到模板编译后的渲染函数会稍微麻烦一些,需要手动调用 Vue2 提供的 @vue/compiler-sfc
包。主要代码如下:
const compiler = require("@vue/compiler-sfc");
const fs = require("fs/promises");
// 使用 readFile 获取 Vue 单文件组件的代码内容
async function getVueString() {
const vueData = await fs.readFile("./App.vue", { encoding: "utf8" });
return vueData;
}
// 编译单文件组件的模板
async function compileSfc() {
const vFileString = await getVueString();
const res = compiler.parse({
source: vFileString,
filename: "App.vue",
});
const templateContent = res.template.content;
const templateRes = compiler.compileTemplate({
source: templateContent,
filename: "App.vue",
prettify: true,
});
const templateCode = templateRes.code;
return templateCode
}
compileSfc();
-
compiler.parse
函数会把组件源码解析为单文件组件的描述对象 -
compiler.compileTemplate
函数也是返回一个对象,该对象包含 ast 、code 和 source 属性,其中的 code 属性就是模板编译后的渲染函数。
可以看到 v-if
经过 Vue 的编译器编译为了三元表达式。条件表达式为 true
,则正常渲染元素,否则就渲染空注释节点。
_c
实际上就是 createElement
函数,作用是创建虚拟 DOM 。
// src/core/instance/render.ts
export function initRender(vm: Component) {
// ...
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
}
_v
实际上就是 createTextVNode
函数,作用是创建文本类型的虚拟 DOM。
_e
实际上就是 createEmptyVNode
函数,作用是创建一个空的注释节点
// src/core/instance/render-helpers/index.ts
import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'
export function installRenderHelpers(target: any) {
target._v = createTextVNode
target._e = createEmptyVNode
}
Vue 模板的编译过程首先会对模板做解析,解析生成 AST 树,包含 v-if
信息的 AST 元素会由 processIf
函数解析
// src/compiler/parser/index.ts
function processIf(el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
// 处理 v-if 指令
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
// 处理 v-else 指令
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
// 处理 v-else-if 指令
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
本文 Vue2 中的源码均摘自 2.7.15 版本
processIf
函数会调用 addIfCondition
收集 v-if
模板中的条件表达式和虚拟节点信息,存储到 AST 元素对象的 ifConditions 数组中
// src/compiler/parser/index.ts
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
Vue 中的模板经过解析(parse
)输出生成 AST 树后会对这颗树做优化(optimize
),做优化的过程实际上就是深度遍历这个 AST 树,去检测他的每一棵子树是不是静态节点。优化完后会进入代码生成的阶段。
含有 v-if
信息的 AST 元素会传给 genIf
函数,最终由该函数生成三元表达式
// src/compiler/codegen/index.ts
export function genIf(
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
genIf
函数实际上的调用 genIfConditions
函数。
// src/compiler/codegen/index.ts
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
)}:${genIfConditions(conditions, state, altGen, altEmpty)}`
} else {
return `${genTernaryExp(condition.block)}`
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp(el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
v-if 总结
Vue 模板经过解析(parse
),生成 AST 树(抽象语法树),然后由 addIfCondition
函数收集 v-if
的 AST 元素上的条件表达式和虚拟节点信息,存储到 AST 元素对象的 ifConditions 数组中,然后 Vue 编译器会对整个 AST 树做优化(optimize
),优化的过程实际上就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,最后就是把优化后的 AST 树转换成可执行的代码,v-if
的 AST 元素会传入 genIf
函数,最终生成三元表达式。
v-for
<template>
<div id="app">
<li v-for="todo in todos" :key="todo.name">
{{ todo.name }}
</li>
</div>
</template>
上面这段模板会被 Vue 编译器编译为下面的渲染函数
var render = function render() {
var _vm = this,
_c = _vm._self._c;
return _c('div', {
attrs: {
"id": "app"
}
}, _vm._l(_vm.todos, function (todo) {
return _c('li', {
key: todo.name
}, [_vm._v(" " + _vm._s(todo.name) + " ")]);
}), 0);
};
_c
、_v
函数的作用在上文已有说明,这里不在赘述
_s
实际上是 Vue 内部的 toString
函数
// src/core/instance/render-helpers/index.ts
import { toString } from 'shared/util'
export function installRenderHelpers(target: any) {
target._s = toString
}
toString
函数的源码实现比较简单,当传入的参数是 null ,则直接返回空字符串,传入的是数组或对象,则调用 JSON.stringify 方法将数组或对象转为字符串,其他类型的值,则调用 js 原生的 String()
函数,将传入的值转为字符串。
// src/shared/util.ts
const _toString = Object.prototype.toString
/**
* Convert a value to a string that is actually rendered.
*/
export function toString(val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
可以看到,v-for
指令会被编译为 _l
函数。_l
函数实际上是 Vue 内部渲染列表的函数(renderList
)
// src/core/instance/render-helpers/index.ts
import { renderList } from './render-list'
export function installRenderHelpers(target: any) {
target._l = renderList
}
源码的注释也说得很清楚,renderList
是用于渲染 v-for 列表的运行时辅助函数。可以这样理解,v-for 指令最终会被编译为 renderList
函数来执行。
// src/core/instance/render-helpers/render-list.ts
/**
* Runtime helper for rendering v-for lists.
*/
export function renderList(
val: any,
render: (val: any, keyOrIndex: string | number, index?: number) => VNode
): Array<VNode> | null {
let ret: Array<VNode> | null = null,
i,
l,
keys,
key
if (isArray(val) || typeof val === 'string') {
ret = new Array(val.length)
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i)
}
} else if (typeof val === 'number') {
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
} else if (isObject(val)) {
if (hasSymbol && val[Symbol.iterator]) {
ret = []
const iterator: Iterator<any> = val[Symbol.iterator]()
let result = iterator.next()
while (!result.done) {
ret.push(render(result.value, ret.length))
result = iterator.next()
}
} else {
keys = Object.keys(val)
ret = new Array(keys.length)
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i]
ret[i] = render(val[key], key, i)
}
}
}
if (!isDef(ret)) {
ret = []
}
;(ret as any)._isVList = true
return ret
}
renderList
函数中:
isDef
作用是判断传入的值是否有被定义
// src/shared/util.ts
export function isDef<T>(v: T): v is NonNullable<T> {
return v !== undefined && v !== null
}
isObject
作用是判断传入的值是否为非 null 的对象
// src/shared/util.ts
export function isObject(obj: any): boolean {
return obj !== null && typeof obj === 'object'
}
hasSymbol
作用是判断当前宿主环境是否支持原生 Symbol 和 Reflect.ownKeys。首先判断 Symbol 和 Reflect 是否存在,并使用 isNative 函数保证 Symbol 与 Reflect.ownKeys 全部是原生定义。
// src/core/util/env.ts
export const hasSymbol =
typeof Symbol !== 'undefined' &&
isNative(Symbol) &&
typeof Reflect !== 'undefined' &&
isNative(Reflect.ownKeys)
// src/core/util/env.ts
// 判断是否是浏览器原生支持的函数
export function isNative(Ctor: any): boolean {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
对于浏览器原生支持的函数,将该函数转为字符串时,该函数体会变为
native code
字符串。可利用这个特性来判断该函数是否为浏览器原生支持的函数。
renderList
函数的逻辑很清晰,分四种情况。
当传入的值(val
)为数组或字符串时,for 循环直接遍历:
// src/core/instance/render-helpers/render-list.ts
if (isArray(val) || typeof val === 'string') {
ret = new Array(val.length)
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i)
}
}
当传入的值(val
)为数字时,for 循环直接遍历,传入的数字是多少则遍历多少次:
// src/core/instance/render-helpers/render-list.ts
if (isArray(val) || typeof val === 'string') {
// ...
} else if (typeof val === 'number') {
// 传入的值为数字
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
}
当传入的值(val
)为对象时,则分为两种情况,当宿主环境支持 Symbol ,且传入的值含有迭代器是,则调用该对象内置的迭代器的 next() 方法进行遍历。否则,通过 Object.key 生成对象的属性数组,然后通过 for 循环遍历
// src/core/instance/render-helpers/render-list.ts
if (isArray(val) || typeof val === 'string') {
// ...
} else if (typeof val === 'number') {
// ...
} else if (isObject(val)) {
// 传入的值是对象
if (hasSymbol && val[Symbol.iterator]) {
// 宿主环境支持 Symbol 并且支持迭代器
ret = []
const iterator: Iterator<any> = val[Symbol.iterator]()
let result = iterator.next()
while (!result.done) {
ret.push(render(result.value, ret.length))
result = iterator.next()
}
} else {
// 通过 Object.key 生成对象的属性数组,然后通过 for 循环遍历
keys = Object.keys(val)
ret = new Array(keys.length)
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i]
ret[i] = render(val[key], key, i)
}
}
}
当传入的值不是数组、字符串、数字和对象时,则将返回结果 (ret
)设置为空数组,即 v-for 渲染的为空数组
// src/core/instance/render-helpers/render-list.ts
if (!isDef(ret)) {
ret = []
}
通过上面的分析可知,v-for
本质是调用 renderList
函数。那么 v-for
是如何被编译为 renderList
函数的?
Vue 模板的编译过程首先会对模板做解析,解析生成 AST 树,包含 v-for
信息的 AST 元素会由 processFor
函数解析
// src/compiler/parser/index.ts
export function processFor(el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
} else if (__DEV__) {
warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'])
}
}
}
getAndRemoveAttr
函数用于获取并删除 AST 元素上的属性,在这里则是获取 v-for 的属性值,即 todo in todos
。
getAndRemoveAttr
函数会遍历 AST 元素上 attrsList 属性,如果传入了 removeFromMap
参数,则会将属性从 AST 元素上的 attrsMap 属性中删除:
// src/compiler/helpers.ts
export function getAndRemoveAttr(
el: ASTElement,
name: string,
removeFromMap?: boolean
): string | undefined {
let val
// attrsMap 用于快速查找有没有某个属性,attrsList 用于存放所有的属性
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1)
break
}
}
}
if (removeFromMap) {
delete el.attrsMap[name]
}
return val
}
调用 getAndRemoveAttr
函数获得 v-for
指令的表达式后,会将该表达式传给 parseFor
函数,解析出 v-for
遍历的变量和定义的局部变量(alias
) 。
// src/compiler/parser/index.ts
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export function parseFor(exp: string): ForParseResult | undefined {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const res: any = {}
res.for = inMatch[2].trim()
const alias = inMatch[1].trim().replace(stripParensRE, '')
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
res.alias = alias.replace(forIteratorRE, '').trim()
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
} else {
res.alias = alias
}
return res
}
parseFor
函数使用了三个正则表达式:
forAliasRE
用于匹配类似于 for 循环的语法结构,例如 "item in array" 或 "key of object" 。
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
-
([sS]*?)
,这是一个捕获组,用来匹配任意空白字符或非空白字符,并且是非贪婪的,也就是说会尽可能少地匹配字符。这个捕获组的作用是匹配循环语句中的循环变量(例如 "item"、"key")。 -
s+
, 匹配至少一个空白字符。 -
([sS]*)
,这是第二个捕获组,字符或非空白字符,这表示循环中的数组或对象(例如 "array"、"object")
forIteratorRE
用于匹配一个逗号分隔的字符串,该字符串由逗号开头,并且后续由逗号和非逗号、非右大括号、非右中括号的字符组成。
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
借助正则表达式可视化工具可视化上面的正则表达式,则更加容易理解上面正则表达式的作用:
例如对于如下模板:
<div id="app">
<li v-for="(value, name, index) in object" :key="value">
{{ index }}. {{ name }}: {{ value }}
</li>
</div>
forIteratorRE
会匹配到 '(value, name, index) in object'
字符串中的 ', name, index'
。
match 方法返回的数组中,在索引 0 处,为该正则完整的匹配项;在索引 1 处,第一个捕获括号匹配的内容,在索引 2 处,第二个捕获括号匹配的内容,以此类推。
forIteratorRE
正则中有两个捕获组和一个非捕获组:
捕获组匹配到的结果会作为 match 方法的返回数组中的元素,在索引 1 处为第一个捕获括号匹配的内容。在索引 2 处为第二个捕获括号匹配的内容,以此类推。
非捕获组匹配到的结果不会出现在 match 方法返回的数组中。
stripParensRE
用于匹配字符串开头的左括号'('
或者字符串末尾的右括号 ')'
。使用正则可视化工具可视化后的结果:
const stripParensRE = /^\(|\)$/g
^
:表示匹配输入的开始位置,在这里表示左括号('('
)开头的字符串
(|)
:表示匹配左括号或者右括号
|
:是一个逻辑或操作符,表示在匹配时可以选择两个条件中的任意一个
$
:表示匹配输入的结束位置,在这里表示以括号结尾的字符串
g
:表示进行全局匹配,即匹配所有符合条件的子字符串
假如 parseFor
函数传入的参数 exp
为 '(value, name, index) in object'
,则执行过程如下:
// export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// 使用 forAliasRE 匹配 exp 字符串
var inMatch = exp.match(forAliasRE);
得到 inMatch
为如下结果数组
// 如果 inMatch 为空,则直接返回
if (!inMatch) return
// 定义要返回的对象
const res: any = {}
// 取 inMatch 数组中索引为 2 的值,即 forAliasRE 正则
// 第二个捕获括号匹配的结果,也即 v-for 遍历的那个变量
// 在这里是 object
res.for = inMatch[2].trim()
// 取 inMatch 数组中索引为 1 的值,即 forAliasRE 正则
// 第一个捕获括号匹配的结果,在这里是 '(value, name, index)' ,
// 使用 stripParensRE 正则去除该字符串的左右括号,
// 即 alias 的值为 'value, name, index'
const alias = inMatch[1].trim().replace(stripParensRE, '')
// 使用 forIteratorRE 正则匹配,
// 目的是为了得到 v-for 定义的遍历指针
const iteratorMatch = alias.match(forIteratorRE)
得到 iteratorMatch
为如下结果数组
// 如果 `iteratorMatch` 有值
if (iteratorMatch) {
// 则将 alias 中匹配 forIteratorRE 正则的字符串替换为空字符串,
// 得到 v-for 指令中定义的第一个局部变量,保存在 res 的 alias 属性中
res.alias = alias.replace(forIteratorRE, '').trim()
// 取 forIteratorRE 正则第一个捕获括号中匹配到的结果,
// 去除左右两边空字符串,得到 v-for 指令中的第二遍历指针,
// 并保存到 res 的 iterator1 属性中
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
// 取 forIteratorRE 正则第二个捕获括号中匹配到的结果,
// 去除左右两边空字符串,得到 v-for 指令中的第三遍历指针,
// 并保存到 res 的 iterator2 属性中
res.iterator2 = iteratorMatch[2].trim()
}
}
if (iteratorMatch) {
// ...
} else {
// iteratorMatch 数组不存在,则 v-for 指令只定义了一个
// 局部变量,直接将 alias 保存在 res 的 alias 属性中
res.alias = alias
}
// 最后返回 res
return res
最后得到 res
对象为:
其中包含了 v-for
指令需要遍历的变量和迭代指针。
在 processFor
函数中调用 parseFor
解析完 v-for
指针相关信息后,会将解析到的信息存储到 AST 元素中:
// src/compiler/parser/index.ts
export function processFor(el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
// 将解析到的 v-for 指令相关信息,
// 存储到 AST 元素中
extend(el, res)
} else if (__DEV__) {
warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'])
}
}
}
extend
函数的逻辑很简单,就是接收两个对象,将后一个对象合并到前一个对象中。如果遇到 key 相同的情况,则后一个对象的值会覆盖前一个对象的值。
// src/shared/util.ts
/**
* Mix properties into target object.
*/
export function extend(
to: Record<PropertyKey, any>,
_from?: Record<PropertyKey, any>
): Record<PropertyKey, any> {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
与 v-if
指令一样,解析(parse
)输出生成 AST 树后会对这棵树做优化(optimize
),做优化的过程实际上就是深度遍历这个 AST 树,去检测他的每一棵子树是不是静态节点。优化完后会进入代码生成的阶段。
含有 v-for
信息的 AST 元素会传给 genFor
函数,最终由该函数生成执行 renderList
函数的代码。
最终本示例的模板:
<div id="app">
<li v-for="(value, name, index) in object" :key="value">
{{ index }}. {{ name }}: {{ value }}
</li>
</div>
会编译为如下代码执行:
var render = function render() {
var _vm = this,
_c = _vm._self._c;
return _c('div', {
attrs: {
"id": "app"
}
}, /*v-for 指令最终编译为的代码*/_vm._l(_vm.object, function (val, name, index) {
return _c('li', {
key: val
}, [_vm._v(" " + _vm._s(index) + ". " + _vm._s(name) + ": " + _vm._s(val) + " ")]);
}), 0);
};
genFor
函数就是处理 AST 节点上与 v-for
相关的属性,生成对应字符串,该字符串就是最终执行的代码。
// src/compiler/codegen/index.ts
export function genFor(
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (
__DEV__ &&
state.maybeComponent(el) &&
el.tag !== 'slot' &&
el.tag !== 'template' &&
!el.key
) {
state.warn(
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://v2.vuejs.org/v2/guide/list.html#key for more info.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
el.forProcessed = true // avoid recursion
return (
`${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
)
}
v-for 总结
通过阅读源码,可知道 v-for
可以对数组、字符串、数字和对象进行遍历。当遍历的是数字是,他会把模板重复对应次数。
v-for
里面做了异常处理,当传入了不属于数组、字符串、数字和对象的值时,v-for
渲染的是一个空数组。
Vue 模板经过解析(parse
),生成 AST 树(抽象语法树),然后由 parseFor
解析 AST 元素上 v-for
指令表达式中相关信息,并将其存储到 AST 的属性中,然后 Vue 编译器会对整个 AST 树做优化(optimize
),优化的过程实际上就是深度遍历这个 AST 树,去检测他的每一棵子树是不是静态节点,最后就是把优化后的 AST 树转换成可执行代码,v-for
的 AST 元素会传入 genFor
函数,最终生成执行 renderList
函数的代码。
所以说,v-for
指令本质上是执行 Vue 内部的 renderList
函数。
总结
v-if
和 v-for
都是 Vue 提供的指令。
v-if
指令最终会被编译为三元表达式,v-for
指令最终被编译为内部的 renderList
函数执行。
Vue 提供的指令是在模板中使用的,Vue 模板经过解析(parse
),生成 AST 树(抽象语法树)。
对于 v-if
来说,会由 addIfCondition
函数收集 v-if
的 AST 元素上的条件表达式和虚拟节点信息,存储到 AST 元素对象的 ifConditions 数组中,然后 Vue 编译器会对整个 AST 树做优化(optimize
),优化的过程实际上就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,最后就是把优化后的 AST 树转换成可执行的代码,v-if
的 AST 元素会传入 genIf
函数,最终生成三元表达式。
对于 v-for
来说,会由 parseFor
解析 AST 元素上 v-for
指令表达式中相关信息,并将其存储到 AST 的属性中。然后跟 v-if
一样,进入 AST 树的优化(optimize
)阶段,最终进入代码生成阶段,v-for
的 AST 元素会传入 genFor
函数,最终生成执行 renderList
函数的代码。v-for
指令本质上是执行 Vue 内部的 renderList
函数。