Vue源码学习
文章目录
- Vue源码学习
- 前言
- 一、什么是模板编译?
- 二、模板编译成渲染函数的流程
- (1)解析器
- (2)优化器
- (3)代码生成器
- 三、v-if、v-for的优先级
- 总结
前言
Vue.js提供了模板语法,允许我们声明式地描述状态和DOM之间的绑定关系,通过模板来生成真实DOM并将其呈现在用户界面上。
在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数。当应用内部的状态发生变化时,Vue.js结合响应式系统,聪明地找出最小数量的组件进行重新渲染以及最少量地进行DOM操作。
也就是说,平时写的vue模板经过模板编译后,生成render函数,初次渲染或数据发生变化时,都会执行render函数,生成虚拟DOM(数据变化时会再比对新老DOM节点),最后生成真实DOM,渲染在页面上。
一、什么是模板编译?
平时开发写的<template></template>
以及里面的变量、表达式、指令等,不是html语法,是浏览器识别不出来的。模板编译的主要目标是生成渲染函数,渲染函数会将当前的状态生成一份vnode
, 再用vnode转成真实的dom进行渲染。
二、模板编译成渲染函数的流程
可分为三部分内容:
1、将模板解析成AST(Abstract Syntax Tree,抽象语法树);
2、遍历AST标记为静态节点;(静态节点不需要总是重新渲染,标记后,更新节点时,有这个标记就不会重新渲染)
3、将AST生成渲染函数
可以抽象成三个模块实现各自的功能:
1、解析器
2、优化器
3、代码生成器
// 源码位置 src/compiler/parser/index.js
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
//1、 模板解析阶段:用正则等方式解析 template 模板中的指令、class、style等数据,形成AST
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 2、优化阶段:遍历AST,找出其中的静态节点,并打上标记;
optimize(ast, options)
}
// 3、代码生成阶段:将AST转换成渲染函数;
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
(1)解析器
把用户在<template></template>
标签内写的模板使用正则等方式解析成抽象语法树(AST)。
// 代码位置:/src/complier/parser/index.js
/**
* Convert HTML string to AST.
*/
export function parse(template, options) {
// ...
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
start (tag, attrs, unary) {
},
end () {
},
chars (text: string) {
},
comment (text: string) {
}
})
return root
}
parse
函数是解析器的主函数,函数内调用了parseHTML
函数对模板进行解析。具体怎么解析的可以进行代码调试或者看《深入浅出vue.js》。
最终要解析成AST语法树,例如解析:
<div>
<p>{{name}}</p>
</div>
解析后长这样:
{
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并不是个神奇的东西。它只是用JS中的对下来描述一个节点,一个对象表示一个节点,对象中的属性用来保存节点所需的各种数据。比如,parent属性保存了父节点的描述对象,children属性是一个数组,里面保存了一些子节点的描述对象。再比如,type属性表示一个节点的类型等。当很多个独立的节点通过parent属性和children属性连在一起时,就变成了一棵树,而这样用对象描述的节点数就是AST。
(2)优化器
优化器的目标是遍历AST,检测出所有静态子树(永远都不会发生变化的DOM节点)并给其打标记。
<ul>
<li>我是静态节点,我不要发生变化</li>
<li>我是静态节点,2、我不要发生变化</li>
</ul>
ul
下的li
标签里的内容都是不含任何变量的纯文本,也就是说这种标签一旦第一次被渲染成DOM节点以后,之后不管状态怎么变化它都不会变了,像li
标签这种节点称为静态节点,其父节点ul
节点称为静态根节点。
有了这两个概念之后,我们再仔细思考,模板编译最终目的是生成render函数,render函数生成与模板对应的VNode,之后再进行patch算法,最后渲染视图。而中间的patch算法是用来比对新旧VNode差异的。静态节点是不会变的,所以无需比对静态节点。这样就可以提高一些性能。
故,优化阶段干了两件事:
1、标记静态节点
2、标记静态根节点
对应源码位置:
// src/compiler/optimizer.js
export function optimize (root) {
if (!root) return
markStatic(root) // 标记静态节点
markStaticRoots(root) // 标记静态根节点
}
标记后的AST树:
{
tag:"ul",
type:1,
staticRoot:true,
static:true,
...
}
(3)代码生成器
将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。
代码字符串可以被包装在函数中执行,这个函数就是我们常说的渲染函数(update)。
比如,有一份简单的模板:
<div id="el">Hello {{name}}</div>
AST优化后,长这样:
{
tag:"div",
type:1,
plain:false,
attrsList:[{
"name":"id",
"value":'el'
}],
attrsMap:{
"id":"el"
},
children:[
{
type:2,
"expression":"Hello"+_s(name),
"text":"Hello {{name}}",
"static":false
}
],
staticRoot:false,
static:false,
}
代码生成器可以通过上面这个AST来生成代码字符串,生成后的代码字符串是这样的:
'with(this){return _c("div",{attrs:{"id":"el"}},[_v("Hello"+_s(name)])}'
_c()是元素节点的函数调用字符串,_v是文本节点的函数调用字符串,_e是生成注释节点的函数调用字符串。
源码位置src/compiler/codegen/index.js :
export function generate (ast,option) {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
const code = generate(ast, options)
Vue.prototype._c = function() {
return createElementVNode(this, ...arguments) // 生成元素节点
}
Vue.prototype._v = function() {
return createTextVNode(this, ...arguments) // 生成文本节点
}
Vue.prototype._e = function(value) {
if (typeof value != 'object') return value;
return JSON.stringify(value); // 注释节点
}
三、v-if、v-for的优先级
v-for的优先级高于v-if
<div id="app">
<span v-if="flag" v-for="i in 3"></span>
</div>
vue2模板编译生成render函数网站,用这个网站可以生成render函数:
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, _l((3), function (i) {
return (flag) ? _c('span') : _e()
}), 0)
}
}
对应源码位置:
v-for会生成_l()
函数,_l()函数里面再进行v-if的判断,所以v-for的优先级高于v-if。
vue官网写,永远不要把v-for和v-if同时写在同一个元素上。因为,渲染后v-if在v-for的循环里,这让本不应该渲染的元素渲染了。最好在渲染前把v-for里的数组先筛选下,再进行渲染。或者把v-if放在v-for的父元素上。
// v-if 移动至容器元素上 (比如 ul、ol)
<ul v-if="shouldShowUsers">
<li v-for="user in users" :key="user.id" >
{{ user.name }}
</li>
</ul>
总结
模板编译,可以理解为,给它输入模板字符串,会输出render函数。
render函数是在挂载时调用的。也就是Vue原型上的$mount
方法。render函数将AST转化为虚拟DOM,再转化为真实DOM,最后进行渲染。
模板编译可分为三个流程:模板解析成AST语法树、AST标记静态节点、将AST生成渲染函数。
v-for的优先级比v-if高,永远不要把v-for和v-if同时写在同一个元素上。
参考:《深入浅出Vue.js》、模板编译总结