Vue3 模版编译优化

214 阅读13分钟

编译优化

PatchFlags优化

Diff算法无法避免新旧虚拟DOM中无用的比较操作,通过patchFlags来标记动态内容,可以实现快速diff算法

<div>
  <h1>Hello Jiang</h1>
  <span>{{name}}</span>
</div>

此template经过模板编译会变成以下代码:

const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

return function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("h1", null, "Hello Jiang"),
    _createTextVNode(),
    _createElementVNode("span", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}

创建虚拟节点

const VueComponent = {
    setup(){
        let state = reactive({name:'jw'});
        setTimeout(() => {
            state.name = 'zf'
        }, 1000);
        return {
            ...toRefs(state)
        }
    },
    render(_ctx){
        return (openBlock(),createElementBlock('div',null,[
            createElementVNode("h1", null, "Hello Jiang"),
            createElementVNode("span", null, toDisplayString(_ctx.name), 1 /* TEXT */)
        ]))
    }
}
render(h(VueComponent),app)

生成的虚拟DOM是:

{
	type: "div",
    __v_isVNode: true,
    children:[
       {type: 'h1', props: null, key: null, }
       {type: Symbol(), props: null, key: null, }
	   {type: 'span', props: null, key: null, }
    ],
    dynamicChildren:[{type: 'span', children: _ctx.name, patchFlag: 1}]
}

此时生成的虚拟节点多出一个dynamicChildren属性。这个就是block的作用,block可以收集所有后代动态节点。这样后续更新时可以直接跳过静态节点,实现靶向更新

动态标识

export const enum PatchFlags {
  TEXT = 1, // 动态文本节点
  CLASS = 1 << 1, // 动态class
  STYLE = 1 << 2, // 动态style
  PROPS = 1 << 3, // 除了class\style动态属性
  FULL_PROPS = 1 << 4, // 有key,需要完整diff
  HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
  STABLE_FRAGMENT = 1 << 6, // 稳定序列,子节点顺序不会发生变化
  KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
  UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
  NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
  DYNAMIC_SLOTS = 1 << 10, // 动态插槽
  DEV_ROOT_FRAGMENT = 1 << 11, 
  HOISTED = -1, // 表示静态节点,内容变化,不比较儿子
  BAIL = -2 // 表示diff算法应该结束
}

靶向更新实现

export { createVnode as createElementVNode }
let currentBlock = null
export function openBlock(){ // 创建block
    currentBlock = []
}
export function closeBlock(){ //关闭block
    currentBlock = null;
}
export function createElementBlock(type,props?,children?,patchFlag?){ // 创建block元素
    return setupBlock(createVNode(type,props,children,patchFlag))// 将动态元素挂载到block节点上
}
export function setupBlock(vnode){ 
    vnode.dynamicChildren = currentBlock;
    closeBlock();
    return vnode;
}
export function createTextVNode(text: ' ', flag = 0) { // 创建文本虚拟节点
    return createVNode(Text, null, text, flag)
}
export function toDisplayString(val){ // 就是JSON.stringify
    return isString(val)? val : val == null ? '' :isObject(val)? JSON.stringify(val): String(val);
}
export const createVNode = (type,props,children = null,patchFlag =0)=>{
    // ...
    if(currentBlock && vnode.patchFlag > 0){
        currentBlock.push(vnode);
    }
    return vnode;
}

靶向更新

const patchElement = (n1,n2) =>{ // 比较两个元素的差异
    let el = (n2.el = n1.el);
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    let {patchFlag} = n2;
    if(patchFlag){ // 单独处理标识属性
        if(patchFlag & PatchFlags.CLASS){
            if(oldProps.class !== newProps.class){
                hostPatchProp(el,'class',null,newProps.class);
            }
        }
        if (patchFlag & PatchFlags.TEXT) {
            if (n1.children !== n2.children) {
                hostSetElementText(el, n2.children)
            }
        }
    }else{ // 处理所有属性
        patchProps(oldProps,newProps,el);
    }
    if(n2.dynamicChildren){ // 比较动态节点
        patchBlockChildren(n1,n2);
    }else{
        patchChildren(n1,n2,el); 
    }
}
function patchBlockChildren(n1,n2){
    for(let i = 0 ; i < n2.dynamicChildren.length;i++){
        patchElement(n1.dynamicChildren[i],n2.dynamicChildren[i]);
    }
}

