Vue3_14(虚拟DOM | 三大核心系统:Compiler、Runtime、Reactivity)

虚拟DOM的优势

对真实的元素节点进行抽象,抽象成VNode(虚拟节点),方便对其进行各种操作:

  直接操作DOM来说是有很多的限制的,比如diff、clone等等;
  可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的;

其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点:

  如渲染在canvas、WebGL、SSR、Native(iOS、Android)上;
  并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染;

虚拟DOM的渲染过程

 

VUE三大核心系统 

  Compiler模块:编译模板系统;
  Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
  Reactivity模块:响应式系统;

 

 

简洁版的Mini-Vue框架,该Vue包括三个模块:渲染系统模块;可响应式系统模块;应用程序入口模块;

渲染系统实现

  功能一:h函数,用于返回一个VNode对象;
  功能二:mount函数,用于将VNode挂载到DOM上;
  功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;

h函数的实现:直接返回一个VNode对象即可

const h = (tag, props, children) => {
  return {
    tag,
    props,
    children
  }
} 

mount函数的实现:
  第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
  第二步:处理props属性 
    如果以on开头,那么监听事件;
    普通属性直接通过 setAttribute 添加即可;
  第三步:处理子节点
    如果是字符串节点,那么直接设置textContent;
    如果是数组节点,那么遍历调用 mount 函数;

const 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')) {
        el.addEventListener(key.slice(2).toLowerCase(), value)
      } else {
        el.setAttribute(key, value)
      }
    }
  }

  if (vnode.children) {
    if (typeof vnode.children === 'string') {
      el.textContent = vnode.children
    } else {
      vnode.children.forEach((item) => {
        mount(item, el)
      })
    }
  }
  container.appendChild(el)
}

Patch函数 – 对比两个VNode

n1和n2是不同类型的节点:
  找到n1的el父节点,删除原来的n1节点的el;
  挂载n2节点到n1的el父节点上;
n1和n2节点是相同的节点:
  处理props的情况
    先将新节点的props全部挂载到el上;
    判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
  处理children的情况
    如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
    如果新节点不同一个字符串类型:
      旧节点是一个字符串类型
        将el的textContent设置为空字符串;
        旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
      旧节点也是一个数组类型
        取出数组的最小长度;
        遍历所有的节点,新节点和旧节点进行path操作;
        如果新节点的length更长,那么剩余的新节点进行挂载操作;
        如果旧节点的length更长,那么剩余的旧节点进行卸载操作;

const patch = (n1, n2) => {
  if (n1.tag !== n2.tag) {
    const p = n1.el.parentNode
    p.removeChild(n1.el)
    mount(n2, p)
  } else {
    const el = (n2.el = n1.el)

    //对象diff
    const oldProps = n1.props || {}
    const newProps = n2.props || {}

    //新对象覆盖原对象,新增或更新key
    for (const key in newProps) {
      const oldValue = oldProps[key]
      const newValue = newProps[key]
      if (oldValue != newValue) {
        if (key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), newValue)
        } else {
          el.setAttribute(key, newValue)
        }
      }
    }
    //原对象对比新对象,删除多出key
    for (const key in oldProps) {
      if (key.startsWith('on')) {
        const value = oldProps[key]
        el.removeEventListener(key.slice(2).toLowerCase(), value)
      }

      if (!(key in newProps)) {
        // if (key.startsWith('on')) {
        //   el.removeEventListener(key.slice(2).toLowerCase(), value)
        // } else {
        el.removeAttribute(key)
        // }
      }
    }

    const oldChildren = n1.children || []
    const newChildren = n2.children || []

    // 情况一: newChildren本身是一个string
    if (typeof newChildren === 'string') {
      if (typeof oldChildren === 'string') {
        if (newChildren != oldChildren) {
          el.textContent = newChildren
        }
      } else {
        el.textContent = newChildren
      }
    } else {
      // 情况二: newChildren本身是一个数组
      if (typeof oldChildren === 'string') {
        el.textContent = ''
        newChildren.forEach((item) => {
          mount(item, el)
        })
      } else {
        //数组diff
        const oldLen = oldChildren.length
        const newLen = newChildren.length
        const commonLen = Math.min(oldLen, newLen)

        // oldChildren: [v1, v2, v3, v8, v9]
        // newChildren: [v1, v5, v6]
        //前面有相同节点的原生进行patch操作
        for (let i = 0; i < commonLen; i++) {
          patch(oldChildren[i], newChildren[i])
        }

        //新数组 比 旧数组多,则新增新
        if (oldLen < newLen) {
          newChildren.slice(commonLen).forEach((item) => mount(item, el))
        }

        //新数组比旧数组少,则删除旧
        if (oldLen > newLen) {
          oldChildren
            .slice(commonLen)
            .forEach((item) => el.removeChild(item.el))
        }
      }
    }

    // console.log(el)
  }
}

