G
Q
Q
and
M
E

vue渲染原理简单实现

实现功能:
1.渲染系统:
· 功能一:h函数,用于创建并返回一个VNode(虚拟对象);
· 功能二:mount函数,用于将VNode挂载到节点上;
· 功能三: patch函数,用于对比两个VNode,决定该如何处理新的VNode;

1.新建一个index.html的页面

其中有一个id为app的div元素,之后我们写的所有DOM都会挂载到此元素下;然后引用了一个名为renderer.js的js文件,用来实现虚拟DOM转化和真实挂载。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./renderer.js"></script>
    <script>
         //h函数和mount函数存储在renderer.js中

        // 1.通过h函数来创建一个vnode
        const vnode = h('div', { class: 'why' }, [
            h('h2', null, "当前计数:100"),
            h('button', {onClick:()=>{console.log("+1")}}, "+1"),
        ]);

        // 通过mount函数,将vnode挂载到div#app上
        mount(vnode,document.querySelector("#app"));
    </script>
</body>
</html>

2.编写renderer.js文件

  1. 其中h()函数用于产生虚拟DOM,他将传入的标记属性子元素集成在一个对象之中,然后返回,产生如图的一个javaScript对象

2.mount()函数的作用是将vnode(虚拟节点)转化成真实DOM然后挂载到对应的容器。

//转化成虚拟DOM
const h = (tag, props, children) => {
  return {
    tag,
    props,
    children,
  };
};

//递归挂载
const mount = (vnode, container) => {
  // vnode ->element
  //1.创建出真实DOM,并在vnode上保留el
  const el = (vnode.el = document.createElement(vnode.tag));
  //2.处理props
  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key];
      //对事件监听的判断
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), value);
      }
      el.setAttribute(key, value);
    }
  }

  //3. 处理children
  if (vnode.children) {
    //如果children的内容直接是字符串
    if(typeof vnode.children === 'string'){
        el.textContent = vnode.children;
    }else{
        //如果children的内容是数组,即此元素还拥有子元素,递归调用mount
        vnode.children.forEach(item => {
            mount(item,el);
        });
    }
  }

  //4.将el挂载到container中
  container.appendChild(el);
};

通过调用这两个函数,我们即实现了将虚拟DOM转化成真实DOM,并显示到浏览器界面中的功能.

3.模拟页面元素更新时的流程

当页面中的元素更新的时候,通过调用patch()函数来对比前一次和后一次的VNode,添加不同的边界判断来区分各种情况,代码如下:

const patch = (n1, n2) => {
  //判断两个元素的类型
  if (n1.tag !== n2.tag) {
    //如果节点的类型不相同则没有比较的必要,那么可以直接移除n1,挂载n2
    const n1ElParent = n1.el.parentElement;
    n1ElParent.removeChild(n1.el);
    mount(n2, n1ElParent);
  } else {
    // 1. 取出element对象,并且在n2中进行保存
    const el = (n2.el = n1.el);

    //2.处理props
    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    //2.1获取所有newProps添加到el
    for (const key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];
      if (newValue !== oldValue) {
        if (key.startsWith("on")) {
          //对事件监听的判断
          el.addEventListener(key.slice(2).toLowerCase(), newValue);
        } else {
          el.setAttribute(key, newValue);
        }
      }
    }
    //2.2删除旧的props
    for (const key in oldProps) {
      //如果旧属性名称没有在新属性名称中出现,则删除旧的属性
      if (!(key in newProps)) {
        if (key.startsWith("on")) {
          const value = oldProps[key];
          el.removeEventListener(key.slice(2).toLowerCase(), value);
        } else {
          el.removeAttribute(key);
        }
      }
    }

    //3.处理children
    const oldChildren = n1.children || [];
    const newChildren = n2.children || [];

    if (typeof newChildren === "string") {
      //边界判断
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren;
        }
      } else {
        el.innerHTML = newChildren;
      }
    } else {
      //newChildren是一个数组
      if (typeof oldChildren === "string") {
        //oldChildren是个字符串
        el.innerHTML = "";
        newChildren.forEach((item) => {
          mount(item, el);
        });
      } else {
        // oldChildren和newChildren都不是string

        //以最短方式再进行比对替换操作 (长度相等的情况下)
        const commonLength = Math.min(oldChildren.length, newChildren.length);
        //因为递归调用,倒置oldChildren
        oldChildren.reverse();
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i]);
        }
        //newChildren.length > oldChildren.length
        //把newCildren中比oldChildren多出来的部分直接挂载到el上
        if (newChildren.length > oldChildren.length) {
          newChildren.slice(oldChildren.length).forEach((item) => {
            mount(item, el);
          });
        }
        //newChildren.length < oldChildren.length
        //新节点数组长度小于旧节点数组长度,需要移除旧节点数组中的中的部分节点
        if (newChildren.length < oldChildren.length) {
          oldChildren.slice(newChildren.length).forEach((item) => {
            el.removeChild(item.el);
          });
        }
      }
    }
  }
};

index.html中做出对应更改,来模拟DOM的更新:

setTimeout(() => {
            // 3.创建新的VNode
            const vnode1 = h('div', { class: 'coderWhy', id: "aaaa" }, [
                h('h3', { class: 'myTitle' }, "这是我的标题"),
                h('button', { onClick: console.log("新的打印函数") }, "打印"),
                h('button', { onClick: console.log("新的打印函数") }, "打印2"),
                h('button', { onClick: console.log("新的打印函数") }, "打印3"),
            ]);
            patch(vnode, vnode1);
        }, 2000);
posted @ 2023-07-05 10:18  sy0313  阅读(65)  评论(0编辑  收藏  举报