由此可以看出性能被大幅度提升,从tree级别的比对,变成了线性结构比对。

BlockTree

为什么我们还要提出blockTree的概念? 只有block不就挺好的么? 问题出在block在收集动态节点时是忽略虚拟DOM树层级的。

<div>
    <p v-if="flag">
        <span>{{a}}</span>
    </p>
    <div v-else>
        <span>{{a}}</span>
    </div>
</div>

这里我们知道默认根节点是一个block节点,如果要是按照之前的套路来搞,这时候切换flag的状态将无法从p标签切换到div标签。 解决方案:就是将不稳定的结构也作为block来进行处理

不稳定结构

所谓的不稳结构就是DOM树的结构可能会发生变化。不稳定结构有哪些呢? (v-if/v-for/Fragment)

v-if

<div>
    <div v-if="flag">
        <span>{{a}}</span>
    </div>
    <div v-else>
        <p><span>{{a}}</span></p>
    </div>
</div>

编译后的结果:

return function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    (_ctx.flag)
      ? (_openBlock(), _createElementBlock("div", { key: 0 }, [
          _createElementVNode("span", null, _toDisplayString(_ctx.a), 1 /* TEXT */)
        ]))
      : (_openBlock(), _createElementBlock("div", { key: 1 }, [
          _createElementVNode("p", null, [
            _createElementVNode("span", null, _toDisplayString(_ctx.a), 1 /* TEXT */)
          ])
        ]))
  ]))
}
Block(div)
	Blcok(div,{key:0})
	Block(div,{key:1})

父节点除了会收集动态节点之外,也会收集子block。 更新时因key值不同会进行删除重新创建

v-for

随着v-for变量的变化也会导致虚拟DOM树变得不稳定

<div>
    <div v-for="item in fruits">{{item}}</div>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.fruits, (item) => {
    return (_openBlock(), _createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */))
  }), 256 /* UNKEYED_FRAGMENT */))
}

可以试想一下,如果不增加这个block,前后元素不一致是无法做到靶向更新的。因为dynamicChildren中还有可能有其他层级的元素。同时这里还生成了一个Fragment,因为前后元素个数不一致,所以称之为不稳定序列

稳定Fragment

这里是可以靶向更新的, 因为稳定则有参照物

<div>
    <div v-for="item in 3">{{item}}</div>  
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    (_openBlock(), _createElementBlock(_Fragment, null, _renderList(3, (item) => {
      return _createElementVNode("div", null, _toDisplayString(item), 1 /* TEXT */)
    }), 64 /* STABLE_FRAGMENT */))
  ]))
}

静态提升

<div>
  <span>hello</span> 
  <span a=1 b=2>{{name}}</span>
  <a><span>{{age}}</span></a>
</div>

我们把模板直接转化成render函数是这个酱紫的,那么问题就是每次调用render函数都要重新创建虚拟节点。

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "hello"),
    _createElementVNode("span", {
      a: "1",
      b: "2"
    }, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello", -1 /* HOISTED */)
const _hoisted_2 = {
  a: "1",
  b: "2"
}

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("span", _hoisted_2, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}

静态提升则是将静态的节点或者属性提升出去。静态提升是以树为单位。也就是说树中节点有动态的不会进行提升。

预字符串化

静态提升的节点都是静态的,我们可以将提升出来的节点字符串化。 当连续静态节点超过20个时,会将静态节点序列化为字符串。

<div>
  <span></span>
       ...
       ...
  <span></span>
</div>
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>....</span>", 20)

缓存函数

<div @click="e=>v=e.target.value"></div>

每次调用render的时都要创建新函数

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    onClick: e=>_ctx.v=e.target.value
  }, null, 8 /* PROPS */, ["onClick"]))
}

开启函数缓存后,函数会被缓存起来,后续可以直接使用

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    onClick: _cache[0] || (_cache[0] = e=>_ctx.v=e.target.value)
  }))
}

模版转化AST语法树

模板编译 Vue中对template属性会编译成render方法。在线模板编译器

增添新的包compiler-core/package.json

