likes
comments
collection
share

Vue2剥丝抽茧-模版编译之静态render

作者站长头像
站长
· 阅读数 14

上篇文章 模版编译之生成AST 中将模版转为了 AST ,这篇文章会将 AST 转为最终的 render 函数。

静态节点

模版编译之生成AST 中我们转换的例子是 "<div><span>3<5吗</span><span>?</span></div>" ,可以看到所有的节点不涉及变量和任何 vue 的指令,因此当 虚拟 dom 重新 patch 的时候是不需要进行 diff 的,所以我们可以在生成 render 函数的时候将这些静态节点单独标记出来。

生成 render 函数前,我们对所有的 AST 进行遍历标记,options 提供一些平台相关的变量,isReservedTag 函数来判断是否是合法的标签,比如 divspan 等,虚拟dom之组件 有写过。

const options = {
    isReservedTag,
};
optimize(ast, options);
export function optimize(root, options) {
    if (!root) return;
    isPlatformReservedTag = options.isReservedTag;
    // first pass: mark all non-static nodes.
    markStatic(root);
    // second pass: mark static roots.
    markStaticRoots(root);
}

optimize 函数分为两步,第一步就是找出所有的静态节点 markStatic,第二步是在第一步的基础上找到静态根节点 markStaticRoots

看一下 markStatic 的实现:

function markStatic(node) {
    node.static = isStatic(node);
    if (node.type === 1) {
        if (!isPlatformReservedTag(node.tag)) {
            return;
        }
        for (let i = 0, l = node.children.length; i < l; i++) {
            const child = node.children[i];
            markStatic(child);
            if (!child.static) {
                node.static = false;
            }
        }
    }
}

首先调用了 isStatic 函数标记当前 node

function isStatic(node) {
    if (node.type === 2) {
        // expression
        return false;
    }
    if (node.type === 3) {
        // text
        return true;
    }
    return isPlatformReservedTag(node.tag);
}

如果是一个正常的节点默认就会标记为 isPlatformReservedTag(node.tag) 返回的 true

接下来会调用 for 循环判断子节点的情况,如果子节点存在非 static 的节点,当前节点会修正为 false

for (let i = 0, l = node.children.length; i < l; i++) {
  const child = node.children[i];
  markStatic(child);
  if (!child.static) {
    node.static = false;
  }
}

接下来是第二步,标记静态根节点 markStaticRoots

export function optimize(root, options) {
    if (!root) return;
    isPlatformReservedTag = options.isReservedTag;
    // first pass: mark all non-static nodes.
    markStatic(root);
    // second pass: mark static roots.
    markStaticRoots(root);
}
function markStaticRoots(node) {
    if (node.type === 1) {
        // For a node to qualify as a static root, it should have children that
        // are not just static text. Otherwise the cost of hoisting out will
        // outweigh the benefits and it's better off to just always render it fresh.
        if (
            node.static &&
            node.children.length &&
            !(node.children.length === 1 && node.children[0].type === 3)
        ) {
            node.staticRoot = true;
            return;
        } else {
            node.staticRoot = false;
        }
        if (node.children) {
            for (let i = 0, l = node.children.length; i < l; i++) {
                markStaticRoots(node.children[i]);
            }
        }
    }
}

因为处理静态节点也是有代价的,渲染的时候需要维护一个静态节点树。如果某个 dom 节点仅包含一个文本节点,此时进行 diff 其实代价很低,这种情况我们就将 staticRoot 置为 false ,不把它看成静态节点。

通过调用 optimize 函数,我们就可以将下边 Ast

Vue2剥丝抽茧-模版编译之静态render

标记出静态根节点:

Vue2剥丝抽茧-模版编译之静态render

后边生成 render 函数时候我们只会用到 staticRootstatic 就用不到了。

render 代码生成

const ast = parse(template);
const options = {
    isReservedTag,
};
optimize(ast, options);
const code = generate(ast);

optimize 标记完静态根节点后就调用 generate 函数来生成 render 函数。

export class CodegenState {
    staticRenderFns;

    constructor() {
        this.staticRenderFns = [];
    }
}

export function generate(ast) {
    const state = new CodegenState();
    const code = genElement(ast, state);
    return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns,
    };
}

定义了一个 CodegenState ,类中保存一些数据,在生成 code 的过程中进行传递。

接下来看一下 genElement 的实现:

export function genElement(el, state) {
    if (el.staticRoot && !el.staticProcessed) {
        return genStatic(el, state);
    } else {
        // component or element
        let code;
        let data; // 先不考虑

        const children = genChildren(el, state);
        code = `_c('${el.tag}'${
            data ? `,${data}` : "" // data
        }${
            children ? `,${children}` : "" // children
        })`;
        return code;
    }
}

首先判断是否是静态根节点并且是否在生成静态根节点的过程中,满足情况的话调用 genStatic 函数。

