This article will take you to deeply analyze the source code, look at the problem from the vue source code, and see how the vue compiler generates the rendering function. I hope it will be helpful to everyone.
The first two articles mainly understand the analysis and optimization of the vue compiler:
Let’s take a look at vue How the compiler generates the running rendering function from the AST syntax tree.
createCompiler() method - Entry
File location: /src/compiler/index.js
The most important one is the generate(ast, options)
method, which is responsible for generating rendering functions from the AST
syntax tree.
/* 在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如 web 平台 1、将 html 模版解析成 ast 2、对 ast 树进行静态标记 3、将 ast 生成渲染函数 - 静态渲染函数放到 code.staticRenderFns 数组中 - 动态渲染函数 code.render - 在将来渲染时执行渲染函数能够得到 vnode */ export const createCompiler = createCompilerCreator(function baseCompile( template: string, options: CompilerOptions ): CompiledResult { /* 将模版字符串解析为 AST 语法树 每个节点的 ast 对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等 */ const ast = parse(template.trim(), options) /* 优化,遍历 AST,为每个节点做静态标记 - 标记每个节点是否为静态节点,保证在后续更新中跳过这些静态节点 - 标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数 优化,遍历 AST,为每个节点做静态标记 */ if (options.optimize !== false) { optimize(ast, options) } /* 从 AST 语法树生成渲染函数 如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)" */ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })
generate() method
File location: src\compiler\codegen\index.js
When assigning a value to code
, the main content is through the genElement(ast, state)
method Generated .
/* 从 AST 生成渲染函数: - render 为字符串的代码 - staticRenderFns 为包含多个字符串的代码,形式为 `with(this){return xxx}` */ export function generate ( ast: ASTElement | void, // ast 对象 options: CompilerOptions // 编译选项 ): CodegenResult { /* 实例化 CodegenState 对象,参数是编译选项,最终得到 state ,其中大部分属性和 options 一样 */ const state = new CodegenState(options) /* 生成字符串格式的代码,比如:'_c(tag, data, children, normalizationType)' - data 为节点上的属性组成 JSON 字符串,比如 '{ key: xx, ref: xx, ... }' - children 为所有子节点的字符串格式的代码组成的字符串数组,格式: `['_c(tag, data, children)', ...],normalizationType`, - normalization 是 _c 的第四个参数,表示节点的规范化类型(非重点,可跳过) 注意:code 并不一定就是 _c,也有可能是其它的,比如整个组件都是静态的,则结果就为 _m(0) */ const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } }
genElement() method
File location: src\compiler\codegen\index.js
export function genElement (el: ASTElement, state: CodegenState): string { if (el.parent) { el.pre = el.pre || el.parent.pre } if (el.staticRoot && !el.staticProcessed) { /* 处理静态根节点,生成节点的渲染函数 1、将当前静态节点的渲染函数放到 staticRenderFns 数组中 2、返回一个可执行函数 _m(idx, true or '') */ return genStatic(el, state) } else if (el.once && !el.onceProcessed) { /* 处理带有 v-once 指令的节点,结果会有三种: 1、当前节点存在 v-if 指令,得到一个三元表达式,`condition ? render1 : render2` 2、当前节点是一个包含在 v-for 指令内部的静态节点,得到 `_o(_c(tag, data, children), number, key)` 3、当前节点就是一个单纯的 v-once 节点,得到 `_m(idx, true of '')` */ return genOnce(el, state) } else if (el.for && !el.forProcessed) { /* 处理节点上的 v-for 指令,得到: `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})` */ return genFor(el, state) } else if (el.if && !el.ifProcessed) { /* 处理带有 v-if 指令的节点,最终得到一个三元表达式:`condition ? render1 : render2` */ return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget && !state.pre) { /* 当前节点是 template 标签也不是 插槽 和 带有 v-pre 指令的节点时走这里 生成所有子节点的渲染函数,返回一个数组,格式如: `[_c(tag, data, children, normalizationType), ...]` */ return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { /* 生成插槽的渲染函数,得到: `_t(slotName, children, attrs, bind)` */ return genSlot(el, state) } else { /* component or element 处理 动态组件 和 普通元素(自定义组件、原生标签、平台保留标签,如 web 平台中的每个 html 标签) */ let code if (el.component) { /* 处理动态组件,生成动态组件的渲染函数,得到 `_c(compName, data, children)` */ code = genComponent(el.component, el, state) } else { // 处理普通元素(自定义组件、原生标签) let data if (!el.plain || (el.pre && state.maybeComponent(el))) { /* 非普通元素或者带有 v-pre 指令的组件走这里,处理节点的所有属性,返回一个 JSON 字符串, 比如: '{ key: xx, ref: xx, ... }' */ data = genData(el, state) } /* 处理子节点,得到所有子节点字符串格式的代码组成的数组,格式: `['_c(tag, data, children)', ...],normalizationType` 其中的 normalization 表示节点的规范化类型(非重点,可跳过) */ const children = el.inlineTemplate ? null : genChildren(el, state, true) /* 得到最终的字符串格式的代码,格式:_c(tag, data, children, normalizationType) */ code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } /* 如果提供了 transformCode 方法,则最终的 code 会经过各个模块(module)的该方法处理, 不过框架没提供这个方法,不过即使处理了,最终的格式也是 _c(tag, data, children) module transforms */ for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } // 返回 code return code } }
genChildren() method
File location: src\compiler\codegen\ index.js
/* 生成所有子节点的渲染函数,返回一个数组,格式如: `[_c(tag, data, children, normalizationType), ...]` */ export function genChildren ( el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function ): string | void { // 获取所有子节点 const children = el.children if (children.length) { // 第一个子节点 const el: any = children[0] // optimize single v-for if (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot' ) { /* 优化处理: - 条件:只有一个子节点 && 子节点的上有 v-for 指令 && 子节点的标签不为 template 或者 slot - 方式:直接调用 genElement 生成该节点的渲染函数,不需要走下面的循环然后调用 genCode 最后得到渲染函数 */ const normalizationType = checkSkip ? state.maybeComponent(el) ? `,1` : `,0` : `` return `${(altGenElement || genElement)(el, state)}${normalizationType}` } // 获取节点规范化类型,返回一个 number: 0、1、2(非重点,可跳过) const normalizationType = checkSkip ? getNormalizationType(children, state.maybeComponent) : 0 // 是一个函数,负责生成代码的一个函数 const gen = altGenNode || genNode /* 返回一个数组,其中每个元素都是一个子节点的渲染函数 格式:['_c(tag, data, children, normalizationType)', ...] */ return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }` } }
genNode() method
File location: src\compiler \codegen\index.js
function genNode (node: ASTNode, state: CodegenState): string { // 处理普通元素节点 if (node.type === 1) { return genElement(node, state) } else if (node.type === 3 && node.isComment) { // 处理文本注释节点 return genComment(node) } else { // 处理文本节点 return genText(node) } }
genComment() method
##File location: src\compiler\codegen\index.js
// 得到返回值,格式为:`_e(xxxx)` export function genComment (comment: ASTText): string { return `_e(${JSON.stringify(comment.text)})` }
genText() method
File location: src\compiler\codegen\index.js
// 得到返回值,格式为:`_v(xxxxx)` export function genText (text: ASTText | ASTExpression): string { return `_v(${text.type === 2 ? text.expression // no need for () because already wrapped in _s() : transformSpecialNewlines(JSON.stringify(text.text)) })` }
genData() method
File location: src\compiler\codegen\index.js
/* 处理节点上的众多属性,最后生成这些属性组成的 JSON 字符串, 比如 data = { key: xx, ref: xx, ... } */ export function genData(el: ASTElement, state: CodegenState): string { // 节点的属性组成的 JSON 字符串 let data = '{' /* 首先先处理指令,因为指令可能在生成其它属性之前改变这些属性 执行指令编译方法,如 web 平台的 v-text、v-html、v-model,然后在 el 对象上添加相应的属性, 如 v-text:el.textContent = _s(value, dir) v-html:el.innerHTML = _s(value, dir) 当指令在运行时还有任务时,比如 v-model, 则返回 directives: [{ name, rawName, value, arg, modifiers }, ...}] */ const dirs = genDirectives(el, state) if (dirs) data += dirs + ',' // key,data = { key: xxx } if (el.key) { data += `key:${el.key},` } // ref,data = { ref: xxx } if (el.ref) { data += `ref:${el.ref},` } // 带有 ref 属性的节点在带有 v-for 指令的节点的内部,data = { refInFor: true } if (el.refInFor) { data += `refInFor:true,` } // pre,v-pre 指令,data = { pre: true } if (el.pre) { data += `pre:true,` } // 动态组件 <component is="xxx">,data = { tag: 'component' } if (el.component) { data += `tag:"${el.tag}",` } /* 为节点执行模块 (class、style) 的 genData 方法, 得到 data = { staticClass: xx, class: xx, staticStyle: xx, style: xx } module data generation functions */ for (let i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el) } /* 其它属性,得到 data = { attrs: 静态属性字符串 } 或者 data = { attrs: '_d(静态属性字符串, 动态属性字符串)' } attributes */ if (el.attrs) { data += `attrs:${genProps(el.attrs)},` } // DOM props,结果 el.attrs 相同 if (el.props) { data += `domProps:${genProps(el.props)},` } /* 自定义事件 - data = { `on${eventName}:handleCode` } 或者 - { `on_d(${eventName}:handleCode`, `${eventName},handleCode`) } event handlers */ if (el.events) { data += `${genHandlers(el.events, false)},` } /* 带 .native 修饰符的事件, - data = { `nativeOn${eventName}:handleCode` } 或者 - { `nativeOn_d(${eventName}:handleCode`, `${eventName},handleCode`) */ if (el.nativeEvents) { data += `${genHandlers(el.nativeEvents, true)},` } /* 非作用域插槽,得到 data = { slot: slotName } slot target only for non-scoped slots */ if (el.slotTarget && !el.slotScope) { data += `slot:${el.slotTarget},` } // scoped slots,作用域插槽,data = { scopedSlots: '_u(xxx)' } if (el.scopedSlots) { data += `${genScopedSlots(el, el.scopedSlots, state)},` } /* 处理 v-model 属性,得到 data = { model: { value, callback, expression } } component v-model */ if (el.model) { data += `model:{value:${el.model.value },callback:${el.model.callback },expression:${el.model.expression }},` } /* inline-template,处理内联模版,得到: data = { inlineTemplate: { render: function() { render 函数 }, staticRenderFns: [ function() {}, ... ] } } */ if (el.inlineTemplate) { const inlineTemplate = genInlineTemplate(el, state) if (inlineTemplate) { data += `${inlineTemplate},` } } // 删掉 JSON 字符串最后的 逗号,然后加上闭合括号 } data = data.replace(/,$/, '') + '}' /* v-bind 动态参数包装 必须使用相同的 v-bind 对象应用动态绑定参数 合并辅助对象,以便正确处理 class/style/mustUseProp 属性。 */ if (el.dynamicAttrs) { data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})` } // v-bind data wrap if (el.wrapData) { data = el.wrapData(data) } // v-on data wrap if (el.wrapListeners) { data = el.wrapListeners(data) } return data }
genDirectives() method
File location: src\compiler\codegen\index.js
/** 运行指令的编译方法,如果指令存在运行时任务,则返回 directives: [{ name, rawName, value, arg, modifiers }, ...}] */ function genDirectives(el: ASTElement, state: CodegenState): string | void { // 获取指令数组 const dirs = el.directives // 不存在指令,直接结束 if (!dirs) return // 指令的处理结果 let res = 'directives:[' // 用于标记指令是否需要在运行时完成的任务,比如 v-model 的 input 事件 let hasRuntime = false let i, l, dir, needRuntime // 遍历指令数组 for (i = 0, l = dirs.length; i < l; i++) { dir = dirs[i] needRuntime = true // 获取节点当前指令的处理方法,比如 web 平台的 v-html、v-text、v-model const gen: DirectiveFunction = state.directives[dir.name] if (gen) { // 执行指令的编译方法,如果指令还需要运行时完成一部分任务,则返回 true,比如 v-model needRuntime = !!gen(el, dir, state.warn) } if (needRuntime) { // 表示该指令在运行时还有任务 hasRuntime = true // res = directives:[{ name, rawName, value, arg, modifiers }, ...] res += `{name:"${dir.name}",rawName:"${dir.rawName}"${dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : '' }${dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : '' }${dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : '' }},` } } // 只有指令存在运行时任务时,才会返回 res if (hasRuntime) { return res.slice(0, -1) + ']' } }
genDirectives() method
File location: src\compiler\codegen\index.js
/* 遍历属性数组 props,得到所有属性组成的字符串 如果不存在动态属性,则返回:'attrName,attrVal,...' 如果存在动态属性,则返回:'_d(静态属性字符串, 动态属性字符串)' */ function genProps(props: Array<ASTAttr>): string { // 静态属性 let staticProps = `` // 动态属性 let dynamicProps = `` // 遍历属性数组 for (let i = 0; i < props.length; i++) { // 属性 const prop = props[i] // 属性值 const value = __WEEX__ ? generateValue(prop.value) : transformSpecialNewlines(prop.value) if (prop.dynamic) { // 动态属性,`dAttrName,dAttrVal,...` dynamicProps += `${prop.name},${value},` } else { // 静态属性,'attrName:attrVal,...' staticProps += `"${prop.name}":${value},` } } // 闭合静态属性字符串,并去掉静态属性最后的 ',' staticProps = `{${staticProps.slice(0, -1)}}` if (dynamicProps) { // 如果存在动态属性则返回:_d(静态属性字符串,动态属性字符串) return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])` } else { // 说明属性数组中不存在动态属性,直接返回静态属性字符串 return staticProps } }
genHandlers() method
File location: src\compiler\codegen\events.js
/* 生成自定义事件的代码 动态:'nativeOn|on_d(staticHandlers, [dynamicHandlers])' 静态:`nativeOn|on${staticHandlers}` */ export function genHandlers ( events: ASTElementHandlers, isNative: boolean ): string { // 原生为 nativeOn,否则为 on const prefix = isNative ? 'nativeOn:' : 'on:' // 静态 let staticHandlers = `` // 动态 let dynamicHandlers = `` /* 遍历 events 数组 events = [{ name: { value: 回调函数名, ... } }] */ for (const name in events) { const handlerCode = genHandler(events[name]) if (events[name] && events[name].dynamic) { // 动态,dynamicHandles = `eventName,handleCode,...,` dynamicHandlers += `${name},${handlerCode},` } else { // staticHandlers = `eventName:handleCode,...,` staticHandlers += `"${name}":${handlerCode},` } } // 闭合静态事件处理代码字符串,去除末尾的 ',' staticHandlers = `{${staticHandlers.slice(0, -1)}}` if (dynamicHandlers) { // 动态,on_d(statickHandles, [dynamicHandlers]) return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])` } else { // 静态,`on${staticHandlers}` return prefix + staticHandlers } }
genStatic() method
File location: src\compiler\codegen\index.js
/* 生成静态节点的渲染函数 1、将当前静态节点的渲染函数放到 staticRenderFns 数组中 2、返回一个可执行函数 _m(idx, true or '') hoist static sub-trees out */ function genStatic(el: ASTElement, state: CodegenState): string { // 标记当前静态节点已经被处理过了 el.staticProcessed = true /* 某些元素(模板)在 v-pre 节点中需要有不同的行为 所有 pre 节点都是静态根,因此可将其用作包装状态更改并在退出 pre 节点时将其重置 */ const originalPreState = state.pre if (el.pre) { state.pre = el.pre } /* 将静态根节点的渲染函数 push 到 staticRenderFns 数组中, 比如:[`with(this){return _c(tag, data, children)}`] */ state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) state.pre = originalPreState /* 返回一个可执行函数:_m(idx, true or '') idx = 当前静态节点的渲染函数在 staticRenderFns 数组中下标 */ return `_m(${state.staticRenderFns.length - 1 }${el.staticInFor ? ',true' : '' })` }
genOnce() method
File location: src\compiler\codegen\index.js
/* 处理带有 v-once 指令的节点,结果会有三种: 1、当前节点存在 v-if 指令,得到一个三元表达式,condition ? render1 : render2 2、当前节点是一个包含在 v-for 指令内部的静态节点, 得到 `_o(_c(tag, data, children), number, key)` 3、当前节点就是一个单纯的 v-once 节点,得到 `_m(idx, true of '')` v-once */ function genOnce(el: ASTElement, state: CodegenState): string { // 标记当前节点的 v-once 指令已经被处理过了 el.onceProcessed = true if (el.if && !el.ifProcessed) { /* 如果含有 v-if 指令 && if 指令没有被处理过 则处理带有 v-if 指令的节点,最终得到一个三元表达式: condition ? render1 : render2 */ return genIf(el, state) } else if (el.staticInFor) { /* 说明当前节点是被包裹在还有 v-for 指令节点内部的静态节点 获取 v-for 指令的 key */ let key = '' let parent = el.parent while (parent) { if (parent.for) { key = parent.key break } parent = parent.parent } // key 不存在则给出提示,v-once 节点只能用于带有 key 的 v-for 节点内部 if (!key) { process.env.NODE_ENV !== 'production' && state.warn( `v-once can only be used inside v-for that is keyed. `, el.rawAttrsMap['v-once'] ) return genElement(el, state) } // 生成 `_o(_c(tag, data, children), number, key)` return `_o(${genElement(el, state)},${state.onceId++},${key})` } else { /* 上面几种情况都不符合,说明就是一个简单的静态节点, 和处理静态根节点时的操作一样,得到 _m(idx, true or '') */ return genStatic(el, state) } }
genFor () Method
File location:src\compiler\codegen\index.js
/* 处理节点上 v-for 指令 得到 `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})` */ export function genFor( el: any, state: CodegenState, altGen?: Function, altHelper?: string ): string { // v-for 的迭代器,比如 一个数组 const exp = el.for // 迭代时的别名 const alias = el.alias // iterator 为 v-for = "(item ,idx) in obj" 时会有,比如 iterator1 = idx const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' // 提示,v-for 指令在组件上时必须使用 key if (process.env.NODE_ENV !== 'production' && state.maybeComponent(el) && el.tag !== 'slot' && el.tag !== 'template' && !el.key ) { state.warn( `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` + `v-for should have explicit keys. ` + `See https://vuejs.org/guide/list.html#key for more info.`, el.rawAttrsMap['v-for'], true /* tip */ ) } // 标记当前节点上的 v-for 指令已经被处理过了 el.forProcessed = true // avoid recursion // 返回 `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})` return `${altHelper || '_l'}((${exp}),` + `function(${alias}${iterator1}${iterator2}){` + `return ${(altGen || genElement)(el, state)}` + '})' }
genIf() method
File location: src\compiler\codegen\index.js
// 处理带有 v-if 指令的节点,最终得到一个三元表达式,condition ? render1 : render2 export function genIf( el: any, state: CodegenState, altGen?: Function, altEmpty?: string ): string { // 标记当前节点的 v-if 指令已经被处理过了,避免无效的递归 el.ifProcessed = true // avoid recursion // 得到三元表达式,condition ? render1 : render2 return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty) } function genIfConditions( conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string ): string { // 长度若为空,则直接返回一个空节点渲染函数 if (!conditions.length) { return altEmpty || '_e()' } // 从 conditions 数组中拿出第一个条件对象 { exp, block } const condition = conditions.shift() // 返回结果是一个三元表达式字符串,condition ? 渲染函数1 : 渲染函数2 if (condition.exp) { /* 如果 condition.exp 条件成立,则得到一个三元表达式, 如果条件不成立,则通过递归的方式找 conditions 数组中下一个元素, 直到找到条件成立的元素,然后返回一个三元表达式 */ return `(${condition.exp})?${genTernaryExp(condition.block) }:${genIfConditions(conditions, state, altGen, altEmpty) }` } else { return `${genTernaryExp(condition.block)}` } // v-if with v-once should generate code like (a)?_m(0):_m(1) function genTernaryExp(el) { return altGen ? altGen(el, state) : el.once ? genOnce(el, state) : genElement(el, state) } }
genSlot() method
File location: src\compiler\codegen\index.js
/* 生成插槽的渲染函数,得到:_t(slotName, children, attrs, bind) */ function genSlot(el: ASTElement, state: CodegenState): string { // 插槽名称 const slotName = el.slotName || '"default"' // 生成所有的子节点 const children = genChildren(el, state) // 结果字符串,_t(slotName, children, attrs, bind) let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}` const attrs = el.attrs || el.dynamicAttrs ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({ // slot props are camelized name: camelize(attr.name), value: attr.value, dynamic: attr.dynamic }))) : null const bind = el.attrsMap['v-bind'] if ((attrs || bind) && !children) { res += `,null` } if (attrs) { res += `,${attrs}` } if (bind) { res += `${attrs ? '' : ',null'},${bind}` } return res + ')' }
genComponent() method
File location: src\compiler\codegen\index.js
/* 生成动态组件的渲染函数,返回 `_c(compName, data, children)` componentName is el.component, take it as argument to shun flow's pessimistic refinement */ function genComponent( componentName: string, el: ASTElement, state: CodegenState ): string { // 所有的子节点 const children = el.inlineTemplate ? null : genChildren(el, state, true) // 返回 `_c(compName, data, children)`,compName 是 is 属性的值 return `_c(${componentName},${genData(el, state)}${children ? `,${children}` : '' })` }
There are two types of renderings generated by the compiler:What is the generation process of the rendering function?
function, responsible for generating the
vnode
of the dynamic node
static rendering function# in the array ##, responsible for generating static nodes vnode
:<ul>
<li>
<code>tag
is the tag name
attr
is the attribute object children
is composed of child nodes Array, the format of each element is in the form of _c(tag, attr, children, normalizationTYpe)
, represents the normalization type of the node, which is a Numbers 0, 1, 2
The processing of static nodes is divided into two steps:How are static nodes processed?
function into the
staticRenderFns array
The executable function is to execute the function with the subscript
idx in the
staticRenderFns array to generate the
vnode
#How to deal with v-once, v-if, v-for, components, etc.?v-once##Simple
v-if
The processing result of the
v-for function, which is responsible for generating the
vnode## of the
v-for node.
#The processing result of the component is the same as that of ordinary elements. What is obtained is executable code in the shape of _c(compName)
, and the
# of the component is generated. ##[Related recommendations: vue.js tutorial
]The above is the detailed content of Parse the source code and see how the vue compiler generates the rendering function!. For more information, please follow other related articles on the PHP Chinese website!