vue3复习-源码-编译原理-迷你版

155 阅读3分钟

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 生成代码

image.png

在线转换

<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

image.png vue3在线转换

自己实现

模板

<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…