Skip to content

实现迷你Vue

我们把之前实现的内容合并到一起,尝试组合成一个迷你的Vue

完成以下内容:

html
<div id="app"></div>

<script>
function h(tag, props, children) {
  return {
    tag,
    props,
    children
  }
}

function mount(vNode, container) {
  const { tag, data, children } = vNode

  const el = vNode.el = document.createElement(tag)

  if(data) {
    Object.keys(data).forEach(key => {
      el.setAttribute(key, data[key])
    })
  }

  if(typeof children === 'string') {
    el.textContent = children
  } else {
    children.forEach(child => {
      mount(child, el)
    })
  }

  container.appendChild(el)
}

function patch(n1, n2) {
  if(n1.tag === n2.tag) {
    const el = n1.el

    // props
    const oldProps = n1.props
    const newProps = n2.props
    for(const key in newProps) {
      if(oldProps[key] !== newProps[key]) {
        el.setAttribute(key, newProps[key])
      }
    }
    for(const key in oldProps) {
      if(!newProps[key]) {
        el.removeAttribute(key)
      }
    }


    // 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 = children
      }
    } else {
      if(typeof oldChildren === 'string') {
        mount(child, el)
      } else {

        const oldChildrenLen = n1.children.length
        const newChildrenLen = n2.children.length
        const commonLen = Math.min(oldChildrenLen, newChildrenLen)

        for(let i = 0; i < newChildrenLen; i++) {
          patch(oldChildren[i], newChildren[i])
        }

        if(newChildrenLen > oldChildrenLen) {
          // 节点增加了
          newChildren.slice(oldChildrenLen).forEach(child => {
            mount(child, el)
          })
        } else if (newChildrenLen < oldChildrenLen) {
          // 节点减少了
          oldChildren.slice(newChildrenLen).forEach(child => {
            el.removeChild(child.el)
          })
        }
        
      }
    }

  } else {
    // replace
  }

}

mount(vNode1, document.getElementById('app'))

patch(vNode1, vNode2)

class Dep {
  subscribers = new Set()

  depend() {
    if(activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify() {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}

const targetMap = new WeakMap()

const handler = {
  get(target, key, receiver) {
    const dep = getDep(target, key)
    dep.depend()
    return Reflect.get(target, key, receiver)
  },
  set(target, key, newValue, receiver) {
    const dep = getDep(target, key)
    const result = Reflect.set(target, key, newValue, receiver)
    dep.notify()
    return result
  }
}

function reactive(raw) {
  return new Proxy(raw, handler)
}

function getDep(target, key) {
  let map = targetMap.get(target)
    if(!map) {
      map = new Map()
      targetMap.set(target, map)
    }

  let dep = map.get(key)
  if(!dep) {
    dep = new Dep()
    map.set(key, dep)
  }
  return dep

}

let activeEffect = null
function watchEffect(effect) {
  activeEffect = effect
  effect()
  activeEffect = null
}


// TODO


</script>

答案

html
<div id="app"></div>

<script>
  function h(tag, props, children) {
    return {
      tag,
      props,
      children
    }
  }

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

    const el = vNode.el = document.createElement(tag)

    if(props) {
      Object.keys(props).forEach(key => {
        // 新增事件判断
        if(key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), props[key])
        } else {
          el.setAttribute(key, props[key])
        }
      })
    }

    if(typeof children === 'string') {
      el.textContent = children
    } else {
      children.forEach(child => {
        mount(child, el)
      })
    }

    container.appendChild(el)
  }

  function patch(n1, n2) {
    if(n1.tag === n2.tag) {
      const el = n2.el = n1.el

      // props
      const oldProps = n1.props
      const newProps = n2.props
      for(const key in newProps) {
        if(oldProps[key] !== newProps[key]) {
          el.setAttribute(key, newProps[key])
        }
      }
      for(const key in oldProps) {
        if(!newProps[key]) {
          el.removeAttribute(key)
        }
      }

      // 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 = children
        }
      } else {
        if(typeof oldChildren === 'string') {
          mount(child, el)
        } else {

          const oldChildrenLen = n1.children.length
          const newChildrenLen = n2.children.length
          const commonLen = Math.min(oldChildrenLen, newChildrenLen)

          for(let i = 0; i < newChildrenLen; i++) {
            patch(oldChildren[i], newChildren[i])
          }

          if(newChildrenLen > oldChildrenLen) {
            // 节点增加了
            newChildren.slice(oldChildrenLen).forEach(child => {
              mount(child, el)
            })
          } else if (newChildrenLen < oldChildrenLen) {
            // 节点减少了
            oldChildren.slice(newChildrenLen).forEach(child => {
              el.removeChild(child.el)
            })
          }
          
        }
      }

    } else {
      // replace
    }

  }

  class Dep {
    subscribers = new Set()

    depend() {
      if(activeEffect) {
        this.subscribers.add(activeEffect)
      }
    }
    notify() {
      this.subscribers.forEach(effect => {
        effect()
      })
    }
  }

  const targetMap = new WeakMap()

  const handler = {
    get(target, key, receiver) {
      const dep = getDep(target, key)
      dep.depend()
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) {
      const dep = getDep(target, key)
      const result = Reflect.set(target, key, newValue, receiver)
      dep.notify()
      return result
    }
  }

  function reactive(raw) {
    return new Proxy(raw, handler)
  }

  function getDep(target, key) {
    let map = targetMap.get(target)
      if(!map) {
        map = new Map()
        targetMap.set(target, map)
      }

    let dep = map.get(key)
    if(!dep) {
      dep = new Dep()
      map.set(key, dep)
    }
    return dep

  }

  let activeEffect = null
  function watchEffect(effect) {
    activeEffect = effect
    effect()
    activeEffect = null
  }


  // TODO
  const App = {
    data: reactive({
      count: 0
    }),
    render() {
      return h('div', {
        onClick: () => {
          this.data.count++
        }
      }, String(this.data.count)) // 使用String是因为我们没有对数字类型进行处理
    }
  }

  function mountApp(component, container) {
    let isMounted = false
    let oldVNode = null
    watchEffect(() => {
      if(!isMounted) {
        oldVNode = component.render()
        mount(oldVNode, container)
        isMounted = true
      } else {
        let newVNode = component.render()
        patch(oldVNode, newVNode)
      }
    })
  }

  mountApp(App, document.getElementById('app'))
</script>