我理解的 v-for 指令的转换过程

1. 前置知识

在 Vue 2 中,模板编译的核心流程是将模板字符串解析为抽象语法树(AST),接着把 AST 转换为渲染函数,渲染函数返回虚拟 DOM(VNode),最后将虚拟 DOM 渲染为真实的 DOM。v-for 指令用于循环渲染列表,在编译过程中会被转换为相应的 JavaScript 代码来实现循环创建虚拟 DOM 节点。

2. 详细转换过程

步骤 1:模板解析为 AST

对于模板 <li v-for="item in items" :key="item.id">{{ item.name }}</li>,我们需要将其解析为 AST:

ruby 复制代码
{
    "type":1,
    "tag":"li",
    "attrsList":[],
    "attrsMap":{},
    "vFor":"item in items",
    "key":"item.id",
    "children":[
        {"type":2,"expression":"item.name"}
    ]
}

以下是实现该解析的 JavaScript 代码:

ini 复制代码
function parseTemplate(template) {
    template = template.trim();
    let startIndex = template.indexOf('<') + 1;
    let endIndex = template.indexOf(' ', startIndex);
    if (endIndex === -1) {
        endIndex = template.indexOf('>', startIndex);
    }
    const tag = template.slice(startIndex, endIndex);

    const attrsList = [];
    const attrsMap = {};
    let vFor;
    let key;
    let attrStart = endIndex;

    while (attrStart < template.length) {
        attrStart = template.indexOf(' ', attrStart);
        if (attrStart === -1 || template[attrStart + 1] === '>') {
            break;
        }
        attrStart += 1; // Skip the space
        
        const attrEnd = template.indexOf('=', attrStart);
        if (attrEnd === -1) break;
        
        const attrName = template.slice(attrStart, attrEnd);
        const valueStart = template.indexOf('"', attrEnd);
        if (valueStart === -1) break;
        
        const valueEnd = template.indexOf('"', valueStart + 1);
        if (valueEnd === -1) break;
        
        const attrValue = template.slice(valueStart + 1, valueEnd);

        if (attrName === 'v-for') {
            vFor = attrValue;
        } else if (attrName === ':key') {
            key = attrValue;
        } else {
            attrsList.push({ name: attrName, value: attrValue });
            attrsMap[attrName] = attrValue;
        }
        attrStart = valueEnd + 1;
    }

    const contentStart = template.indexOf('>', endIndex) + 1;
    const contentEnd = template.lastIndexOf('</');
    const textContent = template.slice(contentStart, contentEnd).trim();
    
    let children = [];
    if (textContent) {
        const interpolationRegex = /{{(.+?)}}/g;
        let lastIndex = 0;
        let match;
        
        while ((match = interpolationRegex.exec(textContent)) !== null) {
            if (match.index > lastIndex) {
                children.push({
                    type: 3,
                    text: textContent.slice(lastIndex, match.index)
                });
            }
            
            children.push({
                type: 2,
                expression: match[1].trim()
            });
            
            lastIndex = match.index + match[0].length;
        }
        
        if (lastIndex < textContent.length) {
            children.push({
                type: 3,
                text: textContent.slice(lastIndex)
            });
        }
    }

    const ast = {
        type: 1,
        tag,
        attrsList,
        attrsMap,
        vFor,
        key,
        children
    };
    return ast;
}

const template = '<li v-for="item in items" :key="item.id">{{ item.name }}</li>';
const ast = parseTemplate(template);
console.log(ast);

上述代码中,parseTemplate 函数会解析模板字符串,识别出 v-for:key 指令,并将其信息存储在 AST 中。

步骤 2:处理 v-for 指令并转换为 JavaScript 代码

在得到 AST 后,我们需要将 v-for 指令转换为对应的 JavaScript 代码。以下是实现该转换的代码:

javascript 复制代码
function transformVFor(ast) {
    if (!ast.vFor) return null;
    
    try {
        const [alias, iterable] = ast.vFor.split(' in ');
        const key = ast.key ? ast.key : 'null';
        
        // 处理子节点内容
        let childContent = '';
        if (ast.children && ast.children.length > 0) {
            // 处理文本节点和插值表达式
            childContent = ast.children.map(child => {
                if (child.type === 3) { // 文本节点
                    return `'${child.text}'`;
                } else if (child.type === 2) { // 插值表达式
                    return child.expression;
                }
                return '';
            }).join(' + ');
        }

        return `this.${iterable.trim()}.map((${alias.trim()}) => {
            return _c('${ast.tag}', {
                key: ${key}
            }, [_v(${childContent || "''"})])
        })`;
    } catch (error) {
        console.error('Error transforming v-for:', error);
        return null;
    }
}

const loopCode = transformVFor(ast);
console.log(loopCode);

transformVFor 函数会解析 v-for 指令的语法,将其转换为一个 map 函数调用,用于循环创建虚拟 DOM 节点:

