23_Vue3源码学习之Mini-Vue的实现

Vue3源码学习之Mini-Vue的实现

实现Mini-Vue

这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:

  • 渲染系统模块;
  • 可响应式系统模块;
  • 应用程序入口模块;

一、渲染系统实现

渲染系统,该模块主要包含三个功能:

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

h函数 – 生成VNode

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

在这里插入图片描述

Mount函数 – 挂载VNode

mount函数的实现:

第一步:根据tag,创建HTML元素,并且存储到vnode的el中;

第二步:处理props属性

  • 如果以on开头,那么监听事件;
  • 普通属性直接通过 setAttribute 添加即可;

第三步:处理子节点

  • 如果是字符串节点,那么直接设置textContent;
  • 如果是数组节点,那么遍历调用 mount 函数;

在这里插入图片描述

Patch函数 – 对比两个VNode

patch函数的实现,分为两种情况:

n1和n2是不同类型的节点

  • 找到n1的el父节点,删除原来的n1节点的el;
  • 挂载n2节点到n1的el父节点上;

n1和n2节点是相同的节点

处理props的情况:

  • 先将新节点的props全部挂载到el上;
  • 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;

处理children的情况:

  • 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
  • 如果新节点不同一个字符串类型:
    • 旧节点是一个字符串类型
      • l将el的textContent设置为空字符串;
      • 旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
    • 旧节点也是一个数组类型
      • 取出数组的最小长度;
      • 遍历所有的节点,新节点和旧节点进行path操作;
      • 如果新节点的length更长,那么剩余的新节点进行挂载操作;
      • 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;

在这里插入图片描述

const h = (tag, props, children) => {
  // vNode-->javascript对象-->{}
  return {
    tag,
    props,
    children
  }
}

const mount = (vNode, container) => {
  // vNode-->element
  // 1.创建出真实的原声,并且在vNode上保留el
  const el = vNode.el = document.createElement(vNode.tag)

  // 2.处理props
  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)
      }
    }
  }

  // 3.处理children
  if (vNode.children) {
    if (typeof vNode.children === 'string') {
      el.textContent = vNode.children
    } else {
      vNode.children.forEach(item => {
        mount(item, el)
      })
    }
  }

  // 4.将el挂载到container上
  container.appendChild(el)
}

const patch = (n1, n2) => {
  // 1.如果n1和n2节点的type类型不相同
  if (n1.tag !== n2.tag) {
    const n1ElParent = n1.el.parentElement
    n1ElParent.removeChild(n1.el)
    mount(n2, n1ElParent)
  } else {
    // 2.n1和n2节点类型相同
    // 2.1取出element对象,并且在n2中保存
    const el = n2.el = n1.el

    // 2.2处理props
    const newProps = n2.props || {}
    const oldProps = n1.props || {}
    // 2.2.1处理newProps
    for (const key in newProps) {
      // 对于newProps,就是添加属性
      const newValue = newProps[key]
      const oldValue = oldProps[key]

      if (newValue !== oldValue) {
        if (key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), newValue)
        } else {
          el.setAttribute(key, newValue)
        }
      }
    }

    // 2.2.2处理oldProps
    for (const key in oldProps) {
      // 对于oldProps,只需删除旧的属性
      if (key.startsWith('on')) {
        el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])
      }

      if (!(key in newProps)) {
        el.removeAttribute(key)
      }
    }

    // 3.处理children
    const newChildren = n2.children
    const oldChildren = n1.children

    // 3.1newChildren本身是一个字符串
    if (typeof newChildren === 'string') {
      // 边界判断
      if (typeof oldChildren === 'string') {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren
        }
      } else {
        el.innerHTML = newChildren
      }
    } else {
      // 3.2newChildren本身是一个数组
      // 3.2.1oldChildren是一个字符串
      if (typeof oldChildren === 'string') {
        el.innerHTML = ''
        newChildren.forEach(item => {
          mount(item, el)
        })
      } else {
        // 3.2.2oldChildren也是一个数组
        // oldChildren: [v1, v2, v3, v8, v9]
        // newChildren: [v1, v5, v6]

        // 1.取出oldChildren和newChildren数组的最小长度,进行patch操作
        // 这种对应的diff算法中没有key的操作,效率低
        const commonLength=Math.min(oldChildren.length,newChildren.length)
        for(let i=0;i<commonLength;i++){
          patch(oldChildren[i],newChildren[i])
        }

        // 2.newChildren.length>oldChildren.length-->添加节点
        if(newChildren.length>oldChildren.length){
          newChildren.slice(oldChildren.length).forEach(item=>{
            mount(item,el)
          })
        }

        // 3.newChildren.length<oldChildren.length-->删除节点
        if(newChildren.length<oldChildren.length){
          oldChildren.slice(newChildren.length).forEach(item=>{
            el.removeChild(item.el)
          })
        }
      }

    }
  }
}

二、响应式系统模块

依赖收集系统

在这里插入图片描述

响应式系统Vue2实现

在这里插入图片描述

响应式系统Vue3实现

在这里插入图片描述

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
  effect()
  activeEffect=null
}

const targetMap=new WeakMap()
function getDep(target,key){
  // 1.根据对象(target)取出对应的Map对象
  let depsMap=targetMap.get(target)
  if(!depsMap){
    depsMap=new Map()
    targetMap.set(target,depsMap)
  }

  // 2.取出具体的dep对象
  let dep=depsMap.get(key)
  if(!dep){
    dep=new Dep()
    depsMap.set(key,dep)
  }

  return dep
}

// Vue2响应式对数据劫持
function 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){
        if(value!==newValue){
          value=newValue
          dep.notify()
        }
      }
    })
  })

  return raw
}

// 测试代码
const info=reactive({counter:100,name:'coder'})
const foo=reactive({height:1.88})

// 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)
})

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

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

// 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

三、框架外层API设计

这样我们就知道了,从框架的层面来说,我们需要有两部分内容:

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

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值