MiniVue
本文最后更新于 171 天前,如有失效请评论区留言。

miniVue

实现一个基础的vue,就需要Virtual Dom和reactive相结合

所以先把之前的代码搬过来

// vdom
function h (tag, props, children) {
  return {
    tag,
    props,
    children
  }
}
function mount (vnode, container) {
  const el = vnode.el = document.createElement(vnode.tag)
  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key]
      el.setAttribute(key, value)
    }
  }
  if (vnode.children) {
    if (typeof vnode.children === 'string') {
      el.textContent = vnode.children
    } else {
      vnode.children.forEach(child => {
        if (typeof child === 'string') {
          el.appendChild(document.createTextNode(child))
        } else {
          mount(child, el)
        }
      })
    }
  }
  container.appendChild(el)
}
function patch (n1, n2) {
  // 首先要确保他们是同一个标签
  if (n1.tag === n2.tag) {
    const el = n2.el = n1.el
    const oldProps = n1.props || {}
    const newProps = n2.props || {}

    // 如果有了template模板,下面的这些对比都有可能直接跳过(比如检测到静态标签直接跳过对比)

    // 对比新老中是否有key更改了
    for (const key in newProps) {
      const oldValue = oldProps[key]
      const newValue = newProps[key]
      if (newValue != oldValue) {
        el.setAttribute(key, newValue)
      }
    }
    // 当旧的中没有key要去除
    for (const key in oldProps) {
      if (!(key in newProps)) {
        el.removeAttribute(key)
      }
    }

    // 接着处理children,可能是一个'string' 或者 []
    const oldChildren = n1.children
    const newChildren = n2.children
    if (typeof newChildren === 'string') {
      if (typeof oldChildren === 'string') {
        // 新老的children都是字符串 不同就直接替换
        if (newChildren !== oldChildren) {
          el.textContent = newChildren
        }
      } else {
        el.textContent = newChildren
      }
    } else {
      // 新的children是数组,老的是字符串就遍历循环添加
      if (typeof oldChildren === 'string') {
        el.innerHTML = ''
        newChildren.forEach(child => {
          mount(child, el)
        })
      } else {
        // 当新老的children都是数组,有两种情况,一种使用v-for提供了一个唯一的key,用key去对比
        // 另一种就是双端指针 头头尾尾,vue2中是 头头,尾尾,旧头新尾,旧尾新头
        // 以上四种情况都匹配不到的话,就以新头对旧的vnode进行查找,看是否找到,都找不到就新建元素

        // 这里只是做简单的对比,不一样就直接替换
        const commonLength = Math.min(oldChildren.length, newChildren.length)
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i])
        }
        // 还需要考虑newChildren更长或者更短的情况下进行新增和删除
        if (newChildren.length > oldChildren.length) {
          newChildren.slice(oldChildren.length).forEach(child => {
            mount(child, el)
          })
        } else if (newChildren.length < oldChildren.length) {
          oldChildren.slice(newChildren.length).forEach(child => {
            el.removeChild(child.el)
          })
        }
      }
    }
  } else {
    // 当不是同一个类型就直接情况再挂载
    const el = n2.el = n1.el
    el.innerHTML = ''
    mount(n2, el)
  }
}

// reactivity
let activeEffect

