vue渲染器简单实现

784 阅读1分钟

渲染器简单实现

参考vue实现一个简易的渲染器, 这边只考虑基本html元素不考虑自定义组件, 通过render的h函数到mount函数在最后到patch函数实现一个生成虚拟dom渲染到真实dom的效果

应用示例

  //1. 通过h函数实现一个vnode虚拟节点
  const vnode = h(
    'div', 
    {
      class: 'cqc'
    },
    [
      h('h2', null, '这是一个h2'),
      h('button', {
        onClick: function() {
          console.log('这是一个点击方法')
        }
      }, 'btn')
    ]
  );


  // 2. 通过mount函数挂载虚拟节点至 div#app
  mount(vnode, document.querySelector('#app'))

方法实现

h、mount 函数实现


  // 转成虚拟dom
  const h = function(tag, props, children) {
    return {
      tag,
      props,
      children
    }
  }


  const mount = function(vnode, container) {
    const {
      children,
      tag,
      props
    } = vnode;

    const el = vnode.el = document.createElement(tag);
    

    // 处理porps
    if(props) {
      for(const key in props) {
        const value = props[key];
        if(key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), value);
        } else {
          el.setAttribute(key, value);
        }
      }
    }

    // 处理children
    if(children) {
      if(typeof children === 'string') {
        el.textContent = children;
      } else {
        children.forEach(item => mount(item, el));
      }
    }


  container.appendChild(el);
  }

效果图

aaa.png

patch 函数实现

// 对比新旧虚拟节点
const patch = (n1, n2) => {
  const el = n1.el;

  // 标签不同直接移除原先节点插入新节点
  if(n1.tag !== n2.tag) {
    const n1Parent = el.parentElement;
    n1Parent.removeChild(el);
    mount(n2, n1Parent);
  } else {
    // 保存el至新虚拟节点
    n2.el = el;

    // 处理props
    const oldProps = n1.props || {};
    const newProps = n2.props || {};

    for(const key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];

      // 添加新属性至el
      if(oldValue !== newValue) {
        if(key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), newValue);
        } else {
          el.setAttribute(key, newValue);
        }
      }


      // 删除旧props
      for(const key in oldProps) {
        const oldValue = oldProps[key];
        if(!(key in newProps)) {
          if(key.startsWith('on')) {
            el.removeEventListener(key.slice(2).toLowerCase(), oldValue);
          } else {
            el.removeAttribute(key);
          }
        }
      }



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

      if(typeof newChildren === 'string'){
        if(oldChildren !== newChildren) {
          el.innerHTML = newChildren;
        }
      } else {
        
        // 旧节点是字符串新节点是虚拟节点数组
        if(typeof oldChildren === 'string') {
          el.innerHTML = '';
          newChildren.forEach(o => mount(o, el));
        } else {
          // 两者都是虚拟节点数组
          /**
           * oldChildren: [v1, v2, v3]
           * newChildren: [v1, v4, v5]
          */

         const oldLen = oldChildren.length;
         const newLen = newChildren.length;
         const commonLen = Math.min(oldLen, newLen);

         // 相同节点patch
         for(let i = 0; i < commonLen; i++) {
           patch(oldChildren[i], newChildren[i]);
         }

          // 新节点比旧节点多  挂载新节点
         if(newLen > oldLen) {
           newChildren.slice(oldLen).forEach(o => mount(o, el));
         }


         // 旧节点比新节点多  卸载
         if(newLen < oldLen) {
           oldChildren.slice(newLen).forEach(o => el.removeChild(o.el));
         }

        }


      }
    }

  }

}

  <script>

    // 1. 通过h创建一个 VNode
      const vnode = h(
      'div',
      {
      class: 'cqc'
      },
      [
      h('h2', null, '这是一个h2'),
      h('button', {
      onClick: function() {
      console.log('这是一个点击方法')
      }
      }, 'btn')
      ]
      );

    // 2. 通过mount函数, 将vnode挂载到 div#app 上 (虚拟dom -> 真实dom)
    mount(vnode, document.querySelector('#app'));

    
    const vnode1 = h('div', { class: 'qcqcqc'}, [h(
      'div', 
      {
        class: 'cqc'
      },

      [
        h('h2', null, 'hhh'),
        h('button', {
          onClick: function() {
            console.log('cqqc');
          }
        }, '+1')
      ]
    )]);

    patch(vnode, vnode1);
  </script>

image.png