【Vue.js 设计与实现】渲染器实现

大家好,我是一个刚刚吃完 肯德基 🐔 的 前端开发者 村头一只鹅鹅 😉

感谢阅读我写的文章,你的支持是我更新的动力

1、背景

最近阅读了《Vue.js 设计与实现》这本书,不得不佩服大佬太厉害了,膜拜,膜拜,膜拜。

阅读了前3章之后,看到了 渲染器 与 编译器 这一块,我就按照自己的思路来实现一个很简单的渲染器,来加深印象

2、代码实现

javascript 复制代码
// utils
const isArray = Array.isArray;
const isObject = (value) => value && typeof value === "object";
const isVNode = (value) => value && value.tag;

const appendText = (element, text) =>
  element.appendChild(document.createTextNode(text));

const returnVNode = (tag, props, children) => ({
  tag,
  props,
  children,
});

// 处理 props
const propsTree = [
  // 处理 style
  [
    (key) => key === "style",
    (params) => {
      const { element, propsValue } = params;
      Reflect.ownKeys(propsValue).forEach((key) => {
        element.style[key] = propsValue[key];
      });
    },
  ],
  // 处理 class
  [
    (key) => key === "class",
    (params) => {
      const { element, propsValue } = params;
      if (typeof propsValue === "string") {
        element.className = propsValue;
      } else if (isObject(propsValue)) {
        Reflect.ownKeys(propsValue).forEach((key) => {
          if (propsValue[key]) element.classList.add(key);
        });
      }
    },
  ],
  // 处理 事件
  [
    (key) => /^on/.test(key),
    (params) => {
      const { element, propsValue, key } = params;
      element.addEventListener(key.slice(2).toLocaleLowerCase(), propsValue);
    },
  ],
];
const strategiesHandler = (strategies, flag, params) => {
  const target = strategies.find((item) => item[0](flag));
  if (target) {
    return target[1](params);
  } else {
    throw new Error("error, don't find");
  }
};

// h函数
function h(type, propsOrChildren, children) {
  const argLength = arguments.length;

  if (argLength === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 第二个参数是 vnode
      if (isVNode(propsOrChildren)) {
        return returnVNode(type, null, [propsOrChildren]);
      }

      // 第二个参数为 props
      return returnVNode(type, propsOrChildren);
    } else {
      // 第二个参数为 vnode
      return returnVNode(type, null, propsOrChildren);
    }
  } else {
    return returnVNode(type, propsOrChildren, children);
  }
}

// 挂载元素
function mountElement(vnode, container) {
  const element = document.createElement(vnode.tag);

  // 处理 props
  for (const key in vnode.props) {
    strategiesHandler(propsTree, key, {
      key: key,
      element: element,
      propsValue: vnode.props[key],
    });
  }

  // 处理 children
  if (typeof vnode.children === "string") {
    appendText(element, vnode.children);
  } else if (isObject(vnode.children) && !isArray(vnode.children)) {
    renderer(vnode.children, element);
  } else if (isArray(vnode.children)) {
    vnode.children.forEach((child) => {
      typeof child === "string"
        ? appendText(element, child)
        : renderer(child, element);
    });
  }

  container.appendChild(element);
}

// 挂载组件
function mountComponent(vnode, container) {
  const subtree = vnode();
  renderer(subtree, container);
}

// 渲染函数
function renderer(vnode, container) {
  if (typeof vnode.tag === "string") {
    mountElement(vnode, container);
  } else if (typeof vnode === "function") {
    mountComponent(vnode, container);
  }
}
javascript 复制代码
// 示例
// Hello 组件
const Hello = () => {
  return h("h3", "我是 Component - Hello");
};

