前言

上一篇文章 Vue 源码解读(8)—— 编译器 之 解析 详细详解了编译器的第一部分,如何将 html 模版字符串编译成 AST。今天带来编译器的第二部分,优化 AST,也是大家常说的静态标记。

目标

深入理解编译器的静态标记过程

源码解读

入口

/src/compiler/index.js

/**
* 在这之前做的所有的事情,只有一个目的,就是为了构建平台特有的编译选项(options),比如 web 平台
*
* 1、将 html 模版解析成 ast
* 2、对 ast 树进行静态标记
* 3、将 ast 生成渲染函数
* 静态渲染函数放到 code.staticRenderFns 数组中
* code.render 为动态渲染函数
* 在将来渲染时执行渲染函数得到 vnode
*/
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 将模版解析为 AST,每个节点的 ast 对象上都设置了元素的所有信息,比如,标签信息、属性信息、插槽信息、父节点、子节点等。
// 具体有那些属性,查看 start 和 end 这两个处理开始和结束标签的方法
const ast = parse(template.trim(), options)
// 优化,遍历 AST,为每个节点做静态标记
// 标记每个节点是否为静态节点,然后进一步标记出静态根节点
// 这样在后续更新的过程中就可以跳过这些静态节点了
// 标记静态根,用于生成渲染函数阶段,生成静态根节点的渲染函数
if (options.optimize !== false) {
optimize(ast, options)
}
// 从 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
}
})

optimize

/src/compiler/optimizer.js

/**
* 优化:
* 遍历 AST,标记每个节点是静态节点还是动态节点,然后标记静态根节点
* 这样在后续更新的过程中就不需要再关注这些节点
*/
export function optimize(root: ?ASTElement, options: CompilerOptions) {
if (!root) return
/**
* options.staticKeys = 'staticClass,staticStyle'
* isStaticKey = function(val) { return map[val] }
*/
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 平台保留标签
isPlatformReservedTag = options.isReservedTag || no
// 遍历所有节点,给每个节点设置 static 属性,标识其是否为静态节点
markStatic(root)
// 进一步标记静态根,一个节点要成为静态根节点,需要具体以下条件:
// 节点本身是静态节点,而且有子节点,而且子节点不只是一个文本节点,则标记为静态根
// 静态根节点不能只有静态文本的子节点,因为这样收益太低,这种情况下始终更新它就好了
markStaticRoots(root, false)
}

markStatic

/src/compiler/optimizer.js

/**
* 在所有节点上设置 static 属性,用来标识是否为静态节点
* 注意:如果有子节点为动态节点,则父节点也被认为是动态节点
* @param {*} node
* @returns
*/
function markStatic(node: ASTNode) {
// 通过 node.static 来标识节点是否为 静态节点
node.static = isStatic(node)
if (node.type === 1) {
/**
* 不要将组件的插槽内容设置为静态节点,这样可以避免:
* 1、组件不能改变插槽节点
* 2、静态插槽内容在热重载时失败
*/
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
// 递归终止条件,如果节点不是平台保留标签 && 也不是 slot 标签 && 也不是内联模版,则直接结束
return
}
// 遍历子节点,递归调用 markStatic 来标记这些子节点的 static 属性
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 中节点的 static
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
}
}
}
}
}

isStatic

/src/compiler/optimizer.js

/**
* 判断节点是否为静态节点:
* 通过自定义的 node.type 来判断,2: 表达式 => 动态,3: 文本 => 静态
* 凡是有 v-bind、v-if、v-for 等指令的都属于动态节点
* 组件为动态节点
* 父节点为含有 v-for 指令的 template 标签,则为动态节点
* @param {*} node
* @returns boolean
*/
function isStatic(node: ASTNode): boolean {
if (node.type === 2) { // expression
// 比如:{{ msg }}
return false
}
if (node.type === 3) { // text
return true
}
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
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}

markStaticRoots

/src/compiler/optimizer.js

/**
* 进一步标记静态根,一个节点要成为静态根节点,需要具体以下条件:
* 节点本身是静态节点,而且有子节点,而且子节点不只是一个文本节点,则标记为静态根
* 静态根节点不能只有静态文本的子节点,因为这样收益太低,这种情况下始终更新它就好了
*
* @param { ASTElement } node 当前节点
* @param { boolean } isInFor 当前节点是否被包裹在 v-for 指令所在的节点内
*/
function markStaticRoots(node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
// 节点是静态的 或者 节点上有 v-once 指令,标记 node.staticInFor = true or false
node.staticInFor = isInFor
} if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
// 节点本身是静态节点,而且有子节点,而且子节点不只是一个文本节点,则标记为静态根 => node.staticRoot = true,否则为非静态根
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)
}
}
}
}

总结

  • 面试官 问:简单说一下 Vue 的编译器都做了什么?

    Vue 的编译器做了三件事情:

    • 将组件的 html 模版解析成 AST 对象

    • 优化,遍历 AST,为每个节点做静态标记,标记其是否为静态节点,然后进一步标记出静态根节点,这样在后续更新的过程中就可以跳过这些静态节点了;标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数

    • 从 AST 生成运行渲染函数,即大家说的 render,其实还有一个,就是 staticRenderFns 数组,里面存放了所有的静态节点的渲染函数


  • 面试官:详细说一下静态标记的过程

    • 标记静态节点

      • 通过递归的方式标记所有的元素节点

      • 如果节点本身是静态节点,但是存在非静态的子节点,则将节点修改为非静态节点

    • 标记静态根节点,基于静态节点,进一步标记静态根节点

      • 如果节点本身是静态节点 && 而且有子节点 && 子节点不全是文本节点,则标记为静态根节点

      • 如果节点本身不是静态根节点,则递归的遍历所有子节点,在子节点中标记静态根


  • 面试官:什么样的节点才可以被标记为静态节点?

    • 文本节点

    • 节点上没有 v-bind、v-for、v-if 等指令

    • 非组件

