手写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
}


posted @ 2021-09-08 16:56  shine_lovely  阅读(47)  评论(0编辑  收藏  举报