Skip to content
本页目录

render 渲染函数

编译流程

template 通过 parse() 方法 -> ast -> 通过 generate() 方法生成 render 函数 -> 通过 render 函数生成 VNode -> patch() 方法生成真实 DOM

  • parse:语法分析器,将 template 生成 AST(抽象语法树) var ast = parse(template, options);
  • optimize:对 AST 进行优化处理,标记静态节点
  • generate:对 AST 对象转为字符串形式的 JS 代码

1. parse 函数

parse(template, options) 是将 template 模板语法编译为 ast 的函数。它的两个参数分别是:

  • template 就是 .vue 文件中的模板字符串;
  • options 就是实例化组件时的 options 配置项,可以配置解析规则,如是否保留 html 注释,模板语法分隔符规则等。

1.1 parseHTML 函数

parseHTML(html, options) 函数是 parse 函数的核心方法。

  1. 先遍历整个 html 字符串,使用正则将 html 拆分为对象数组结构,等待 createASTElement() 方法调用。
  2. createASTElement(tag, attrs, parent) 方法是将拆分后的数组转为 Ast 抽象语法树

2. generate 函数

generate(ast, options) 函数是将 Ast 语法树转换为 render 的方法。

JS
function generate(ast, options) {

    const state = new CodegenState(options)
    const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'

    return {
        render: `with(this){ return ${code} }` // 函数字符串,
        staticRenderFns: state.staticRenderFns
    }
}
function generate(ast, options) {

    const state = new CodegenState(options)
    const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'

    return {
        render: `with(this){ return ${code} }` // 函数字符串,
        staticRenderFns: state.staticRenderFns
    }
}

2.1 genElement 函数

它会对传入的 ast 进行判断,不同类型的 Ast Element 会调用不同的方法并生成 render,比如:

JS
if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state) // 静态节点,optimize 函数优化结果
} else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)   // v-once
} else if (el.for && !el.forProcessed) {
    return genFor(el, state)    // v-for
} else if (el.if && !el.ifProcessed) {
    return genIf(el, state)     // v-if
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0' // 子组件
} else if (el.tag === 'slot') {
    return genSlot(el, state)   // 插槽
}
// 还有个大的组件的处理
if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state) // 静态节点,optimize 函数优化结果
} else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)   // v-once
} else if (el.for && !el.forProcessed) {
    return genFor(el, state)    // v-for
} else if (el.if && !el.ifProcessed) {
    return genIf(el, state)     // v-if
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0' // 子组件
} else if (el.tag === 'slot') {
    return genSlot(el, state)   // 插槽
}
// 还有个大的组件的处理

3. 生成 VNode

render 函数里面是一个 createElement 方法,它与模板生产的 render 中的 _c 是一样的。createElement 方法中主要有两个方法,一个是 VNode 构造函数,创建普通节点;一个是 createComponent,创建组件节点。

3.1 VNode 构造函数

createElement 中的 VNode 构造函数,传入对应参数实例化后将返回虚拟节点,它主要有两个方法:

  • createEmptyVNode:创建空节点
  • createTextVNode:创建虚拟文本节点

3.2 createComponent 方法

这个函数主要是提取出组件的 propsDatalisteners,并将其保存在 componentOptions 下。提取之后,VNodeData 中剩余的属性和事件属于 VNode 对应的 dom 元素。这种情况下我们想要给组件添加事件和属性时可能需要修改 vnode.componentOptions 下的 propsDatalisteners 以及 vnode.data.attrs,他们分别对应了组件内部的 $props,$listeners,$attrs

4. 生成真实 DOM

生成真实 dom 是从 _update 函数开始,之后调用 __patch__ 方法,对虚拟 dom 进行 diff 算法比较优化,在数据处理后,调用真实 dom 的一些操作方法,比如 insertBefore appendChild 等方法插入到真实的 dom 中。

关于 patch 方法,请看 diff 章节

完整过程伪代码实现

JS
let ast = parse(template, options) // 将模板生成 ast 语法树

optimize(ast, options) // 标记静态节点,就是无需动态改变的标签

let code = generate(ast, options); // 将 ast 生成 render 函数字符串

let render = with(this){ return ${code} };  // with 方法是为了将 this 指向 vm 实例,在某种程度上实现对于作用域的动态注入

let renderFn = new Function(render); // 将 render 字符串实例化为 render 函数,这就是我们 vue 中的 render() 方法了

_update(renderFn)   //
let ast = parse(template, options) // 将模板生成 ast 语法树

optimize(ast, options) // 标记静态节点,就是无需动态改变的标签

let code = generate(ast, options); // 将 ast 生成 render 函数字符串

let render = with(this){ return ${code} };  // with 方法是为了将 this 指向 vm 实例,在某种程度上实现对于作用域的动态注入

let renderFn = new Function(render); // 将 render 字符串实例化为 render 函数,这就是我们 vue 中的 render() 方法了

_update(renderFn)   //

render 函数在平时写 vue 时可能极少使用到,一般我们都会使用 template 实现模板,但 template 模板,会在 beforeMount 生命周期中被编译为 render 函数。

elememt-ui 中的自定义 message-box 组件,就需要传入一个 render 函数

区别:

  • template 模板编写容易;
  • render 函数灵活性高,性能更好(省去了编译步骤),优先级也更高;

语法:

JS
/**
 * @param { String | Object | Function } param1 html标签名称 | 组件 | 异步组件
 * @param { Object } [attribute] 模板中 attribute 对应的数据对象、样式、事件等
 * @param { String | Array } [VNodes] 文本节点或者 createElement() 创建的子节点
 * */ 
createElement(param1, attribute, VNodes)
/**
 * @param { String | Object | Function } param1 html标签名称 | 组件 | 异步组件
 * @param { Object } [attribute] 模板中 attribute 对应的数据对象、样式、事件等
 * @param { String | Array } [VNodes] 文本节点或者 createElement() 创建的子节点
 * */ 
createElement(param1, attribute, VNodes)
JS
// data 数据
data() {
    return {
        list: ['one', 'two', 'three']
    }
}
// data 数据
data() {
    return {
        list: ['one', 'two', 'three']
    }
}
  • 模板写法:
html
<template>
    <ul>
        <li v-for="item in list">{{ item }}</li>
    </ul>
</template>
<template>
    <ul>
        <li v-for="item in list">{{ item }}</li>
    </ul>
</template>
  • createElement 写法:
JS
render(createElement) {
    return createElement('ul', {}, this.list.map(item => {
        return createElement('li', {}, item)
    }))
}
render(createElement) {
    return createElement('ul', {}, this.list.map(item => {
        return createElement('li', {}, item)
    }))
}
  • JSX 写法:
JS
render(h) {
    return (
        <ul>
            { list.map(item => <li>{item}</li>) }
        </ul>
    )
}
render(h) {
    return (
        <ul>
            { list.map(item => <li>{item}</li>) }
        </ul>
    )
}

语法

JS
render(crateElmentFn,context){}
render(crateElmentFn,context){}