render函数
编译过程-模板编译成render函数
通过文章前半段的学习,我们对Vue的挂载流程有了一个初略的认识,接下来将先从模板编译的过程展开,阅读源码时发现,模板编译的过程也是相当复杂的,要在短篇幅内将整个编译过程姜凯是不切实际的,这里只对实现思路做简单的介绍。
template的三种写法
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实例。我们先大致了解一下配置中的几个默认选项
- delimiters:该选项可以改变纯文本插入分隔符,当不传递值时,Vue默认的分隔符为{{}},用户可通过该选项修改
- comments当设为true时,将会保留且渲染模板中的HTML注释。默认行为是舍弃它们。
接着一步步寻找compileToFunctions根源
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模板和基础的配置选项作为参数,实现的功能有两个
- 吧模板解析成抽象的语法树,简称AST,代码中对应parse部分
- 可选:优化AST语法树,执行optimize方法
- 根据不同平台将AST语法树生成需要的代码,对应generate函数
具体看看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)}}
在options没有传render函数的时候会调用compileToFunction
,然后将其返回的render函数赋值给options.render
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
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
在有没有给传入render方法最终都会调用return mount.call(this, el, hydrating)
,实际上是执行以下代码
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
上面代码我们主要分析vm._render()
,它来自于render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
// 这里的render函数可以是由编译而来,也可以是由用户编辑而来
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
}
由上面代码可以关注到vnode = render.call(vm._renderProxy, vm.$createElement)
调用了options的render函数,由将render函数this指向vm._renderProxy
,并且传入vm.$createElement
参数