mini_vue之渲染系统的实现

渲染系统就是把虚拟DOM转化为真实DOM。

目标

主要实现以下三个功能:

  • 功能一: h函数,用于返回一个vnode对象。
  • 功能二: mount函数,用于将vnode挂载到DOM上。
  • 功能三:patch,用于对vnode进行对比,决定如何处理新的vnode

实现过程

<body>
    <div id="app"></div>
    
    <script src="renderer.js"></script>
</body>

创建一个id 为 app 的 div,用来挂载我们即将创建的节点

renderer 就是渲染renderer.js实现三个函数 h函数mount函数patch函数

h函数

h函数就是返回一个vnode,h函数中又有子节点,又是一个h函数,那么也会返回一个vnode,拿着这样来的话,多个vnode组成不就成了vdom嘛

vnode ---> javascript对象 ----> {}

//tag: 标签  props: 属性, children: 子节点
const h = function(tag, props, children) {
    return {
        tag,
        props,
        children
    }
}

mount函数

  1. 根据vnode的tag, 利用document.createElement()来创建一个节点,顺便在vnode中也保存当前的节点。

    const mount = function(vnode, box){
        const el = vnode.el = document.createElement(vnode.tag)
    }
    
  2. 根据 vnode 的props,来添加属性

    • 判断是否存在props
    • 遍历拿到,根据key值拿到value值
    • 判断value值是函数,还是字符串
    if(vnode.props) {
        for(let key in vnode.props) {
            const value = vnode.props[key]
            if(key.startsWith('on')) {    //处理监听函数onClick
                el.addEventListener(key.slice(2).toLowerCase(), value)
            } else {
                el.setAttribute(key, value)
            }
        }
    }
    
  3. 处理children(目前只考虑是字符串和数组的形式)

    • 判断children是否存在
    • children是字符串还是数组
    if(vnode.children) {
        if(typeof vnode.children === 'string') {  //如果是字符串
            el.textContent = vnode.children
        } else {   //数组的形式
            vnode.children.forEach(item => {
                //item函数,又是一个h函数,也需要调用mount函数,递归
                mount(item, el)
            })
        }
    }
    
  4. 挂载到容器, 利用appendChild()

    box.appendChild(el)
    

完整的mount函数:

const mount = function(vnode, box){
    const el = vnode.el = document.createElement(vnode.tag)
    if(vnode.props) {
        for(let key in vnode.props) {
            const value = vnode.props[key]
            if(key.startsWith('on')) {    //处理监听函数onClick----click
                el.addEventListener(key.slice(2).toLowerCase(), value)
            } else {
                el.setAttribute(key, value)
            }
        }
    }
    if(vnode.children) {
        if(typeof vnode.children === 'string') {  //如果是字符串
            el.textContent = vnode.children
        } else {   //数组的形式
            vnode.children.forEach(item => {
                //item函数,又是一个h函数,也需要调用mount函数,递归
                mount(item, el)
            })
        }
    }
    box.appendChild(el)
}

patch函数

当节点发生更新的时候,就需要patch,进行比较,更新不同的部分。

这里的patch函数实现,并没有考虑key的存在的性能优化

patch接收两个参数:

  • n1: 旧的节点
  • n2: 新的节点
const patch = function(n1, n2) {
    //由于n2并没有调用mount方法,所以里面并不存在el属性
    //引用相同,一起修改
    const el = n2.el = n1.el
}

分情况讨论:

  1. 根据tag是否相同
