前言
最近复习了一下Vue的相关知识点,毕竟温顾而知新,然后我也看了很多小伙伴写的文章但是感觉都没有我心中想要的那种描述的感觉,所以有了我个人的这次的一点点🤏🏻心得及理解分享
目录/相关知识点:
-
模板编译是什么
- 现实比喻
- 官方说明-渲染管线
-
模板编译的作用
- VNode虚拟DOM简单介绍
- render:Vue输出的render
- 模板编译器Vue Template Explorer
- Vue2.x Template Explorer VS Vue3 Template Explorer
-
模板编译代码解析
- 模板编译流程图
- 模板编译相关API源码解析
- compileToFunctions
- compile
- createCompilerCreator-baseCompile
- 解析parse
- 优化optimize
- 生成generate
-
作者寄语
模板编译是什么
现实比喻
兄弟们先来看一张好玩的图
有没有设想过我们使用Vue的时候就像一辆"🚗"在运行
"🚗跑起来"驱动的"油"是数据-数据驱动
数据的变化通过"引擎"展示到视图中让"🚗跑起来"-diff对比虚拟DOM更新视图
那么模板编译在我看来就是把"设计图"转换为"🚗"的过程(这里与跑起来的🚗是有区别的)-template模板转换为render
看到这里大家应该有点感觉,又不是很透彻,那我们再看看下面的官方说明
官方说明-渲染管线
从高层面的视角看,Vue 组件挂载后发生了如下这几件事:
- 编译:Vue 模板被编译为了渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
- 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
- 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
现实比喻VS官方说明
我相信看到这里小伙伴们对于模板编译以及对于Vue的渲染机制有个大概的了解,可能引擎提供动力让🚗跑起来
或许有点疑惑,那么我多打个比方:
- 动画:我们都知道动画是
一帧一帧的静态图
组合而成的,你可以理解为第一帧静态图就是一辆🚗,而第二帧静态图就是第一帧的🚗的基础上修改了一些东西,第三帧,第四帧...第一百帧;当我们高速浏览这些帧静态图的时候
,因为每一帧都会有差别看起来就形成了动画;当我们高速浏览这些帧静态图的时候
这个动作就是对比不同状态的🚗就是引擎
模板编译的作用
模板编译的主要目的是将模板(template)
转换为渲染函数(render)
- Vue2.x使用
VNode描述视图以及各种交互
,用户自己编写VNode比较复杂
- 用户只需要编写
类似HTML的代码-Vue.js模板
,通过编译器将模板转换为返回VNode的render函数
- .vue文件会被webpack在构建的过程中转换成render函数
VNode虚拟DOM简单介绍
一句话总结虚拟DOM就是一个用来描述真实DOM的javaScript对象,这样说可能不够形象,那我们来举个🌰:分别用代码来描述真实DOM
以及虚拟DOM
真实DOM
:
<ul class="list">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
对应的虚拟DOM
:
let vnode = h('ul.list', [
h('li','a'),
h('li','b'),
h('li','c'),
])
console.log(vnode)
控制台打印出来的Vnode:
h函数生成的虚拟DOM这个JS对象(Vnode)的源码:
export interface VNodeData {
props?: Props
attrs?: Attrs
class?: Classes
style?: VNodeStyle
dataset?: Dataset
on?: On
hero?: Hero
attachData?: AttachData
hook?: Hooks
key?: Key
ns?: string // for SVGs
fn?: () => VNode // for thunks
args?: any[] // for thunks
[key: string]: any // for any other 3rd party module
}
export type Key = string | number
const interface VNode = {
sel: string | undefined, // 选择器
data: VNodeData | undefined, // VNodeData上面定义的VNodeData
children: Array<VNode | string> | undefined, //子节点,与text互斥
text: string | undefined, // 标签中间的文本内容
elm: Node | undefined, // 转换而成的真实DOM
key: Key | undefined // 字符串或者数字
}
render:Vue输出的render
<template>
<div id="app">
0
{{test}}
<h1>Vue</h1><span>模板编译</span>
<test :bbc="1"></test>
something
</div>
</template>
mounted() {
// 页面已渲染完成
console.log(this.$options.render);// 查看输出的render函数
},
// 控制台输出的render函数
function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
//这里返回的就是一个虚拟DOM树
return _c(
"div",
{ attrs: { id: "app" } },
[
_vm._v(" 0 " + _vm._s(_vm.test) + " "),
_c("h1", [_vm._v("Vue")]),
_c("span", [_vm._v("模板编译")]),
_c("test", { attrs: { bbc: 1 } }),
_vm._v(" something ")
],
1
)
}
render中返回的虚拟DOM树中的_c,_v_s...又是什么
-
_c = _vm._self_c || _h(_h就是我们上文提到也是常说的h函数)
_h
也就是$createElement
对手写reder函数进行渲染
的方法_vm._self_c
对编译生成的render进行渲染
的方法- 两者本质上都是调用
createElement方法
,传的一个参数不一样,其参数代表是手写的render还是编译生成的render - 对于
createElement方法
,有兴趣的同学可以在本文的尾部[1]
得到更详细的说明
-
_v,_s是什么?->看源码
// vue.runtime.esm.js
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString; // 转换为字符串
target._l = renderList;
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode; // 创建一个文本节点
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
// _s函数
/**
* Convert a value to a string that is actually rendered.
*/
function toString (val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
// _v函数
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
更直观的体验vue的模板编译工具->模板编译器Vue Template Explorer
- Vue2.x Template Explorer:vue2compile.itclan.cn/
- Vue 3 Template Explorer:vue-next-template-explorer.netlify.app/
Vue2.xTemplate VS Vue3Template
- Fragments(片段特性),模板中不需要创建一个唯一的根节点,模板里可以直接放文本内容或很多同级的标签
- 静态提升
- Patch flag(静态标记)
- 缓存事件处理函数
注意:vue有运行时以及完整版两个版本,两者主要的区别就是是否带有编译器
- 运行时需要配合vue-loader/webpack把template内容转换为render来使用
- vue默认使用的是运行时版本,内存空间比较少
- 一般我们直接通过script引用的UMD版本就是这里所阐述的完整版
UMD 版本可以通过
<script>
标签直接用在浏览器中。jsDelivr CDN 的 cdn.jsdelivr.net/npm/vue@2.7… 默认文件就是运行时 + 编译器的 UMD 版本 (vue.js
)。-官方说明
模板编译代码解析
流程图
-
compilerToFunctions生成render的入口
- 获取
缓存结果
,没有即进行编译
- 把
compile的字符串代码
转化为render
函数
- 获取
-
compile
- 合并用户选项参数以及默认参数
- 进行编译
-
baseCompile核心API
parse
函数把template转换成AST抽象语法树
optimize
优化AST抽象语法树
generate
生成字符串形式的JS代码
compilerToFunctions
//xxx/node_modules/vue/src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// detect possible CSP restriction
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
const compiled = compile(template, options)
// check compilation errors/tips
if (process.env.NODE_ENV !== 'production') {
if (compiled.errors && compiled.errors.length) {
if (options.outputSourceRange) {
compiled.errors.forEach(e => {
warn(
`Error compiling template:\n\n${e.msg}\n\n` +
generateCodeFrame(template, e.start, e.end),
vm
)
})
} else {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
}
if (compiled.tips && compiled.tips.length) {
if (options.outputSourceRange) {
compiled.tips.forEach(e => tip(e.msg, vm))
} else {
compiled.tips.forEach(msg => tip(msg, vm))
}
}
}
// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
warn(
`Failed to generate render function:\n\n` +
fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
vm
)
}
}
return (cache[key] = res)
}
}
compile
//xxx/node_modules/vue/src/compiler/create-compiler.js
/* @flow */
import { extend } from 'shared/util'
import { detectErrors } from './error-detector'
import { createCompileToFunctionFn } from './to-function'
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// $flow-disable-line
const leadingSpaceLength = template.match(/^\s*/)[0].length
warn = (msg, range, tip) => {
const data: WarningMessage = { msg }
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength
}
}
(tip ? tips : errors).push(data)
}
}
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
baseCompile:模板编译核心API
// xxx/node_modules/vue/src/compiler/index.js
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1.把模板转换成AST抽象语法树 // 抽象语法树:用树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 2.优化AST抽象语法树
optimize(ast, options)
}
// 3.把抽象语法树生成字符串形式的JS代码
const code = generate(ast, options)
return {
ast,
// 渲染函数
render: code.render,
// 静态渲染函数,生成静态VNode树(静态根节点)
staticRenderFns: code.staticRenderFns
}
})
模板编译三阶段总结
- 分为三个阶段
-
解析阶段:使用大量
正则对template解析转化成AST抽象语法树(parse函数)
- 使用一个开源库simple-html-parse
- 返回AST
-
优化阶段:
遍历AST,找出静态节点,静态根节点进行标记
,执行diff算法的时候跳过这些节点** **(optimize函数)
-
静态节点
node.type === 3的纯文本节点
node.type === 1的元素节点(简单可以理解为非文本节点)进一步判断
- 如果节点使用了v-pre指令,那就断定它是静态节点;
- 如果节点没有使用v-pre指令,那它要成为静态节点必须满足:
- 不能使用动态绑定语法,即标签上不能有v-、@、:开头的属性;
- 不能使用v-if、v-else、v-for指令;
- 不能是内置组件,即标签名不能是slot和component;
- 标签名必须是平台保留标签,即不能是组件;
- 当前节点的父节点不能是带有 v-for 的 template 标签;
- 节点的所有属性的 key 都必须是静态节点才有的 key,注:静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一;
-
静态根节点
-
节点本身必须是
静态节点
; -
必须拥有子节点 children
; -
子节点
不能只是只有一个文本节点
;
-
-
-
生成阶段:将最终的
AST转换成render
函数 (generate函数)首先通过generator生成器把AST对象转换成JS形式的字符串代码
-
-
模板预编译
- 指的就是在项目构建的时候把template提前转换为render函数给页面渲染
补充[1]
_c与$createElement的区别
//create.element.js
import {
normalizeChildren,
simpleNormalizeChildren
} from './helpers/index'
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
//....
if (normalizationType === ALWAYS_NORMALIZE) {
// 把一个多维数组转换为一个一维数组(用户手写render)
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 把一个二维数组转换为一个一维数组(编译生成render)
children = simpleNormalizeChildren(children)
}
//...
}
normalizeChildren与simpleNormalizeChildren及simpleNormalizeChildren
//normalize-children.js
// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
// 对应_c函数
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
// 对应$createElement函数
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// nested
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
寄语
最后希望大家看在我辛勤造车
的努力下,给我点个赞
,同时也欢迎大家交流评论,我开着我的🚗载大家去往前端的二仙桥