# 模板编译

模板编译的主要目标是将模板(template)转换为渲染函数(render) Vue 2.0需要用到VNode描述视图以及各种交互,手写显然不切实际,因此用户只需编写类似HTML代码的Vue模板,通过编译器将模板转换为可返回VNode的render函数。

# 体验模板编译

带编译器的版本中,可以使用template或el的方式声明模板

<div id="demo">
    <h1>Vue.js测试</h1>
    <p>{{foo}}</p>
</div>
<script>
// 使用el方式
new Vue({
    data: { foo: 'foo' },
    el: "#demo",
});
</script>

然后输出渲染函数

<script>
    const app = new Vue({});
    // 输出render函数
    console.log(app.$options.render);
</script>

输出结果大致如下:

ƒunction anonymous() {
    with (this) {
        return _c('div', { attrs: { "id": "demo" } }, [
            _c('h1', [_v("Vue.js测试")]),
            _v(" "),
            _c('p', [_v(_s(foo))])
        ])
    }
}

元素节点使用createElement创建,别名_c 本文节点使用createTextVNode创建,别名_v 表达式先使用toString格式化,别名_s

模板编译过程

实现模板编译共有三个阶段:解析、优化和生成

解析 - parse 解析器将模板解析为抽象语法树AST,只有将模板解析成AST后,才能基于它做优化或者生成代码字符串。 调试查看得到的AST,/src/compiler/parser/index.js

解析器内部分了HTML解析器、文本解析器和过滤器解析器,最主要是HTML解析器,核心算法说明:

//src/compiler/parser/index.js
parseHTML(tempalte, {
    start(tag, attrs, unary){}, // 遇到开始标签的处理
    end(){},// 遇到结束标签的处理
    chars(text){},// 遇到文文本标签的处理
    comment(text){}// 遇到注释标签的处理
})

# 优化 - optimize

优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不不变的节点,如纯文本节点。 标记静态子树的好处:

  • 每次重新渲染,不需要为静态子树创建新节点
  • 虚拟DOM中patch时,可以跳过静态子树

代码实现,src/compiler/optimizer.js - optimize

export function optimize (root: ?ASTElement, options: CompilerOptions) {
    if (!root) return
    isStaticKey = genStaticKeysCached(options.staticKeys || '')
    isPlatformReservedTag = options.isReservedTag || no
    // 找出静态节点并标记
    markStatic(root)
    // 找出静态根节点并标记
    markStaticRoots(root, false)
}

# 代码生成 - generate

将AST转换成渲染函数中的内容,即代码字符串。 generate方法生成渲染函数代码,src/compiler/codegen/index.js

export function generate(
    ast: ASTElement | void,
    options: CompilerOptions
): CodegenResult {
    const state = new CodegenState(options)
    const code = ast ? genElement(ast, state) : '_c("div")'
    return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns
    }
}
//生成的code长这样
`_c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("Vue.js测试")]),
_c('p',[_v(_s(foo))])
])`

# v-if、v-for

着重观察几个结构性指令的解析过程

// 解析v-if,parser/index.js
function processIf(el) {
    const exp = getAndRemoveAttr(el, 'v-if') // 获取v-if=“exp"中exp并删除v-if属性
    if (exp) {
        el.if = exp // 为ast添加if表示条件
        addIfCondition(el, { // 为ast添加ifConditions表示各种情况对应结果
            exp: exp,
            block: el
        })
    } else { // 其他情况处理理
        if (getAndRemoveAttr(el, 'v-else') != null) {
            el.else = true
        }
        const elseif = getAndRemoveAttr(el, 'v-else-if')
        if (elseif) {
            el.elseif = elseif
        }
    }
}
// 代码生成,codegen/index.js
function genIfConditions(
    conditions: ASTIfConditions,
    state: CodegenState,
    altGen?: Function,
    altEmpty?: string
): string {
    const condition = conditions.shift() // 每次处理一个条件
    if (condition.exp) { // 每种条件生成一个三元表达式
        return `(${condition.exp})?${genTernaryExp(condition.block)
            }:${genIfConditions(conditions, state, altGen, altEmpty)
            }`
    } else {
        return `${genTernaryExp(condition.block)}`
    }
    // v-if with v-once should generate code like (a)?_m(0):_m(1)
    function genTernaryExp(el) { }
}

生成结果:

"with(this){return _c('div',{attrs:{"id":"demo"}},[
(foo) ? _c('h1',[_v(_s(foo))]) : _c('h1',[_v("no title")]),
_v(" "),_c('abc')],1)}"

# 插槽

组件编译的顺序是先编译父组件,再编译子组件

普通插槽是在父组件编译和渲染阶段生成 vnodes ,数据的作用域是父组件,子组件渲染的时候直接拿到这些渲染好的 vnodes作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点保留一个scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes ,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。 解析相关代码:

// processSlotContent:处理<template v-slot:xxx="yyy">
const slotBinding = getAndRemoveAttrByRegex(el, slotRE) // 查找v-slot:xxx
if (slotBinding) {
    const { name, dynamic } = getSlotName(slotBinding) // name是xxx
    el.slotTarget = name // xxx赋值到slotTarget
    el.slotTargetDynamic = dynamic
    el.slotScope = slotBinding.value || emptySlotScopeToken // yyy赋值到
    slotScope
}
// processSlotOutlet:处理理<slot>
if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name') // 获取slot的name并赋值到slotName
}

生成相关代码:

// genScopedSlot:这里里把slotScope作为形参转换为工⼚函数返回内容
const fn = `function(${slotScope}){` +
    `return ${el.tag === 'template'
        ? el.if && isLegacySyntax
            ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
            : genChildren(el, state) || 'undefined'
        : genElement(el, state)
    }}`
// reverse proxy v-slot without scope on this.$slots
const reverseProxy = slotScope ? `` : `,proxy:true`
return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`

# vue.js项目中一些最佳实践

范例:项目要使用icon,传统方案是图标字体(字体文件+样式文件),不便维护;svg方案采用svg-sprite-loader自动加载打包,方便维护。

使用icon前先安装依赖:svg-sprite-loader

npm i svg-sprite-loader -D

下载图标,存入src/icons/svg中 修改规则和新增规则,vue.config.js

// resolve定义一个绝对路径获取函数
const path = require('path')
function resolve(dir) {
    return path.join(__dirname, dir)
}
//...
chainWebpack(config) {
    // 配置svg规则排除icons目录中svg文件处理
    config.module
        .rule("svg")
        .exclude.add(resolve("src/icons"));
    // 新增icons规则,设置svg-sprite-loader处理icons目录中的svg
    config.module.rule("icons") //新增icons规则
        .test(/\.svg$/) //test选项
        .include.add(resolve("src/icons")) // include选项是数组
        .end() //add完上下文是数组不是icons规则,使用end()回退
        .use("svg-sprite-loader") //添加use选项
        .loader("svg-sprite-loader") //切换上下文为svg-sprite-loader
        .options({ symbolId: "icon-[name]" }) // 为svg-sprite-loader新增选项
        .end();// 回退
}

图标自动导入

// icons/index.js
// 指定require上下文
const req = require.context('./svg', false, /\.svg$/)
// 遍历加载上下文中所有项
req.keys().map(req);
// main.js
import './icons'
Last Updated: 12/10/2021, 6:23:25 PM