链接

感谢各位的:关注点赞收藏评论,我们下期见。


当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

Vue 源码解读(9)—— 编译器 之 优化的更多相关文章

  1. Vue 源码解读(8)—— 编译器 之 解析(上)

    特殊说明 由于文章篇幅限制,所以将 Vue 源码解读(8)-- 编译器 之 解析 拆成了上下两篇,所以在阅读本篇文章时请同时打开 Vue 源码解读(8)-- 编译器 之 解析(下)一起阅读. 前言 V ...

  2. Vue 源码解读(8)—— 编译器 之 解析(下)

    特殊说明 由于文章篇幅限制,所以将 Vue 源码解读(8)-- 编译器 之 解析 拆成了两篇文章,本篇是对 Vue 源码解读(8)-- 编译器 之 解析(上) 的一个补充,所以在阅读时请同时打开 Vu ...

  3. Vue 源码解读(10)—— 编译器 之 生成渲染函数

    前言 这篇文章是 Vue 编译器的最后一部分,前两部分分别是:Vue 源码解读(8)-- 编译器 之 解析.Vue 源码解读(9)-- 编译器 之 优化. 从 HTML 模版字符串开始,解析所有标签以 ...

  4. Vue 源码解读(2)—— Vue 初始化过程

    当学习成为了习惯,知识也就变成了常识. 感谢各位的 点赞.收藏和评论. 新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn 文章已收录到 github 仓库 liyongning/blog ...

  5. Vue 源码解读(11)—— render helper

    前言 上一篇文章 Vue 源码解读(10)-- 编译器 之 生成渲染函数 最后讲到组件更新时,需要先执行编译器生成的渲染函数得到组件的 vnode. 渲染函数之所以能生成 vnode 是通过其中的 _ ...

  6. Vue 源码解读(1)—— 前言

    当学习成为了习惯,知识也就变成了常识. 感谢各位的 点赞.收藏和评论. 新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn 文章已收录到 github 仓库 liyongning/blog ...

  7. Vue 源码解读(3)—— 响应式原理

    前言 上一篇文章 Vue 源码解读(2)-- Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了 new Vue(options) 都做了什么,其中关于 数据响应式 的实现用一句话简单的带过 ...

  8. Vue 源码解读(6)—— 实例方法

    前言 上一篇文章 Vue 源码解读(5)-- 全局 API 详细介绍了 Vue 的各个全局 API 的实现原理,本篇文章将会详细介绍各个实例方法的实现原理. 目标 深入理解以下实例方法的实现原理. v ...

  9. Vue 源码解读(12)—— patch

    前言 前面我们说到,当组件更新时,实例化渲染 watcher 时传递的 updateComponent 方法会被执行: const updateComponent = () => { // 执行 ...

随机推荐

  1. gin框架中图形验证码的生成和验证

    功能和验证码使用原理 本案例中没有使用redis作为缓存,而是使用的内存存储方法 github链接地址 下载命令 go get github.com/mojocn/base64Captcha 请求处理 ...

  2. IoC容器-Bean管理注解方式(创建对象)

    IoC操作Bean管理(基于注解方式) 1,什么是注解 (1)注解是代码特殊标记,格式:@注解名称(属性名称=属性值,属性名称=属性值...) (2)使用注解,注解作用在类上面,方法上面,属性上面 ( ...

  3. Mac系统U盘制作教程

    您可以将外置驱动器或备用宗卷用作安装 Mac 操作系统的启动磁盘. 以下高级步骤主要适用于系统管理员以及熟悉命令行的其他人员.升级 macOS 或重新安装 macOS 不需要可引导安装器,但如果您要在 ...

  4. jenkins pipeline设置

    转:https://mp.weixin.qq.com/s/MwyBsUWUQASGMr_erGfzZA

  5. Windows 10 Version 21h1安装

    好久没安装过Windows 10系统了,这两天在下载电脑管家时,使用bing搜索,没注意就选择了第一个,安装后,噩梦就来了,非法广告.各种软件的推送,怎么也清不干净. 没办法,到PCBeta下载了最新 ...

  6. JAVA多线程学习十一-线程锁技术

    前面我们讲到了synchronized:那么这节就来将lock的功效. 一.locks相关类 锁相关的类都在包java.util.concurrent.locks下,有以下类和接口: |---Abst ...

  7. linux 进程信号

    转载请注明来源:https://www.cnblogs.com/hookjc/ signal 函数的使用方法简单,但并不属于 POSIX 标准,在各类 UNIX 平台上的实现不尽相同,因此其用途受 到 ...

  8. iOS 性能优化系列

    Objective-C 高性能的循环 使用 Swift 和 Objective-C 执行 iOS 内存管理的 7 个简单技巧 @autoreleasepool-内存的分配与释放

  9. Linux 内核引导参数简介

    概述 内核引导参数大体上可以分为两类:一类与设备无关.另一类与设备有关.与设备有关的引导参数多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导参数.比如,如果你想知道可以向 AHA ...

  10. LVS负载均衡群集部署——NAT模式

    LVS负载均衡群集部署--NAT模式 1.群集应用概述 2.NAT模式LVS负载均衡群集部署 1.群集应用概述: 存在的问题: 互联网应用中,随着站点对硬件性能.响应速度.服务稳定性.数据可靠性等要求 ...