[Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串
本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。
- 一起来学Vue双向绑定原理-数据劫持和发布订阅
- 一起来学Vue模板编译原理(一)-Template生成AST
- 一起来学Vue模板编译原理(二)-AST生成Render字符串
- 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法
这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得star收藏。
编译过程
模板编译是Vue中比较核心的一部分。关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:
第一步:将模板字符串转换成element ASTs(解析器)
第二步:对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步:使用element ASTs生成render函数代码字符串(代码生成器)
对应的Vue源码如下,源码位置在src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1.parse,模板字符串 转换成 抽象语法树(AST)
const ast = parse(template.trim(), options)
// 2.optimize,对 AST 进行静态节点标记
if (options.optimize !== false) {
optimize(ast, options)
}
// 3.generate,抽象语法树(AST) 生成 render函数代码字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
这篇文档主要讲第三步使用element ASTs生成render函数代码字符串,对应的源码实现我们通常称之为代码生成器。
代码生成器运行过程
在分析代码生成器的原理前,我们先举例看下代码生成器的具体作用。
<div>
<p>{{name}}</p>
</div>
在上节"Template生成AST"中,我们已经说了通过解析器会把上面模板解析成抽象语法树(AST),解析结果如下:
{
tag: "div"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag: "div", ...},
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
- 而在这一节我们会将AST转换成可以直接执行的JavaScript字符串,最终结果如下:
with(this) {
return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}
现在看不懂不要紧。其实生成器的过程就是 generate 函数拿到解析好的 AST 对象,递归 AST 树,为不同的 AST 节点创建不同的内部调用方法,然后组合成可执行的JavaScript字符串,等待后面的调用。
内部调用方法
我们看上面示例生成的JavaScript字符串,会发现里面会有_v
、_c
、_s
这样的东西,这些其实就是Vue内部定义的一些调用方法。
其中 _c
函数定义在 src/core/instance/render.js
中。
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 定义的_c函数是用来创建元素的
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
而其他 _s
、_v
是定义在 src/core/instance/render-helpers/index.js
中:
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList //生成列表VNode
target._t = renderSlot //生成解析slot节点
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic //生成静态元素
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps //绑定对象属性
target._v = createTextVNode //创建文本VNode
target._e = createEmptyVNode //创建空节点VNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
以上都是会在生成的JavaScript字符串中用到的,像比较常用的 _c
执行 createElement
去创建 VNode
, _l
对应 renderList
渲染列表;_v
对应 createTextVNode
创建文本 VNode
;_e
对于 createEmptyVNode
创建空的 VNode
。
整体逻辑
代码生成器的入口函数是generate
,具体定义如下,源码位置在src/compiler/codegen/index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 初始化一些options
const state = new CodegenState(options)
// 传入ast和options进行生成
const code = ast ? genElement(ast, state) : '_c("div")' //重点
return {
// 最外层包一个 with(this) 之后返回
render: `with(this){return ${code}}`,
// 这个数组中的函数与 VDOM 中的 diff 算法优化相关
// 那些被标记为 staticRoot 节点的 VNode 就会单独生成 staticRenderFns
staticRenderFns: state.staticRenderFns
}
}
生成器入口函数就比较简单,先初始化一些配置options,然后传入ast进行生成,没有ast时直接生成一个空div,最后返回生成的JavaScript字符串和静态节点的渲染函数。这里会把静态节点区分出来,便于后面的Vnode diff,即Vue比较算法更新DOM,现在先略过。
现在我们重点看看genElement函数,代码位置在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) {
return genStatic(el, state) // 静态节点处理生成函数
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state) // v-once处理生成函数
} else if (el.for && !el.forProcessed) {
return genFor(el, state) // v-for处理生成函数
} else if (el.if && !el.ifProcessed) {
return genIf(el, state) // v-if处理生成函数
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0' // 子节点处理生成函数
} else if (el.tag === 'slot') {
return genSlot(el, state) // slot处理生成函数
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state) // component组件处理生成函数
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state) // attributes节点属性处理生成函数
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
你会发现genElement函数里面有很多条件判断。这是因为Vue里面的指令写法实在太多,像 v-if
、 v-for
、 v-slot
等,每种指令写法都分离出一个函数来单独处理,这样代码阅读起来也清晰明了。下面,我们重点来看看 genFor
和 genData
的具体处理逻辑。
genFor
genFor 函数是用来处理 v-for 指令写法的,源码位置在src/compiler/codegen/index.js
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
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 */
)
}
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
genFor 的逻辑其实就是,首先 AST 元素节点中获取了和 for 相关的一些属性,然后返回了一个代码字符串。
genData
genData 函数是用来处理节点属性的,源码位置在src/compiler/codegen/index.js
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}
// DOM props
if (el.props) {
data += `domProps:${genProps(el.props)},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target,only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
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
}
总结一下
代码生成是模板编译的第三步,它完成了AST到Render的转换,即将抽象语法树AST转换成可以直接执行的JavaScript字符串。
其中genElement的代码比较多,因为需要分别处理的情况非常多,这里只是对genFor和genData的代码逻辑进行了说明。
相关
- https://ustbhuangyi.github.io/vue-analysis/compile/codegen.html#generate
- https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90render%E5%AD%97%E7%AC%A6%E4%B8%B2.md
- https://github.com/lihongxun945/myblog/issues/29
- https://segmentfault.com/a/1190000012922342
[Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串的更多相关文章
- [Vue源码]一起来学Vue模板编译原理(一)-Template生成AST
本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vu ...
- [Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅
有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫 ...
- Vue源码分析(一) : new Vue() 做了什么
Vue源码分析(一) : new Vue() 做了什么 author: @TiffanysBear 在了解new Vue做了什么之前,我们先对Vue源码做一些基础的了解,如果你已经对基础的源码目录设计 ...
- 学习Vue源码前的几项必要储备(二)
7项重要储备 Flow 基本语法 发布/订阅模式 ES6+ 语法 原型链.闭包 函数柯里化 event loop 接上讲 聊到了ES6的几个重要语法,加下来到第四点继续开始. 4.原型链.闭包 原型链 ...
- Vue 源码解读(3)—— 响应式原理
前言 上一篇文章 Vue 源码解读(2)-- Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了 new Vue(options) 都做了什么,其中关于 数据响应式 的实现用一句话简单的带过 ...
- Vue.js 源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解
Vue有三个属性和模板有关,官网上是这样解释的: el ;提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标 template ;一个字符串模板作为 Vue 实例的标识使用.模板将会 ...
- 【js】vue 2.5.1 源码学习 (十一) 模板编译compileToFunctions渲染函数
大体思路(九) 本节内容: 1. compileToFunctions定位 1. compileToFunctions定位 ==> createCompiler = createCompiler ...
- Vue源码学习(零):内部原理解析
本篇文章是在阅读<剖析 Vue.js 内部运行机制>小册子后总结所得,想要了解详细内容,请参考原文:https://juejin.im/book/5a36661851882538e2259 ...
- 从Vue源码中我学到了几点精妙方法
话不多说,赶快试试这几个精妙方法吧!在工作中肯定会用得到. 立即执行函数 页面加载完成后只执行一次的设置函数. (function (a, b) { console.log(a, b); // 1,2 ...
随机推荐
- 微信小程序中支持es7的async语法
最近在原生的微信小程序项目中需要把原来es6的promise方法改成es7的async await,这样代码看起来更直观,也方便以后的兄弟维护,但是改了代码之后项目就报错了. 提示的错误是:regen ...
- AtCoder Grand Contest 019 B - Reverse and Compare【思维】
AtCoder Grand Contest 019 B - Reverse and Compare 题意:给定字符串,可以选定任意i.j且i<=j(当然i==j时没啥卵用),然后翻转i到j的字符 ...
- 01docker简单使用和配置(容器、镜像)
一:容器中运行简单应用程序 1:hello world 使用docker可以在容器中运行应用程序,使用docker run命令即可.比如下面的命令: $ docker run ubuntu /bin ...
- oracle 识别’低效执行’的SQL语句
用下列SQL工具找出低效SQL: SELECT EXECUTIONS , DISK_READS, BUFFER_GETS, ROUND((BUFFER_GETS-DISK_READS)/BUFFER_ ...
- xshell评估期已过怎么办
重新下载更新即可,不要看到英文一脸懵逼,直接在页面中有红色*号的地方输入个人信息,licensetype 必须选 Home and School use(家庭和个人使用),最主要的是输入邮箱(必须是个 ...
- 析构函数 p157
析构函数 确保对象的各部分被正确的清除,及做一些用户指定的其他清理工作. 当对象超出它的作用域时,编译器将自动调用析构函数:手动用new在堆上分配的对象空间,需要调用'delete 对象地址'进行手动 ...
- H3C NAPT
- [转]Redis哨兵模式(sentinel)学习总结及部署记录(主从复制、读写分离、主从切换)
Redis的集群方案大致有三种:1)redis cluster集群方案:2)master/slave主从方案:3)哨兵模式来进行主从替换以及故障恢复. 一.sentinel哨兵模式介绍Sentinel ...
- [转]C#操作Word的超详细总结
本文中用C#来操作Word,包括: 创建Word: 插入文字,选择文字,编辑文字的字号.粗细.颜色.下划线等: 设置段落的首行缩进.行距: 设置页面页边距和纸张大小: 设置页眉.页码: 插入图片,设置 ...
- 【js】vue 2.5.1 源码学习 (七) 初始化之 initState 响应式系统基本思路
大体思路(六) 本节内容: 一.生命周期的钩子函数的实现 ==> callHook(vm , 'beforeCreate') beforeCreate 实例创建之后 事件数据还未创建 二.初始化 ...