前面的六篇博客已经帮助我们基本搞清楚了,vue的响应式原理,数据驱动,组件化和组件更新等概念和原理,而其中关于组件化和组件更新的部分,我们是在已有render函数的基础上来讲的,也就是说我们已经有了vnode,再去讨论的vnode如何渲染更新的。
这篇博客我们就回过头来看看vue的compiler是如何将template变成render函数的。
了解编译的流程有什么好处呢?我个人觉得可以帮助我们对于一些vue自身的语法糖的原理有个更好的理解。
由于Compiler的细节非常多,所以大概有个结论和印象即可,其中很多关于vue自身的语法处理也不会细讲,只会用slot来做个例子。
Compiler入口对于compiler的入口,vue做了一系列柯里化的处理,来最终返回编译函数。
这样处理是为了将与平台有关的配置进行预处理,比如web编译和ssr的编译会有些许配置上的不同,而平台在编译过程中又不会发生改变,所以先通过柯里化将这些不同封装在函数闭包中,这样在不同组件执行compiler时就不需要一次次判断平台了。
柯里化想要看看是怎么柯里化的,可以看看这一小节,不看也不影响我们理解编译的流程。
当我们使用 Runtime + Compiler 的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js
,看一下它对 $mount
函数的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 const mount = Vue .prototype .$mount Vue .prototype .$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query (el) if (el === document .body || el === document .documentElement ) { process.env .NODE_ENV !== 'production' && warn ( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this .$options if (!options.render ) { let template = options.template if (template) { if (typeof template === 'string' ) { if (template.charAt (0 ) === '#' ) { template = idToTemplate (template) if (process.env .NODE_ENV !== 'production' && !template) { warn ( `Template element not found or is empty: ${options.template} ` , this ) } } } else if (template.nodeType ) { template = template.innerHTML } else { if (process.env .NODE_ENV !== 'production' ) { warn ('invalid template option:' + template, this ) } return this } } else if (el) { template = getOuterHTML (el) } if (template) { if (process.env .NODE_ENV !== 'production' && config.performance && mark) { mark ('compile' ) } const { render, staticRenderFns } = compileToFunctions (template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters : options.delimiters , comments : options.comments }, this ) options.render = render options.staticRenderFns = staticRenderFns if (process.env .NODE_ENV !== 'production' && config.performance && mark) { mark ('compile end' ) measure (`vue ${this ._name} compile` , 'compile' , 'compile end' ) } } } return mount.call (this , el, hydrating) }
这段函数逻辑之前分析过,关于编译的入口就是在这里:
1 2 3 4 5 6 7 8 const { render, staticRenderFns } = compileToFunctions (template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters : options.delimiters , comments : options.comments }, this ) options.render = render options.staticRenderFns = staticRenderFns
compileToFunctions
方法就是把模板 template
编译生成 render
以及 staticRenderFns
,它的定义在 src/platforms/web/compiler/index.js
中:
1 2 3 4 5 6 import { baseOptions } from './options' import { createCompiler } from 'compiler/index' const { compile, compileToFunctions } = createCompiler (baseOptions)export { compile, compileToFunctions }
可以看到 compileToFunctions
方法实际上是 createCompiler
方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 createCompiler
方法的定义,在 src/compiler/index.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export const createCompiler = createCompilerCreator (function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse (template.trim (), options) if (options.optimize !== false ) { optimize (ast, options) } const code = generate (ast, options) return { ast, render : code.render , staticRenderFns : code.staticRenderFns } })
createCompiler
方法实际上是通过调用 createCompilerCreator
方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile
函数里执行,那么 createCompilerCreator
又是什么呢,它的定义在 src/compiler/create-compiler.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 export function createCompilerCreator (baseCompile: Function ): Function { return function createCompiler (baseOptions: CompilerOptions ) { function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object .create (baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip ) => { (tip ? tips : errors).push (msg) } if (options) { if (options.modules ) { finalOptions.modules = (baseOptions.modules || []).concat (options.modules ) } if (options.directives ) { finalOptions.directives = extend ( Object .create (baseOptions.directives || null ), options.directives ) } for (const key in options) { if (key !== 'modules' && key !== 'directives' ) { finalOptions[key] = options[key] } } } const compiled = baseCompile (template, finalOptions) if (process.env .NODE_ENV !== 'production' ) { errors.push .apply (errors, detectErrors (compiled.ast )) } compiled.errors = errors compiled.tips = tips return compiled } return { compile, compileToFunctions : createCompileToFunctionFn (compile) } } }
可以看到该方法返回了一个 createCompiler
的函数,它接收一个 baseOptions
的参数,返回的是一个对象,包括 compile
方法属性和 compileToFunctions
属性,这个 compileToFunctions
对应的就是 $mount
函数调用的 compileToFunctions
方法,它是调用 createCompileToFunctionFn
方法的返回值,我们接下来看一下 createCompileToFunctionFn
方法,它的定义在 src/compiler/to-function/js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 export function createCompileToFunctionFn (compile: Function ): Function { const cache = Object .create (null ) return function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { options = extend ({}, options) const warn = options.warn || baseWarn delete options.warn if (process.env .NODE_ENV !== 'production' ) { try { new Function ('return 1' ) } catch (e) { if (e.toString ().match (/unsafe-eval|CSP/ )) { warn ( 'It seems you are using the standalone build of Vue.js in an ' + 'environment with Content Security Policy that prohibits unsafe-eval. ' + 'The template compiler cannot work in this environment. Consider ' + 'relaxing the policy to allow unsafe-eval or pre-compiling your ' + 'templates into render functions.' ) } } } const key = options.delimiters ? String (options.delimiters ) + template : template if (cache[key]) { return cache[key] } const compiled = compile (template, options) if (process.env .NODE_ENV !== 'production' ) { if (compiled.errors && compiled.errors .length ) { warn ( `Error compiling template:\n\n${template} \n\n` + compiled.errors .map (e => `- ${e} ` ).join ('\n' ) + '\n' , vm ) } if (compiled.tips && compiled.tips .length ) { compiled.tips .forEach (msg => tip (msg, vm)) } } const res = {} const fnGenErrors = [] res.render = createFunction (compiled.render , fnGenErrors) res.staticRenderFns = compiled.staticRenderFns .map (code => { return createFunction (code, fnGenErrors) }) if (process.env .NODE_ENV !== 'production' ) { if ((!compiled.errors || !compiled.errors .length ) && fnGenErrors.length ) { warn ( `Failed to generate render function:\n\n` + fnGenErrors.map (({ err, code } ) => `${err.toString()} in\n\n${code} \n` ).join ('\n' ), vm ) } } return (cache[key] = res) } }
至此我们总算找到了 compileToFunctions
的最终定义,它接收 3 个参数、编译模板 template
,编译配置 options
和 Vue 实例 vm
。核心的编译过程就一行代码:
1 const compiled = compile (template, options)
compile
函数在执行 createCompileToFunctionFn
的时候作为参数传入,它是 createCompiler
函数中定义的 compile
函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object .create (baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip ) => { (tip ? tips : errors).push (msg) } if (options) { if (options.modules ) { finalOptions.modules = (baseOptions.modules || []).concat (options.modules ) } if (options.directives ) { finalOptions.directives = extend ( Object .create (baseOptions.directives || null ), options.directives ) } for (const key in options) { if (key !== 'modules' && key !== 'directives' ) { finalOptions[key] = options[key] } } } const compiled = baseCompile (template, finalOptions) if (process.env .NODE_ENV !== 'production' ) { errors.push .apply (errors, detectErrors (compiled.ast )) } compiled.errors = errors compiled.tips = tips return compiled }
compile
函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:
1 const compiled = baseCompile (template, finalOptions)
baseCompile
在执行 createCompilerCreator
方法时作为参数传入
baseCompile这个函数就是我们实际去进行编译将template转化为render的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 export const createCompiler = createCompilerCreator (function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse (template.trim (), options) optimize (ast, options) const code = generate (ast, options) return { ast, render : code.render , staticRenderFns : code.staticRenderFns } })
它分为三个步骤:
将template转化为ast树 对ast树进行优化,为一些节点添加一些属性用于后面的渲染优化,如一些肯定不会发生变化的节点加上一个属性表明它是静态节点。 根据优化后的ast树生成代码,这个代码就可以用来生成vnode 通过一个例子来看编译的流程 parse编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。
这个过程是比较复杂的,它会用到大量正则表达式对字符串解析,如果对正则不是很了解,建议先去补习正则表达式的知识。为了直观地演示 parse
的过程,我们先来看一个例子:
1 2 3 <ul :class ="bindCls" class ="list" v-if ="isShow" > <li v-for ="(item,index) in data" @click ="clickItem(index)" > {{item}}:{{index}}</li > </ul >
经过 parse
过程后,生成的 AST 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 ast = { 'type' : 1 , 'tag' : 'ul' , 'attrsList' : [], 'attrsMap' : { ':class' : 'bindCls' , 'class' : 'list' , 'v-if' : 'isShow' }, 'if' : 'isShow' , 'ifConditions' : [{ 'exp' : 'isShow' , 'block' : }], 'parent' : undefined , 'plain' : false , 'staticClass' : 'list' , 'classBinding' : 'bindCls' , 'children' : [{ 'type' : 1 , 'tag' : 'li' , 'attrsList' : [{ 'name' : '@click' , 'value' : 'clickItem(index)' }], 'attrsMap' : { '@click' : 'clickItem(index)' , 'v-for' : '(item,index) in data' }, 'parent' : 'plain' : false , 'events' : { 'click' : { 'value' : 'clickItem(index)' } }, 'hasBindings' : true , 'for' : 'data' , 'alias' : 'item' , 'iterator1' : 'index' , 'children' : [ 'type' : 2 , 'expression' : '_s(item)+":"+_s(index)' 'text' : '{{item}}:{{index}}' , 'tokens' : [ {'@binding' :'item' }, ':' , {'@binding' :'index' } ] ] }] }
可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element
,除了它自身的一些属性,还维护了它的父子关系,如 parent
指向它的父节点,children
指向它的所有子节点。先对 AST 有一些直观的印象,那么接下来我们来分析一下这个 AST 是如何得到的。
optimize当我们的模板 template
经过 parse
过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化,optimize
的逻辑是远简单于 parse
的逻辑,所以理解起来会轻松很多。
为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch
的过程跳过对他们的比对。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 ast = { 'type' : 1 , 'tag' : 'ul' , 'attrsList' : [], 'attrsMap' : { ':class' : 'bindCls' , 'class' : 'list' , 'v-if' : 'isShow' }, 'if' : 'isShow' , 'ifConditions' : [{ 'exp' : 'isShow' , 'block' : }], 'parent' : undefined , 'plain' : false , 'staticClass' : 'list' , 'classBinding' : 'bindCls' , 'static' : false , 'staticRoot' : false , 'children' : [{ 'type' : 1 , 'tag' : 'li' , 'attrsList' : [{ 'name' : '@click' , 'value' : 'clickItem(index)' }], 'attrsMap' : { '@click' : 'clickItem(index)' , 'v-for' : '(item,index) in data' }, 'parent' : 'plain' : false , 'events' : { 'click' : { 'value' : 'clickItem(index)' } }, 'hasBindings' : true , 'for' : 'data' , 'alias' : 'item' , 'iterator1' : 'index' , 'static' : false , 'staticRoot' : false , 'children' : [ 'type' : 2 , 'expression' : '_s(item)+":"+_s(index)' 'text' : '{{item}}:{{index}}' , 'tokens' : [ {'@binding' :'item' }, ':' , {'@binding' :'index' } ], 'static' : false ] }] }
codegen编译的最后一步就是把优化后的 AST 树转换成可执行的代码。
为了方便理解,我们还是用之前的例子:
1 2 3 <ul :class ="bindCls" class ="list" v-if ="isShow" > <li v-for ="(item,index) in data" @click ="clickItem(index)" > {{item}}:{{index}}</li > </ul >
它经过编译,执行 const code = generate(ast, options)
,生成的 render
代码串如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 with (this ){ return (isShow) ? _c ('ul' , { staticClass : "list" , class : bindCls }, _l ((data), function (item, index ) { return _c ('li' , { on : { "click" : function ($event ) { clickItem (index) } } }, [_v (_s (item) + ":" + _s (index))]) }) ) : _e () }
这里的 _c
函数定义在 src/core/instance/render.js
中。
1 vm._c = (a, b, c, d ) => createElement (vm, a, b, c, d, false )
而 _l
、_v
定义在 src/core/instance/render-helpers/index.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 }
顾名思义,_c
就是执行 createElement
去创建 VNode,而 _l
对应 renderList
渲染列表;_v
对应 createTextVNode
创建文本 VNode;_e
对于 createEmptyVNode
创建空的 VNode。
源码分析通过刚才的例子,我们可以对整个编译的流程以及每一步的结果有个初步的了解。
如果有兴趣可以看下vue的源码,由于源码非常多,这里只是简单分析下。
parse首先来看一下 parse
的定义,在 src/compiler/parser/index.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export function parse ( template: string, options: CompilerOptions ): ASTElement | void { getFnsAndConfigFromOptions (options) parseHTML (template, { start (tag, attrs, unary) { let element = createASTElement (tag, attrs) processElement (element) treeManagement () }, end () { treeManagement () closeElement () }, chars (text : string) { handleText () createChildrenASTOfText () }, comment (text : string) { createChildrenASTOfComment () } }) return astRootElement }
parse
函数的代码很长,贴一遍对同学的理解没有好处,我先把它拆成伪代码的形式,方便同学们对整体流程先有一个大致的了解。接下来我们就来分解分析每段伪代码的作用。
从 options 中获取方法和配置对应伪代码:
1 getFnsAndConfigFromOptions (options)
parse
函数的输入是 template
和 options
,输出是 AST 的根节点。template
就是我们的模板字符串,而 options
实际上是和平台相关的一些配置,它的定义在 src/platforms/web/compiler/options
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { isPreTag, mustUseProp, isReservedTag, getTagNamespace } from '../util/index' import modules from './modules/index' import directives from './directives/index' import { genStaticKeys } from 'shared/util' import { isUnaryTag, canBeLeftOpenTag } from './util' export const baseOptions : CompilerOptions = { expectHTML : true , modules, directives, isPreTag, isUnaryTag, mustUseProp, canBeLeftOpenTag, isReservedTag, getTagNamespace, staticKeys : genStaticKeys (modules) }
这些属性和方法之所以放到 platforms
目录下是因为它们在不同的平台(web 和 weex)的实现是不同的。
我们用伪代码 getFnsAndConfigFromOptions
表示了这一过程,它的实际代码如下:
1 2 3 4 5 6 7 8 9 10 11 warn = options.warn || baseWarn platformIsPreTag = options.isPreTag || no platformMustUseProp = options.mustUseProp || no platformGetTagNamespace = options.getTagNamespace || no transforms = pluckModuleFunction (options.modules , 'transformNode' ) preTransforms = pluckModuleFunction (options.modules , 'preTransformNode' ) postTransforms = pluckModuleFunction (options.modules , 'postTransformNode' ) delimiters = options.delimiters
这些方法和配置都是后续解析时候需要的,可以不用去管它们的具体作用,我们先往后看。
解析 HTML 模板对应伪代码:
1 parseHTML (template, options)
对于 template
模板的解析主要是通过 parseHTML
函数,它的定义在 src/compiler/parser/html-parser
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 export function parseHTML (html, options ) { let lastTag while (html) { if (!lastTag || !isPlainTextElement (lastTag)){ let textEnd = html.indexOf ('<' ) if (textEnd === 0 ) { if (matchComment) { advance (commentLength) continue } if (matchDoctype) { advance (doctypeLength) continue } if (matchEndTag) { advance (endTagLength) parseEndTag () continue } if (matchStartTag) { parseStartTag () handleStartTag () continue } } handleText () advance (textLength) } else { handlePlainTextElement () parseEndTag () } } }
由于 parseHTML
的逻辑也非常复杂,因此我也用了伪代码的方式表达,整体来说它的逻辑就是循环解析 template
,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 advance
函数不断前进整个模板字符串,直到字符串末尾。
1 2 3 4 function advance (n ) { index += n html = html.substring (n) }
整个匹配过程分为以下几步:
找到第一个左尖括号的位置,如果是0,表明这是个标签的,进入标签的处理流程,如果不是,说明这个尖括号处在某个字符串中,进入handlePlainTextElement()流程 如果是个标签,判断是否是注释节点,如果是注释节点,跳过注释部分 如果是doctype,跳过 如果是结束标签,也就是说 这种,则前进标签长度,并处理闭合标签如果是开始标签,则前进标签长度,执行handleStartTag(),这个函数首先是创建一个ast的node出来,第二就是把当前标签压栈,并赋值给lastTag,为什么要压栈呢?是为了在上一步处理闭合标签时如果标签相同从栈里弹出来,最终html编译完成之后栈为空才说明所有标签闭合的。 当然这个流程是一个非常简单的介绍,里面隐藏了非常多的细节,有兴趣可以自己去看看源码。
optimize来看一下 optimize
方法的定义,在 src/compiler/optimizer.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export function optimize (root: ?ASTElement, options: CompilerOptions ) { if (!root) return isStaticKey = genStaticKeysCached (options.staticKeys || '' ) isPlatformReservedTag = options.isReservedTag || no markStatic (root) markStaticRoots (root, false ) } function genStaticKeys (keys: string ): Function { return makeMap ( 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' + (keys ? ',' + keys : '' ) ) }
我们在编译阶段可以把一些 AST 节点优化成静态节点,所以整个 optimize
的过程实际上就干 2 件事情,markStatic(root)
标记静态节点 ,markStaticRoots(root, false)
标记静态根。
标记静态节点1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function markStatic (node: ASTNode ) { node.static = isStatic (node) if (node.type === 1 ) { 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 } } } } } function isStatic (node: ASTNode ): boolean { if (node.type === 2 ) { return false } if (node.type === 3 ) { return true } return !!(node.pre || ( !node.hasBindings && !node.if && !node.for && !isBuiltInTag (node.tag ) && isPlatformReservedTag (node.tag ) && !isDirectChildOfTemplateFor (node) && Object .keys (node).every (isStaticKey) )) }
首先执行 node.static = isStatic(node)
isStatic
是对一个 AST 元素节点是否是静态的判断,如果是表达式,就是非静态;如果是纯文本,就是静态;对于一个普通元素,如果有 pre 属性,那么它使用了 v-pre
指令,是静态,否则要同时满足以下条件:没有使用 v-if
、v-for
,没有使用其它指令(不包括 v-once
),非内置组件,是平台保留的标签,非带有 v-for
的 template
标签的直接子节点,节点的所有属性的 key
都满足静态 key;这些都满足则这个 AST 节点是一个静态节点。
如果这个节点是一个普通元素,则遍历它的所有 children
,递归执行 markStatic
。因为所有的 elseif
和 else
节点都不在 children
中, 如果节点的 ifConditions
不为空,则遍历 ifConditions
拿到所有条件中的 block
,也就是它们对应的 AST 节点,递归执行 markStatic
。在这些递归过程中,一旦子节点有不是 static
的情况,则它的父节点的 static
均变成 false。
标记静态根1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function markStaticRoots (node: ASTNode, isInFor: boolean ) { if (node.type === 1 ) { if (node.static || node.once ) { node.staticInFor = isInFor } 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
第二个参数是 isInFor
,对于已经是 static
的节点或者是 v-once
指令的节点,node.staticInFor = isInFor
。 接着就是对于 staticRoot
的判断逻辑,从注释中我们可以看到,对于有资格成为 staticRoot
的节点,除了本身是一个静态节点外,必须满足拥有 children
,并且 children
不能只是一个文本节点,不然的话把它标记成静态根节点的收益就很小了。
接下来和标记静态节点的逻辑一样,遍历 children
以及 ifConditions
,递归执行 markStaticRoots
。
codegengenerate
函数的定义在 src/compiler/codegen/index.js
中:
1 2 3 4 5 6 7 8 9 10 11 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 } }
generate
函数首先通过 genElement(ast, state)
生成 code
,再把 code
用 with(this){return ${code}}}
包裹起来。这里的 state
是 CodegenState
的一个实例,稍后我们在用到它的时候会介绍它。先来看一下 genElement
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 export function genElement (el: ASTElement, state: CodegenState ): string { if (el.staticRoot && !el.staticProcessed ) { 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 ) { return genChildren (el, state) || 'void 0' } else if (el.tag === 'slot' ) { return genSlot (el, state) } else { let code if (el.component ) { code = genComponent (el.component , el, state) } else { const data = el.plain ? undefined : genData (el, state) const children = el.inlineTemplate ? null : genChildren (el, state, true ) code = `_c('${el.tag} '${ data ? `,${data} ` : '' // data } ${ children ? `,${children} ` : '' // children } )` } for (let i = 0 ; i < state.transforms .length ; i++) { code = state.transforms [i](el, code) } return code } }
基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数。
slot上面的源码分析部分省略了很多很多的细节,有的是html解析相关的,如parseHTML函数只写了伪代码,有的是vue语法糖处理相关的,如codegen只写了genIF,genSlot,具体做了什么没有说,这里我们就用slot举个例子,其他的有需要大家可以自己看。
还是先从编译说起,我们知道编译是发生在调用 vm.$mount
的时候,所以编译的顺序是先编译父组件,再编译子组件。
首先编译父组件,在 parse
阶段,会执行 processSlot
处理 slot
,它的定义在 src/compiler/parser/index.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function processSlot (el ) { if (el.tag === 'slot' ) { el.slotName = getBindingAttr (el, 'name' ) if (process.env .NODE_ENV !== 'production' && el.key ) { warn ( `\`key\` does not work on <slot> because slots are abstract outlets ` + `and can possibly expand into multiple elements. ` + `Use the key on a wrapping element instead.` ) } } else { let slotScope if (el.tag === 'template' ) { slotScope = getAndRemoveAttr (el, 'scope' ) if (process.env .NODE_ENV !== 'production' && slotScope) { warn ( `the "scope" attribute for scoped slots have been deprecated and ` + `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` + `can also be used on plain elements in addition to <template> to ` + `denote scoped slots.` , true ) } el.slotScope = slotScope || getAndRemoveAttr (el, 'slot-scope' ) } else if ((slotScope = getAndRemoveAttr (el, 'slot-scope' ))) { if (process.env .NODE_ENV !== 'production' && el.attrsMap ['v-for' ]) { warn ( `Ambiguous combined usage of slot-scope and v-for on <${el.tag} > ` + `(v-for takes higher priority). Use a wrapper <template> for the ` + `scoped slot to make it clearer.` , true ) } el.slotScope = slotScope } const slotTarget = getBindingAttr (el, 'slot' ) if (slotTarget) { el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget if (el.tag !== 'template' && !el.slotScope ) { addAttr (el, 'slot' , slotTarget) } } } }
当解析到标签上有 slot
属性的时候,会给对应的 AST 元素节点添加 slotTarget
属性,然后在 codegen
阶段,在 genData
中会处理 slotTarget
,相关代码在 src/compiler/codegen/index.js
中:
1 2 3 if (el.slotTarget && !el.slotScope ) { data += `slot:${el.slotTarget} ,` }
会给 data
添加一个 slot
属性,并指向 slotTarget
,之后会用到。在我们的例子中,父组件最终生成的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 with (this ){ return _c ('div' , [_c ('app-layout' , [_c ('h1' ,{attrs :{"slot" :"header" },slot :"header" }, [_v (_s (title))]), _c ('p' ,[_v (_s (msg))]), _c ('p' ,{attrs :{"slot" :"footer" },slot :"footer" }, [_v (_s (desc))] ) ]) ], 1 )}
接下来编译子组件,同样在 parser
阶段会执行 processSlot
处理函数,它的定义在 src/compiler/parser/index.js
中:
1 2 3 4 5 6 function processSlot (el ) { if (el.tag === 'slot' ) { el.slotName = getBindingAttr (el, 'name' ) } }
当遇到 slot
标签的时候会给对应的 AST 元素节点添加 slotName
属性,然后在 codegen
阶段,会判断如果当前 AST 元素节点是 slot
标签,则执行 genSlot
函数,它的定义在 src/compiler/codegen/index.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function genSlot (el: ASTElement, state: CodegenState ): string { const slotName = el.slotName || '"default"' const children = genChildren (el, state) let res = `_t(${slotName} ${children ? `,${children} ` : '' } ` const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)} :${a.value} ` ).join(',' )} }` const bind = el.attrsMap ['v-bind' ] if ((attrs || bind) && !children) { res += `,null` } if (attrs) { res += `,${attrs} ` } if (bind) { res += `${attrs ? '' : ',null' } ,${bind} ` } return res + ')' }
我们先不考虑 slot
标签上有 attrs
以及 v-bind
的情况,那么它生成的代码实际上就只有:
1 2 3 const slotName = el.slotName || '"default"' const children = genChildren (el, state)let res = `_t(${slotName} ${children ? `,${children} ` : '' } `
这里的 slotName
从 AST 元素节点对应的属性上取,默认是 default
,而 children
对应的就是 slot
开始和闭合标签包裹的内容。来看一下我们例子的子组件最终生成的代码,如下:
1 2 3 4 5 6 7 8 9 10 with (this ) { return _c ('div' ,{ staticClass :"container" },[ _c ('header' ,[_t ("header" )],2 ), _c ('main' ,[_t ("default" ,[_v ("默认内容" )])],2 ), _c ('footer' ,[_t ("footer" )],2 ) ] ) }
在编译章节我们了解到,_t
函数对应的就是 renderSlot
方法,它的定义在 src/core/instance/render-heplpers/render-slot.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 export function renderSlot ( name: string, fallback: ?Array <VNode>, props: ?Object , bindObject: ?Object ): ?Array <VNode > { const scopedSlotFn = this .$scopedSlots [name] let nodes if (scopedSlotFn) { props = props || {} if (bindObject) { if (process.env .NODE_ENV !== 'production' && !isObject (bindObject)) { warn ( 'slot v-bind without argument expects an Object' , this ) } props = extend (extend ({}, bindObject), props) } nodes = scopedSlotFn (props) || fallback } else { const slotNodes = this .$slots [name] if (slotNodes) { if (process.env .NODE_ENV !== 'production' && slotNodes._rendered ) { warn ( `Duplicate presence of slot "${name} " found in the same render tree ` + `- this will likely cause render errors.` , this ) } slotNodes._rendered = true } nodes = slotNodes || fallback } const target = props && props.slot if (target) { return this .$createElement('template' , { slot : target }, nodes) } else { return nodes } }
render-slot
的参数 name
代表插槽名称 slotName
,fallback
代表插槽的默认内容生成的 vnode
数组。先忽略 scoped-slot
,只看默认插槽逻辑。如果 this.$slot[name]
有值,就返回它对应的 vnode
数组,否则返回 fallback
。那么这个 this.$slot
是哪里来的呢?我们知道子组件的 init
时机是在父组件执行 patch
过程的时候,那这个时候父组件已经编译完成了。并且子组件在 init
过程中会执行 initRender
函数,initRender
的时候获取到 vm.$slot
,相关代码在 src/core/instance/render.js
中:
1 2 3 4 5 6 export function initRender (vm: Component ) { const parentVnode = vm.$vnode = options._parentVnode const renderContext = parentVnode && parentVnode.context vm.$slots = resolveSlots (options._renderChildren , renderContext) }
vm.$slots
是通过执行 resolveSlots(options._renderChildren, renderContext)
返回的,它的定义在 src/core/instance/render-helpers/resolve-slots.js
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 export function resolveSlots ( children: ?Array <VNode>, context: ?Component ): { [key : string]: Array <VNode > } { const slots = {} if (!children) { return slots } for (let i = 0 , l = children.length ; i < l; i++) { const child = children[i] const data = child.data if (data && data.attrs && data.attrs .slot ) { delete data.attrs .slot } if ((child.context === context || child.fnContext === context) && data && data.slot != null ) { const name = data.slot const slot = (slots[name] || (slots[name] = [])) if (child.tag === 'template' ) { slot.push .apply (slot, child.children || []) } else { slot.push (child) } } else { (slots.default || (slots.default = [])).push (child) } } for (const name in slots) { if (slots[name].every (isWhitespace)) { delete slots[name] } } return slots }
resolveSlots
方法接收 2 个参数,第一个参数 chilren
对应的是父 vnode
的 children
,在我们的例子中就是 <app-layout>
和 </app-layout>
包裹的内容。第二个参数 context
是父 vnode
的上下文,也就是父组件的 vm
实例。
resolveSlots
函数的逻辑就是遍历 chilren
,拿到每一个 child
的 data
,然后通过 data.slot
获取到插槽名称,这个 slot
就是我们之前编译父组件在 codegen
阶段设置的 data.slot
。接着以插槽名称为 key
把 child
添加到 slots
中,如果 data.slot
不存在,则是默认插槽的内容,则把对应的 child
添加到 slots.defaults
中。这样就获取到整个 slots
,它是一个对象,key
是插槽名称,value
是一个 vnode
类型的数组,因为它可以有多个同名插槽。
这样我们就拿到了 vm.$slots
了,回到 renderSlot
函数,const slotNodes = this.$slots[name]
,我们也就能根据插槽名称获取到对应的 vnode
数组了,这个数组里的 vnode
都是在父组件创建的,这样就实现了在父组替换子组件插槽的内容了。
对应的 slot
渲染成 vnodes
,作为当前组件渲染 vnode
的 children
,之后的渲染过程之前分析过,不再赘述。