通过文章前半段的学习,我们对Vue的挂载流程有了一个初略的认识,接下来将先从模板编译的过程展开,阅读源码时发现,模板编译的过程也是相当复杂的,要在短篇幅内将整个编译过程姜凯是不切实际的,这里只对实现思路做简单的介绍。
template模板的编写有三种方式,分别是:
// 1. 熟悉的字符串模板 const vm = new Vue({ el:'#app', template: '<div>模板字符串</div>' }) // 2. 选择符匹配元素的innerHTML模板 <div id="app"> <div>test1</div> <script type="x-template" id="test"> <p>test</p> </script> </div> const vm = new Vue({ el: '#app', template: '#test' }) // 3. dom元素匹配的innerHTML模板 <div id="app"> <div>test1</div> <span id="test"><div class="test2">test2</div></span> </div> var vm = new Vue({ el: '#app', template: document.querySelector('#test') })
三种写法对应代码的三个不同的分支
var template = opitons.template if(template) { // 针对字符串模板和选择符匹配模板 if(typeof template === 'string') { // 选择符匹配模板 if(template.charAt(0) === '#') { // 获取匹配元素的innerHTML template = idToTemplate(template) if(!template) { warn( ("Template element not found or is empty: " + (options.template)), this ); } } // 针对dom元素匹配 } else if (template.nodeType) { // 获取匹配元素的innerHTML template = template.innerHTML }else { // 其他类型则判定为非法传入 if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if(el) { // 如果没有传入template模板,则默认el元素所属的根节点作为基础模板 template = getOuterHTML(el) }
其中X-Template模板方式一般用于模板特别大的demo或极小型的应用,官方不建议在其他情形下使用,因为这会将模板和组件的其他定义分离开。
Vue源码中编译流程比较绕,设计的函数处理逻辑比较多,实现流程中巧妙的运用了偏函数的技巧将配置项处理和编译核心逻辑抽取出来,为了理解这个设计思路,下图可帮助理解
即便有流程图,编译逻辑理解起来依然比较晦涩,接下来,结合代码法系每个环节的执行过程
var ref = compileToFunctions(template, { outputSourceRange: "development" !== 'production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this); // 将compileToFunction方法暴露给Vue作为静态方法存在 Vue.compile = compileToFunctions;
这是编译的入口,也是Vue对外暴露的编译方法。compileToFunction
需要传递三个参数:template模板,编译配置选项以及Vue实例。我们先大致了解一下配置中的几个默认选项
var createCompiler = createCompilerCreator(function baseCompile (template,options) { //把模板解析成抽象的语法树 var ast = parse(template.trim(), options); // 配置中有代码优化选项则会对Ast语法树进行优化 if (options.optimize !== false) { optimize(ast, options); } var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns } });
createCompilerCreator
角色定位为创建编译器的创建者,它传递了一个基础的编译器baseCompile
作为参数,baseCompile
是真正执行编译功能的地方,它传递template模板和基础的配置选项作为参数,实现的功能有两个
具体看看createCompilerCreator
的实现方式。
function createCompilerCreator (baseCompile) { return function createCompiler (baseOptions) { // 内部定义compile方法 function compile (template, options) { ··· // 将剔除空格后的模板以及合并选项后的配置作为参数传递给baseCompile方法,其中finalOptions为baseOptions和用户options的合并 var compiled = baseCompile(template.trim(), finalOptions); { detectErrors(compiled.ast, warn); } compiled.errors = errors; compiled.tips = tips; return compiled } return { compile: compile, compileToFunctions: createCompileToFunctionFn(compile) } } }
createCompilerCreator
函数只有一个作用,利用偏函数将baseCompile基础编译方法缓存,并返回一个编译器函数。该函数内部定义了真正执行编译的compile方法,并最终将compile和compileToFunction作为两个对象属性返回。这也是compileToFunction的来源。而内部compile的作用,是为了将基础的配置baseOpitons和用户自定义的配置options进行合并,baseOptions是跟外部平台相关的配置,最终返回并合并配置后的baseCompile编译方法。
compileToFunctions 来源于 createCompileToFunctionFn 函数的返回值,该函数会将编译的方法 compile 作为参数传入。
function createCompileToFunctionFn (compile) { var cache = Object.create(null); return function compileToFunctions (template,options,vm) { options = extend({}, options); ··· // 缓存的作用:避免重复编译同个模板造成性能的浪费 if (cache[key]) { return cache[key] } // 执行编译方法 var compiled = compile(template, options); ··· // turn code into functions var res = {}; var fnGenErrors = []; // 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数 res.render = createFunction(compiled.render, fnGenErrors); // 渲染优化相关 res.staticRenderFns = compiled.staticRenderFns.map(function (code) { return createFunction(code, fnGenErrors) }); ··· return (cache[key] = res) } }
最终我们找到了compileToFunction真正的执行过程var compiled = compile(template, options);
,并将编译后的函数体字符串通过 creatFunction 转化为 render 函数返回。
function createFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err: err, code: code }); return noop } }
其中函数体字符串类似于with(this){return _m(0)}
,最终的render渲染函数为 function(){with(this){return _m(0)}}