楔子
笔者公司的前端小组掀起了Vue源码学习小组,前后几个月的共同学习,让小组成员都已经对Vue对大致框架有了个模糊对轮廓。
现在已经进入第二阶段:整理。
我们将小组分为四个部分,vue对整理也分为三个大模块:数据绑定、从template到vnode、vnode转化为dom对patch。
笔者对小组被分到了template到vnode对部分,拿到手后,感觉内容比较多,就先将内容根据源码对布局分为两小块:parse 和 render过程。
再者,古人云:兵马未动,粮草先行。笔者打算先介绍思想和思路,然后在具体到一些细枝末节。
本文准备到就是parse部分思想。
正文
一、整个编译过程的目的
我们都知道使用vue到时候,我们使用定制样式到几种方式大致为:
- 1.options template。
- 2.el。(挂载的节点)
- 3.render 方法。
我们需要明白到是无论是哪种方式,我们最终的目的都是
生成以vnode为单位vdom树
而生成的vdom树,最终是用于patch过程中,生成真实dom节点。
所以我们的编译的最终目的是获得:
Virtue dom。
形式大致如下图:
暂时不用管每个属性的作用,我们已经知道我们的目标是怎样的了。那么还需要知道的是 起始点和过程。
正常情况下,如果我们这样初始化的话:
new Vue({
el: '#el',
template: `
<div>
<div v-for="(item,index) in options" :key="item.id">
{{item.id}}
<div>{{item.text}}</div>
</div>
</div>
`,
data: {
name:"dinglei",
options: [
{ id: 1, text: 'Hello' },
{ id: 2, text: 'World' }
]
}
})
那我们的初始化的状态则是 template的 一串 String。
当然初始化为String的写法还是满多的。 比如:
- 不传template,直接使用使用$el。
- template,使用一个模版。
所以我们从的转换过程,就是一串String 到 virtue dom的过程。
但是值得一提的是,vue 从模版到vdom的过程并非是直接一次性到位的过程。可能是因为尤大的设计的vnode和原生的dom属性差距过大,直接编译成vnode不好完成。其次是考虑到性能优化等方面。
所以vue的编译过程其实是分为三个过程:
- parse
- optimize
- codegen for render And render
分别对应三个过程:
- 从模版到 astElement
- 优化添加标记staticRoot,当然这个层面的static是用于codegen里面的。
- codegen 生成 render函数,render 绑定实例后执行生成vnode。
接下来我们将进入本文的主题
二、parse、optimize、codegen的核心思想解读。
1)parse解读
首先需要明白astElement包括哪些属性。在vue源码 flow文件目录下的compiler.js可以找到astElement的模型
declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<ASTAttr>;
attrsMap: { [key: string]: any };
rawAttrsMap: { [key: string]: ASTAttr };
parent: ASTElement | void;
children: Array<ASTNode>;
start?: number;
end?: number;
processed?: true;
static?: boolean;
staticRoot?: boolean;
staticInFor?: boolean;
staticProcessed?: boolean;
hasBindings?: boolean;
text?: string;
attrs?: Array<ASTAttr>;
dynamicAttrs?: Array<ASTAttr>;
props?: Array<ASTAttr>;
plain?: boolean;
pre?: true;
ns?: string;
component?: string;
inlineTemplate?: true;
transitionMode?: string | null;
slotName?: ?string;
slotTarget?: ?string;
slotTargetDynamic?: boolean;
slotScope?: ?string;
scopedSlots?: { [name: string]: ASTElement };
ref?: string;
refInFor?: boolean;
if?: string;
ifProcessed?: boolean;
elseif?: string;
else?: true;
ifConditions?: ASTIfConditions;
for?: string;
forProcessed?: boolean;
key?: string;
alias?: string;
iterator1?: string;
iterator2?: string;
staticClass?: string;
classBinding?: string;
staticStyle?: string;
styleBinding?: string;
events?: ASTElementHandlers;
nativeEvents?: ASTElementHandlers;
transition?: string | true;
transitionOnAppear?: boolean;
model?: {
value: string;
callback: string;
expression: string;
};
directives?: Array<ASTDirective>;
forbidden?: true;
once?: true;
onceProcessed?: boolean;
wrapData?: (code: string) => string;
wrapListeners?: (code: string) => string;
// 2.4 ssr optimization
ssrOptimizability?: number;
// weex specific
appendAsTree?: boolean;
};
整个流程我们需要的归纳为:
- 识别器(parseHTML) (1)
- 存储栈(stack) (2)
- 创建函数(createASTElement) (3)
- 上下文(currentParent)(4)
整个工作方式:
- 1.就是识别器(1)利用正则从前到后识别所有敏感字段。如(标签、事件、迭代、数据绑定、插槽等等)
- 2.open标签 如
<div>{{test}}</div>的<div>
,识别器识别到此类标签,就会将div 存放到stack当中,利用创建函数(3)创建createASTElement并处理大部分非组件属性(具体如v-model、v-if、v-for)。需要注意到是:- 1.一元标签如:img等,会直接走3处理。
- p标签会做特殊处理,目的是为了和浏览器等识别方式达到目标一致。具体点击此处
- 3.识别到闭合标签,则处理下原生属性如:id 、 class等等、还有ref、slot、component、key等等,并跟上下文(4)建立起父子关系。最终形成了树的结构。
其中的细节特别多,如果有兴趣请关注我们的动态,我们会在下期给出详细过程讲解。
2) optimize思想解读。
我们在上面已经给出了astElement的详细属性,其中有两个属性叫做staticRoot 和 staticInFor。而optimize 的过程,就是给astElement打上这两个标记。这里是为了让这类静态节点,在render过程,能够走缓存的方式,只渲染一次。
好处很明显,能够减少重复对比和渲染的过程,提高性能。
3)code generate解读
这个过程其实还没有生成vnode,而是生成一个执行函数,且包含了this的执行code,其格式如下:
with(this) { _c('div'...xxx...xxx) }
我们在强调下,这次又是从astElement直接到另一个String。
上面astElement是个树状结构。
然后在这个过程,基本一个astElement就对应一个短函数。
最基本短函数是createElement 也就是_c。
最终的树状结构,会以函数的形式表现处理函数如下
_c(
'div',
{
key:'xxx',
ref:'xx',
pre:'xxx',
domPro:xxx,
....
},
[ // chidren
_v(_s('ding')),
_c('p',{model:'isshow',}, [ ...xxx ])
]
)
可以清晰的看到,最终形成的string,依然是一个树状形式,是以function形式展示的树状,而且所有属性都已经抽离成createElement的第二个参数。
一句话概括下code generate做的事情就是:
生成vnode的前置工作,抽离astElement所有的属性,形成短函数链。
短函数对应大致如下:
export function installRenderHelpers (target: any) {
target._o = markOnce // v-once
target._n = toNumber
target._s = toString
target._l = renderList // v-for
target._t = renderSlot // slot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic // static
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots // scopeSlot
target._g = bindObjectListeners // linsners
target._d = bindDynamicKeys
target._p = prependModifier
}
4) render 函数。
这个过程是直接执行,就会得到vnode,其细节包括了component的处理,会有单独一节去介绍。
这里展示下vnode的格式。
declare interface VNodeData {
key?: string | number;
slot?: string;
ref?: string;
is?: string;
pre?: boolean;
tag?: string;
staticClass?: string;
class?: any;
staticStyle?: { [key: string]: any };
style?: string | Array<Object> | Object;
normalizedStyle?: Object;
props?: { [key: string]: any };
attrs?: { [key: string]: string };
domProps?: { [key: string]: any };
hook?: { [key: string]: Function };
on?: ?{ [key: string]: Function | Array<Function> };
nativeOn?: { [key: string]: Function | Array<Function> };
transition?: Object;
show?: boolean; // marker for v-show
inlineTemplate?: {
render: Function;
staticRenderFns: Array<Function>;
};
directives?: Array<VNodeDirective>;
keepAlive?: boolean;
scopedSlots?: { [key: string]: Function };
model?: {
value: any;
callback: Function;
};
};
总结
- 1.编译四大过程parse、optimize、codegen、render。
- 2.用户自定义的render其实是忽略了前面三个步骤,直接定制的方式。
- 3.vdom 最终是以vnode为节点的树状结构,在patch过程,会以新老vdom进行对比,来决定哪些dom是需要更新或添加、删除的。