if(n1.tag !== n2.tag) {
    //拿到旧节点的父节点
    const n1Parent = n1.el.parentElement
    //父节点删除旧节点
    n1Parent.removeChild(n1.el)
    //添加新的节点
    mount(n2, n1Parent)
} else {}
  1. tag相同的情况

    逻辑一:处理props

    • 拿到旧的props 和 新的props
    • 把新的props全部添加到节点上
    • 对比旧的key是否在新的属性上,如果没有就删除
    //对比n1的props和n2的props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    
    //遍历新节点的props添加el
    for(const key in newProps) {
        const oldValue = oldProps[key]
        const newValue = newProps[key]
        if(newValue !== oldValue) {
            if(key.startsWith('on')) {    //处理监听函数onClick
                el.addEventListener(key.slice(2).toLowerCase(), newValue)
            } else {
                el.setAttribute(key, newValue)
            }
        }
    }
    //遍历旧的props,查看key值是否在newProps中,如果不存在就移除掉
    for(const key in oldProps) {
        if(!(key in newProps)) {
            const value = oldProps[key]
            if(key.startsWith('on')) {    //处理监听函数onClick
                el.removeEventListener(key.slice(2).toLowerCase(), value)
            } else {
                el.removeAttribute(key)
            }
        }
    }
    

    逻辑二: 处理children

    • 情况1: 如果 newChild是字符串
      • 情况1.1: oldChild也是字符串
      • 情况1.2:oldChild是数组
    const oldChild = n1.children || []
    const newChild = n2.children || []
    //情况1
    if(typeof newChild === 'string') {
        //情况1.1
        if(typeof oldChild === 'string') {
            if(newChild !== oldChild) {
                el.textContent = newChild
            }
        } 
        //情况1.2
        else {
            el.innerHTML = newChild
        }
    }
    
    • 情况2:如果newChild是数组

      • 情况2.1: oldChild是字符串

        else {
            //情况2.1
            if(typeof oldChild === 'string') {
                //先置空,添加
                el.innerHTML = ''
                newChild.forEach(item => {
                    mount(item, el)
                })
            }
        
      • 情况2.2: oldChild是数组

        //找出最小长度,然后依次对比(patch)
        const commonLength = Math.min(oldChild.length, newChild.length)
        for(const i = 0; i < commonLength; i++) {
            patch(oldChild[i], newChild[i])
        }
        
        • 情况2.2.1: newChild的长度大于oldChild的长度

          // oldChild: [v1, v2, v3]
          // newChild: [v1, v2, v3, v4, v5]
          if(newChild.length > oldChild.length) {
              newChild.slice(commonLength).forEach(item => {
                  mount(item, el)
              })
          }
          
        • 情况2.2.2 : newChild的长度小于oldChild的长度

          // oldChild: [v1, v2, v3, v4]
          // newChild: [v1, v2]
          if(newChild.length < oldChild.length) {
              oldChild.slice(commonLength).forEach(item => {
                  el.removeChild(item.el)
              })
          }
          

      完整的patch函数

      const patch = function(n1, n2) {
          const el = n2.el = n1.el
          //n1的tag和n2的tag不相同
          if(n1.tag !== n2.tag) {
              const n1Parent = n1.el.parentElement
              n1Parent.removeChild(n1.el)
              mount(n2, n1Parent)
          } else {
              //对比n1的props和n2的props
              const oldProps = n1.props || {}
              const newProps = n2.props || {}
              //遍历新节点的props添加el
              for(const key in newProps) {
                  const oldValue = oldProps[key]
                  const newValue = newProps[key]
                  if(newValue !== oldValue) {
                      if(key.startsWith('on')) {    //处理监听函数onClick
                          el.addEventListener(key.slice(2).toLowerCase(), newValue)
                      } else {
                          el.setAttribute(key, newValue)
                      }
                  }
              }
              //遍历旧的props,查看key值是否在newProps中,如果不存在就移除掉
              for(const key in oldProps) {
                  if(!(key in newProps)) {
                      const value = oldProps[key]
                      if(key.startsWith('on')) {    //处理监听函数onClick
                          el.removeEventListener(key.slice(2).toLowerCase(), value)
                      } else {
                          el.removeAttribute(key)
                      }
                  }
              }
              
              //处理children
              const oldChild = n1.children || []
              const newChild = n2.children || []
              //情况1: 如果newChild是字符串
              if(typeof newChild === 'string') {
                  if(typeof oldChild === 'string') {
                      if(newChild !== oldChild) {
                          el.textContent = newChild
                      }
                  } 
                  else {
                      el.innerHTML = newChild
                  }
              }
              //情况2: 如果newChild是数组 [v1, v2, v3]
              else {
                  if(typeof oldChild === 'string') {
                      el.innerHTML = ''
                      newChild.forEach(item => {
                          mount(item, el)
                      })
                  }
                  //情况2.2: 如果oldChild也是数组
                  else {
                      //找出最小长度,然后依次对比(patch)
                      const commonLength = Math.min(oldChild.length, newChild.length)
                      for(const i = 0; i < commonLength; i++) {
                          patch(oldChild[i], newChild[i])
                      }
                      if(newChild.length > oldChild.length) {
                          newChild.slice(commonLength).forEach(item => {
                              mount(item, el)
                          })
                      }
                      if(newChild.length < oldChild.length) {
                          oldChild.slice(commonLength).forEach(item => {
                              el.removeChild(item.el)
                          })
                      }
                  }
              }
          }
      }
      

      总结

      自己手写一遍,加深印象,收获挺多,需要多看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值