Vue 模板编译和渲染 | 字数总计: 3.3k | 阅读时长: 13分钟 | 阅读量:
模板编译与渲染 什么是模板编译 在 Vue 中推崇 使用.vue 文件,在.vue 文件中使用 标签来编写 vue 语法。
Vue 会把在 标签 中的内容(类似于原生 HTML)进行编译,把“原生 HTML”的内容找出来,再把“非原生 HTML”找出来,经过一系列的逻辑处理生成渲染函数,也就是 render 函数。
render 函数 会把模板内容生成对应的 VNode, VNode 经过 patch 过程 从而得到 可渲染的视图中的 VNode
最后根据 VNode 创建 真实的 DOM 节点,并插入到视图中,最终完成视图的渲染更新。
其中,【Vue 会把在 标签 中的内容(类似于原生 HTML)进行编译,把“原生 HTML”的内容找出来,再把“非原生 HTML”找出来,经过一系列的逻辑处理生成渲染函数,也就是 render 函数。】 这一过程称之为模版编译。
模板转换成视图的过程
Vue.js 通过编译将 template 模板转换成渲染函数 render,执行渲染函数就可以得到一个虚拟节点树。
在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对于的 update 来修改视图。这个视图主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行 DOM 操作来更新视图。
简单点来说,在 Vue 的底层实现上,Vue 将模版编译成虚拟 DOM 渲染函数。结合 Vue 自带的响应式系统。在状态改变时,Vue 会计算出重新渲染组件的最小代价并应用到 DOM 操作上
通过以上图帮助理解模板转换成视图的过程,由此也可得出,模板转换视图的关键词:模板函数(render)、VNode、patch
渲染函数:用来生成 Virtual DOM 的。Vue 推荐使用模板来构建我们的应用界面,在底层实现中 Vue 会将模板(template)编译成渲染函数(render),当然我们也可以不写模板,直接写渲染函数,以获得更好的性能。
VNode 虚拟节点:它可以代表一个真实的 DOM 节点。通过 createElement 方法能将 VNode 渲染成 DOM 节点。简单来说 vnode 可以理解成节点描述对象,它描述了应该怎样去创建真实的 DOM 节点。
patch: 虚拟 DOM 最核心的部分,它可以将 Vnode 渲染成真实的 DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新。其实际作用是在现有 DOM 上进行修改来实现更新视图的目的。Vue 的 Virtual DOM Patching 算法是基于 Snabbdom 实现的。并在其基础上做了很多调整和改进。
模板编译原理 模板编译中有个环节是将模板编译成 render 函数,这个过程我们把它称作为编译 。虽然我们可以为组件编写 render 函数,但使用 template 模板更加直观,也更符合我们的开发习惯。
Vue.js 提供了两个版本,一个是 Runtime + Compiler,一个是 Runtime Only。前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render 函数
编译入口 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 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, { outputSourceRange : process.env .NODE_ENV !== 'production' , 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 9 10 11 12 13 const { render, staticRenderFns } = compileToFunctions ( template, { outputSourceRange : process.env .NODE_ENV !== 'production' , shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters : options.delimiters , comments : options.comments }, this ); options.render = render; options.staticRenderFns = staticRenderFns;
compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns。
进入 compileToFunctions 方法中可以看到,compileToFunctions 实际上是 createCompiler 方法的返回值,该方法接受一个编译配置参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 } })
查看 crateCompiler 方法 看到,它实际又是通过调用 crateCompilerCreator 方法返回的。 这个方法中 有个 baseCompile,真正的编译过程都在这个函数。 所以 baseCompile 才是真正的模板编译流程及原理所在。 接下来我们从 createCompilerCreator 开始分析。
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) } } }
createCompilerCreator 方法返回了一个 createCOmpiler 的函数,接收一个 baseOptions 的参数,返回的是一个对象,包括 compile 方法 和 compileToFunctions 属性,这个 compileToFunctions 就是对应 $mount 函数调用的 compileToFunctions 方法, 它又是调用 createCompileToFunction 方法的返回值。 接下来看下 createCompileToFunction 方法
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) } }
compilerToFunctions 接收3个参数:编译模板 template,编译配置 options 和 Vue 实例 vm 最终返回 render 和 staticRenderFns 函数。
总结 模板编译过程:template —> compiler(parse,optimize, generate) —> render 函数(VNode)
模板编译是通过 Compiler 完成,compiler 可以分成 parse、optimize 和 generate 三个阶段,最终得到 render function。
compile 函数在执行 createCompilerToFunction 的时候作为参数传入,它是 createCompiler 函数中定义的 compile 函数。
Vue 在实现 compile 的过程中 利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其他逻辑如对编译配置处理、缓存处理等剥离开。很巧妙的设计
编译入口最终终于找到了,主要就是执行了如下几个逻辑:
Parse 解析:解析模板字符串生成 AST Parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成 AST
Optimize优化:优化语法树 Optimize 主要作用是标记 static 静态节点,这是Vue 在编译过程中做优化,后面当 update 视图更新时,patch 过程中 diff 算法会直接跳过静态节点,从而减少 比较的过程 优化 patch 的性能。
Generate 生成:生成 render Generate 是将 AST 转换成 render function 字符串的过程,得到的结果是 render 字符串已经 staticRenderFns 字符串。 最终 baseCompile 的返回值1 2 3 4 5 { ast : ast, render : code.render staticRenderFns : code.staticRenderFns }
最终返回了 ast、render、staticRenderFns, 且通过 generate 处理 ast 之后得到的是个对象。
经历了这三个阶段后,vue 的 模板 template 就转换成 它 渲染 Vnode 所需的 render 函数了。
Parse 解析 解析整个模板的时候它的流程应该是这样子的
HTML 解析器是主线,先用 HTML 解析器进行解析整个模板,在解析过程中如果碰到文本内容,那么就调用文本解析器来解析文本,如果碰到文本中包含过滤器的那就调用过滤器解析器来解析
parse 解析流程:
模板解析其实就是根据被解析内容的特点使用正则等方式将有效信息解析提取出来,根据解析内容的不同分为 HTML解析器、文本解析器和过滤器解析器。
文本信息与过滤器信息又存在于 HTML 标签中,所以在解析器主线函数 parse 中先调用 HTML 解析器 parseHTML 函数对模板字符串进行解析。
解析器内维护了一个栈,用来保证构建的 AST 节点层级与真正 DOM 层级一致。
文本解析器的作用就是将 HTML 解析器解析得到的文本内容进行二次解析,解析文本内容中是否包含变量,如果包含变量,则将变量提取出来进行加工,为后续生成 render 函数做准备。
Optimize 优化 有一种节点一旦首次渲染上了之后不管状态再怎么变化它都不会变了,这种节点叫做静态节点。
模板编译的最终目的是用模板生成一个 render 函数,而用 render 函数就可以生成与模板对应的 VNode,之后再进行 patch 算法 完成视图渲染。
patch 算法 用来对比新旧 VNOde 之间的差异。
在上面我们还说了,静态节点不管状态怎么变化它是不会变的,因此,我们可以在模板编译的时候就先找出模板中所有的静态节点和静态根节点,然后给它们打上标记,告诉后面 patch 过程 打了标记的节点是不需要对比的,只需要 克隆 一份。
优化阶段实际上就干了两件事:
在 AST 中找出所有静态节点并打上标记
在 AST 中找出所有的静态根节点打上标记1 2 3 4 5 6 7 export function optimize (root: ?ASTElement, options:CompilerOptions ) { if (!root) return isStaticKey = genStaticKeysCached (options.staticKey || '' ) isPlatformReservedTag = options.isReservedTag || no markStatic (root) markStaticRoots (root, false ) }
标记静态节点 首先先从根节点开始写,先标记根节点是否为静态美蒂娜,然后看根节点如果是元素节点,那么就向下递归它的子节点,子节点还有子节点就继续递归,直到标记完所有节点
静态节点需满足一下几点要求:
如果使用了 v-pre,那么他就是静态节点
如果没有使用 v-pre 那么它需要满足
不能使用动态绑定语法 即 v-、@、:开头的属性
不能使用 v-if、v-else 这些
不能是内置组件,即标签名不能是slot、component
不能是自定义组件
节点的所有属性的key 都必须是静态节点才有的key,
标记静态根节点 和静态节点类似,都是从 AST 根节点向下递归寻找,它要想成为静态根节点,必须满足
节点本身必须是静态节点
必须拥有子节点
子节点不能是只有一个文本节点
Generate 生成 根据模板对应的抽象语法树 AST 生成一个函数供组件挂载时调用,通过调用这个函数就可以得到模板对应的虚拟DOM。
模板编译整体流程