{
    "name": "@vue/compiler-core",
    "version": "1.0.0",
    "description": "@vue/compiler-core",
    "main": "index.js",
    "module": "dist/compiler-core.esm-bundler.js",
    "buildOptions": {
        "name": "VueCompilerCore",
        "compat": true,
        "formats": [
            "esm-bundler",
            "cjs"
        ]
    }
}

我们将开发环境下的打包入口改为 compile-core,这里我们先提供所需要ast的节点类型

export function compile(template){
    // 1.将模板转化成ast语法树
    const ast = baseParse(template);
    // 2.对ast语法树进行转化
    transform(ast);
    // 3.生成代码
    return generate(ast)
}

生成ast语法树

准备语法树相关type

export const enum NodeTypes {
    ROOT, // 根节点
    ELEMENT, // 元素
    TEXT, // 文本
    COMMENT, // 注释
    SIMPLE_EXPRESSION, // 简单表达式
    INTERPOLATION, // 模板表达式
    ATTRIBUTE,
    DIRECTIVE,
    // containers
    COMPOUND_EXPRESSION, // 复合表达式
    IF,
    IF_BRANCH,
    FOR,
    TEXT_CALL, // 文本调用
    // codegen
    VNODE_CALL, // 元素调用
    JS_CALL_EXPRESSION, // js调用表达式
}

创建解析上下文

创建解析上下文,并且根据类型做不同的处理解析。 ast转化

function createParserContext(content) {
    return {
        line: 1,
        column: 1,
        offset: 0,
        source: content, // source会不停的被截取
        originalSource: content // 原始内容
    }
}
function isEnd(context) {
    const source = context.source;
    return !source;
}
function parseChildren(context) {
    const nodes = [];
    while (!isEnd(context)) {
        const s = context.source;
        let node;
        if (s.startsWith('{{')){ // 处理表达式类型
        }else if(s[0] === '<'){ // 标签的开头
            if(/[a-z]/i.test(s[1])){} // 开始标签
        }
        if(!node){ // 文本的处理
            
        }
        nodes.push(node);
    }
    return nodes;
}
function baseParse(template){
    const context =  createParserContext(template);
    return parseChildren(context);
}

处理文本节点

采用假设法获取文本结束位置

function parseText(context) { // 123123{{name}}</div>
    const endTokens = ['<', '{{'];
    let endIndex = context.source.length; // 文本的总长度
    // 假设遇到 < 就是文本的结尾 。 在假设遇到{{ 是文本结尾。 最后找离的近的
    // 假设法
    for (let i = 0; i < endTokens.length; i++) {
        const index = context.source.indexOf(endTokens[i], 1);
        if (index !== -1 && endIndex > index) {
            endIndex = index;
        }
    }
}

处理文本内容,删除匹配到的结果,计算最新上下文位置信息

function parseText(context) {
    // ...
    let start = getCursor(context); // 1.获取文本开始位置
    const content = parseTextData(context, endIndex); // 2.处理文本数据

    return {
        type: NodeTypes.TEXT,
        content,
        loc: getSelection(context, start) // 3.获取全部信息
    }
}
function getCursor(context) { // 获取当前位置
    let { line, column, offset } = context;
    return { line, column, offset }
}
function parseTextData(context, endIndex) {
    const rawText = context.source.slice(0, endIndex);
    advanceBy(context, endIndex); // 截取内容
    return rawText
}
function advanceBy(context, endIndex) {
    let s = context.source;
    advancePositionWithMutation(context, s, endIndex) // 更改位置信息
    context.source = s.slice(endIndex);
}
function advancePositionWithMutation(context, s, endIndex) { // 更新最新上下文信息
    let linesCount = 0; // 计算行数
    let linePos = -1; // 计算其实行开始位置
    for (let i = 0; i < endIndex; i++) {
        if (s.charCodeAt(i) === 10) { // 遇到\n就增加一行
            linesCount++;
            linePos = i; // 记录换行后的字节位置
        }
    }
    context.offset += endIndex; // 累加偏移量
    context.line += linesCount; // 累加行数
    // 计算列数,如果无换行,则直接在原列基础 + 文本末尾位置,否则 总位置减去换行后的字节位置
    context.column = linePos == -1 ? context.column + endIndex : endIndex - linePos
}
function getSelection(context,start){
    const end = getCursor(context);
    return {
        start,
        end,
        source:context.originalSource.slice(start.offset,end.offset)
    }
}

转化成最终ast节点结果,标记ast节点类型

处理表达式节点

获取表达式中的变量,计算表达式的位置信息

function parseInterpolation(context) { 
    const start = getCursor(context); // 获取表达式的开头位置
    const closeIndex = context.source.indexOf('}}', '{{'); // 找到结束位置
    advanceBy(context, 2); // 去掉  {{
    const innerStart = getCursor(context); // 计算里面开始和结束
    const innerEnd = getCursor(context);
    const rawContentLength = closeIndex - 2; // 拿到内容
    const preTrimContent = parseTextData(context, rawContentLength);
    const content = preTrimContent.trim(); 
    const startOffest = preTrimContent.indexOf(content);
    if (startOffest > 0) { // 有空格
        advancePositionWithMutation(innerStart, preTrimContent, startOffest); // 计算表达式开始位置
    }
    const endOffset = content.length + startOffest;
    advancePositionWithMutation(innerEnd, preTrimContent, endOffset)
    advanceBy(context, 2);
    return {
        type: NodeTypes.INTERPOLATION,
        content: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            isStatic: false,
            content,
            loc: getSelection(context, innerStart, innerEnd) // 需要修改getSelection方法
        },
        loc: getSelection(context, start)
    }
}

处理元素节点

处理标签

获取标签名称,更新标签位置信息

function advanceSpaces(context){
    const match = /^[ \t\r\n]+/.exec(context.source);
    if(match){
        advanceBy(context,match[0].length);
    }
}
function parseTag(context){
    const start = getCursor(context); // 获取开始位置
    const match = /^</?([a-z][^ \t\r\n/>]*)/.exec(context.source); // 匹配标签名
    const tag = match[1];
    advanceBy(context,match[0].length); // 删除标签
    advanceSpaces(context); // 删除空格
    const isSelfClosing = context.source.startsWith('/>'); // 是否是自闭合
    advanceBy(context,isSelfClosing?2:1); // 删除闭合 /> >
    return {
        type:NodeTypes.ELEMENT,
        tag,
        isSelfClosing,
        loc:getSelection(context,start) 
    }
}
function parseElement(context) {
    // 1.解析标签名 
    let ele = parseTag(context);
    if(context.source.startsWith('</')){
        parseTag(context); // 解析标签,标签没有儿子,则直接更新标签信息的结束位置
    }
    ele.loc = getSelection(context,ele.loc.start); // 更新最终位置
    return ele;
}

处理子节点

递归处理子节点元素

function isEnd(context) {
    const source = context.source;
    if(context.source.startsWith('</')){ // 如果遇到结束标签说明没有子节点
        return true;
    }
    return !source;
}
function parseElement(context) {
    let ele = parseTag(context);
    const children = parseChildren(context); // 因为结尾标签, 会再次触发parseElement,这里如果是结尾需要停止
    if(context.source.startsWith('</')){
        parseTag(context); 
    }
    ele.loc = getSelection(context,ele.loc.start); // 更新最终位置
    (ele as any).children = children; // 添加children
    return ele;
}

处理属性

在处理标签后处理属性

function parseTag(context){
    const start = getCursor(context); 
    const match = /^</?([a-z][^ \t\r\n/>]*)/.exec(context.source); 
    const tag = match[1];
    advanceBy(context,match[0].length); 
    advanceBySpaces(context);
    let props = parseAttributes(context); // 处理属性
    // ......
    return {
        type:NodeTypes.ELEMENT,
        tag,
        isSelfClosing,
        loc:getSelection(context,start),
        props
    }
}
function parseAttributes(context) {
    const props: any = [];
    while (context.source.length > 0 && !context.source.startsWith('>')) {
        const attr = parseAttribute(context)
        props.push(attr);
        advanceSpaces(context); // 解析一个去空格一个
    }
    return props
}
function parseAttribute(context) {
    const start = getCursor(context);
    const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
    const name = match[0]; // 捕获到属性名
    advanceBy(context, name.length); // 删除属性名

    let value
    if (/^[\t\r\n\f ]*=/.test(context.source)) { // 删除空格 等号
        advanceSpaces(context);
        advanceBy(context, 1);
        advanceSpaces(context);
        value = parseAttributeValue(context); // 解析属性值
    }
    const loc = getSelection(context, start)
    return {
        type: NodeTypes.ATTRIBUTE,
        name,
        value: {
            type: NodeTypes.TEXT,
            content: value.content,
            loc: value.loc
        },
        loc
    }
}
function parseAttributeValue(context) {
    const start = getCursor(context);
    const quote = context.source[0];
    let content
    const isQuoteed = quote === '"' || quote === "'";
    if (isQuoteed) {
        advanceBy(context, 1);
        const endIndex = context.source.indexOf(quote); 
        content = parseTextData(context, endIndex);  // 解析引号中间的值
        advanceBy(context, 1);
    }
    return { content, loc: getSelection(context, start) }
}

