zoukankan      html  css  js  c++  java
  • 手写vue -3 -- 优化:虚拟DOM、diff算法

    index.html

    <!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>Document</title>
    </head>
    <body>
      <div id="app"> </div>
    
      <script type="module">
        import Vue from './vue.js'
    
        new Vue({
          // el: '#app',
          data() {
            return {
              title: '这里是标题',
              li2: [{ tag: 'span', children: 'li2的初始值' }],
              li3: 'li3'
            }
          },
          render(h) {
            // return h('div', { 'class': 'title' }, this.title)
            return h('ul', { 'class': 'list' }, [
              { tag: 'li', children: 'li1' },
              { tag: 'li', children: this.li2 },
              { tag: 'li', children: this.li3 }
            ])
          },
          mounted() {
            setTimeout(() => {
              // this.title = '哈哈哈,标题改变了'
              // console.log('title的值变为:',this.title);
              // this.li3 = 'li3的值改变了!'
              this.li2 = 'li2的值改变了'
              this.li3 = [{ tag: 'span', children: 'li3的值改变了!' }]
            }, 2000)
          }
        }).$mount('#app')
      </script>
    </body>
    </html>
    

    vue.js

    export default class Vue {
      constructor(options) {
        this.$options = options
        this.$data = options.data()
    
        this.proxy(this.$data)
        this.observe(this.$data)
    
        if (options.el) {
          this.$mount(options.el)
        }
    
        return this
      }
    
      // {title: '标题'} => this.$data.title => this.title
      proxy(data) {
        Object.keys(data).forEach(key => {
          Object.defineProperty(this, key, {
            get() {
              return this.$data[key]
            },
            set(v) {
              this.$data[key] = v
            },
          })
        })
      }
    
      // 劫持、响应化
      observe(data) {
        if (typeof data !== 'object' || data === null) {
          return
        }
        if (Array.isArray(data)) {
          data.forEach(d => {
            this.observe(d)
          })
        } else {
          Object.keys(data).forEach(key => {
            // data:{form: {user:'', certNo: ''}} => data[key]是对象
            // 遍历所有层次
            this.observe(data[key])
            new Observe(this, data, key)
          })
        }
      }
    
      // 挂载:
      // $mount 函数,返回一个mountComponent函数, 该函数中,主要:
      // ①: 定义updateComponent函数,该函数主要用来①初始化(首次渲染), ② 更新渲染
      //      updateComponent函数,主要执行_update()函数,这个函数的参数,就是render函数返回的虚拟dom, _update函数的作用:将虚拟dom渲染到dom上
      //      render函数获取虚拟dom,将虚拟dom传入_update函数,在_update函数中调用_patch_函数,渲染至dom
      // ②: 实例化Watcher, 将updateComponent函数传入
      $mount(el) {
        this.$el = document.querySelector(el)
        // 优先级: render>template>el
        // 如果 options中传入render函数,则执行mountComponent()
        // 否则,判断是否传入el,如果传入el,则将el => 转换为template,再用 compileToFunction函数来生成render函数,再赋给options中,然后再调用mountComponent函数
        const mountComponent = () => {
          const updateComponent = () => {
             /* 
            if(!this.$options.render){
              if(el){
                // todo: 将el转为template
              }
              // todo: 将template传入 compileToFunction函数,生成返回render函数。再讲render函数加入 options中。
            }
            */
            // 假定 在options中传入了render函数
            const { render } = this.$options
            // render函数有2个参数,第一个参数:h => $createElement(args),$createElement将传入的js对象转为虚拟dom,返回虚拟dom树
            // render函数实际就是返回$createElement(args)的结果
            const vnode = render.call(this, this.$createElement)
            console.log(vnode)
            // _update(vnode)将虚拟节点转为真实dom : 函数内部调用_path_(diff算法)找出不同,再定点生成dom,定点打补丁
            this._update(vnode)
          }
          new Watcher(this, updateComponent)
        }
        return mountComponent()
      }
    
      // vnode => dom
      // _update函数主要是将虚拟node转为真实dom,主要是执行 _patch_函数,将虚拟dom转为真实dom,_patch_也是执行diff算法的函数
      // 获取上一次更新vnode树,① 如果不存在,则为初始化; ② 如果存在,则为视图更新操作
      _update(vnode) {
        // 获取上一次更新的vnode树
        const prevVnode = this._vnode // this._vnode,在_patch_执行时,会将生成的vnode树存储到this._vnode上
        if (!prevVnode) {
          this._patch_(this.$el, vnode) // 初始化
        } else {
          this._patch_(prevVnode, vnode) // 更新操作
        }
      }
    
      // 源码中,调用_patch_函数,实际就是调用 createPatchFunction函数返回的patch函数。这里就简化了。
      // patch函数其实就是diff的过程。
      // ① 旧节点存在, 新节点不存在。(组件销毁时), 调用旧节点的destroy生命周期函数
      // ② 旧节点不存在, 新节点存在。增加新节点创建dom
      // ③ 新旧节点都存在
      //    => 1. 旧节点是真实dom  => 执行 ☆ createElm(vnode),进行初始化
      //    => 2. 旧节点不是真实dom,新旧节点相同 => 执行 ☆ patchVnode,节点更新打补丁
      //    => 3. 旧节点不是真实dom是vnode,但是新旧节点不相同 => 执行 ☆ createElm(vnode), 销毁旧节点以及dom
      _patch_(oldVnode, vnode) {
        // 真实dom节点存在nodeType: 1:元素节点 3:文本节点
        if (oldVnode.nodeType) {
          const parent = oldVnode.parentNode
          const nextSibling = oldVnode.nextSibling
          const el = this.createElm(vnode)
          parent.insertBefore(el, nextSibling)
          parent.removeChild(oldVnode)
    
          // mounted钩子函数执行
          if (this.$options.mounted) {
            this.$options.mounted.call(this)
          }
    
          this._vnode = vnode // 是为了在_update()函数中获取上一次更新的vnode树
        } else {
          // 判断新旧节点是否相同: tag、key 相同就是同一个节点。 这里就不考虑key了。
          // 新旧节点相同 => patchVnode,进行diff
          if (oldVnode.tag === vnode.tag) {
            this.patchVnode(oldVnode, vnode)
          } else {
            // todo: el.parentNode.replaceChild(this.createElm(newVnode), el)
            const oldElm = oldVnode.elm
            const parentElm = oldElm.parent
            const el = this.createElm(vnode)
            vnode.elm = el
            parentElm.replaceChild(el, oldElm)
          }
          this._vnode = vnode // 是为了在_update()函数中获取上一次更新的vnode树
        }
      }
    
      // 新旧节点是同一个节点,进行patchVnode操作
      // ① 如果新旧节点完全相同,引用地址相同 , return;
      // ② 新节点不是文本节点:
      //    1. 新旧节点都存在子节点,且新旧节点的子节点数组不相同 ch !== oldCh =>  updateChildren
      //    2. 新节点有子节点, 旧节点没有子节点 => 旧节点是文本节点,则清空文本,并创建新节点的子节点,并新增
      //    3. 新节点没有子节点, 旧节点有子节点 => 移除旧节点的子节点及dom
      //    4. 新旧节点都没有子节点 => 清除文本
      patchVnode(oldVnode, vnode) {
        if (oldVnode === vnode) return
        // 新旧节点是同一个节点: 将旧节点的elm赋值给新节点的elm,新旧节点保持一致
        // 获取oldVnode对应的真实dom, 用于做真实的dom操作, 并将这个真实dom,存储到新节点的el变量上,以方便下次更新是使用
        const el = (vnode.elm = oldVnode.elm)
        const oldCh = oldVnode.children
        const ch = vnode.children
    
        if (oldCh && ch) {
          if (Array.isArray(oldCh) && Array.isArray(ch)) {
            // 新旧节点都存在子节点 => 更新子节点 updateChildren
            this.updateChildren(el, oldCh, ch)
          } else if (typeof ch === 'string') {
            el.textContent = ch
          } else {
            el.textContent = ''
            this.addVnodes(el, ch, 0, ch.length - 1)
          }
        } else if (ch) {
          // 新节点存在子节点,旧节点不存在子节点 => 清空旧节点文本,创建新的子节点,并添加
          if (typeof ch === 'string') {
            el.textContent = ch
          } else {
            this.addVnodes(el, ch, 0, ch.length - 1)
          }
        } else if (oldCh) {
          // 旧节点存在子节点,新节点不存在子节点 => 移除旧节点子节点及dom
          this.removeVnodes(oldCh, 0, oldVnode.length - 1)
        } else {
          el.textContent = ''
        }
      }
    
      updateChildren(parentElm, oldCh, newCh) {
        // oldCh、ch都是子节点数组 => diff
        let oldStartIdx = 0
        let newStartIdx = 0
        let oldEndIdx = oldCh.length - 1
        let newEndIdx = newCh.length - 1
        let oldStartVnode = oldCh[0]
        let oldEndVnode = oldCh[oldEndIdx]
        let newStartVnode = newCh[0]
        let newEndVnode = newCh[newEndIdx]
    
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (sameVnode(oldStartVnode, newStartVnode)) {
            this.patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            this.patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) {
            this.patchVnode(oldStartVnode, newEndVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldEndVnode, newStartVnode)) {
            this.patchVnode(oldEndVnode, newStartVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
            // 新旧节点的首位都没有相同节点,则新数组头节点的key去旧数组剩余节点中进行匹配,如果存在相同key的节点,就patchVnode(),并将旧数组中该位置的值设为undefined。
            // 否则,就createElm()
            // 这里就简写成全部创建了。
            parentElm.appendChild(this.createElm(newStartVnode))
            newStartVnode = newCh[++newStartIdx]
          }
        }
    
        // 如果旧数组遍历完成,即oldStartIdx > oldStartIdx,此时newStartIdx、newEndIdx之间的节点为新增节点
        if (oldStartIdx > oldEndIdx) {
          this.addVnodes(parentElm, newCh, newStartIdx, newEndIdx)
        } else if (newStartIdx > newEndIdx) {
          // 如果新数组遍历完成,即newStartIdx > newEndIdx, 此时oldStartIdx、oldEndIdx之间的节点为需要删除的节点
          this.removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }
    
      addVnodes(parent, vnodes, startIdx, endIdx) {
        for (; startIdx <= endIdx; ++startIdx) {
          parent.appendChild(this.createElm(vnodes[startIdx]))
        }
      }
    
      // ul 删除 第1个-第3个 li, 还剩第4个li
      removeVnodes(vnodes, startIdx, endIdx) {
        const parent = vnodes[0].elm.parentNode
        parent.textContent = ''
      }
    
      // 将ast语法转为虚拟节点
      // @returns { vnode }
      $createElement(tag, props, children) {
        return { tag, props, children }
      }
    
      // 将虚拟node转为真实dom
      // vnode : { tag, props, children } => { tag: 'div', props: { class:'list'}, children: '这里是个列表'}
      /* vnode : { tag: 'ul', props: { class:'list'}, children: [
                                                     { tag: 'li', props: { class:'item'}, children: '1'},
                                                     { tag: 'li', props: { class:'item'}, children: '2'},
                                                    ]}
      */
      createElm(vnode) {
        const el = document.createElement(vnode.tag)
    
        if (vnode.props) {
          Object.keys(vnode.props).forEach(propName => {
            el.setAttribute(propName, vnode.props[propName])
          })
        }
    
        if (vnode.children) {
          if (typeof vnode.children === 'string') {
            el.textContent = vnode.children
          } else {
            vnode.children.forEach(child => {
              const childEl = this.createElm(child)
              el.appendChild(childEl)
            })
          }
        }
    
        // vnode.elm 存在: 说明该vnode已经被渲染过了
        vnode.elm = el
        return el
      }
    }
    
    // 数据响应式处理
    class Observe {
      constructor(vm, data, key) {
        this.$vm = vm
        this.defineReactive(data, key, data[key])
      }
    
      defineReactive(data, key, val) {
        const vm = this.$vm
        // 一个key对应一个dep
        const dep = new Dep()
    
        Object.defineProperty(data, key, {
          get() {
            // Dep.target里面存储的是watcher实例, 只有实例化Watcher时,Dep.target才有值
            // Watcher,在初始化时会进行实例化,在vue使用过程中使用watch监听时,也会产生Watcher实例
            // initState先执行,执行之后才会进行$mount挂载,在挂载时才会实例化Watcher,所以在首次定义响应式时,是不存在Dep.target值的。
            // 在$mount挂载之后,使用key值时,Dep.target才存在
            if (Dep.target) {
              dep.addDep(Dep.target)
            }
            return val
          },
          set(v) {
            if (v !== val) {
              // 考虑到用户在使用this.title时,以前为string字符串,后进行重新赋值为this.title = {name: '我是标题'}
              vm.observe(v) // 重新对新值v做响应式处理
              val = v
    
              // 通知依赖更新
              dep.notify()
            }
          },
        })
      }
    }
    
    // 依赖收集器
    class Dep {
      constructor() {
        // Set数据容器,存放 无重复有序列表 set.add(),set.delete(),set.has();set.size ; set.forEach()
        this.deps = new Set()
      }
      addDep(watcher) {
        this.deps.add(watcher)
      }
    
      notify() {
        this.deps.forEach(watcher => {
          watcher.update()
        })
      }
    }
    
    class Watcher {
      constructor(vm, callback) {
        this.$vm = vm
        // callback: updateComponent
        this.$cb = callback
        // 在watcher实例化的时候,进行收集依赖,执行渲染
        this.getter()
      }
      // getter()方法: ① 初始化首次渲染 ② 更新组件再次渲染
      // 收集依赖,执行渲染
      getter() {
        // 将watcher实例赋值给Dep.target, 方便data数据,在get访问器中进行收集
        Dep.target = this
        // 执行updateComponent渲染函数,这个函数是在$mount挂载的时候,对Watcher进行实例化时传入的参数
        this.$cb.call(this.$vm)
        console.log('updateComponent组件更新方法执行!')
        Dep.target = null
      }
    
      update() {
        // 组件更新时,执行组件渲染函数
        this.getter()
      }
    }
    
    function sameVnode(a, b) {
      return a.tag === b.tag
    }
    
    
    
  • 相关阅读:
    Logback日志格式配置相关记录
    前后端分离验证码之cookie+redis方案
    聊一聊Swagger ui登录功能实现方案
    nginx-thinkphp5
    jmeter常用的性能测试监听器
    jvm内存
    TCP连接状态详解
    原生Javascript实现图片轮播效果
    适用于CSS2的各种运动的javascript运动框架
    JS中for循环里面的闭包问题的原因及解决办法
  • 原文地址:https://www.cnblogs.com/shine-lovely/p/15243514.html
Copyright © 2011-2022 走看看