可响应式系统

1、依赖收集

class Dep {
  constructor() {
    this.subscribes = new Set()
  }
  depend() {
    if (activeEffect) {
      this.subscribes.add(activeEffect)
    }
  }
  notify() {
    this.subscribes.forEach((effect) => effect())
  }
}

let activeEffect = null
function watchEffect(effect) {
  activeEffect = effect
  dep.depend()
  effect()
  activeEffect = null
}

const dep = new Dep()
watchEffect(function () {
  console.log('effect1:', info.counter * 2, info.name)
})
dep.notify()

2、响应式系统Vue2实现

const targetMap = new WeakMap()
const 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
}

//vue2对raw进行数据劫持
const reactive = (raw) => {
  Object.keys(raw).forEach((key) => {
    const dep = getDep(raw, key)
    let value = raw[key]
    Object.defineProperty(raw, key, {
      get() {
        dep.depend()
        return value
      },
      set(newValue) {
        value = newValue
        dep.notify()
      }
    })
  })
  return raw
}

3、响应式系统Vue3实现

// vue3对raw进行数据劫持
const reactive = (raw) => {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key)
      dep.depend()
      return target[key]
    },
    set(target, key, newValue) {
      const dep = getDep(target, key)
      target[key] = newValue
      dep.notify()
    }
  })
}

完整响应式系统

let activeEffect = null

const targetMap = new WeakMap()
const 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
}

// vue2对raw进行数据劫持
// const reactive = (raw) => {
//   Object.keys(raw).forEach((key) => {
//     const dep = getDep(raw, key)
//     let value = raw[key]
//     Object.defineProperty(raw, key, {
//       get() {
//         dep.depend()
//         return value
//       },
//       set(newValue) {
//         value = newValue
//         dep.notify()
//       }
//     })
//   })
//   return raw
// }

// vue3对raw进行数据劫持
const reactive = (raw) => {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key)
      dep.depend()
      return target[key]
    },
    set(target, key, newValue) {
      const dep = getDep(target, key)
      target[key] = newValue
      dep.notify()
    }
  })
}

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

class Dep {
  constructor() {
    this.subscribes = new Set()
  }
  depend() {
    if (activeEffect) {
      this.subscribes.add(activeEffect)
    }
  }
  notify() {
    this.subscribes.forEach((effect) => effect())
  }
}

const info = reactive({ counter: 100, name: 'why' })

// watchEffect1
watchEffect(function () {
  console.log('effect1:', info.counter * 2, info.name)
})

// watchEffect2
// watchEffect(function () {
//   console.log('effect2:', info.counter * info.counter)
// })

// // watchEffect3
// watchEffect(function () {
//   console.log('effect3:', info.counter + 10, info.name)
// })

// watchEffect(function () {
//   console.log('effect4:', foo.height)
// })

// info.counter++
info.name = 'why123'

// foo.height = 2

为什么Vue3选择Proxy呢?

Object.definedProperty 是劫持对象的属性时,如果新增元素:
  那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;
修改对象的不同:
  使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
  而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
Proxy 能观察的类型比 defineProperty 更丰富
  has:in操作符的捕获器;
  deleteProperty:delete 操作符的捕捉器;
  等等其他操作;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

应用程序入口  

  createApp用于创建一个app对象;
  该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;

function createApp(rootComponent) {
  return {
    mount(selector) {
      const container = document.querySelector(selector)
      let isMounted = false
      let oldVNode = null

      watchEffect(function () {
        if (!isMounted) {
          oldVNode = rootComponent.render()
          mount(oldVNode, container)
          isMounted = true
        } else {
          const newVNode = rootComponent.render()
          patch(oldVNode, newVNode)
          oldVNode = newVNode
        }
      })
    }
  }
}

  

posted @ 2021-12-31 19:09  Hexrui  阅读(788)  评论(0编辑  收藏  举报
返回顶部