javascript 复制代码
this.items.map((item) => {
            return _c('li', {
                key: item.id
            }, [_v(item.name)])
        })
步骤 3:生成渲染函数

将处理后的代码整合到渲染函数中。以下是一个简化的渲染函数生成过程:

javascript

scss 复制代码
function generateRenderFunction(loopCode) {
    return `with(this) { return _c('ul', {}, ${loopCode}) }`;
}

const renderFunction = generateRenderFunction(loopCode);
console.log(renderFunction);

generateRenderFunction 函数会将 v-for 转换后的代码插入到渲染函数中,生成最终的渲染函数:

javascript 复制代码
with(this) { return _c('ul', {}, this.items.map((item) => {
            return _c('li', {
                key: item.id
            }, [_v(item.name)])
        })) }
步骤 4:执行渲染函数生成虚拟 DOM

当渲染函数被执行时,会根据当前组件的状态生成虚拟 DOM。以下是一个简化的虚拟 DOM 生成过程:

javascript

javascript 复制代码
// 模拟 Vue 实例
const vm = {
    items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
    ]
};

// 模拟 Vue 的 _c 和 _v 函数
function _c(tag, data, children) {
    return {
        tag,
        data,
        children
    };
}

function _v(text) {
    return {
        type: 3,
        text
    };
}

// 执行渲染函数
const vnode = new Function(renderFunction).call(vm);
console.log(vnode);

这里模拟了一个 Vue 实例 vm,包含 items 数组。_c 函数用于创建虚拟 DOM 节点,_v 函数用于创建文本虚拟节点。通过 new Function(renderFunction).call(vm) 执行渲染函数,生成虚拟 DOM 节点:

json 复制代码
{
    "tag":"ul",
    "data":{},
    "children":[
        {"tag":"li","data":{"key":1},"children":[{"type":3,"text":"Item 1"}]},
        {"tag":"li","data":{"key":2},"children":[{"type":3,"text":"Item 2"}]},
        {"tag":"li","data":{"key":3},"children":[{"type":3,"text":"Item 3"}]}
    ]
}
步骤 5:虚拟 DOM 渲染为真实 DOM

最后,Vue 会比较新旧虚拟 DOM 的差异,只更新需要更新的真实 DOM 节点。以下是一个简化的渲染过程:

javascript

ini 复制代码
function renderVNode(vnode) {
    if (vnode.type === 3) {
        return document.createTextNode(vnode.text);
    }
    const el = document.createElement(vnode.tag);
    if (vnode.data) {
        for (const key in vnode.data) {
            if (key === 'key') {
                continue;
            }
            el.setAttribute(key, vnode.data[key]);
        }
    }
    if (vnode.children) {
        vnode.children.forEach(child => {
            el.appendChild(renderVNode(child));
        });
    }
    return el;
}

const realDom = renderVNode(vnode);
document.body.appendChild(realDom);

renderVNode 函数会递归地将虚拟 DOM 节点转换为真实的 DOM 节点,并添加到页面中。

3. 转换后结果

最终渲染的 HTML 如下:

html

xml 复制代码
<ul>
    <li key="1">Item 1</li>
    <li key="2">Item 2</li>
    <li key="3">Item 3</li>
</ul>

可以看到,根据 dataitems 数组的长度生成了多个 li 标签。

总结

通过以上步骤,我们详细讲解了 v-for 指令从模板到最终渲染的 HTML 的转换过程,并通过代码实现了这个过程。核心在于将 v-for 指令转换为 JavaScript 代码,使用循环创建多个虚拟 DOM 节点,最终渲染为真实的 DOM 列表。

相关推荐
2501_915373883 小时前
Vue 3零基础入门:从环境搭建到第一个组件
前端·javascript·vue.js
运维@小兵6 小时前
vue开发用户注册功能
前端·javascript·vue.js
香蕉可乐荷包蛋9 小时前
vue数据可视化开发echarts等组件、插件的使用及建议-浅看一下就行
vue.js·信息可视化·echarts
老马啸西风9 小时前
sensitive-word-admin v2.0.0 全新 ui 版本发布!vue+前后端分离
vue.js·ui·ai·nlp·github·word
湛海不过深蓝9 小时前
【ts】defineProps数组的类型声明
前端·javascript·vue.js
layman05289 小时前
vue 中的数据代理
前端·javascript·vue.js
layman052810 小时前
vue中理解MVVM
前端·javascript·vue.js
鸡鸭扣12 小时前
DRF/Django+Vue项目线上部署:腾讯云+Centos7.6(github的SSH认证)
前端·vue.js·python·django·腾讯云·drf
2401_8319433213 小时前
Element Plus对话框(ElDialog)全面指南:打造灵活弹窗交互
前端·vue.js·交互
计算机学姐13 小时前
基于SpringBoot的在线教育管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis