Vue 编译器 -- 生成render函数
前言
今天实在是太忙了,一下午埋头改 "祖传" 代码,谁能想到侧边栏是镶嵌在每个页面里的啊 😭
一个好好的组件也被抽取的乱七八糟,真的不至于把菜单抽取到文字的程度吧
代码写的也一点都不规范,这着实令人头大
本来是今天就要完成的功能,直到下班前才把页面结构调整完开始步入正轨😭
山真的是太折磨人了,看来今天的更文计划又只能吃老本了 😂
那今天就来讲讲 vue 的编译原理吧,为后续的学习打下坚实的基础 💪
前置知识
vue 开发过程中编写 DOM 元素的方式
- template 模板
- render 函数 使用h函数来编写渲染的内容
- 通过 .vue文件中的template来编写模板
模板的处理方式
- template 模板通过源码中的一部分代码进行编译
- render函数 返回虚拟节点(vnode)
- .vue 文件中的 template 可以通过 vue-loader 对其进行编译和处理
vue 版本分为 运行时+编译器、仅运行时
- 运行时+编译器 runtime-compiler:包含了对template 模板的编译代码,代码量大
template(html)->ast->render ->vdom ->真实dom
- 仅运行时 runtime-only:没有包含对template 模板的编译代码,相对较小 render ->vdom ->真实dom
如果选择对template进行编译,就需要选择第一种,因为compiler就是对template进行编译。 但是vue默认使用 runtime-only, 不包含对template的编译,所以编译不会成功,界面渲染不出来。
vue打包后不同版本解析
- vue(.runtime).global(.prod).js (不需要编译template可以只使用runtime版本)
- 通过浏览器中的
- 通过CDN引入和下载的vue
- 会暴露一个全局的vue来使用
- .prod 是要不要使用生产版本
- vue(.runtime).esm-browser(.prod).js
- 用于通过原生ES模块导入使用(在浏览器中通过
- vue(.runtime).esm-bundler.js:(默认)仅运行时,且要求所有模板必须预编译
- 用于webpack、rollup、parcel等构建工具
- 构建工具中默认是vue.runtime.esm-bundler.js
- 解析 template 需要手动指定 vue.esm-bundler.js
- 包含运行时编译器。当你使用了构建工具但仍然想编译运行时模板
- vue.cjs(.prod).js
- 服端渲染时使用
- 用过require() 在 nodejs中使用
编译的目的
编译的目标主要是用于生成 render 函数,执行 render 函数生成 vnode 实现页面的更新
const updateComponent = () => {
// 执行 vm._render() 函数,得到 虚拟 DOM,
// 并将 vnode 传递给 _update 方法,接下来就该到 patch 阶段了
// 将vnode转换为真实DOM,并且更新到页面中
vm._update(vm._render(), hydrating)
}
// 监听当前组件状态,当有数据变化时,更新组件
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 数据更新引发的组件更新
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
当更新一个渲染 watcher 时,执行的是 updateComponent 方法: 根据源码可以发现每次更新前都需要先执行一下 vm._render() 方法 vm._render 就是大家经常听到的 render 函数,render函数的生成方式有两种:
- 用户自己提供,在编写组件时,用 render 选项代替模版
- 由编译器编译组件模版生成 render 选项
render的作用主要是生成 vnode
编译入口
编译入口 是从 $mount 开始的
暂存原本的 mount以便最后调用初始的挂载方法同时对运行时编译的mount 以便最后调用 初始的挂载方法 同时对运行时编译 的 mount以便最后调用初始的挂载方法同时对运行时编译的mount 进行重写
/**
* 编译器的入口
* 运行时的 Vue.js 包就没有这部分的代码,通过 打包器 结合 vue-loader + vue-compiler-utils 进行预编译,将模版编译成 render 函数
*
* 就做了一件事情,得到组件的渲染函数,将其设置到 this.$options 上
*/
// 暂存原本的 $mount 以便最后调用 挂载
const mount = Vue.prototype.$mount
// 运行时编译 对 $mount 的重写
Vue.prototype.$mount = function (
// 传入的 挂载点
el?: string | Element,
//
hydrating?: boolean
): Component {
el = el && query(el)
// vue 不允许直接挂载到body或页面文档上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
/**
* 如果用户提供了 render 配置项,则直接跳过编译阶段,否则进入编译阶段
* 解析 template 和 el,并转换为 render 函数
* 优先级:render > template > el
*/
if (!options.render) {
let template = options.template
// 解析 template 和 el,并转换为 render 函数
if (template) {
// 存在template模板,解析vue模板文件
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
// template 获取其 innerHtml 作为模版
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 获取 el 选择器的 outerHtml 作为模版
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
// 模版就绪,进入编译阶段
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
/**
* 重点 进入 模版编译,得到 动态渲染函数和静态渲染函数
* 1.将类 HTML 字符串模版解析成 AST 对象
* 2.优化,遍历 AST,标记静态节点及静态根节点,静态根节点用于生成静态根节点的渲染函数
* 3.将 ast 语法树生成render方法
*/
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 执行挂载
return mount.call(this, el, hydrating)
}
在$mount 中主要做了三件事
- 暂存原本的$mount 挂载方法
- 获取 template 模板:
- 有 render 配置项则直接跳过编译阶段执行挂载
- 获取template的 innerHtml 作为模版
- 获取 el 选择器的 outerHtml 作为模版
- 调用 compileToFunctions,会将 template 解析成render函数
- 生成render函数,挂载到vm上后,会再次调用mount方法
被暂存的 $mount ,vue初始化时定义的方法
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 渲染组件
return mountComponent(this, el, hydrating)
}
mountComponent 主要用于挂载组件,其中实现了 beforeMount 到 mounted 组件的挂载过程
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有获取解析的render函数,则会抛出警告
// render是解析模板文件生成的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
// 没有获取到vue的模板文件
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 执行beforeMount钩子
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
// 执行_update 进入更新阶段
// _update主要功能是调用patch,首先执行 _render 将组件变成 VNode ,
// 再将vnode转换为真实DOM,并且更新到页面中
vm._update(vm._render(), hydrating)
}
}
// 监听当前组件状态,当有数据变化时,更新组件
// 在实例化 watcher 时会主动触发 watcher.get() 首次渲染会触发实现依赖收集
// 通过 updateComponent 回调(_render)实现页面的更新
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 数据更新引发的组件更新
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
编译开始
编译的开始正是从 compileToFunctions 开始 顺着 **compileToFunctions ** 向上查找 可以发现 **compileToFunctions ** 来源于createCompilerCreator 的执行
export const createCompiler = createCompilerCreator(
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 解析 ===>> 将html模板字符串解析为ast对象
// 解析模板生成AST树 对每个结点的ast对象上设置了元素的所有属性
// 例如:标签信息、属性信息、插槽信息、父节点、子节点等。
// 具体有哪些属性,查看 start 和 end 这两个处理开始和结束标签的方法
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化 遍历ast语法树 标记静态节点 和静态根节点
// 标记每个节点是否为静态节点,然后进一步标记出静态根节点
// 这样在后续更新的过程中就可以跳过这些静态节点了
// 标记静态根,用于生成渲染函数阶段,生成静态根节点的渲染函数
optimize(ast, options)
}
// 代码生成 render() 将ast 转化为可执行的render函数的字符串形式
// 从 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 // 静态渲染函数放到 code.staticRenderFns 数组中
}
}
)
这里使用了JavaScript中的偏函数,它是一种高阶函数,借鉴自函数式编程思想。偏函数利用了JavaScript中的函数既可以作为参数传入另一个函数,也可以作为一个函数的返回值的特性,可以将一些通用函数包装成专用函数。
通用的编译器的构造器createCompilerCreator,它接受一个基础构造器,这里的大部分代码都是在定义这个基础构造器。
export const createCompiler = createCompilerCreator(function baseCompile (){ ... })
// createCompiler的值是createCompilerCreator的返回值 (baseCompiler的返回值)
解析
在解析的过程中 主要实现 类 html 模板字符串转化成AST语法树的过程
源码分析
解析入口 parse
/**
*
* 将 HTML 字符串转换为 AST
* @param {*} template HTML 模版
* @param {*} options 平台特有的编译选项
* @returns root
*/
export function parse(
template: string,
options: CompilerOptions
): ASTElement | void {
// 日志
warn = options.warn || baseWarn
// 是否为 pre 标签
platformIsPreTag = options.isPreTag || no
// 必须使用 props 进行绑定的属性
platformMustUseProp = options.mustUseProp || no
// 获取标签的命名空间
platformGetTagNamespace = options.getTagNamespace || no
// 是否是保留标签(html + svg)
const isReservedTag = options.isReservedTag || no
// 判断一个元素是否为一个组件
maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
// 分别获取 options.modules 下的 class、model、style 三个模块中的 transformNode、preTransformNode、postTransformNode 方法
// 负责处理元素节点上的 class、style、v-model
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
// 界定符,比如: {{}}
delimiters = options.delimiters
const stack = []
// 空格选项
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
// 根节点,以 root 为根,处理后的节点都会按照层级挂载到 root 下,最后 return 的就是 root,一个 ast 语法树
let root
// 当前元素的父元素
let currentParent
let inVPre = false
let inPre = false
let warned = false
// 解析 html 模版字符串,处理所有标签以及标签上的属性
// 这里的 parseHTMLOptions 在后面处理过程中用到,再进一步解析
// 提前解析的话容易让大家岔开思路
parseHTML(template, parseHtmlOptions)
// 返回生成的 ast 对象
return root
处理html标签
/**
* 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性
* @param {*} html html 模版
* @param {*} options 配置项
*/
export function parseHTML(html, options) {
const stack = []
const expectHTML = options.expectHTML
// 是否是自闭合标签
const isUnaryTag = options.isUnaryTag || no
// 是否可以只有开始标签
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 记录当前在原始 html 字符串中的开始位置
let index = 0
let last, lastTag
while (html) {
last = html
// 确保不是在 script、style、textarea 这样的纯文本元素中
if (!lastTag || !isPlainTextElement(lastTag)) {
// 找第一个 < 字符
let textEnd = html.indexOf('<')
// textEnd === 0 说明在开头找到了
// 分别处理可能找到的注释标签、条件注释标签、Doctype、开始标签、结束标签
// 每处理完一种情况,就会截断(continue)循环,并且重置 html 字符串,将处理过的标签截掉,下一次循环处理剩余的 html 字符串模版
if (textEnd === 0) {
// 处理注释标签 <!-- xx -->
if (comment.test(html)) {
// 注释标签的结束索引
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 是否应该保留 注释
if (options.shouldKeepComment) {
// 得到:注释内容、注释的开始索引、结束索引
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
// 调整 html 和 index 变量
advance(commentEnd + 3)
continue
}
}
// 处理条件注释标签:<!--[if IE]>
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
// 找到结束位置
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
// 调整 html 和 index 变量
advance(conditionalEnd + 2)
continue
}
}
// 处理 Doctype,<!DOCTYPE html>
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
/**
* 处理开始标签和结束标签是这整个函数中的核型部分,其它的不用管
* 这两部分就是在构造 element ast
*/
// 处理结束标签,比如 </div>
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// 处理结束标签
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 处理开始标签,比如 <div id="app">,startTagMatch = { tagName: 'div', attrs: [[xx], ...], start: index }
const startTagMatch = parseStartTag()
if (startTagMatch) {
// 进一步处理上一步得到结果,并最后调用 options.start 方法
// 真正的解析工作都是在这个 start 方法中做的
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
// 能走到这儿,说明虽然在 html 中匹配到到了 <xx,但是这不属于上述几种情况,
// 它就只是一个普通的一段文本:<我是文本
// 于是从 html 中找到下一个 <,直到 <xx 是上述几种情况的标签,则结束,
// 在这整个过程中一直在调整 textEnd 的值,作为 html 中下一个有效标签的开始位置
// 截取 html 模版字符串中 textEnd 之后的内容,rest = <xx
rest = html.slice(textEnd)
// 这个 while 循环就是处理 <xx 之后的纯文本情况
// 截取文本内容,并找到有效标签的开始位置(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 则认为 < 后面的内容为纯文本,然后在这些纯文本中再次找 <
next = rest.indexOf('<', 1)
// 如果没找到 <,则直接结束循环
if (next < 0) break
// 走到这儿说明在后续的字符串中找到了 <,索引位置为 textEnd
textEnd += next
// 截取 html 字符串模版 textEnd 之后的内容赋值给 rest,继续判断之后的字符串是否存在标签
rest = html.slice(textEnd)
}
// 走到这里,说明遍历结束,有两种情况,一种是 < 之后就是一段纯文本,要不就是在后面找到了有效标签,截取文本
text = html.substring(0, textEnd)
}
// 如果 textEnd < 0,说明 html 中就没找到 <,那说明 html 就是一段文本
if (textEnd < 0) {
text = html
}
// 将文本内容从 html 模版字符串上截取掉
if (text) {
advance(text.length)
}
// 处理文本
// 基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,
// 即 currentParent.children 数组中
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
} else {
// 处理 script、style、textarea 标签的闭合标签
let endTagLength = 0
// 开始标签的小写形式
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
// 匹配并处理开始标签和结束标签之间的所有文本,比如 <script>xx</script>
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
parseEndTag(stackedTag, index - endTagLength, index)
}
// 到这里就处理结束,如果 stack 数组中还有内容,则说明有标签没有被闭合,给出提示信息
if (html === last) {
options.chars && options.chars(html)
if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
}
break
}
}
// Clean up any remaining tags
parseEndTag()
}
将 html 字符串模版变成 AST 对象
整个解析过程的核心是处理开始标签和结束标签
- 遍历 HTML 模版字符串,通过正则表达式匹配 "<"
- 跳过某些不需要处理的标签,比如:注释标签、条件注释标签、Doctype。
- 处理开始标签
- 得到一个对象,包括 标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引位置
- 进一步处理上一步得到的 attrs 属性,将其变成 [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] 的形式
- 通过标签名、属性对象和当前元素的父元素生成 AST 对象,其实就是一个 普通的 JS 对象,通过 key、value 的形式记录了该元素的一些信息
- 接下来进一步处理开始标签上的一些指令,比如 v-pre、v-for、v-if、v-once,并将处理结果放到 AST 对象上
- 处理结束将 ast 对象存放到 stack 数组
- 处理完成后会截断 html 字符串,将已经处理掉的字符串截掉
- 解析闭合标签
- 如果匹配到结束标签,就从 stack 数组中拿出最后一个元素,它和当前匹配到的结束标签是一对。
- 再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:key、ref、scopedSlot、样式等,并将处理结果放到元素的 AST 对象上
- 然后将当前元素和父元素产生联系,给当前元素的 ast 对象设置 parent 属性,然后将自己放到父元素的 ast 对象的 children 数组中
- 最后遍历完整个 html 模版字符串以后,返回 ast 对象
优化
从 HTML 模版字符串开始,解析所有标签以及标签上的各个属性,得到 AST 语法树,然后基于 AST 语法树进行静态标记,首先标记每个节点的static属性,判断其是否为静态的,然后进一步标记出静态根节点。从而将模板编译成渲染函数。
优化的目的
优化ast语法树在后续的更新中可以跳过静态根节点的更新,从而提高性能。
Vue标记静态节点分为两步:
- 标记所有静态节点
- 标记静态根节点
标记静态节点的作用:
- 每次重新渲染的时候不需要为静态节点创建新节点
- 在 Virtual DOM 中 patching 的过程可以被跳过
- 标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
源码分析
优化入口 optimize
/src/compiler/index.js
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
// 函数 获取静态key options.staticKeys 比如 staticClass、 classStatic
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 判断是否是平台保留标签
isPlatformReservedTag = options.isReservedTag || no
// 遍历所有子节点 给每个属性设置 static属性 用于标记静态节点
markStatic(root)
// 基于静态节点做进一步的标记静态根节点
// 节点本身是静态的 && 有子节点 && 子节点不能只有一个文本节点
// 静态根节点不能只有静态文本的子节点
markStaticRoots(root, false)
}
markStatic
目的:就是找到所有静态根节点,然后设置其static = true,这样在进行 patch 的过程跳过该节点。
/src/compiler/optimizer.js
// 标记每个节点是否为静态节点, 通过static属性来标记
function markStatic (node: ASTNode) {
// 在节点设置static 属性, 判断节点是否为静态节点
// 根节点为静态则整体是静态的 否则整体都是非静态的
node.static = isStatic(node)
if (node.type === 1) {
// 元素节点
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
// 非平台保留标签 && 不是slot标签 && 没有 inline-template 属性,则直接结束
return
}
// 遍历子节点 递归对每个子节点做静态标记
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
// 如果子节点为动态节点,则需要同时更新父节点为动态节点
if (!child.static) {
node.static = false
}
}
// 节点存在 v-if、v-else-if、 v-else 指令 则对block节点进行静态标记
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
// 如果子节点为动态节点,则需要同时更新父节点为动态节点
if (!block.static) {
node.static = false
}
}
}
// 因为所有的 elseif 和 else 节点都不在 children 中,
//如果节点的 ifConditions 不为空,则遍历 ifConditions 拿到所有条件中的 block,也就是它们对应的 AST 节点,递归执行 markStatic。
}
}
1 是文本节点(node.type === 3)。直接判定为静态节点。 2 有pre属性。这表示开发者不希望节点的内容被编译,因此认定为静态节点。 3 没有动态绑定、没有if/for条件、不是Vue内置组件、不是自定义组件、不是带有for条件的template的直接子元素以及staticKeys没有被缓存。满足这里的所有条件也将被判定为静态节点。
isStatic
/src/compiler/optimizer.js
// 非静态节点(动态节点):
// 表达式、有指令绑定、框架内置标签、v-for 指令内部的template标签
// 其他情况则为静态节点: 比如文本节点
// 判断是否ast元素节点是静态的
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression 表达式
return false
}
if (node.type === 3) { // text 文本节点
return true
}
// node.pre 则包含 v-pre 指令,判断节点为静态节点
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in 框架内置标签 component、 slot
isPlatformReservedTag(node.tag) && // not a component 平台保留标签 不是一个组件
!isDirectChildOfTemplateFor(node) && //不是在v-for 所在节点内的template标签
Object.keys(node).every(isStaticKey)
))
}
静态节点:
文本节点 或是 包含pre指令的标签 (v-pre 以原始的信息进行显示,跳过编译过程 可保留空格、换行)
否则要同时满足以下条件:
- 没有使用
v-if
、v-for
, - 没有使用其它指令(不包括
v-once
), - 非内置组件,是平台保留的标签,
- 非带有
v-for
的template
标签的直接子节点, - 节点的所有属性的
key
都满足静态 key;
markStaticRoots
静态根节点:如果某个节点的所有直接子节点都是静态节点,那么该节点就是静态根节点。
一个节点一旦被标记为静态根节点,那么它的直接子节点本身是静态的(但是子节点的子节点不一定)。
这里判定一个节点是否为静态根节点,不需要检查它子节点的后代节点,因为diff算法在做比较的时候,每次就只比较当前节点及其子节点(后代节点通过递归的形式进行比较,与当前节点无关)。因此,只要一个节点是静态的,且直接子元素不变,那它就被认为是静态根节点。
随后就需要对当前节点的子元素进行遍历,来判断它的子元素是否为静态根节点,然后一直递归下去
/src/compiler/optimizer.js
// 对元素节点进行静态根节点标记
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
// 元素节点
if (node.static || node.once) {
// 静态节点或存在 v-once 指令的节点 标记当前节点是否被包裹在 v-for 指令所在的节点内部
node.staticInFor = isInFor
}
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
// 节点是静态节点 && 存在子节点 && 子节点不能只有一个文本节点
node.staticRoot = true
return
} else {
// 不是静态根节点
node.staticRoot = false
}
// 如果当前节点不是静态根节点 则继续处理子字节,对子节点进行静态根节点的标记
if (node.children) {
// 遍历子节点 通过递归在所有子节点上标记是否为静态根节点
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
// 节点存在 v-if、 v-else-if、 v-else时 对block做静态根节点标记
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
- 被标记为static。
- 直接子元素存在(children.length > 0)。
- 不满足直接子元素只有一个,且是文本节点。
生成渲染函数
代码生成,将 ast 转换成可执行的 render 函数的字符串形式,在调用render() 时可用于生成虚拟node
源码分析
代码生成入口 generate
/**
* 从 AST 生成渲染函数
* @returns {
* render: `with(this){return _c(tag, data, children)}`,
* staticRenderFns: state.staticRenderFns
* }
*/
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 实例化 CodegenState 对象,生成代码的时候需要用到其中的一些东西
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 ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
函数 _c 是在初始化render环境的时候添加到vue实例上,用来创建 vnode 的全局实例方法。 它可以通vue实例直接调用,负责生成组件或 HTML 元素的 VNode。
编译结果
<div id="app">
<div v-for="item in arr" :key="item">{{ item }}</div>
</div>
经过编译后生成可执行的字符串
with (this) {
return _c(
'div',
{
attrs:
{
"id": "app"
}
},
_l(
(arr),
function (item) {
return _c(
'div',
{
key: item
},
[_v(_s(item))]
)
}
),
0
)
}
render helper 生成 Vnode
渲染函数之所以能生成 vnode 是通过其中的 _c、_l、、_v、_s 等方法实现的。比如:
- 普通的节点被编译成了可执行 _c 函数
- v-for 节点被编译成了可执行的 _l 函数
什么是vnode?
vnode 就是组件模版的 JS 对象表现形式,它就是一个普通的 JS 对象,详细描述了组件中各节点的信息。
export function installRenderHelpers (target: any) {
// _c = $createElement 将组件转变为 vnode
// 处理 v-once 指令
target._o = markOnce
// 将值转化为数值 通过 praseFloat 实现
target._n = toNumber
// 将值转化为字符串 对象通过 JSON.stringify, 原始值通过 String 强转
target._s = toString
// v-for
target._l = renderList
// 插槽 <slot>
target._t = renderSlot
// 类似判断两个值是否相等 ==
target._q = looseEqual
// 从数组中查找指定元素 并返回元素下标 类似indexOf
target._i = looseIndexOf
// 渲染静态节点 负责生成静态树 VNode
target._m = renderStatic
// 解析 filter
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
// 为文本节点生成 VNode
target._v = createTextVNode
// 为空节点生成 VNode
target._e = createEmptyVNode
// 作用域插槽
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
将一个组件生成 VNode 的具体工作是由 render 函数中的 _c、_o、_l、_m 等方法完成的,这些方法都被挂载到 Vue 实例上面,负责在运行时生成组件 VNode
设置组件配置信息,然后通过 new VNode(组件信息) 生成组件的 VNode
- _c,负责生成组件或 HTML 元素的 VNode
- _l,运行时渲染 v-for 列表的帮助函数,循环遍历 val 值,依次为每一项执行 render 方法生成 VNode,最终返回一个 VNode 数组
- _m,负责生成静态节点的 VNode,即执行 staticRenderFns 数组中指定下标的函数
_c
// createElement 用来生成虚拟DOM
// 将createElement函数挂载到 vm 的 _c 和 $createElement 两个属性上
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)
案例
- 断点调试
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
<script src="./dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
new Vue({
el:' #app',
template: `
<div>
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item, index) in data" :key="index" @click="clickItem(index)">
{{item}}:<{{index}}
</li>
</ul>
<div v-else>
<p>111</p>
</div>
</div>`,
data() {
return {
bindCls: 'a',
isShow: true,
data: ['A', 'B', 'C', 'D']
}
},
methods: {
clickItem(index) {
console.log(index)
}
}
})
</script>
</body>
</html>
因为所有的 elseif 和 else 节点都不在 children 中, 如果节点的 ifConditions 不为空,则遍历 ifConditions 拿到所有条件中的 block ,也就是它们对应的AST 节点,递归执行 markStatic。
- prase 解析生成的 ast 语法树
根据生成的 ast树 ,所有的 elseif 和 else 节点都不在 children 中,
如果节点的 ifConditions 不为空,则遍历 ifConditions 拿到所有条件中的 block,也就是它们对应的 AST 节点,递归执行 markStatic。
- optimize 优化过后的 ast 语法树
发现每一个 AST 元素节点都多了 staic属性,并且 type为 1 的普通元素 AST 节点多了 staticRoot 属性。
总结
这又是一篇长文啊 😂,但其实本文的目的很简单就是想说明 Vue 在编译阶段完成的三件事:
- 将组件的 html 模版解析成 AST 对象
- 优化 -- 遍历 AST,为每个节点做静态标记,标记其是否为静态节点,然后进一步标记出静态根节点,这样在后续更新的过程中就可以跳过这些静态节点了;标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
- 将 AST 语法树生成运行渲染函数,即大家说的 render函数,同时还生成了 staticRenderFns 数组,里面存放了所有的静态节点的渲染函数
讲解的过程可能不是很流畅,但是希望能够帮助大家深入学习,
了解更多关于 vue 的底层原理上的实现,与大家一起分享学习的过程,逐步提高,共同进步 💪
转载自:https://juejin.cn/post/7245223225575030841