class Dep {
  subscribers = new Set()
  depend () {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify (effect) {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}
function watchEffect (effect) {
  activeEffect = effect
  effect()
  activeEffect = null
}

const targetMap = new WeakMap()

function getDep (target, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }
  return dep
}
const reactiveHandles = {
  get (target, key, receiver) {
    const dep = getDep(target, key)
    dep.depend()
    // return target[key]
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    const dep = getDep(target, key)
    const result = Reflect.set(target, key, value, receiver)
    dep.notify()
    return result
  }
}

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

image-20240205164652107

接着使用这4个核心函数

首先创建一个App根组件,定义了一个count属性,并把这个count值加到渲染函数中

const App = {
  data: reactive({
    count: 0
  }),
  render () {
    // 这里也可以String(this.data.count)
    return h('div', null, this.data.count)
  }
}

要对mount做出一点修改,这里的this.data.count就是0,需要增加number属性的判断,并且也要加强vnode.children判断

原:

if (vnode.children) {
    if (typeof vnode.children === 'string') {

新:

if (vnode.children != null && vnode.children !== '') {
    if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {

然后我们再定义一个挂载函数,把id为app的div传入当作根,再把App传入进行渲染,根据是否是首次渲染,判断是挂载还是更新

function mountApp (component, containerString) {
  const container = document.querySelector(containerString)
  if (!container) {
    return console.warn(`[Vue warn]: Failed to mount app: mount target selector "${containerString}" returned null.`);
  }
  let isMounted = false
  let oldVdom
  watchEffect(() => {
    if (!isMounted) {
      oldVdom = component.render()
      mount(oldVdom, container)
      isMounted = true
    } else {
      const newVdom = component.render()
      patch(oldVdom, newVdom)
      oldVdom = newVdom
    }
  })
}
mountApp(App, '#app')

我个人觉得可以把首次mount提取出来,减少一次判断

function mountApp (component, containerString) {
  const container = document.querySelector(containerString)
  if (!container) {
    return console.warn(`[Vue warn]: Failed to mount app: mount target selector "${containerString}" returned null.`);
  }
  let oldVdom = component.render()
  mount(oldVdom, container)
  watchEffect(() => {
    const newVdom = component.render()
    patch(oldVdom, newVdom)
    oldVdom = newVdom
  })
}
mountApp(App, '#app')

这是我们就可以添加一个点击事件改变 this.data.count 来达到动态更新,

const App = {
  data: reactive({
    count: 0
  }),
  render () {
    return h('div', {
      onClick: () => {
        this.data.count++
      }
    }, this.data.count)
  }
}

但是由于我们之前没有添加事件的监听,所以也需要修改mount方法中props的判断 (这也是很多三方库为什么代码量这么多的原因,因为需要考虑所有传入情况,特别是异常情况,还要告知用户什么异常,以及如何处理)

原:

for (const key in vnode.props) {
  const value = vnode.props[key]
  el.setAttribute(key, value)
}

新:

for (const key in vnode.props) {
  const value = vnode.props[key]
  if (key.startsWith('on')) {
    const name = key.slice(2).toLowerCase()
    el.addEventListener(name, value)
  }else{
    el.setAttribute(key, value)
  }
}

当然这里也是只处理onClick事件,还有很多 onSubmit、onInput、onKeypress、onKeydown、onKeyup、onMousedown、onMouseup、onMouseover、onMouseout等事件都没有添加

然而你会发现还是点不动,但是这时this.data.count的值确实是增加了,只是patch方法中我们没有添加上对应number类型children的处理

原:

if (typeof newChildren === 'string') {
  if (typeof oldChildren === 'string') {
    // 新老的children都是字符串 不同就直接替换
    if (newChildren !== oldChildren) {
      el.textContent = newChildren
    }
  } else {
    el.textContent = newChildren
  }
}

新:

if (typeof newChildren === 'string' || typeof newChildren === 'number') {
  if (typeof oldChildren === 'string' || typeof newChildren === 'number') {
    // 新老的children都是字符串或数字 不同就直接替换
    if (newChildren !== oldChildren) {
      el.textContent = newChildren
    }
  } else {
    el.textContent = newChildren
  }
}

就成功实现一个点击更新count并对比更新渲染的效果

源码:

<!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>
    // vdom
    function h (tag, props, children) {
      return {
        tag,
        props,
        children
      }
    }
    function mount (vnode, container) {
      const el = vnode.el = document.createElement(vnode.tag)
      if (vnode.props) {
        for (const key in vnode.props) {
          const value = vnode.props[key]
          if (key.startsWith('on')) {
            const name = key.slice(2).toLowerCase()
            el.addEventListener(name, value)
          } else {
            el.setAttribute(key, value)
          }
        }
      }
      if (vnode.children != null && vnode.children !== '') {
        if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {
          el.textContent = vnode.children
        } else {
          vnode.children.forEach(child => {
            if (typeof child === 'string') {
              el.appendChild(document.createTextNode(child))
            } else {
              mount(child, el)
            }
          })
        }
      }
      container.appendChild(el)
    }
    function patch (n1, n2) {
      // 首先要确保他们是同一个标签
      if (n1.tag === n2.tag) {
        const el = n2.el = n1.el
        const oldProps = n1.props || {}
        const newProps = n2.props || {}

        // 如果有了template模板,下面的这些对比都有可能直接跳过(比如检测到静态标签直接跳过对比)

        // 对比新老中是否有key更改了
        for (const key in newProps) {
          const oldValue = oldProps[key]
          const newValue = newProps[key]
          if (newValue != oldValue) {
            el.setAttribute(key, newValue)
          }
        }
        // 当旧的中没有key要去除
        for (const key in oldProps) {
          if (!(key in newProps)) {
            el.removeAttribute(key)
          }
        }

        // 接着处理children,可能是一个'string' 或者 []
        const oldChildren = n1.children
        const newChildren = n2.children
        if (typeof newChildren === 'string' || typeof newChildren === 'number') {
          if (typeof oldChildren === 'string' || typeof newChildren === 'number') {
            // 新老的children都是字符串或数字 不同就直接替换
            if (newChildren !== oldChildren) {
              el.textContent = newChildren
            }
          } else {
            el.textContent = newChildren
          }
        } else {
          // 新的children是数组,老的是字符串就遍历循环添加
          if (typeof oldChildren === 'string') {
            el.innerHTML = ''
            newChildren.forEach(child => {
              mount(child, el)
            })
          } else {
            // 当新老的children都是数组,有两种情况,一种使用v-for提供了一个唯一的key,用key去对比
            // 另一种就是双端指针 头头尾尾,vue2中是 头头,尾尾,旧头新尾,旧尾新头
            // 以上四种情况都匹配不到的话,就以新头对旧的vnode进行查找,看是否找到,都找不到就新建元素

            // 这里只是做简单的对比,不一样就直接替换
            const commonLength = Math.min(oldChildren.length, newChildren.length)
            for (let i = 0; i < commonLength; i++) {
              patch(oldChildren[i], newChildren[i])
            }
            // 还需要考虑newChildren更长或者更短的情况下进行新增和删除
            if (newChildren.length > oldChildren.length) {
              newChildren.slice(oldChildren.length).forEach(child => {
                mount(child, el)
              })
            } else if (newChildren.length < oldChildren.length) {
              oldChildren.slice(newChildren.length).forEach(child => {
                el.removeChild(child.el)
              })
            }
          }
        }
      } else {
        // 当不是同一个类型就直接情况再挂载
        const el = n2.el = n1.el
        el.innerHTML = ''
        mount(n2, el)
      }
    }

    // reactivity
    let activeEffect

    class Dep {
      subscribers = new Set()
      depend () {
        if (activeEffect) {
          this.subscribers.add(activeEffect)
        }
      }
      notify (effect) {
        this.subscribers.forEach(effect => {
          effect()
        })
      }
    }
    function watchEffect (effect) {
      activeEffect = effect
      effect()
      activeEffect = null
    }

    const targetMap = new WeakMap()

    function getDep (target, key) {
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
      }
      let dep = depsMap.get(key)
      if (!dep) {
        dep = new Dep()
        depsMap.set(key, dep)
      }
      return dep
    }
    const reactiveHandles = {
      get (target, key, receiver) {
        const dep = getDep(target, key)
        dep.depend()
        // return target[key]
        return Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        const dep = getDep(target, key)
        const result = Reflect.set(target, key, value, receiver)
        dep.notify()
        return result
      }
    }

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

    const App = {
      data: reactive({
        count: 0
      }),
      render () {
        return h('div', {
          onClick: () => {
            this.data.count++
          }
          // }, String(this.data.count))
        }, this.data.count)
      }
    }

    function mountApp (component, containerString) {
      const container = document.querySelector(containerString)
      if (!container) {
        return console.warn(`[Vue warn]: Failed to mount app: mount target selector "${containerString}" returned null.`);
      }
      let oldVdom = component.render()
      mount(oldVdom, container)
      watchEffect(() => {
        const newVdom = component.render()
        patch(oldVdom, newVdom)
        oldVdom = newVdom
      })
    }
    mountApp(App, '#app')
  </script>
</body>

</html>
版权声明:本文为BIMiracle原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