处理空节点

function parseChildren(context) {
    const nodes: any = [];
    while (!isEnd(context)) {
        //....
    }
    for(let i = 0 ;i < nodes.length; i++){
        const node = nodes[i];
        if(node.type == NodeTypes.TEXT){ // 如果是文本 删除空白文本,其他的空格变为一个
            if(!/[^\t\r\n\f ]/.test(node.content)){
                nodes[i] = null
            }else{
                node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
            }
        }
    }
    return nodes.filter(Boolean)
}

创建根节点

将解析出的节点,再次进行包裹,这样可以支持模板下多个根节点的情况, 也是我们常说的 Fragment

export function createRoot(children,loc){
   return {
       type:NodeTypes.ROOT,
       children,
       loc
   }
}
function baseParse(template) {
   // 标识节点的信息  行 列 偏移量
   const context = createParserContext(template);
   const start = getCursor(context);
   return createRoot(
       parseChildren(context),
       getSelection(context,start)
   )
}

遍历AST语法树

我们需要遍历ast语法树,访问树中节点进行语法树的转化

function transformElement(node){
    console.log('元素处理',node)
}
function transformText(node){
    console.log('文本处理',node)
}
function transformExpression(node){
    console.log('表达式')
}
function traverseNode(node,context){
    context.currentNode = node;
    const transforms = context.nodeTransforms;
    for(let i = 0; i < transforms.length;i++){
        transforms[i](node,context); // 调用转化方法进行转化
        if(!context.currentNode) return
    }
    switch(node.type){
        case NodeTypes.ELEMENT:
        case NodeTypes.ROOT:
            for(let i = 0; i < node.children.length;i++){
                context.parent = node;
                traverseNode(node.children[i],context);
            }
            
    }
}

function createTransformContext(root){
    const context = {
        currentNode:root, // 当前转化节点 
        parent:null,   // 当前转化节点的父节点
        nodeTransforms:[ // 转化方法
            transformElement,
            transformText,
            transformExpression
        ],
        helpers: new Map(), // 创建帮助映射表,记录调用方法次数
        helper(name){
            const count = context.helpers.get(name) || 0;
            context.helpers.set(name,count+1)
            return name
        }
    }
    return context
}

function transform(root){
    // 创建转化的上下文, 记录转化方法及当前转化节点
    let context = createTransformContext(root)
    // 递归遍历
    traverseNode(root,context)
}
export function compile(template){
    const ast = baseParse(template);
    transform(ast);
}

退出函数

表达式不需要退出函数,直接处理即可。元素需要在遍历完所有子节点在进行处理

function transformExpression(node){
    if(node.type == NodeTypes.INTERPOLATION){
        console.log('表达式')
    }
}
function transformElement(node){
    if(node.type === NodeTypes.ELEMENT ){
        return function postTransformElement(){ // 元素处理的退出函数
            // 如果这个元素
            console.log('元素',node)
        }
    }
}
function transformText(node){
    if(node.type === NodeTypes.ELEMENT || node.type === NodeTypes.ROOT){
        return ()=>{
            console.log('元素/root',node)
        }   
    }
}
function traverseNode(node,context){
  	// ...
    for(let i = 0; i < transforms.length;i++){
        let onExit = transforms[i](node,context); // 调用转化方法进行转化
        if(onExit){
            exitsFns.push(onExit)
        }
        if(!context.currentNode) return
    }
    // ...
    // 最终context.currentNode 是最里面的
    context.currentNode = node; // 修正currentNode;
    let i = exitsFns.length
    while (i--) {
        exitsFns[i]()
    }
}

转化表达式

runtimeHelpers.ts

