【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传递、插槽机制、生命周期钩子、指令系统等等,当前的主要聚焦是渲染功能的实现。下图是这一节的核心思想

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

相关推荐
lpfasd1234 小时前
node-sass 停止维护!老项目迁移 Dart Sass 完整教程
前端·rust·sass
Data analyse4566 小时前
rnweb 回放数据量太大怎么办?分片存储与断点续传的实现思路
前端·网络·算法·数据分析
xiaogai_gai6 小时前
金蝶收料通知单查询策略实战教程:从源系统拉取到中间层落地的完整路径
java·前端·数据库
环境栈笔记7 小时前
指纹浏览器 WebRTC 怎么设置?从 ICE 候选、代理出口到功能验证
前端·人工智能·后端·自动化
赵大仁7 小时前
官网性能优化实战:LCP 3.2s → 2.1s、CLS 0.107 → 0,以及被我推翻的 6 个判断
前端·性能优化·cls·lcp·core web vitals
xiancai_xianyu7 小时前
Text2SQL落地生产库前,SQL要过哪三道闸
前端·数据库·sql·sql注入·text2sql·ai agent开发·企业级ai框架
小羊没烦恼!8 小时前
T-SQL查询进阶--理解SQL SERVER中的分区表
服务器·开发语言·前端·javascript·html
史瓦辛格8 小时前
给 DeepSeek Harness 写费用插件:把「这次会话花了多少钱」算准
前端·deepseek
星光不问赶路人8 小时前
Vue 3 + TypeScript 中 enum 的使用实践
前端·vue.js
鸥梨菌Honevid8 小时前
UVM 中 REQ 和 RSP 类型详解:从通信模型到 item_done 与 put_response
java·前端·servlet