「手写实现」mini-vue强化版

396 阅读4分钟

前言

  • 前面实现了简化版的Vue,juejin.cn/post/701995…
  • 简化版的Vue是需要真实DOM的替换。
  • 一个Dep管理多个Watcher,在Watcher过多的情况下就会依赖过多导致卡顿,这就是Vue之前不支持大型项目的原因。
  • 今天来实现Vue2.x版本中是如何优化上面的问题的。下面大部分是模拟源码实现。

实现模版

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue2.x</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./vue2.js"></script>
    <script>
      const app = new Vue({
        el: '#app',
        data: {
          counter: 1,
        },
        // 编写render函数
        render(h) {
          return h('div', { id: 'app' }, [
            h('p', { class: 'title' }, this.counter + ''),
          ])
        },
      })
      setInterval(() => {
        app.counter++
      }, 1000)
    </script>
  </body>
</html>

Vue2的实现

  1. 去除编译函数

    class Vue {
      constructor(options) {
        this.$options = options
        this.$el = options.el
        this.$data = options.data
    ​
        // 1. 将data数据进行响应式数据
        observer(this.$data)
    ​
        // 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问
        proxy(this.$data, this)
    ​
        // 2. 这次要实现render函数编译,去除Vue1.x版本的编译函数
        // new Compiler(this.$el, this)
      }
    }
    
  2. 加入$mount挂载方法

     $mount(el) {
        // 获取宿主
        this.$el = document.querySelector(el)
    ​
        const updateComponent = () => {
          // 执行render
          const { render } = this.$options
          const el = render.call(this)
          // 获取指定节点的父级
          const parent = this.$el.parentElement
          // 插入到父级的参考元素边,参考元素this.$el.nextSibling
          parent.insertBefore(el, this.$el.nextSibling)
          // 删除原来的节点
          parent.removeChild(this.$el)
          // 更新最新的节点
          this.$el = el
        }
    ​
        new Watcher(this, updateComponent)
      }
    
  3. Watcher函数更改

    // 实现Watcher监听
    class Watcher {
      constructor(vm, fn) {
        this.vm = vm
    ​
        // 接收更新函数
        this.getter = fn
    ​
        // 执行getter方法
        this.get()
      }
    ​
      get() {
        // 创建watcher时执行getter
        Dep.target = this
        this.getter.call(this.vm)
        Dep.target = null
      }
      
      update() {
        this.get()
      }
    }
    
  4. Dep函数更改

    // 实现Dep,多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。
    class Dep {
      constructor() {
        // 不能重复添加
        this.deps = new Set()
      }
    ​
      addDep(dep) {
        this.deps.add(dep)
      }
    ​
      notify() {
        this.deps.forEach((dep) => dep.update())
      }
    }
    
  5. 执行mount方法,学习Vue源码中,如果有el选项,手动触发$mount函数,也可以在new Vue的时候进行挂载。

    class Vue {
      constructor(options) {
        this.$options = options
        this.$el = options.el
        this.$data = options.data// 1. 将data数据进行响应式数据
        observer(this.$data)
    ​
        // 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问
        proxy(this.$data, this)
    ​
        // 2. 编译
        // new Compiler(this.$el, this)// 挂载
        if (options.el) {
          this.$mount(options.el)
        }
      }
    }
    
    • 方式一

      <script>
        const app = new Vue({
          el: '#app',
          data: {
            counter: 1,
          },
          // 编写render函数
          render(h) {
            return h('div', { id: 'app' }, [
              h('p', { class: 'title' }, this.counter + ''),
            ])
          },
        })
      </script>
      
    • 方式二

      <script>
        const app = new Vue({
          data: {
            counter: 1,
          },
          // 编写render函数
          render(h) {
            return h('div', { id: 'app' }, [
              h('p', { class: 'title' }, this.counter + ''),
            ])
          },
        }).$mount('#app')
      </script>
      
  6. 虚拟DOM的加入

    1. 修改$mount中的updateComponent方法

      $mount(el) {
        // 获取宿主
        this.$el = document.querySelector(el)
      ​
        const updateComponent = () => {
          // 真实节点操作
          // 执行render
          // const { render } = this.$options
          // const el = render.call(this)
          // // 获取指定节点的父级
          // const parent = this.$el.parentElement
          // // 插入到父级的参考元素边,参考元素this.$el.nextSibling
          // parent.insertBefore(el, this.$el.nextSibling)
          // // 删除原来的节点
          // parent.removeChild(this.$el)
          // // 更新最新的节点
          // this.$el = el
      ​
          // 虚拟节点操作
          const { render } = this.$options
          const vnode = render.call(this, this.$createElement)
          this._update(vnode)
        }
      ​
        new Watcher(this, updateComponent)
      }
      
    2. $createElement转换为虚拟DOM

      // 输出一个对象,不考虑边界情况
      $createElement(tag, props, children) {
        return { tag, props, children }
      }
      
    3. _update更新

       _update(vnode) {
         // 获取上次执行的vnode
         const prevVnode = this._vnode
      ​
         if (!prevVnode) {
           // init
           this.__patch__(this.$el, vnode)
         } else {
           // update
           this.__patch__(prevVnode, vnode)
         }
       }
      
    4. patch对比

      __patch__(oldVnode, vnode) {
        if (oldVnode.nodeType) {
          //init
          const parent = oldVnode.parentElement
          const refElm = oldVnode.nextSibling
      ​
          const el = this.createElm(vnode)
          parent.insertBefore(el, refElm)
          parent.removeChild(oldVnode)
        } else {
          // update
          // 获取el
          const el = (vnode.el = oldVnode.el)
          // props
          const oldProps = oldVnode.props || {}
          const newProps = vnode.props || {}
      ​
          for (const key in newProps) {
            el.setAttribute(key, newProps[key])
          }
          for (const key in oldProps) {
            if (!(key in newProps)) {
              el.removeAttribute(key)
            }
          }
      ​
          // children
          const oldCh = oldVnode.children
          const newCh = vnode.children
      ​
          if (typeof newCh === 'string') {
            if (typeof oldCh === 'string') {
              if (newCh !== oldCh) {
                el.textContent = newCh
              }
            } else {
              // 以前没文本
              el.textContent = newCh
            }
          } else {
            if (typeof oldCh === 'string') {
              // 清空
              el.innerHTML = ''
              newCh.forEach((child) => {
                el.appendChild(this.createElm(child))
              })
            } else {
              // 重排
              this.updateChildren(el, oldCh, newCh)
            }
          }
        }
      ​
        // 保存vnode
        this._vnode = vnode
      }
      
      • createElm虚拟DOM转换真实DOM

        // vnode => dom
        createElm(vnode) {
          const el = document.createElement(vnode.tag)
        ​
          //props
          if (vnode.props) {
            for (const key in vnode.props) {
              const value = vnode.props[key]
              el.setAttribute(key, value)
            }
          }
        ​
          //children
          if (vnode.children) {
            if (typeof vnode.children === 'string') {
              // text
              el.textContent = vnode.children
            } else {
              vnode.children.forEach((v) => {
                const child = this.createElm(v)
                el.appendChild(child)
              })
            }
          }
        ​
          // 保存创建真实的dom
          vnode.el = el
        ​
          return el
        }
        
      • updateChildren比较孩子的重排操作

        // 重排
        updateChildren(parentElm, oldCh, newCh) {
          const len = Math.min(oldCh.length, newCh.length)
          for (let i = 0; i < len; i++) {
            this.__patch__(oldCh[i], newCh[i])
          }
        ​
          if (newCh.length > oldCh.length) {
            newCh.slide(len).forEach((child) => {
              const el = this.createElm(child)
              parentElm.appendChild(el)
            })
          } else if (newCh.length < oldCh.length) {
            oldCh.slide(len).forEach((child) => {
              parentElm.removeChild(child.el)
            })
          }
        }
        