function genStatic(el, state) {
    el.staticProcessed = true;
    state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`);
    return `_m(${state.staticRenderFns.length - 1})`;
}

我们将生成的静态节点的 code pushstaticRenderFns 中,最终通过 _m 函数进行包裹,_m 函数后边会讲。

此时会走回 genElement 函数中,因为已经将 staticProcessed 标记为了 true ,因此就会进入 else 分支中。

export function genElement(el, state) {
    if (el.staticRoot && !el.staticProcessed) {
        return genStatic(el, state);
    } else {
        // component or element
        let code;
        let data; // 先不考虑

        const children = genChildren(el, state);
        code = `_c('${el.tag}'${
            data ? `,${data}` : "" // data
        }${
            children ? `,${children}` : "" // children
        })`;
        return code;
    }
}

调用 genChilden 生成子节点的 code ,最后将 tagdatachildern 传给 _c 函数,_c 函数后边讲。

来看一下 genChildren

export function genChildren(el, state) {
    const children = el.children;
    if (children.length) {
        const gen = genNode;
        return `[${children.map((c) => gen(c, state)).join(",")}]`;
    }
}
function genNode(node, state) {
    if (node.type === 1) {
        return genElement(node, state);
    } else {
        return genText(node);
    }
}

export function genText(text) {
    // JSON.stringify 是为了给 text 加双引号,作为参数传给 _v
    return `_v(${JSON.stringify(text.text)})`;
}

for 循环调用 genNode ,如果是 type === 1 继续调用 genElement ,否则的话调用 genText 生成 text 节点,这里的 _v 函数也后边讲。

通过 generate 函数:

export function generate(ast) {
    const state = new CodegenState();
    const code = genElement(ast, state);
    return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns,
    };
}

对于 "<div><span>3<5吗</span><span>?</span></div>" 模版 generate 返回的对象如下:

Vue2剥丝抽茧-模版编译之静态render

_m(0) 代表取 staticRenderFns 的第一个值,"with(this){return _c('div',[_c('span',[_v(\"3<5吗\")]),_c('span',[_v(\"?\")])])}" 其实就是 render 函数的字符串形式了。

调用 generate 函数后,我们只需要通过 Function 函数生成最终的 render 函数即可。

const code = generate(ast);
const render = new Function(code.render);

当然因为 render 函数中我们还使用了 _m、_c、_v 函数,下边看一下这些函数的实现。

_c _v _m

_c

_c 其实就是生成一个正常的 vnode ,和我们之前在 render 中接收到的 createElement 其实是同一个函数。

new Vue({
    el: "#root",
    data() {
        return {
            text: "world",
            title: "hello",
        };
    },
    components: { Hello },
    methods: {
        click() {
            this.title = "hello2";
            // this.text = "hello2";
        },
    },
    render(createElement) {
        const test = createElement(
            "div",
            {
                on: {
                    // click: this.click,
                },
            },
            [
                createElement("Hello", { props: { title: this.title } }),
                this.text,
            ]
        );
        return test;
    },
});

Vue2剥丝抽茧-模版编译之静态render

他们调用的都是 createElement 函数。

_v

这些字母函数都定义在 src/core/instance/render-helpers/index.js 中:

Vue2剥丝抽茧-模版编译之静态render

并且挂在了 Vue 的原型对象上,这样在 render 函数中就可以访问到了。

Vue2剥丝抽茧-模版编译之静态render

我们来先看一下 _v 做了什么。

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

很简单,生成了一个 textVNode

_m

_m 对应 renderStatic, 接收一个下标参数,也就是 staticRenderFns 对应的位置。

export function renderStatic (
  index
){
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  if (tree) {
    return tree
  }
  // otherwise, render a fresh tree.
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  markStatic(tree, `__static__${index}`, false)
  return tree
}

加了一个 cashed ,如果缓存没有命中,就调用相应的 staticRenderFns 函数来生成 VNode,当然 staticRenderFns 也会提前调用 new Function 将字符串实例化为函数。

上边的 markStatic(tree, __static__${index}, false) 函数是将 VNode 加上 isStatic 标记,这样以后在 diff 的过程中可以直接跳过。

function markStaticNode (node, key, isOnce) {
  node.isStatic = true
  node.key = key
  node.isOnce = isOnce
}

今天是模版编译的最后一步了,第一步是 模版编译之分词,第二步是 模版编译之生成AST,今天是最后一步,遍历 AST 包装一些字母函数 _c_m 等生成 render 函数的字符串,最后通过 new Function 来生成 render 函数。

因为目前为止我们的模版还没有涉及到变量以及一些 v- 指令,所以上边的模版还属于静态模版,引入了 staticRenderFns 来生成。未来几篇文章会介绍包含变量的文本、常用的 v-ifv-for 指令等,一步步完善我们的模版编译。

文章对应源码详见 vue.windliang.wang/