Vue.js源码剖析-模板编译
模板编译简介
-
模板编译的主要目的是将模板(template)转换为渲染函数(render)
<div> <h1 @click="handler">title</h1> <p>some content</p> </div>
-
渲染函数render
h('div', [ h('h1', { on: { click: this.handler } }, 'title'), h('p', 'some content') ])
-
模板编译的作用
- Vue2.x使用VNode描述视图及各种交互,用户自己编写VNode比较复杂
- 用户只需要编写类似HTML的代码--vue模板,通过vue内部编译器将模板转换为返回VNode的编译函数
- .vue文件会在webpack构建的过程中转换为render函数,webpack本身不支持编译模板,是通过vue-loader转换的
- 根据运行时间,编译过程分为运行时编译和构建时编译(打包时编译),运行时编译必须使用完整版vue(包含运行时版本+编译器),在项目运行的过程中将模板编译成render函数,缺点时vue体积大,运行速度慢;vue-cli默认加载运行时版本的vue,不带编译器,构建时编译,优点是体积小,运行速度快
模板编译结果
-
带编译器版本的Vue.js中,使用template或el的方式设置模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>compile</title> </head> <body> <div id="app"> <h1>Vue<span>模板编译过程</span></h1> <p>{{ msg }}</p> <comp @myclick="handler"></comp> </div> <script src="../../dist/vue.js"></script> <script> Vue.component('comp', { template: '<div>I am a comp</div>' }) const vm = new Vue({ el: '#app', data: { msg: 'Hello compiler' }, methods: { handler () { console.log('test') } } }) console.log(vm.$options.render) </script> </body> </html>
-
编译后render输出的结果
(function anonymous() { []() with (this) { return _c( "div", { attrs: { id: "app" } }, [ _m(0), // renderStatic函数,处理静态内容 _v(" "), // 创建空白文本节点,换行 _c("p", [_v(_s(msg))]), // 创建p标签对应的VNode _v(" "), _c("comp", { on: { myclick: handler } }), // 创建自定义组件对应的VNOde ], 1 // 设置children为一维数组 ); } });
with语句将某个对象添加到作用域链的顶部,如果在statement中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值
不推荐使用
with
,在 ECMAScript 5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。with (expression) { statement } const a = obj.a const b = obj.b const c = obj.c // 用with的方法 with(obj){ const a = a const b = b const c = c }
-
_c
是createElement()方法,定义的位置instance/render.js中 -
相关的渲染函数(_开头的方法定义),在instance/render-helps/index.js 中
// instance/render-helps/index.js target._v = createTextVNode target._m = renderStatic // core/vdom/vnode.js export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) } // 在 instance/render-helps/render-static.js export function renderStatic ( index: number, isInFor: boolean ): VNode | Array<VNode> { const cached = this._staticTrees || (this._staticTrees = []) let tree = cached[index] // if has already-rendered static tree and not inside v-for, // we can reuse the same tree. if (tree && !isInFor) { return tree } // otherwise, render a fresh tree. tree = cached[index] = this.$options.staticRenderFns[index].call( this._renderProxy, null, this // for render fns generated for functional component templates ) markStatic(tree, `__static__${index}`, false) return tree }
Vue Template Explorer
-
Vue 2.6 把模板编译成 render 函数的工具,空格换行尽量去除
-
Vue 3.0 beta 把模板编译成 render 函数的工具
模板编译的入口
-
src\platforms\web\entry-runtime-with-compiler.js,调用compileToFunctions将模板编译成render函数
Vue.prototype.$mount = function ( …… if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } // 把 template 转换成 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 } …… )
-
compileToFunctions()定义在src/platforms/web/compiler/index.js,由createCompiler(baseOptions)函数生成,接收和平台相关的参数
// src\platforms\web\compiler\optins.js export const baseOptions: CompilerOptions = { expectHTML: true, modules, // 模块 klass、style、model处理类样式、行内样式以及和v-if一起使用的v-model directives, // 指令v-model、v-text、v-html isPreTag, isUnaryTag, mustUseProp, canBeLeftOpenTag, isReservedTag, getTagNamespace, staticKeys: genStaticKeys(modules) }
-
createCompiler()定义在src\compiler\index.js,由createCompilerCreator创建,传入baseCompile(template, finalOptions)函数作为参数,finalOptions只合并之后的选项;baseCompile是模板编译的核心函数,解析parse、优化optimize、生成generate,最后返回createCompiler函数
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { // 1.把模板转换成 ast 抽象语法树 // 抽象语法树,用来以树形的方式描述代码结构 const ast = parse(template.trim(), options) if (options.optimize !== false) { // 2.优化抽象语法树 optimize(ast, options) } // 3.把抽象语法树生成字符串形式的 js 代码 const code = generate(ast, options) return { ast, // 渲染函数 render: code.render, // 静态渲染函数,生成静态 VNode 树 staticRenderFns: code.staticRenderFns } })
-
createCompilerCreator()定义在src\compiler\create-compiler.js,返回createCompiler函数,在createCompiler函数中定义了compile(template, options)函数,接收模板和用户传入的选项,compile中会把createCompiler (baseOptions: CompilerOptions)中和平台相关的选项baseOptions和用户传入的选项options进行合并,然后调用baseCompile(template.trim(), finalOptions),把合并之后的选项传给baseCompile开始编译模板,最后生成compileToFunctions函数,即编译入口函数,compileToFunctions是通过createCompileToFunctionFn(compile)函数返回
xport function createCompilerCreator (baseCompile: Function): Function { // baseOptions 平台相关的options // src\platforms\web\compiler\options.js 中定义 return function createCompiler (baseOptions: CompilerOptions) { function compile ( template: string, options?: CompilerOptions ): CompiledResult { ... return { compile, compileToFunctions: createCompileToFunctionFn(compile) } } }
模板编译过程
-
createCompileToFunctionFn函数
export function createCompileToFunctionFn (compile: Function): Function { // 通过闭包缓存编译之后的结果 const cache = Object.create(null) return function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { // 防止污染vue中的options options = extend({}, options) // check cache // 1. 读取缓存中的 CompiledFunctionResult 对象,如果有直接返回 const key = options.delimiters ? String(options.delimiters) + template : template if (cache[key]) { return cache[key] } // compile // 2. 把模板编译为编译对象(render, staticRenderFns),字符串形式的js代码 const compiled = compile(template, options) // 3. 把字符串形式的js代码转换成js方法 res.render = createFunction(compiled.render, fnGenErrors) res.staticRenderFns = compiled.staticRenderFns.map(code => { return createFunction(code, fnGenErrors) }) // 4. 缓存并返回res对象(render, staticRenderFns方法) return (cache[key] = res) } }
- .读取缓存中的 CompiledFunctionResult 对象,如果有直接返回
- 没有则开始编译,调用compile把模板编译为编译对象(render, staticRenderFns),字符串形式的js代码
- 调用createFunction把字符串形式的js代码转换成js方法
- 最后缓存并且返回
-
compile函数,在createCompilerCreator中定义
function compile ( template: string, options?: CompilerOptions ): CompiledResult { // 合并baseOptions和options const finalOptions = Object.create(baseOptions) // 开始合并baseOptinos和optinos if (options) { } // 模板编译核心函数 // baseCompile把模板编译成render函数,返回render函数和staticFunction,此时保存的字符串类型的js代码,在compileToFunctions中转为函数 const compiled = baseCompile(template.trim(), finalOptions) return compiled } return { compile, compileToFunctions: createCompileToFunctionFn(compile) } }
-
在compile合并完选项后,调用baseCompile开始编译模板
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { // 1.把模板转换成 ast 抽象语法树 // 抽象语法树,用来以树形的方式描述代码结构 const ast = parse(template.trim(), options) if (options.optimize !== false) { // 2.优化抽象语法树 optimize(ast, options) } // 3.把抽象语法树生成字符串形式的 js 代码 const code = generate(ast, options) return { ast, // 渲染函数 render: code.render, // 静态渲染函数,生成静态 VNode 树 staticRenderFns: code.staticRenderFns } })
-
解析 - parse
抽象语法树简称AST(Abstract Syntax Tree),是使用对象的形式描述树形的代码结构,对象中记录父子节点形成树形结构,这里的抽象语法树用来描述树形结构的HTML字符串
为什么要使用抽象语法树
- 模板字符串转换为AST后,可通过AST对模板做优化处理
- 标记模板中的静态内容(纯文本内容),在patch的时候直接跳过,不需要对比和重新渲染,从而优化性能
解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码 字符串。
const ast = parse(template.trim(), options) //src\compiler\parser\index.js parse()
整体流程:parse函数处理过程中会调用parseHTML依次遍历html模板字符串,把html模板字符串转换成AST对象,html中的属性和指令都会记录在AST对象的相应属性上
通过astexplorer查看得到的 AST tree
结构化指令的处理
v-if最终生成成三元表达式
// src\compiler\parser\index.js // structural directives // 结构化的指令 // v-for processFor(element) processIf(element) processOnce(element) // src\compiler\codegen\index.js export function genIf ( el: any, state: CodegenState, altGen?: Function, altEmpty?: string ): string { el.ifProcessed = true // avoid recursion return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty) } // 最终调用 genIfConditions 生成三元表达式
v-if 最终编译的结果
ƒ anonymous( ) { with(this){ return _c('div',{attrs:{"id":"app"}},[ _m(0), _v(" "), (msg)?_c('p',[_v(_s(msg))]):_e(),_v(" "), _c('comp',{on:{"myclick":onMyClick}}) ],1) } }
v-if/v-for 结构化指令只能在编译阶段处理,如果我们要在 render 函数处理条件或循环只能使用 js 中的 if 和 for
Vue.component('comp', { data: () { return { msg: 'my comp' } }, render (h) { if (this.msg) { return h('div', this.msg) } return h('div', 'bar') } })
-
优化 - optimize
通过parse生成抽象语法树后,调用optimize去优化,优化的目的是为了标记抽象语法树中的静态节点,在patch的时候可以直接跳过
export function optimize (root: ?ASTElement, options: CompilerOptions) { if (!root) return isStaticKey = genStaticKeysCached(options.staticKeys || '') isPlatformReservedTag = options.isReservedTag || no // first pass: mark all non-static nodes. // 标记静态节点 markStatic(root) // second pass: mark static roots. // 标记静态根节点(标签中包含子标签,并且没有动态内容,都是纯文本内容) markStaticRoots(root, false) }
markStatic中会调用isStatic(node)判断当前 astNode 是否是静态的
function isStatic (node: ASTNode): boolean { // 表达式 if (node.type === 2) { // expression return false } if (node.type === 3) { // text return true } return !!(node.pre || ( // pre !node.hasBindings && // no dynamic bindings !node.if && !node.for && // not v-if or v-for or v-else !isBuiltInTag(node.tag) && // not a built-in 不能是内置组件 isPlatformReservedTag(node.tag) && // not a component 不能是组件 !isDirectChildOfTemplateFor(node) && // 不能是v-for下的直接子节点 Object.keys(node).every(isStaticKey) )) }
-
生成 - generate
// src\compiler\index.js const code = generate(ast, options) // src\compiler\codegen\index.js 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 } } // 把字符串转换成函数 // src\compiler\to-function.js function createFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } }
-
模板编译总结
- 模板编译入口compileToFunctions(template, ...),首先从缓存中加载编译好的render函数,如果没有则调用compile(template, options)
- 在compiler(template, options)中,首先合并选项,然后调用baseCompile(template.trim(), finalOptions)编译模板
- baseCompile(template.trim(), finalOptions)中完成模板编译核心的三件事,首先通过parse()把模板字符串转换成AST树,然后通过optimize()对抽象语法树进行优化,标记AST树中的静态根节点,静态根节点不需要每次被重绘,patch的过程中会跳过静态根节点,最后通过generate()将AST树生成js的创建代码
- baseCompile执行完毕,回到入口函数compileToFunctions中,通过调用createFunction(compiled.render, fnGenErrors)把上一步生成的字符串形式js代码转换为函数形式,当render和staticRenderFns初始化完毕,挂载到Vue实例的options对应的属性中,到此模板编译结束
组件化机制
- 一个Vue组件就是一个拥有预定义选项的一个Vue实例
- 一个组件可以组成页面上一个功能完备的区域,组件可以包括脚本、样式、模板
- 组件化可以让我们方便的把页面拆分成多个可重用的组件
- 组件是独立的,系统内可重用,组件之间可以嵌套
组件注册
-
全局组件定义方式
Vue.component('comp', { template: '<h1>hello</h1>' })
-
Vue.component()入口
// src\core\global-api\index.js // 注册 Vue.directive()、 Vue.component()、Vue.filter() initAssetRegisters(Vue) // src\core\global-api\assets.js // isPlainObject是否是原始Object对象 if (type === 'component' && isPlainObject(definition)) { definition.name = definition.name || id // 把组件配置转换为组件的构造函数 this.options._base即vue构造函数 definition = this.options._base.extend(definition) } …… // 全局注册,存储资源并赋值 // this.options['components']['comp'] = Ctor this.options[type + 's'][id] = definition // src\core\global-api\index.js // this is used to identify the "base" constructor to extend all plainobject // components with in Weex's multi-instance scenarios. Vue.options._base = Vue // src\core\global-api\extend.js Vue.extend()
-
组件构造函数的创建
// 定义一个唯一cid目的是保证创建一个包裹的子构造函数通过原型继承并且能够缓存他们 Vue.cid = 0 // 基于传入的选项对象创建组件的构造函数 组件的构造函数继承vue的构造函数 所以组件对象拥有和vue实例一样的成员 Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} // Vue 构造函数 const Super = this const SuperId = Super.cid // 从缓存中加载组件的构造函数 _Ctor构造函数 const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { // 如果是开发环境验证组件的名称 validateComponentName(name) } const Sub = function VueComponent (options) { // 调用 _init() 初始化 this._init(options) } // 原型继承自 Vue Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ // 合并 options Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // allow further extension/mixin/plugin usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup // 把组件构造构造函数保存到 Ctor.options.components.comp = Ctor if (name) { Sub.options.components[name] = Sub } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // cache constructor // 把组件的构造函数缓存到 options._Ctor cachedCtors[SuperId] = Sub return Sub } }
-
调试 Vue.component() 调用的过程
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> </div> <script src="../../dist/vue.js"></script> <script> const Comp = Vue.component('comp', { template: '<h2>I am a comp</h2>' }) const vm = new Vue({ el: '#app', render (h) { return h(Comp) } }) </script> </body> </html>
组件创建和挂载
组件VNode的创建过程
-
创建根组件,首次 _render() 时,会得到整棵树的 VNode 结构
-
整体流程:new Vue() --> $mount() --> vm._render() --> createElement() --> createComponent()
-
创建组件的 VNode,初始化组件的 hook 钩子函数
// 1. _createElement() 中调用 createComponent() // src\core\vdom\create-element.js else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 查找自定义组件构造函数的声明 // 根据 Ctor 创建组件的 VNode // component vnode = createComponent(Ctor, data, context, children, tag) // 2. createComponent() 中调用创建自定义组件对应的 VNode // src\core\vdom\create-component.js export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { if (isUndef(Ctor)) { return } …… // install component management hooks onto the placeholder node // 安装组件的钩子函数 init/prepatch/insert/destroy // 初始化了组件的 data.hooks 中的钩子函数 installComponentHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag // 创建自定义组件的 VNode,设置自定义组件的名字 // 记录this.componentOptions = componentOptions const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) return vnode } // 3. installComponentHooks() 初始化组件的 data.hook function installComponentHooks (data: VNodeData) { const hooks = data.hook || (data.hook = {}) // 用户可以传递自定义钩子函数 // 把用户传入的自定义钩子函数和 componentVNodeHooks 中预定义的钩子函数合并 for (let i = 0; i < hooksToMerge.length; i++) { const key = hooksToMerge[i] const existing = hooks[key] const toMerge = componentVNodeHooks[key] if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } } // 4. 钩子函数定义的位置(init()钩子中创建组件的实例) // inline hooks to be invoked on component VNodes during patch const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { // 创建组件实例挂载到 vnode.componentInstance const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) // 调用组件对象的 $mount(),把组件挂载到页面 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { …… }, insert (vnode: MountedComponentVNode) { …… }, destroy (vnode: MountedComponentVNode) { …… } } //5 .创建组件实例的位置,由自定义组件的 init() 钩子方法调用 export function createComponentInstanceForVnode ( vnode: any, // we know it's MountedComponentVNode but flow doesn't parent: any, // activeInstance in lifecycle state ): Component { const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent } // check inline-template render functions const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } // 创建组件实例 return new vnode.componentOptions.Ctor(options) }
组件实例的创建和挂载过程
-
Vue._update() --> patch() --> createElm() --> createComponent()
// src\core\vdom\patch.js // 1. 创建组件实例,挂载到真实 DOM function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { // 调用 init() 方法,创建和挂载组件实例 // init() 的过程中创建好了组件的真实 DOM,挂载到了 vnode.elm 上 i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { // 调用钩子函数(VNode的钩子函数初始化属性/事件/样式等,组件的钩子函数) initComponent(vnode, insertedVnodeQueue) // 把组件对应的 DOM 插入到父元素中 insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } } // 2. 调用钩子函数,设置局部作用于样式 function initComponent (vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) vnode.data.pendingInsert = null } vnode.elm = vnode.componentInstance.$el if (isPatchable(vnode)) { // 调用钩子函数 invokeCreateHooks(vnode, insertedVnodeQueue) // 设置局部作用于样式 setScope(vnode) } else { // empty component root. // skip all element-related modules except for ref (#3455) registerRef(vnode) // make sure to invoke the insert hook insertedVnodeQueue.push(vnode) } } // 3. 调用钩子函数 function invokeCreateHooks (vnode, insertedVnodeQueue) { // 调用 VNode 的钩子函数,初始化属性/样式/事件等 for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable // 调用组件的钩子函数 if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } }