此次,来学习下Vue是如何解析HTML代码的。
从以前学习 Render 的过程当中咱们知道,template 的编译在 $mount
方法中出现过。html
// src/platforms/web/entry-runtime-with-compiler.js const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { return this } const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { // 首字母为#号,看做是ID。 template = idToTemplate(template) } } else if (template.nodeType) { // 为真实 DOM,直接获取html template = template.innerHTML } else { return this } } else if (el) { // 获取 HTML template = getOuterHTML(el) } if (template) { // 进行编译并赋值给 vm.$options const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) // 渲染函数 options.render = render // 静态渲染方法 options.staticRenderFns = staticRenderFns } } return mount.call(this, el, hydrating) }
其实以上代码总结起来就4步:前端
关键在于第三步,编译 render 函数上。先获取 template,即获取HTML内容,而后执行 compileToFunctions 来编译,最后将 render 和 staticRenderFns 传给 vm.$options 对象。
顺便看看这两个方法都用在哪里?vue
// src/core/instance/render.js Vue.prototype._render = function (): VNode { try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) } return vnode }
// src/core/instance/render-helpers/render-static.js export function renderStatic ( index: number, isInFor: boolean ): VNode | Array<VNode> { const cached = this._staticTrees || (this._staticTrees = []) let tree = cached[index] if (tree && !isInFor) { return tree } // otherwise, render a fresh tree. tree = cached[index] = this.$options.staticRenderFns[index].call( this._renderProxy, null, this ) markStatic(tree, `__static__${index}`, false) return tree }
因而可知,template 编译生成的方法都用在了渲染行为中。node
下面咱们顺着编译代码往下找。在 mount 方法中执行了 compileToFunctions
方法。git
const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this)
找到方法的所在之处:github
// src/platforms/web/compiler/index.js const { compile, compileToFunctions } = createCompiler(baseOptions)
// src/compiler/index.js export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { // 将template转为AST语法树对象 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 } })
先看里面的 baseCompile 方法,其做用为将 HTML 字符串转为 AST 抽象语法树对象,并进行优化,最后生成渲染代码。返回值中 render 为渲染字符串,staticRenderFns 为渲染字符串数组。
以后再来看看 createCompilerCreator 方法:web
// src/compiler/create-compiler.js 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) { // merge custom modules if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ) } // copy other options 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) } } }
来看 compile 方法:合并 option 配置参数,而后执行外部传入的 baseCompile 方法,返回方法执行的返回结果。最终返回 { compile, compileToFunctions }
,
createCompileToFunctionFn 代码以下:正则表达式
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 // 确认缓存,有缓存直接返回 const key = options.delimiters ? String(options.delimiters) + template : template if (cache[key]) { return cache[key] } // compile const compiled = compile(template, options) // turn code into functions const res = {} const fnGenErrors = [] // 生成 render 和 staticRenderFns 方法 res.render = createFunction(compiled.render, fnGenErrors) res.staticRenderFns = compiled.staticRenderFns.map(code => { return createFunction(code, fnGenErrors) }) // 返回方法并缓存 return (cache[key] = res) } }
这里就找到了咱们在 mount 方法中看到的 render 和 staticRenderFns 方法了。createCompileToFunctionFn 方法其实就是将传入的 render 和 staticRenderFns 字符串转为真实方法。编程
至此,捋一下思路:
template的编译用于render渲染行为中,因此template最后生成渲染函数。
template 的解析过程当中数组
其实 const { compile, compileToFunctions } = createCompiler(baseOptions)
就是 createCompilerCreator 的返回结果。因此,在 mount 中使用的 compileToFunctions 方法就是 createCompileToFunctionFn 方法生成的。
总体思路滤清了,来看看关键的 baseCompile 方法。该方法进行了三步操做:
先来说讲AST抽象语法树。维基百科的解释是:
在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。
parse 方法的最终目的就是将 template 解析为 AST 元素对象。在 parse 解析方法中,用到了大量的正则。正则的具体用法以前写过一篇文章:一块儿来理解正则表达式。代码量不少,考虑了各类解析的状况。这里不赘述太多,找一条主线来学习,其余内容我将在项目中注释。
来看看 parse 方法。
export function parse ( template: string, options: CompilerOptions ): ASTElement | void { // 定义了各类参数和方法 parseHTML(template, { warn, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, start (tag, attrs, unary) {}, end () {} chars (text: string) {}, comment (text: string) {} ) return root }
实际上 parse 就是 parseHTML 的过程,最后返回AST元素对象。其中,传入的 options 配置对象中,start、end、chars、comment方法都会在 parseHTML 方法中用到。其实相似于生命周期钩子,在某个阶段执行。
parseHTML 方法是正则解析HTML的过程,这部分我将在以后的博客中单独说下,也能够看项目的注释,将不定时更新项目注释。
该方法只是作了些标记静态节点的行为,目的是为了在从新渲染时不重复渲染静态节点,以达到性能优化的目的。
export function optimize (root: ?ASTElement, options: CompilerOptions) { if (!root) return isStaticKey = genStaticKeysCached(options.staticKeys || '') isPlatformReservedTag = options.isReservedTag || no // 标记全部非静态节点 markStatic(root) // 标记静态根节点 markStaticRoots(root, false) }
generate 方法用于将 AST 元素生成 render 渲染字符串。
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 } }
最后生成以下这样的渲染字符串:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on:{"click":hey}},[_v(_s(message))])])}
其中的 _c _v _s 等方法在哪里呢~这个咱们以前提及过:
// src/core/instance/render.js // 建立vnode元素 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // src/core/instance/render-helper/index.js 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 }
其实template部分真的内容展开超级多,以后会展开细说。本来计划大前天就把博客写出来的,结果看代码看着看着绕进去了。因此,仍是那句话,看代码得抓住主线,带着问题去看,不要在乎细枝末节。
这也算是个人经验教训了,之后每次看代码,牢记待着明确的问题去看去解决。想一次看懂整个项目的代码是不可行的。
下期预告,parseHTML 细节解析
鉴于前端知识碎片化严重,我但愿可以系统化的整理出一套关于Vue的学习系列博客。
本文源码已收入到GitHub中,以供参考,固然能留下一个star更好啦^-^。
https://github.com/violetjack/VueStudyDemos
VioletJack,高效学习前端工程师,喜欢研究提升效率的方法,也专一于Vue前端相关知识的学习、整理。
欢迎关注、点赞、评论留言~我将持续产出Vue相关优质内容。
新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
简书: http://www.jianshu.com/users/...
Github: https://github.com/violetjack