前言

上一篇文章 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. WSL与gnome-desktop

    WSL与gome-desktop 经过测试和检索 确定WSL1无法在gome-desktop实现GUI桌面 只能实现其中应用的现实,比如打开记事本在Xserver https://www.reddit ...

  2. Go 面向对象编程应用

    #### Go 面向对象编程应用前面学习了很多的基础知识,这一节来实际写一个小案例:涉及到的知识: 1. 数组的基本使用2. 结构体3. 切片 4. 方法5. 循环6. 函数返回值(命名返回值,普通返 ...

  3. python操作MySQL--实例

    有一段时间,没更新,但这都不是自己懒惰的借口,希望自己能坚持下去: python可以操作MySQL 数据库,需要安装的是MySQL-python,并且实现读入excel表及生成excel 表,则需要x ...

  4. rm误操作 which查看命令存放路径

    目录 一:rm误操作 which查看命令存放路径 一:rm误操作 which查看命令存放路径 解决rm命令误操作 让别人使用不了自己的rm命令 将rm命令改一个名称 mv rm abc 查看命令存放路 ...

  5. LCT 入门

    这是一份 \(\rm LCT\) 入门总结. 关于 \(\rm LCT\) 的复杂度这里不会提及,只会记录 \(\rm LCT\) 的基本操作和经典例题,但神奇的 \(\rm LCT\) 虽然常数巨大 ...

  6. git命令log与reflog的比较

    感谢原文作者:杨鲜生 原文链接:https://blog.csdn.net/u013252047/article/details/80230781 用git命令,想看到自己的操作记录,则可以使用log ...

  7. 连接docker里面的mysql失败解决

    场景:在虚拟机的docker容器中安装latest版本的mysql之后,在宿主机中使用navicat连接虚拟机中的mysql出现下图报错: 解决方法: 1.首先docker ps命令查看正在运行的容器 ...

  8. 关于linux shell编程,alias rm='cp $@ ~/backup; rm $@'

    书上的这个例子需要在ubuntu的低版本的系统才支持,现在基本上都不支持了,想实现也很简单自己写一个脚本先备份再删除. alias也只是做了一次替换alias rm='cp $@ ~/backup; ...

  9. 获取联系人列表的时候contact_id出现null的值

    因为删除联系人只是把它的contact_id设置为null,所以只要手机上删除过联系人id就会有null,用之前先判断是不是null就好了

  10. finally块

    /* finally 块: finally块的 使用前提是必须要存在try块才能使用. finally块的代码在任何情况下都会执行的,除了jvm退出的情况. finally非常适合做资源释放的工作,这 ...