export const TO_DISPLAY_STRING = Symbol(`toDisplayString`);
export const helperNameMap = {
    [TO_DISPLAY_STRING]: "toDisplayString",
}
switch(node.type){
    case NodeTypes.INTERPOLATION:
        context.helper(TO_DISPLAY_STRING); // 用于JSON.stringify
        break
    // ...
}

最后生成的代码我们取值时需要从上下文中进行取值。

export function transformExpression(node){
    if(node.type == NodeTypes.INTERPOLATION){
        node.content.content = `_ctx.${node.content.content}`; // 修改content信息
    }
}

转化文本元素

文本元素的转化我们需要将多个文本连接起来,如果是动态文本添加patchFlags标识,最终生成文本调用逻辑

function isText(node) {
    return node.type == NodeTypes.INTERPOLATION || node.type == NodeTypes.TEXT;
}
export function transformText(node,context){
    if(node.type === NodeTypes.ELEMENT || node.type === NodeTypes.ROOT){
        return ()=>{
              // 如果这个元素
              let hasText = false;
              const children = node.children;
               let currentContainer = undefined // 合并儿子
               for (let i = 0; i < children.length; i++) {
                   let child = children[i];
                   if (isText(child)) {
                       hasText = true;
                       for (let j = i + 1; j < children.length; j++) {
                           const next = children[j];
                           if(isText(next)){
                               if(!currentContainer){
                                   currentContainer = children[i] = { // 合并表达式
                                       type:NodeTypes.COMPOUND_EXPRESSION,
                                       loc:child.loc,
                                       children:[child]
                                   }
                               }
                               currentContainer.children.push(` + `,next);
                               children.splice(j,1);
                               j--;
                           }else{
                               currentContainer = undefined;
                               break;
                           }
                       }
                   }
               }
              if (!hasText || children.length == 1) { // 一个元素不用管,可以执行innerHTML
                  return
              }
              for (let i = 0; i < children.length; i++) {
                  const child = children[i]
                  if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) {
                      const callArgs = []
                      callArgs.push(child)
                      if (child.type !== NodeTypes.TEXT) { // 如果不是文本
                          callArgs.push(PatchFlags.TEXT + '')
                      }
                      // 全部格式话成文本调用
                      children[i] = {
                          type: NodeTypes.TEXT_CALL, // 最终需要变成createTextVnode() 增加patchFlag
                          content: child,
                          loc: child.loc, 
                          codegenNode: createCallExpression(context,callArgs) // 创建表达式调用
                      }
                  }
              }
        }   
    }
}

runtimeHelpers.ts

export const CREATE_TEXT = Symbol(`createTextVNode`)
export const helperNameMap = {
    [CREATE_TEXT]:`createTextVNode`
}

ast.ts

export function createCallExpression(context,args){
    let callee = context.helper(CREATE_TEXT); // 生成代码时需要createTextVNode方法
    return {
        callee,
        type: NodeTypes.JS_CALL_EXPRESSION,
        arguments: args
    }
}

转化元素

export function createObjectExpression(properties){
    return {
        type: NodeTypes.JS_OBJECT_EXPRESSION,
        properties
    }
}
export function transformElement(node,context){
    if(node.type === NodeTypes.ELEMENT ){
        return function postTransformElement(){ // 元素处理的退出函数
            let vnodeTag = `'${node.tag}'`;
            let properties = [];
            let props = node.props
           for(let i = 0 ; i< props.length; i++){ // 这里属性其实应该在codegen里在处理
                properties.push({
                    key:props[i].name,
                    value:props[i].value.content
                })
            }
            const propsExpression = props.length > 0 ? createObjectExpression(properties):null
            let vnodeChildren = null;
            if (node.children.length === 1) {
                // 只有一个孩子节点 ,那么当生成 render 函数的时候就不用 [] 包裹
                const child = node.children[0];
                vnodeChildren = child;
            }else{ 
                if(node.children.length > 0){ // 处理儿子节点
                    vnodeChildren = node.children
                }
           }
           // 代码生成
           node.codegenNode = createVNodeCall(context,vnodeTag,propsExpression,vnodeChildren);
        }
    }
}

runtimeHelpers.ts