最终实现

function definReactive(obj, key, val) {
   // 对象递归处理
   observer(val)

   const dep = new Dep()

   Object.defineProperty(obj, key, {
     get() {
       // 有Watcher就收集依赖
       Dep.target && dep.addDep(Dep.target)
       console.log(`get ${key}`)
       return val
     },
     set(newVal) {
       if (newVal !== val) {
         console.log(`set ${newVal}`)
         observer(val)
         val = newVal

         // 修改执行通知更新
         dep.notify()
       }
     },
   })
 }

 // 数组的响应式处理
 // 1. 替换数组中的方法
 const orginalProto = Array.prototype
 // 拷贝一份,进行修改
 const arrayProto = Object.create(orginalProto)
 // 举例常用的4个方法
 ;[('push', 'pop', 'shift', 'unshift')].forEach((method) => {
   arrayProto[method] = function() {
     // 执行原始操作
     orginalProto[method].apply(this, arguments)
     const dep = new Dep()

     // 执行新的操作更新
     dep.notify()
   }
 })

 function observer(obj) {
   // 判断是否是对象
   if (typeof obj !== 'object' || obj === null) return

   new Observer(obj)
 }

 function proxy(data, vm) {
   Object.keys(data).forEach((key) => {
     Object.defineProperty(vm, key, {
       get() {
         return vm.$data[key]
       },
       set(newVal) {
         vm.$data[key] = newVal
       },
     })
   })
 }

 class Vue {
   constructor(options) {
     this.$options = options
     this.$el = options.el
     this.$data = options.data

     // 1. 将data数据进行响应式数据
     observer(this.$data)

     // 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问
     proxy(this.$data, this)

     // 2. 编译
     // new Compiler(this.$el, this)

     // 挂载
     if (options.el) {
       this.$mount(options.el)
     }
   }

   $mount(el) {
     // 获取宿主
     this.$el = document.querySelector(el)

     const updateComponent = () => {
       // 真实节点操作
       // 执行render
       const { render } = this.$options
       // const el = render.call(this)
       // // 获取指定节点的父级
       // const parent = this.$el.parentElement
       // // 插入到父级的参考元素边,参考元素this.$el.nextSibling
       // parent.insertBefore(el, this.$el.nextSibling)
       // // 删除原来的节点
       // parent.removeChild(this.$el)
       // // 更新最新的节点
       // this.$el = el

       // 虚拟节点操作
       const vnode = render.call(this, this.$createElement)
       this._update(vnode)
     }

     new Watcher(this, updateComponent)
   }

   $createElement(tag, props, children) {
     return { tag, props, children }
   }

   _update(vnode) {
     // 获取上次执行的vnode
     const prevVnode = this._vnode

     if (!prevVnode) {
       // init
       this.__patch__(this.$el, vnode)
     } else {
       // update
       this.__patch__(prevVnode, vnode)
     }
   }

   __patch__(oldVnode, vnode) {
     if (oldVnode.nodeType) {
       //init
       const parent = oldVnode.parentElement
       const refElm = oldVnode.nextSibling

       const el = this.createElm(vnode)
       parent.insertBefore(el, refElm)
       parent.removeChild(oldVnode)
     } else {
       // update
       // 获取el
       const el = (vnode.el = oldVnode.el)
       // props
       const oldProps = oldVnode.props || {}
       const newProps = vnode.props || {}

       for (const key in newProps) {
         el.setAttribute(key, newProps[key])
       }
       for (const key in oldProps) {
         if (!(key in newProps)) {
           el.removeAttribute(key)
         }
       }

       // children
       const oldCh = oldVnode.children
       const newCh = vnode.children

       if (typeof newCh === 'string') {
         if (typeof oldCh === 'string') {
           if (newCh !== oldCh) {
             el.textContent = newCh
           }
         } else {
           // 以前没文本
           el.textContent = newCh
         }
       } else {
         if (typeof oldCh === 'string') {
           // 清空
           el.innerHTML = ''
           newCh.forEach((child) => {
             el.appendChild(this.createElm(child))
           })
         } else {
           // 重排
           this.updateChildren(el, oldCh, newCh)
         }
       }
     }

     // 保存vnode
     this._vnode = vnode
   }

   // 重排
   updateChildren(parentElm, oldCh, newCh) {
     const len = Math.min(oldCh.length, newCh.length)
     for (let i = 0; i < len; i++) {
       this.__patch__(oldCh[i], newCh[i])
     }

     if (newCh.length > oldCh.length) {
       newCh.slide(len).forEach((child) => {
         const el = this.createElm(child)
         parentElm.appendChild(el)
       })
     } else if (newCh.length < oldCh.length) {
       oldCh.slide(len).forEach((child) => {
         parentElm.removeChild(child.el)
       })
     }
   }

   // vnode => dom
   createElm(vnode) {
     const el = document.createElement(vnode.tag)

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

     //children
     if (vnode.children) {
       if (typeof vnode.children === 'string') {
         // text
         el.textContent = vnode.children
       } else {
         vnode.children.forEach((v) => {
           const child = this.createElm(v)
           el.appendChild(child)
         })
       }
     }

     // 保存创建真实的dom
     vnode.el = el

     return el
   }
 }

 // 实现编译
 class Compiler {
   constructor(el, vm) {
     this.$vm = vm
     this.$el = document.querySelector(el)

     if (this.$el) {
       this.compile(this.$el)
     }
   }

   compile(el) {
     const childNodes = el.childNodes
     childNodes.forEach((node) => {
       if (this.isElement(node)) {
         // 元素节点编译
         if (node.childNodes.length > 0) {
           this.compile(node)
         }
         this.compileElement(node)
       } else if (this.isInertText(node)) {
         // 文本节点编译
         this.compileText(node)
       }
     })
   }

   // 判断节点是不是元素节点
   isElement(node) {
     return node.nodeType === 1
   }

   // 判断节点是不是文本节点并且是插值文本{{}}
   isInertText(node) {
     return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
   }

   // 判断是不是h-开头的指令
   isDir(attr) {
     return attr.startsWith('h-')
   }

   // 判断是不是@开头的方法
   isEvent(attr) {
     return attr.startsWith('@')
   }

   // 编译文本
   compileText(node) {
     // node.textContent = this.$vm[RegExp.$1]
     this.update(node, RegExp.$1, 'text')
   }

   // 编译元素
   compileElement(node) {
     const attributes = node.attributes
     Array.from(attributes).forEach((attr) => {
       // h-text="count"
       const attrName = attr.name // h-text
       const exp = attr.value // count

       if (this.isDir(attrName)) {
         const dir = attrName.substring(2)
         this[dir] && this[dir](node, exp)
       } else if (this.isEvent(attrName)) {
         const dir = attrName.substring(1)
         this.eventHandler(node, exp, dir)
       }
     })
   }

   // 方法
   eventHandler(node, exp, dir) {
     const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
     node.addEventListener(dir, fn.bind(this.$vm))
   }

   text(node, exp) {
     // node.textContent = this.$vm[exp]
     this.update(node, exp, 'text')
   }

   textUpdater(node, val) {
     node.textContent = val
   }

   html(node, exp) {
     // node.innerHTML = this.$vm[exp]
     this.update(node, exp, 'html')
   }

   htmlUpdater(node, val) {
     node.innerHTML = val
   }

   model(node, exp) {
     this.update(node, exp, 'model')

     node.addEventListener('input', (e) => {
       this.$vm[exp] = e.target.value
     })
   }

   modelUpdater(node, val) {
     node.value = val
   }

   // 一个key对应一个Watcher实例
   update(node, exp, dir) {
     const fn = this[dir + 'Updater']
     fn && fn(node, this.$vm[exp])
     new Watcher(this.$vm, exp, function(val) {
       fn && fn(node, val)
     })
   }
 }

 // 实现Dep,多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。
 class Dep {
   constructor() {
     // 不能重复添加
     this.deps = new Set()
   }

   addDep(dep) {
     this.deps.add(dep)
   }

   notify() {
     this.deps.forEach((dep) => dep.update())
   }
 }

 // 实现Watcher监听
 class Watcher {
   constructor(vm, fn) {
     this.vm = vm

     // 接收更新函数
     this.getter = fn

     // 执行getter方法
     this.get()
   }

   get() {
     // 创建watcher时执行getter
     Dep.target = this
     this.getter.call(this.vm)
     Dep.target = null
   }

   update() {
     this.get()
   }
 }

 // 实现响应式
 class Observer {
   constructor(value) {
     this.value = value

     if (typeof value === 'object') {
       this.walk(this.value)
     } else if (Array.isArray(value)) {
       this.arrayWalk(value)
     }
   }

   // 数组执行方法
   arrayWalk(obj) {
     obj.__proto__ = arrayProto
     const keys = Object.keys()
     for (let i = 0; i < keys.length; i++) {
       observer(obj[i])
     }
   }

   walk(obj) {
     Object.keys(obj).forEach((key) => {
       definReactive(obj, key, obj[key])
     })
   }
 }

结语

  1. Vue2.x版本中加入了虚拟DOM,使的在编译的时候减少了很多真实DOM的操作大大提高了性能。
  2. Vue2.x版本中把Dep和Watcher的依赖关系进行变化,一个组件对应的一个Dep,一个key值对应一个Wacther,从原来的1对多的关系进化成多对多的关系,并依赖updateComponet的函数使其紧密的关联在一起,提高了性能。