vue3 编译与运行逻辑
- 通过把template转化成render 函数,执行生成vnode
- 使用模块complier-core,complier-dom实现转化。
- 使用runtime-core和runtime-dom运行时执行
使用方式
- 可以使用脚手架在本地先用complier转化为render,然后在runtime的时候直接执行
- 直接运行时调用complier动态转化,如字符串的template
<template>xxx</template>
转化逻辑
- 代码
- 词法分析 生成token list
- 语法分析 生成AST 抽象语法树
- tranform 优化抽象语法树
- generate 生成代码
在线转换
<div :class="{ active }"></div>
<input :id="id" :value="value">
<div>{{ dynamic }}</div>
import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", {
class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */),
_createElementVNode("input", {
id: _ctx.id,
value: _ctx.value
}, null, 8 /* PROPS */, ["id", "value"]),
_createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
// Check the console for the AST
自己实现
模板
<div id="app">
<div @click="()=>console.log('aaa')" :id="name">{{name}}</div>
<h1 :name="title">xxxxx</h1>
<p >yyyy</p>
</div>
执行代码流程
let template =
`
<div id="app">
<div @click="()=>console.log('aaa')" :id="name">{{name}}</div>
<h1 :name="title">xxxxx</h1>
<p >yyyy</p>
</div>
`
function compiler(template) {
const ast = parse(template);
transform(ast)
const code = generate(ast)
return code
}
const renderFunction = compiler(template)
console.log(renderFunction)
tokenizer(token解析)
function tokenizer(input) {
let tokens = []
let type = ''
let val = ''
// 粗暴循环
for (let i = 0; i < input.length; i++) {
let ch = input[i]
if (ch === '<') {
push()
if (input[i + 1] === '/') {
type = 'tagend'
} else {
type = 'tagstart'
}
} if (ch === '>') {
if(input[i-1]=='='){
//箭头函数
}else{
push()
type = "text"
continue
}
} else if (/[\s]/.test(ch)) { // 碰见空格截断一下
push()
type = 'props'
continue
}
val += ch
}
return tokens
function push() {
if (val) {
if (type === "tagstart") val = val.slice(1) // <div => div
if (type === "tagend") val = val.slice(2) // </div => div
tokens.push({
type,
val
})
val = ''
}
}
}
执行输出的json
[
{ "type": "tagstart", "val": "div" },
{ "type": "props", "val": "id=\"app\"" },
{ "type": "tagstart", "val": "div" },
{ "type": "props", "val": "@click=\"()=>console.log('aaa')\"" },
{ "type": "props", "val": ":id=\"name\"" },
{ "type": "text", "val": "{{name}}" },
{ "type": "tagend", "val": "div" },
{ "type": "tagstart", "val": "h1" },
{ "type": "props", "val": ":name=\"title\"" },
{ "type": "text", "val": "xxxxx" },
{ "type": "tagend", "val": "h1" },
{ "type": "tagstart", "val": "p" },
{ "type": "text", "val": "yyyy" },
{ "type": "tagend", "val": "p" },
{ "type": "tagend", "val": "div" }
]
parse
function parse(template) {
const tokens = tokenizer(template)
let cur = 0
let ast = {
type: 'root',
props:[],
children: []
}
while (cur < tokens.length) {
ast.children.push(walk())
}
return ast
function walk() {
let token = tokens[cur]
if (token.type == 'tagstart') {
let node = {
type: 'element',
tag: token.val,
props: [],
children: []
}
token = tokens[++cur]
while (token.type !== 'tagend') {
if (token.type == 'props') {
node.props.push(walk())
} else {
node.children.push(walk())
}
token = tokens[cur]
}
cur++
return node
}
if (token.type === 'tagend') {
cur++
// return token
}
if (token.type == "text") {
cur++
return token
}
if (token.type === "props") {
cur++
const [key, val] = token.val.replace('=','~').split('~')
return {
key,
val
}
}
}
}
输出ast
{
"type": "root",
"props": [],
"children": [
{
"type": "element",
"tag": "div",
"props": [{ "key": "id", "val": "\"app\"" }],
"children": [
{
"type": "element",
"tag": "div",
"props": [
{ "key": "@click", "val": "\"()=>console.log('aaa')\"" },
{ "key": ":id", "val": "\"name\"" }
],
"children": [{ "type": "text", "val": "{{name}}" }]
},
{
"type": "element",
"tag": "h1",
"props": [{ "key": ":name", "val": "\"title\"" }],
"children": [{ "type": "text", "val": "xxxxx" }]
},
{
"type": "element",
"tag": "p",
"props": [],
"children": [{ "type": "text", "val": "yyyy" }]
}
]
}
]
}
transform 优化代码
function transform(ast) {
let context = {
// import { toDisplayString , createVNode , openBlock , createBlock } from "vue"
helpers:new Set(['openBlock','createVnode']), //用于生成导入的包语句
}
traverse(ast, context)
ast.helpers = context.helpers
}
//优化的代码 加入静态的标记,便于diff的时候判断
function traverse(ast, context){
switch(ast.type){
case "root":
context.helpers.add('createBlock') // 动态收集当前需要导入的方法库
// log(ast)
case "element":
ast.children.forEach(node=>{
traverse(node,context)
})
ast.flag = 0
ast.props = ast.props.map(prop=>{
const {key,val} = prop
if(key[0]=='@'){
ast.flag |= PatchFlags.EVENT // 标记event需要更新
return {
key:'on'+key[1].toUpperCase()+key.slice(2),
val
}
}
if(key[0]==':'){
const k = key.slice(1)
if(k=="class"){
ast.flag |= PatchFlags.CLASS // 标记class需要更新
}else if(k=='style'){
ast.flag |= PatchFlags.STYLE // 标记style需要更新
}else{
ast.flag |= PatchFlags.PROPS // 标记props需要更新
}
return{
key:key.slice(1),
val
}
}
if(key.startsWith('v-')){
// pass such as v-model
}
//标记static是true 静态节点
return {...prop,static:true}
})
break
case "text":
// trnsformText
let re = /\{\{(.*)\}\}/g
if(re.test(ast.val)){
//有{{
ast.flag |= PatchFlags.TEXT // 标记props需要更新
context.helpers.add('toDisplayString') // 动态加入引入库
ast.val = ast.val.replace(/\{\{(.*)\}\}/g,function(s0,s1){
return s1
})
}else{
ast.static = true
}
}
}
PatchFlags定义
const PatchFlags = {
"TEXT": 1,
"CLASS": 1 << 1,
"STYLE": 1 << 2,
"PROPS": 1 << 3,
"EVENT": 1 << 4,
}
这样在diff做比较的时候就可以实现位运算
if(vnode.flag & patchFlag.STYLE){ //计算样式处理
}else if(vnode.flag & patchFlag.TEXT){//更新文本
}...
输出
新增的flag 和 static 用于diff时候优化
{
"type": "root",
"props": [],
"children": [
{
"type": "element",
"tag": "div",
"props": [{ "key": "id", "val": "\"app\"", "static": true }],
"children": [
{
"type": "element",
"tag": "div",
"props": [
{ "key": "onClick", "val": "\"()=>console.log('aaa')\"" },
{ "key": "id", "val": "\"name\"" }
],
"children": [{ "type": "text", "val": "{{name}}" }],
"flag": 24
},
{
"type": "element",
"tag": "h1",
"props": [{ "key": "name", "val": "\"title\"" }],
"children": [{ "type": "text", "val": "xxxxx" }],
"flag": 8
},
{
"type": "element",
"tag": "p",
"props": [],
"children": [{ "type": "text", "val": "yyyy" }],
"flag": 0
}
],
"flag": 0
}
],
"flag": 0
}
generate 生成代码
function generate(ast) {
const {helpers} = ast
let code = `
import {${[...helpers].map(v=>v+' as _'+v).join(',')}} from 'vue'\n //这里输出导入的语句
export function render(_ctx, _cache, $props){
return(_openBlock(), ${ast.children.map(node=>walk(node))})}`
function walk(node){
switch(node.type){
case 'element':
let {flag} = node // 编译的标记
let props = '{'+node.props.reduce((ret,p)=>{
if(flag.props){ //动态属性
ret.push(p.key +':_ctx.'+p.val.replace(/['"]/g,'') )
}else{
ret.push(p.key +':'+p.val )
}
return ret
},[]).join(',')+'}'
return `_createVnode("${node.tag}",${props}),[
${node.children.map(n=>walk(n))}
],${JSON.stringify(flag)}`
break
case 'text':
if(node.static){
return '"'+node.val+'"'
}else{
return `_toDisplayString(_ctx.${node.val})`
}
break
}
}
return code
}
测试
function compiler(template) {
const ast = parse(template);
transform(ast)
const code = generate(ast)
return code
}
`
const renderFunction = compiler(template)
console.log(renderFunction)
输出结果
生成render函数
import {openBlock as _openBlock,createVnode as _createVnode,createBlock as _createBlock,toDisplayString as _toDisplayString} from 'vue'
//这里输出导入的语句
export function render(_ctx, _cache, $props){
return(_openBlock(), _createVnode("div",{id:"app"}),[
_createVnode("div",{onClick:"()=>console.log('aaa')",id:"name"}),[
_toDisplayString(_ctx.{{name}})
],24,_createVnode("h1",{name:"title"}),[
_toDisplayString(_ctx.xxxxx)
],8,_createVnode("p",{}),[
_toDisplayString(_ctx.yyyy)
],0
],0)}
源码
github.com/mjsong07/vu… 运行:packages/vue/examples/composition/compiler.html
参考
[玩转vue3](time.geekbang.org/column/intr…