const vnode = h(
  "div",
  {
    class: "main",
    style: { backgroundColor: "#5c7cfa", width: "300px", padding: "10px" },
  },
  [
    h("h1", { style: { color: "#fa5252" } }, "Hello Vue!"),
    Hello,
    h(
      "button",
      {
        onClick: function () {
          console.log("这是一个虚拟组件");
        },
      },
      "点击我吧~"
    ),
    h("h4", "渲染器实现一部分"),
  ]
);
const idEl = document.querySelector("#app");
renderer(vnode, idEl);

3、思路解读

3.1 h

1、首先,我们要编写一个h函数。这个函数的参数列表,我们可以直接在官方网站上找到。使用这个h函数,我们可以输出一个类似{ tag: "div", props: { ... }, children: [ ... ] }形式的对象,这就是 虚拟DOM。通过操作这些对象,我们可以实现页面的更新,再加上diff算法,响应式等处理方式,就是一个简化版本的Vue了

2、h函数的编写,我认为其中难的部分是如何用最好的方式判断 vnode 的输出形式。我认为最关键的是isVNode函数的实现。在Vue的源码中,判断一个节点是否为vnode是通过检查它是否拥有__v_isVNode这个属性。目前,我通过判断是否存在tag参数来简化这一判断过程,这样不准确

3.2 renderer

1、我们来看看renderer函数。这个函数会判断传入的节点是否为组件。在Vue中,组件可以是函数或对象,它们经过编译后会转化为虚拟节点(vnode)进行渲染。为了简化处理,我们在这里假设组件都是函数形式的。renderer函数会识别这些函数组件,并调用它们,将返回的vnode纳入渲染流程中。这样,无论是普通的vnode还是组件,都能通过renderer函数得到妥善处理。

2、接下来,mountElement函数会根据虚拟节点(vnode)来创建真实的DOM元素。在处理过程中,首先会处理节点的属性(props)。为了提高代码的可维护性,我采用了策略模式来处理props,避免了大量的条件判断语句(if语句)的堆积。处理完props后,接下来会处理子节点(children)。根据vnode的结构,递归地创建并挂载这些子节点对应的DOM元素。

3、还实现了一个mountComponent函数。这个函数的本质就是执行组件函数,并输出虚拟节点(vnode)以供渲染。Vue的实现方式更为复杂,能够支持向组件传递props等参数。这里,我就只服务渲染功能的实现,采用了简化。通过mountComponent函数,我们可以将组件函数转化为可渲染的vnode,从而实现了组件的挂载和渲染

4、总结

这样我们就实现了一个很简单的渲染器了,下面就是实现的页面,并且含有事件监听

如果要写完整的实现,还需要考虑很多其他因素,比如组件的props传递、插槽机制、生命周期钩子、指令系统等等,当前的主要聚焦是渲染功能的实现。下图是这一节的核心思想

写原创文章不易!!!如果讲的好,请给出你手动的点赞👍👍👍,如果文章有问题,也可以在评论区留言哦!

相关推荐
wangbing112511 小时前
ES6 (ES2015)新增的集合对象Set
前端·javascript·es6
nvd1112 小时前
企业级 LLM 实战:在受限环境中基于 Copilot API 构建 ReAct MCP Agent
前端·copilot
Dragon Wu12 小时前
TailWindCss cva+cn管理样式
前端·css
烤麻辣烫12 小时前
Web开发概述
前端·javascript·css·vue.js·html
Front思12 小时前
Vue3仿美团实现骑手路线规划
开发语言·前端·javascript
徐同保12 小时前
Nano Banana AI 绘画创作前端代码(使用claude code编写)
前端
Ulyanov12 小时前
PyVista与Tkinter桌面级3D可视化应用实战
开发语言·前端·python·3d·信息可视化·tkinter·gui开发
计算机程序设计小李同学12 小时前
基于Web和Android的漫画阅读平台
java·前端·vue.js·spring boot·后端·uniapp
干前端12 小时前
Message组件和Vue3 进阶:手动挂载组件与 Diff 算法深度解析
javascript·vue.js·算法
lkbhua莱克瓦2412 小时前
HTML与CSS核心概念详解
前端·笔记·html·javaweb