前奏
笔者的整理出vue编译思想篇后,又整理了编译的parse部分。
今天准备的详细得讲解下vue在parse解析生成astElement后,编译过程中另外两个重要的部分optimize和code generate。
正文
一、optimize
让我们先看下vue中关于这部分的注释:
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
大致意思就是不需要重新创建新的nodes,跳过patch的过程。
再次重申下vue的工作流程。
-
- parse 解析template,生成astElement tree。
-
- optimize 遍历 astElement, 给astElement 打上必要的staticRoot标签。
-
- code generate,将astElement转换成函数的形式,但是是以string的形式存在,等待执行。
-
- render函数,执行第三步得到的string,生成vnode 节点,也就是我们经常碎碎念念的虚拟dom了。
-
- patch,对比新旧虚拟dom,生成真实的dom。
笔者花了漫长的时间写完了第一个过程的解析部分,今天带来的源码解析是关于2和3部分的。
而optimize的优化的部分就是在patch过程中直接忽略的。 下面给出这块的模块:
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '') //
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root) // 模块一
// second pass: mark static roots.
markStaticRoots(root, false) // 模块二
}
1)我们先讲下功能函数isStaticKey:
首先我们得知道options.staticKeys其实是一个string:
"staticClass,staticStyle"
genStaticKeysCached的源码是:
const genStaticKeysCached = cached(genStaticKeys);
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
(keys ? ',' + keys : '')
)
}
- 1.genStaticKeysCached 就是 cached的 return 也就是 cachedFn, 所以
genStaticKeysCached(options.staticKeys || '')
就是
cachedFn(options.staticKeys || ''),
- 2.因为一开始cache并不存在,所以会执行
cache[options.staticKeys] = fn(options.staticKeys);
而fn函数是传递进来的函数 genStaticKeys:
也就是执行的其实是:
cache[options.staticKeys] = genStaticKeys(options.staticKeys);
在替换options.staticKeys 到 "staticClass,staticStyle",得到:
cache["staticClass,staticStyle"]= genStaticKeys("staticClass,staticStyle")
所以: 最终结果是 :
markUp('type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle')
这个函数是干嘛的? 源码如下:
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
很容易发现,其实:
字符串'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle',会变成对象:
const map = {
type:true,
tag:true,
attrsList:true,
attrsMap:true,
plain:true,
parent:true,
...,
staticClass:true,
staticStyle:true
}
最后返回的函数其实是:
isStaticKey = (val)=> map[val];
恍然大悟是不是,就是判断到底是不是静态key,所谓的静态key就是上诉的这些属性。 这个函数下面会使用到的。
2)isPlatformReservedTag功能函数详解:
这个函数很简单,其实就是判断是否为平台的tag,web平台也就是普通的标签和svg标签。
3)markStatic函数。
这个函数其实是辅助函数,就是标志ast树的每个节点是否为静态的节点,具体的判断方式,需要拿源码的进行分析。
function markStatic (node: ASTNode) {
node.static = isStatic(node) // 重要函数
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) { // 模块二
const child = node.children[i]
markStatic(child)
if (!child.static) { //
node.static = false
}
}
if (node.ifConditions) { // 模块三
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
先从宏观的角度来思考符合static的标准。
-
- node.type为2的时候一定不是静态节点,因为这个标志代表的是表达式。具体细节请参考笔者的parse篇
-
- node.type 为3的时候一定是静态节点,因为这是文本。
-
- node.type为1的时候这么考量的呢??
很复杂
- node.type为1的时候这么考量的呢??
需要符合下面两个要求:
- 1.符合一系列的要求,这块需要查看isStatic的源码部分。
- 2.子节点必须都是静态节点,或者if的许多块级也不能存在动态节点。
两者缺一不可,我们一一来分析下:
先来看第一点,isStatic源码如下:
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
第一点中所谓的一些列的要求,其实就是这个函数的return部分。
-
当是node.pre,的时候就是静态节点。
-
或同时满足以下所有条件。
- 1). 没有动态bind如:
v-focus :focus @focus .focus - 2)没有if,没有for。
- 3)非built-in 标签,也就是不是 slot或着component标签。
- 4)必须是平台标签。
- 5)不是template for 中的一个节点的直属孩子。
- 6)所有属性都是静态属性,来自于前面分析的isStaticKey函数的判断。
- 1). 没有动态bind如:
再看第二点
也就是说当上面第一点当所有要求都已经达到了,依然只是基本条件。
还有要求就是所有的孩子节点都要满足上述当条件,才能算得上是静态节点。
4)markStaticRoots函数详解
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor // 添加 staticInFor的标志。后面的codegen会用到
}
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
markStaticRoots函数主要给所有符合要求的节点打上两个标志:
staticInFor和staticRoot,而这个标志才是真正能用于下一个大阶段code generate。
staticInFor非常简单,只要节点的祖先节点处于for当中,且自身有static或once属性,就会被标记。
再看看staticRoot,它的条件被分为两个部分,
node.static && node.children.length // 条件一
&& !( // 条件二
node.children.length === 1 &&
node.children[0].type === 3
)
条件一的意思是满足static且有孩子节点。
条件二的意思是。把条件一当中的只有一个孩子,且是动态节点的情况去掉。 举个例子:
<div>somexxx</div>
这种情况下div是不能算是staticRoot。
相信会有人心中会有疑问,为啥?
这点我们后面会在code generate中给出答案。
总结
1.父节点是static,孩子一定是static,,子节点未必是staticRoot,父节点一定是staticRoot。
2.真正起到作用的是staticInFor和staticRoot属性。
二、code generate。
再次强调,generate生成的是字符串,短函数类的字符串,为后面的render的执行做准备。本质是对象形式转换成短函数形式。
解析前的准备
本质上,我们会从一个astElement Tree
变成以函数为单位的树节点:
_c(
'div',
{
key:'xxx',
ref:'xx',
pre:'xxx',
domPro:xxx,
....
},
[ // chidren
_v(_s('ding')),
_c('p',{model:'isshow',}, [ ...xxx ])
]
)
那么一共有哪些短函数呢?
关于短函数,其实是在renderMixin中定义的,让我们看看。
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
我们需要明白 执行玩短函数得到短是另一种树,vnode树。 所以这些短函数要么是创建一些vnode节点,要么是为vnode节点创建一些属性。
所以我们短函数格式主要是
function ('tagName',{ id:'xxx' }, [ ... ]) {}
第一个参数是tag名称,第二个是属性,第三个参数则是子函数,也就是子节点。
接下来让我们看看主函数把。
1)主流程分析。
主流程是generate函数开始的。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options) // 全程记录当前编译状态的参数
const code = ast ? genElement(ast, state) : '_c("div")' // 生成我们需要的短函数字符串
return { // 输出。
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
先看下我们return的内容吧
return { // 输出。
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
可以看到一共两个属性render和staticRenderFns,
简单先说下,optimize部分标记的staticRoot和staticInFor不会放在code当中,而是放在staticRenderFns。 具体的玩法,后面会有详细的讲解。
函数的话,主要只有两个过程,上面的代码我已经将两个模块分别给出了注释。
我们先来看看第一个块的具体情况:
const state = new CodegenState(options) // 全程记录当前编译状态的参数
其实上面的注释并不准确,state还有很多功能函数,CodegenState本身是个class,具体如下:
export class CodegenState {
options: CompilerOptions;
warn: Function;
transforms: Array<TransformFunction>;
dataGenFns: Array<DataGenFunction>;
directives: { [key: string]: DirectiveFunction };
maybeComponent: (el: ASTElement) => boolean;
onceId: number;
staticRenderFns: Array<string>;
pre: boolean;
constructor (options: CompilerOptions) {
this.options = options // 平台传递的options,主要是一些工具函数
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode') // 笔者没弄懂的地方
this.dataGenFns = pluckModuleFunction(options.modules, 'genData') // 处理style 和 class的部分
this.directives = extend(extend({}, baseDirectives), options.directives) // 重点部分,指令处理
const isReservedTag = options.isReservedTag || no // 判断是否为平台 的标签
this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) // 初次判断是否为组件的函数
this.onceId = 0 // 静态节点的记录,后面会有详细解说
this.staticRenderFns = [] // 所有静态生成函数
this.pre = false // 是否在pre当中。。
}
}
需要说明的是directive其实只是生成短函数,真是短处理都是在render函数和patch部分完成的。如果对vue源码的周期还不熟悉的同学,请翻阅笔者的parse思想篇。
再看看第二模块的:
const code = ast ? genElement(ast, state) : '_c("div")'
可以看到,ast的兜底是短函数_c("div"),否则进入核心函数genElement。
genElement是codegen流程最重要的功能函数,处理短函数的绝大部分情况。
看下源码:
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) { // 这里使用了 staicRoot optimize 里面做的标记
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state);
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
let a = genChildren(el, state) || 'void 0'
return a
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state) // 处理 标签所有属性
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
ifelse的分支一共分为8种情况,让我们来一一分析下。
(1)genStatic
先来看看判断的条件
el.staticRoot && !el.staticProcessed
恍然大悟有没有,原来optimize部分的staticRoot是在这个地方处理的,之前我们有说过,staticRoot的节点,在patch的过程中,只做第一次渲染,后面的对比是完全忽略的。
我们需要考虑两个问题
- 1.静态节点,是怎么完成一次渲染的?
- 2.静态节点,是在哪里判断不用再次渲染的?
先看看静态节点怎么渲染的,先观看genStatic代码:
// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
el.staticProcessed = true
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) //保存静态渲染的函数
state.pre = originalPreState
return `_m(${ // 生成短函数
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
state.staticRenderFns以数组的形式保存静态渲染函数,短函数真正执行的守候,根据传递的index,去数组拿需要渲染的静态函数。
看下_m短函数的原函数是renderStatic:
export function renderStatic (
index: number,
isInFor: boolean
): VNode | Array<VNode> {
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree.
if (tree && !isInFor) {
return tree
}
// otherwise, render a fresh tree.
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
) // 直接生成vnode
markStatic(tree, `__static__${index}`, false) // 打上标记
return tree
}
renderStatic的主要做了两件事,那就是生成vnode和打上static标记。所谓的标记就是:
function markStaticNode (node, key, isOnce) {
node.isStatic = true
node.key = key
node.isOnce = isOnce
}
当然这些参数在patch环节会发挥相应的作用,这里不做赘述,笔者后续的源码分析模块会有patch的源码解析。
2)
// v-once 本质就是static
function genOnce (el: ASTElement, state: CodegenState): string {
el.onceProcessed = true
if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.staticInFor) {
let key = ''
let parent = el.parent
while (parent) { // 先找到key
if (parent.for) {
key = parent.key
break
}
parent = parent.parent
}
if (!key) {
process.env.NODE_ENV !== 'production' && state.warn(
`v-once can only be used inside v-for that is keyed. `,
el.rawAttrsMap['v-once']
)
return genElement(el, state)
}
return `_o(${genElement(el, state)},${state.onceId++},${key})`
} else {
return genStatic(el, state)
}
}
genOnce情况主要分为3种:
- 1.v-if标签
- 2.staticInFor (再次遇到optimize阶段打的标记,这里不做太多赘述)
- 3.genStatic。
所以genOnce的基础就是genStatic,但是当持有v-if的标签的时候,就必须走genIf,这是因为if的形式是将节点放在v-if的标签的ifConditions数组当中的,根据具体的条件进行渲染目标模块,并不具备一次渲染的情况
至于staticInFor,情况则比较复杂了。
我们必须知道Vue的数组 后续补充。。