从零手写简易Vue3(三)—— render() & h()

3,467 阅读2分钟

本文使用的vue版本为3.0.2

如何在组合式API中使用渲染函数(render)?

import { h } from 'vue'

export default {
  setup() {
    return () => h('div', ['Hello,Vue3'])
  }
}

上一篇文章中我们已经说过,实例setup选项的返回值可以有两种:

  • Object
  • Function

而返回一个函数的写法,就类似于直接使用实例的render选项:

<script>
import { h } from "vue";

export default {
  setup() {
    return () => h("div", ["Hello,Vue3"]);
  }
};
</script>

/* 等价于 */

<script>
import { h } from "vue";

export default {
   render() {
     return h("div", ["Hello,Vue3"]);
   }
};
</script>

那么,h()这个方法又是什么呢?

h()

  • 返回值:VNode

  • 目的:用于手动编写的render函数

  • 参数:[type,props,children]

    • type:必须,创建的VNode类型(HTML标签,组件,异步组件)。使用null创建注释节点。

    • props:可选,一个对象,可为VNode配置attribute、prop 和事件等。

    • Children:可选,子VNode或HTML标签内容。同样由h()创建。

源码时间

h()部分的源码很简单:

// packages/runtime-core/src/h.ts 

// 上边做了若干次重载

// 实际实现
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length
  if (l === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 无props vnode
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // 无children vnode
      return createVNode(type, propsOrChildren)
    } else {
      // 忽略props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

实际上,在h()内部,是通过不同的参数,调用createVNode创建不同的VNode。

然后包裹一层匿名函数,作为setup()的返回值,最后渲染成dom。

实现

function isObject(val) {
  return val !== null && typeof val === 'object'
}

function isArray(val) {
  return Array.isArray(val)
}

function isVNode(val) {
  return val.isVNode === true
}

// 校验入参调用createVNode
function h(type, propsOrChildren, children) {
  const l = arguments.length

  // 简化版:只考虑type为HTML标签名
  if(typeof type !== 'string') return
  const ele = document.createElement(type)
  type = ele.outerHTML

  if (l === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 无props,数组children
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // 无children
      return createVNode(type, propsOrChildren)
    } else {
      // 无props,单个children
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    // 处理无效参数
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

之前的内容的内容进行补全:

// 创建VNode
function createVNode(temp, props, children) {
  // 维护一个栈,通过进栈出栈匹配嵌套标签
  const stack = [];
  // root节点
  let obj;

  let content = temp;
  while (content.length) {
    if (content.indexOf("<") > 0 && content[0] !== '<') {
      // 以文字开头
      const index = content.indexOf("<");
      let text = content.substring(0, index);
      content = content.substring(index);
      if (stack.length) {
        stack[stack.length - 1].children.push(text);
      } else {
        obj.children.push(text);
      }
    } else if (content.indexOf("<") < 0) {
      // 纯文字
      if (stack.length) {
        stack[stack.length - 1].children.push(content);
      } else {
        obj = {
          tag: "",
          children: [],
        };
        obj.children.push(content);
      }
      content = "";
    } else {
      // 以标签开头
      const endIndex = content.indexOf(">");
      let currentTag = content.substring(1, endIndex);
      if (currentTag.indexOf("/") < 0) {
        // 开始标签
        stack.push({
          tag: currentTag,
          children: [],
        });
      } else {
        // 结束标签
        let child = stack[stack.length - 1];

        obj = child;
        stack.pop();
      }
      content = content.substring(endIndex + 1);
    }
  }
  obj.isVNode = true

  if (props) {
    // 处理组件props
  }

  if (children) {
    obj.children = children
  }
  return obj;
}

在线demo(完整代码可打开控制台在mini-vue.js中找到)