模板编译的作用
- Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode 比较复杂
- 用户只需要编写类似 HTML 的代码 - Vue.js模板,通过编译器将模板转换为返回 VNode 的 render 函数
- .vue 文件会被 webpack 在构建的过程中转换成 render 函数
Vue2和Vue3的一些区别
在Vue2中,要去除无意义的空白内容,因为这些空白会被编译到render函数中。而Vue3自动去除了这些空白内容,所以不用手动去去除。
模板编译入口
入口是createCompileToFunctionFn这个函数
模板编译过程 baseCompile
什么是AST
在Babel中,也是会把代码转换成AST,在把AST转换成降级后的JS代码。
通过转换成AST,在Vue中,可以对节点标记static来判断是否是静态节点,从而优化性能。
模板编译的开端
模板的编译入口函数是定义在compiler/index.js文件,可分为这几步:
- 传入的模板字符串进行parse,生成出语法树AST
- 再进行optimize,优化AST,优化的过程其实就是在标记静态节点、静态根节点
- 再将优化后的AST对象 generate 成字符串形式的JS代码
- 最后再将字符串形式的JS代码,通过
new Function转换成匿名函数
这个匿名函数就是最终的render函数,模板编译就是把模板字符串转换成渲染函数。
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
// 此处又通过createCompilerCreator处理,传入了一个核心函数,再返回一个函数
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 把模板转换成 ast 抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options)
return {
ast,
// 渲染函数
render: code.render,
// 静态渲染函数,生成静态 VNode 树
staticRenderFns: code.staticRenderFns
}
})
parse函数解析
parse函数接收两个参数:模板字符串、合并后的选项。返回的是解析好的AST对象
const ast = parse(template.trim(), options)
parse函数中做了几件事:解析options、对传入模板进行解析、返回解析好的AST对象。
该函数中核心函数是parseHTML。
export function parseHTML (html, options) {
...
...
}
该函数借鉴了一个开源库simplehtmlparser。该方法里,定义了很多正则表达式,作用是匹配HTML字符串模板中的内容。
// Regular Expressions for parsing tags and attributes
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+?][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\-\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(/?)>/
const endTag = new RegExp(`^<\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!--/
const conditionalComment = /^<![/
-
attribute:匹配标签中的属性,包括vue指令
-
startTagOpen、startTagClose:匹配开始标签的
-
endTag:匹配结束标签
-
doctype:匹配文档声明
-
comment:匹配注释节点
在parseHTML函数中,通过while循环,进行如下判断,直到html全都处理完毕
// 判断是否是注释节点,如果是,则执行方法,截取剩余html内容
// Comment:
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
advance(commentEnd + 3)
continue
}
}
// 匹配是否是条件注释
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// 是否是文档声明
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 是否是结束标签
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 是否是开始标签
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
通过该函数来更新处理的最新索引,及文档剩余内容:
function advance (n) {
index += n
html = html.substring(n)
}
在处理开始标签的内容中,有个handleStartTag方法,在该方法中,做了很多判断处理,还会处理标签中的属性。最终调用了外界传进来的start方法,
function handleStartTag (match) {
...
if (options.start) {
// 传入标签名、属性、是否为自闭合标签、起始位置
options.start(tagName, attrs, unary, match.start, match.end)
}
...
}
这里来看传入的start方法
start (tag, attrs, unary, start, end) {
...
// 调用了createASTElement方法,就是在这创建的AST对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
...
}
// 抽象语法树,就是一个对象而已
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
在start方法中,处理Vue指令
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
}
AST优化 - optimize
这里注释说明,优化器的目的是:遍历生成的模板AST树并检测纯静态的子树节点,即DOM中不需要更改的部分。一旦我们检测到这些子树,我们可以:
- 将它们提升为常量,这样我们就不再需要在每次重新渲染时为它们创建新节点;
- 在修补(patch)过程中完全跳过它们。
什么是静态节点:对应的DOM子树永远不会发生变化,比如纯文本内容。
/**
* 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.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
// 是否传入root
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
// 标记root中所有静态节点
markStatic(root)
// second pass: mark static roots.
// 标记root中所有静态根节点
markStaticRoots(root, false)
}
标记静态节点的方法
function markStatic (node: ASTNode) {
node.static = isStatic(node)
// type为1,则是元素节点,则会去遍历它的子元素节点
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
// 这里判断了是否为保留标签,目的是判断是否为组件。如果是组件,则不把组件中的slot标记为静态节点
// 如果组件中的slot被标记为静态节点,那么将来就没法改变
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
// 遍历children
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
// 标记静态
markStatic(child)
if (!child.static) {
// 如果有一个 child 不是 static,那么当前 node 就不是 static
node.static = false
}
}
// 处理条件渲染中的AST对象
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
}
}
}
}
}
判断是否为静态节点
function isStatic (node: ASTNode): boolean {
// 类型为2,是表达式。例如插值表达式,它的内容会发生变化
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) && // 不能是v-for下的直接子节点
Object.keys(node).every(isStaticKey)
))
}
标记静态根节点的方法
function markStaticRoots(node: ASTNode, isInFor: boolean) {
// 判断是否为元素类型
if (node.type === 1) {
// 判断是否为静态的,或者只渲染一次
if (node.static || node.once) {
// 来标记该节点在for循环中,是否是静态的
node.staticInFor = isInFor;
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
// 如果一个元素内只有文本节点,此时这个元素不是静态的Root
// Vue 认为这种优化会带来负面的影响
if (
node.static &&
node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)
) {
node.staticRoot = true;
return;
} else {
node.staticRoot = false;
}
// 检测当前节点的子节点中是否有静态的Root
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);
}
}
}
}
generate-生成字符串形式js代码
该函数接收优化好的AST对象,以及额外配置对象。
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options);
下面是generate源代码,最核心的是genElement这个方法,它是最终将AST对象转换为代码的方法。这里先生成一个状态对象,然后再判断有无AST来调用genElement方法。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// fix #11483, Root level <script> tags should not be rendered.
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
上面代码中返回的render代表的是,根据AST对象生成的,定义vNode的JS代码的字符串形式,其形式如下所示,被包裹在wtih函数中。这就是由模板解析成AST后,在生成的JS代码,用于去生成对应的页面样式。
"with(this){return _c('div',
{attrs:{"id":"app"}},
[_m(0),_v(" "),_c('div',[_v(_s(msg)),_c('p',[_v("hello")])]),_v(" "),_c('div',[_v("是否显示")])]
)}"
CodegenState源代码,它作用是生成代码生成过程中所使用到的状态对象。
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
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
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 = []
// 这个属性记录,当前处理的节点是否使用v-pre标记的
this.pre = false
}
}
最后通过new Function方法,将字符串转为函数:
function createFunction(code, errors) {
try {
return new Function(code);
} catch (err) {
errors.push({ err, code });
return noop;
}
}
模板编译过程-总结
模板编译,先是将模板字符串解析成AST,它有些类似于VNode的结构(AST更多的是编译时生成的中间代码(包括runtime compile),而VNode则是存在于运行时的一种DOM节点及其关系的抽象)。
然后对AST优化后,在转换成JS代码,这JS代码有个重点是_c也就是createElement方法,它返回VNode。这个Vnode最终是在patch方法中会被平台的DOM操作方法为,挂载为真实DOM。
Vue组件化
- 一个Vue组件就是一个拥有预定义选项的一个Vue实例。
- 一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本、样式、模板。
组件化机制
- 组件化可以让我们方便的把页面拆分成多个可重用的组件
- 组件是独立的,系统内可重用,组件之间可以嵌套
- 有了组件可以像搭积木一样开发网页
- 下面我们将从源码的角度来分析 Vue 组件内部如何工作
-
- 组件实例的创建过程是从上而下
- 组件实例的挂载过程是从下而上
Vue.extend源码
它的源码整体来看,就是返回一个组件的构造函数,将传给方法的options和Vue的options合并起来,并且该构造函数继承了Vue的原型,构造函数确定为了执行_init方法的一个自定义函数,而这个函数执行时就会初始化创建整个组件。
Vue.extend中将Vue实例的所有静态、原型方法都继承了下来,并且在传入的选项中定义了一个缓存属性,将该构造函数缓存了下来。
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
// 从缓存中加载组件的构造函数
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
// 如果是开发环境验证组件的名称
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options)
}
// 继承Vue构造函数的原型,原型继承自Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并 options
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
// 初始化所有的基本功能和属性
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
// 把组件构造构造函数保存到 Ctor.options.components.comp = Ctor,在当前组件选项中记录自己
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
// 把组件的构造函数缓存到 options._Ctor
cachedCtors[SuperId] = Sub
// 返回改造后的Vue构造函数
return Sub
}
}
总体是基于传入的选项对象,创建了组件的构造函数,组件的构造函数继承了Vue的原型,所以组件对象拥有和Vue实例一样的构造成员。
调试组件注册过程
这里是注册过程的相关代码:
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 组件注册最终是调用了extend方法来生成组件的构造函数
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 对于component,会在此处进行全局注册
this.options[type + 's'][id] = definition
return definition
}
}
})
}
组件的创建过程
回顾首次渲染过程:
Vue的自定义组件创建是在src/core/vdom/create-element.js文件中,通过调用createComponent方法来创建VNode。
组件真正创建的位置是在组件钩子函数的init函数中:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
最终调用了该方法,调用组件的构造函数,并传入了options:
export function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// 创建组件实例
return new vnode.componentOptions.Ctor(options)
}
组件的patch过程
patch函数中会调用createElm方法,内部有专门处理组件的createComponent方法,有时间重新看一遍。