export const CREATE_ELEMENT_VNODE = Symbol("createElementVNode");
export const helperNameMap = {
    [CREATE_ELEMENT_VNODE]: "createElementVNode", // 创建元素节点标识
}
export function createVNodeCall(context,tag,props,children){
    context.helper(CREATE_ELEMENT_VNODE);
    return {
        type:NodeTypes.VNODE_CALL,
        tag,
        props,
        children
    }
}

转化根节点

转化根节点需要添加block节点

export const FRAGMENT = Symbol("FRAGMENT");
export const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`)
export const OPEN_BLOCK = Symbol(`openBlock`)
export const helperNameMap = {
    [FRAGMENT]: "Fragment",
    [OPEN_BLOCK]: `openBlock`,  // block处理
    [CREATE_ELEMENT_BLOCK]: `createElementBlock`
}
function createTransformContext(root){
    const context = {
        removeHelper(name){
            const count = context.helpers.get(name);
            if(count){
                const currentCount = count - 1;
                if(!currentCount){
                    context.helpers.delete(name);
                }else{
                    context.helpers.set(name,currentCount)
                }
            }
        },
        // ...
    }
    return context
}
function createRootCodegen(root,context){
    let {children } = root
    if(children.length == 1){
        const child = children[0];
        if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
            const codegenNode = child.codegenNode;
            root.codegenNode = codegenNode; 
            context.removeHelper(CREATE_ELEMENT_VNODE); // 不要创建元素
            context.helper(OPEN_BLOCK) 
            context.helper(CREATE_ELEMENT_BLOCK); // 创建元素block就好了
            root.codegenNode.isBlock = true; // 只有一个元素节点,那么他就是block节点
          } else {
            root.codegenNode = child; // 直接用里面的节点换掉
          }
    }else{
        root.codegenNode = createVNodeCall(context,context.helper(FRAGMENT),undefined,root.children)
        context.helper(OPEN_BLOCK)
        context.helper(CREATE_ELEMENT_BLOCK)
        root.codegenNode.isBlock = true; // 增加block fragment
    }
}
export function transform(root){
    // 创建转化的上下文, 记录转化方法及当前转化节点
    let context = createTransformContext(root)
    // 递归遍历
    traverseNode(root,context)
    createRootCodegen(root,context); // 生成根节点的codegen
    root.helpers = [...context.helpers.keys()]
}

整个transform的流程,就是给ast节点添加codegenNode属性,用于方便生成对应的代码,并且收集生成代码所需的方法。

创建生成上下文

生成代码时我们需要将生成的代码拼接成字符串,同时添加换行缩进等。

function createCodegeContext(){
    const context = {
        code:``,
        indentLevel:0,
        helper(key){ return `_${helperNameMap[key]}`},
        push(code){context.code += code;},
        indent(){ // 前进
            newline(++context.indentLevel)
        },
        deindent(withoutnewline = false){ // 缩进
            if (withoutnewline) {
                --context.indentLevel
            } else {
                newline(--context.indentLevel)
            }
        },
        newline(){ newline(context.indentLevel) } // 换行
    }
    function newline(n){ context.push('\n'+`  `.repeat(n))}
    return context
}
function generate(ast){
    const context = createCodegeContext();
}
export function compile(template){
    // 1.将模板转化成ast语法树
    const ast = baseParse(template);
    // 2.对ast语法树进行转化
    transform(ast);
    // // 3.生成代码
    return generate(ast)
}

生成文本代码

 let r = compile('hello, jw')

1

如果仅仅是文本直接返回即可~

function genText(node,context){
    context.push(JSON.stringify(node.content)) // 添加文本代码
}
function genFunctionPreamble(ast,context){ // 生成函数
    const {push,newline} = context
    if(ast.helpers.length > 0){ // 生成导入语句
       push(`const {${ast.helpers.map((s)=>`${helperNameMap[s]}:_${helperNameMap[s]}`).join(', ')}} = Vue`)
    }
    newline()
    push(`return `)
}
function genNode(node,context){
    switch(node.type){
        case NodeTypes.TEXT:
            genText(node,context) 
            break;
    }
}
function generate(ast){
    const context = createCodegeContext();
    const {push,indent} = context
    genFunctionPreamble(ast,context);
    const functionName = 'render';
    const args = ['_ctx','$props'];
    push(`function ${functionName}(${args.join(', ')}){`)
    indent();
    push(`return `)
    if (ast.codegenNode) {
        genNode(ast.codegenNode, context)
    } else {
        push(`null`)
    }
    return context.code
}

生成表达式代码

let r = compile('{{age}}')
function genExpression(node,context){
    const { content } = node
    context.push(content)
}
function genInterpolation(node,context){
    const { push, helper } = context
    push(`${helper(TO_DISPLAY_STRING)}(`)
    genNode(node.content, context)
    push(`)`)
}
function genNode(node,context){
    switch(node.type){
        case NodeTypes.TEXT:
            genText(node,context)
            break;
        case NodeTypes.INTERPOLATION: // 生成表达式
            genInterpolation(node,context)
            break; 
        case NodeTypes.SIMPLE_EXPRESSION: // 简单表达式的处理
            genExpression(node, context)
            break
    }
}

生成元素表达式

let r = compile(`<div a='1' b='2'>123</div>`)
function genNodeList(nodes,context){ // 生成节点列表,用","分割
    const { push } = context;
    for (let i = 0; i < nodes.length; i++) {
        const node = nodes[i];
        if (isString(node)) {
            push(`${node}`); // 如果是字符串直接放入
        }else if(Array.isArray(node)){
            genNodeList(node, context)
        } else {
            genNode(node, context);
        }
        if (i < nodes.length - 1) {
            push(", ");
        }
    }
}
function genVNodeCall(node,context){
    const {push,helper} = context;
    const {tag,props,children,isBlock} = node

    if(isBlock){
        push(`(${helper(OPEN_BLOCK)}(),`)
    }
    // 生成createElementBlock或者createElementVnode
    const callHelper = isBlock ? CREATE_ELEMENT_BLOCK: CREATE_ELEMENT_VNODE; 
    push(helper(callHelper));
    push('(');
    genNodeList([tag, props, children].map(item=>item || 'null'), context);
    push(`)`)
    if(isBlock){
        push(`)`)
    }
}
function genNode(node,context){
    switch(node.type){
        case NodeTypes.VNODE_CALL: // 元素调用
            genVNodeCall(node,context);
            break;
    }
}

生成元素属性

function genObjectExpression(node, context) {
    const { push, newline } = context
    const { properties } = node
    if (!properties.length) {
        push(`{}`)
        return
      }
    push('{')
    for (let i = 0; i < properties.length; i++) {
        const { key, value } = properties[i]
        // key
        push(key);
        push(`: `)
        push(JSON.stringify(value));
        // value
        if (i < properties.length - 1) {
          push(`,`)
        }
    }
    push('}')
}
function genNode(node,context){
    switch(node.type){
        case NodeTypes.JS_OBJECT_EXPRESSION: // 元素属性
            genObjectExpression(node, context)
            break
    }
}

处理Fragment情况

如果是Fragment,则将字符串和symbol直接放入代码中,对元素再次进行递归处理

let r = compile(`<div>123</div><div>123</div>`)
function genNode(node,context){
    if (isString(node)) {
        context.push(node)
        return
    }
    if (isSymbol(node)) {
        context.push(context.helper(node))
        return
    }
    switch(node.type){
        // ...
        case NodeTypes.ELEMENT:
            genNode(node.codegenNode,context)

    }
}

处理复合表达式

let r = compile(`<div>123 {{abc}}</div>`)
function genCompoundExpression(node,context) {
    for (let i = 0; i < node.children!.length; i++) {
        const child = node.children![i]
        if (isString(child)) {
            context.push(child)
        } else {
            genNode(child, context)
        }
    }
}
function genNode(node,context){
    switch(node.type){
        // ...
        case NodeTypes.COMPOUND_EXPRESSION:
            genCompoundExpression(node, context)
            break
    }
}

复合表达式+元素处理#

let r = compile(`<div>123 {{name}} <span>{{name}}</span></div>`)
function genCallExpression(node, context) {
    const { push, helper } = context
    const callee = helper(node.callee)

    push(callee + `(`, node)
    genNodeList(node.arguments, context)
    push(`)`)
}
function genNode(node,context){
    switch(node.type){
        // ...
        case NodeTypes.TEXT_CALL: // 对文本处理
            genNode(node.codegenNode, context)
            break
        case NodeTypes.JS_CALL_EXPRESSION: // 表达式处理
            genCallExpression(node, context)
            break
    }
}