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

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

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

1、背景

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

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

2、代码实现

// 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);
  }
}
// 示例
// 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参数来简化这一判断过程,这样不准确

image-20240420164305690

3.2 renderer

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

image-20240420165141538

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

image-20240420165151508

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

image-20240420165201116

4、总结

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

image-20240420165240455

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

image-20240420165615596

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

转存中…(img-mJ2ZDKdo-1713604504050)]

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

image-20240420170927777
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值