Vue 源码解析3
模板编译
模板编译的主要目标是将模板(template)转为渲染函数(render)
模板编译必要性
Vue2.0 需要用到 Vnode 描述视图以及各种交互,手写显然不切实际,因此用户只需编写类似 HTML 代码的 Vue 模板,通过编译器将模板转换为可返回 Vnode 的 render 函数。
体验模板编译
带编译器的版本中,可以使用 template 或 el 的方式生命模板,测试 demo
(function anonymous (
) {
with (this) {
return _c('div', { attrs: { "id": "demo" } }, [_c('h1', [_v("Vue模板编
译")]),_v(" "),_c('p',[_v(_s(foo))]),_v(" "),_c('comp')],1)}
})
输出结果大致如下:
(function anonymous () {
with (this) {
return _c('div', { attrs: { "id": "demo" } }, [
_c('h1', [_v("Vue模板编译")]),
_v(" "), _c('p', [_v(_s(foo))]),
_v(" "), _c('comp')], 1)
}
})
元素节点使用 createElement 创建,别名 _c
本文节点使用 createTextVNode 创建,别名 _v
表达式先使用 toString 格式化,别名 _s
其他渲染 helpers:src\core\instance\render-helpers\index.js
整体流程
compileToFunctions
若指定 template 或 el 选项,则会执行编译,platforms\web\entry-runtime-with-compiler.js
编译过程
编译分为三步:解析、优化和生成,src\compiler\index.js
模板编译过程
实现模板编译共有三个阶段:解析、优化和生成。
解析 - parse
解析器将模板解析为抽象语法树,基于 AST 可以做优化或者代码生成工作。
调试查看得到的 AST,/src/compiler/parser/index.js,结构如下:
解析器内部分了 HTML 解析器、文本解析器和过滤解析器,最主要是 HTML 解析器。
优化 - optimize
优化器的作用是在 AST 中找出静态子树并打上标记。静态子树是在 AST 中永远不变的节点,如纯文本节点。
标记静态子树的好处:
每次重新渲染,不需要为静态子树创建新节点;
虚拟 DOM 中 patch 时,可以跳过静态子树。
代码实现,src/compiler/optimizer.js - optimize
标记结束
代码生成 - generage
将 AST 转换成渲染函数中的内容,即代码字符串。
generate 方法生成渲染函数代码,src/compiler/codegen/index.js。
生成的 code:
`_c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("Vue.js测试")]),
_c('p',[_v(_s(foo))])
])`
典型指令的实现:v-if、v-for
着重观察几个结构性指令的解析过程。
解析 v-if:parser/index.js
processIf 用于处理 v-if 解析:
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
解析结果:
代码生成,codegen/index.js
genIfConditions 等用于生产条件语句相关代码。
生成结果:
"with(this){return _c('div',{attrs:{"id":"demo"}},[
(foo) ? _c('h1',[_v(_s(foo))]) : _c('h1',[_v("no title")]),
_v(" "),_c('abc')],1)}"
解析 v-for:parser/index.js
processFor 用于处理 v-for 指令:
export function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid v-for expression: ${exp}`,
el.rawAttrsMap['v-for']
)
}
}
}
解析结果:v-for="item in items"
for:"items"
alias:"item"
代码生成,src\compiler\codegen\index.js
genFor 用于生成响应代码。
生成结果:
"with(this){return _c('div',{attrs:{"id":"demo"}},[_m(0),_v(" "),(foo)?_c('p',
[_v(_s(foo))]):_e(),_v(" "),
_l((arr),function(s){return _c('b',{key:s},[_v(_s(s))])})
,_v(" "),_c('comp')],2)}"
v-if、v-for 这些指令只能在编译器阶段处理,如果我们要在 render 函数处理条件或循环只能使用 if 和 for。
Vue.component('comp', {
props: ['foo'],
render (h) { // 渲染内容跟foo的值挂钩,只能⽤if语句
if (this.foo == 'foo') {
return h('div', 'foo')
}
return h('div', 'bar')
}
})
(function anonymous (
) {
with (this) {
return _c('div', { attrs: { "id": "demo" } }, [_m(0), _v(" "), (foo) ? _c('p',
[_v(_s(foo))]) : _e(), _v(" "), _c('comp')], 1)
}
})
组件化机制
组件声明:Vue.component()
initAssetRegisters(Vue) src/core/global-api/assets.js,组件注册使用 extend 方法将配置转换为构造函数并添加到 components 选项。
组件实例创建及挂载
观察生成的渲染函数:
"with(this){return _c('div',{attrs:{"id":"demo"}},[
_c('h1', [_v("虚拟DOM")]), _v(" "),
_c('p', [_v(_s(foo))]), _v(" "),
_c('comp') // 对于组件的处理并⽆特殊之处
], 1)}"
整体流程
首先创建的是跟实例,首次 _render() 时,会得到整棵树的 Vnode 结构,其中自定义组件相关的主要有:
整体流程:
new Vue() => $mount() => vm._render(h) => createElement() => createComponent() => patch => createElm => createComponent()
_createElement - src\core\vdom\create-element.js
_createElement 实际执行 Vnode 创建的函数,由于传入 tag 是非保留标签,因此判定为自定义组件通过 createComponent 去创建。
createComponent - src/core/vdom/create-component.js
创建组件 Vnode,保存了上一步处理得到的组件构造函数,props,事件等
创建组件实例
根组件执行更新函数时,会递归创建子元素和子组件,入口 createElm。
createEle() core/vdom/patch.js line751
首次执行 _update() 时,patch() 会通过 createEle() 创建根元素,子元素创建研究从这里开始。
createComponent core/vdom/patch.js line144
自定义组件创建:
// 组件实例创建、挂载
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 元素引⽤指定vnode.elm,元素属性创建等
initComponent(vnode, insertedVnodeQueue)
// 插⼊到⽗元素
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
总结
Vue源码学习使我们能够深入理解原理,解答很多开发中的疑惑,规避很多潜在的错误,写出更好的代码。学习大神的代码,能够学习编程思想,设计模式,训练基本功,提升内力。