Vue学习记录之进阶篇——vue3模板渲染原理

126 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

我们在前面已经大致的了解了 vue2 和 vue3 的一些特性, 这里我们针对 vue3 的模板渲染原理 ,进行一下深度的剖析 我们先通过一个导图,来大致了解以下 vue3 的渲染器

Snipaste_2022-11-25_11-20-50.png 下面是一个 demo 大家可以配合注释 食用

// 模板渲染原理

/**
 * 用来判定 此 props 是否需要使用属性的方式添加(<input form='form1'> 这种 form 的属性是只读的,其在 Properties 存在,但是不能使用属性设置,需要使用 Atrribute 设置)
 * @param {*} el dom元素
 * @param {*} key 需要给 dom 元素设置的属性
 * @param {*} value 需要给 dom 元素设置的属性值
 * @returns 
 */
function shouldSetAsProps(el,key,value){
  // 特殊处理
  if(key === 'from' && el.tagName === 'INPUT') return false
  // 兜底
  return key in el
}

/**
 * 卸载操作 根据 vnode 获取要卸载的真实 DOM 元素,获取 el 的父元素, 调用 removeChild 移除元素
 * @param {*} vnode 虚拟节点
 */
function unmount(vnode){
  const parent = vnode.el.parentNode;
  if(parent) parent.removeChild(vnode.el)
}

// 全局变量 ,存储当前正在被初始化的组件实例
let currentInstance = null
// 该方法接收组件实例为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance){
  currentInstance = instance
}

// 自定义渲染器 为实现在多端完成功能实现, 需要接收一个配置项,可以使得渲染器不仅能够完成渲染到浏览器的工作。
/**
 * 
 * @param {*} options 
 * @returns 
 */
function createRenderer(options){
  const { createElement, insert, setElementText, patchProps } = options
  /**
   * patch 函数的实现 首次渲染时,container._vnode 是不存在的 即 undefined,意味着 n1 是 undefined,则 patch 函数* 会忽略 n1,直接将 n2 渲染到容器中。
   * @param {*} n1 旧 vnode
   * @param {*} n2 新 vnode
   * @param {*} container 挂载容器
   */
  function patch(n1,n2,container){
    // 挂载方法 vnode type 类型如果是 object 则代表是组件 需要调用 mountComponent 方法
    function mountElement(vnode,container){
      // 创建 DOM 元素,让 vnode.el 引用真实 DOM 元素, 让 vnode 与真实 DOM 之间建立联系,方便卸载操作的完善执行
      const el = vnode.el = document.createElement(vnode.type)
      // 处理子节点,如果子节点是字符串,代表元素具有文本节点
      if(typeof vnode.children === 'string'){
        // 因此只需要设置元素的 textContent 属性即可
        setElementText(el,vnode.children)
        // 如果子节点是数组的话, 循环遍历递归调用
      } else if(Array.isArray(vnode.children)){
        // patch 的第一个参数是 null 因为是挂载阶段,没有旧的 vnode
        vnode.children.forEach(child=>patch(null,child,el))
      }
      // 处理 props
      if(vnode.props){
        for(const key in vnode.props){
          // 调用 patchProps 即可, 第三个参数因为这里是挂载,没有新旧比对, 所以传 null
          patchProps(el,key,null,vnode.props[key])
        }
      }
      // 将元素添加到容器中
      container.appendChild(el)
    }

    if(n1 && n1.type !== n2.type){
      // 如果新旧 vnode 类型不同,则直接卸载旧 vnode
      unmount(n1)
      n1 = null
    }
    // 代码运行到这里 证明 n1 和 n2 类型相同, 需要进一步判断是否是组件
    const { type } = n2 // 如果类型是 string 则证明是标签元素, 如果是 object 则表示是组件
    if(typeof type === 'string'){
      // 如果 n1 不存在,则表示挂载,调用 mountElement 函数完成挂载
      if(!n1){
        mountElement(n2,container)
      } else {
        // n1 存在,意味着打补丁
        patchElement(n1,n2)
      }
    }else if(typeof type === 'object'){
      // 处理组件类型
      mountComponent(n2,container)
    }else if(typeof type === 'xxx'){
      // 处理其他类型
    }
    
  }

  // 完成组件渲染任务 
  function mountComponent(vnode,container,anchor){
      //  通过 vnode 获取组件的选项对象, 即 vnode.type
      const componentOptions = vnode.type
      // 获取组件的渲染函数 render , 组件自身状态 data, 组件的生命周期函数
      const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdated, updated, setup } = componentOptions
      
      function resolveProps(options,propsData){
        const props = {}
        const attrs = {}
        for(const key in propsData){
          // 以字符串 on 开头的 props 无论是否显示的声明, 都将其添加到 props 数据中, 而不是 attrs
          if(key in options || key.startsWith('on')){
            props[key] = propsData[key]
          }else{
            attrs[key] = propsData[key]
          }
        }
        return [props,attrs]
      }

      beforeCreate && beforeCreate()
      // 调用 data 函数得到原始数据,并调用 reactive 函数 将其包装为响应式对象
      const state = data ? reactive(data()) : null
      // 拿到 props 和 attrs 通过 reslovePrps 方法
      const [props,attrs] = resolveProps(propsOption,vnode.props)

      // 定义组件实例, 一个组件实例本质上就是一个对象,包含组件有关的状态信息
      const instance = {
        state, // 组件自身状态
        isMounted:false, // 组件挂载状态
        subTree:null,  // 组件所需要渲染的内容
        props: shallowReactive(props), // props 是一个 浅响应对象
        mounted:[], // 在组件实例中添加 mounted 数组, 用来存储通过 onMounted 函数注册的生命周期钩子函数
      }

      // 定义 emit 函数, 接收两个参数 event 事件名, payload, 传递给事件处理函数的参数
      function emit(event,...payload){
        // 根据约定对事件名称进行处理, 例如 change => onChange
        const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
        // 根据事件处理后的事件名称去 props 中寻找对应的事件处理函数
        const handler = instance.props[eventName]
        if(handler){
          // 调用事件处理函数并传递参数
          handler(...payload)
        }else{
          console.error('事件不存在')
        }
      }

      // setupContext 
      const setupContext = { attrs,emit }
      // 调用setup函数之前, 设置当前组件实例
      setCurrentInstance(instance)
      // 调用 setup 函数, 将只读版本的 props 作为第一个参数传递,避免用户修改 props, 将 setupContext 作为第二个参数传递
      const setupResult = setup(shallowReadonly(instance.props,setupContext))
      // setup 函数执行完毕, 重置当前组件实例
      setCurrentInstance(null)
      // setupState 用来存储由 setup 返回的数据
      let setupState = null
      // 如果 setup 函数返回的是函数, 则将其作为渲染函数
      if(typeof setupResult === 'function'){
        // 报告冲突
        if(render) console.error('setup函数返回渲染函数,render 选项将被忽略')
        // 将 setupResult 作为渲染函数
        render = setupResult
      }else{
        // 如果 setup 返回值不是函数, 则作为数据状态 赋值给 setupState
        setupState = setupResult
      }

      vnode.component = instance
      // 上下文对象, 劫持对 组件选项中 各个 API 的访问
      const renderContext = new Proxy(instance,{
        get(t,k,r){
          const { state, props } = t
          if( state && k in state){
            return state[k]
          }else if(k in props){
            return props[k]
          }else if(setupState && k in setupState){
            // 渲染上下文需要增加对 setupState 的支持
            return setupState[k]
          }else{
            console.error('不存在');
          }
        },
        set(t,k,v,r){
          const { state, props } = t
          if(state && k in state){
            state[k] = v
          }else if(k in props){
            props[k] = v
          }else if(setupState && k in setupState){
            setupState[k] = v
          }else{
            console.error('不存在')
          }
        }
      })

      // 将组件实例设置到 vonde 上, 用于后续更新
      vonde.component = instance
      created && created.call(renderContext)
      // 将组件的 render 函数调用 包装到 effect 内, 使其有能力触发组件的自更新
      effect(()=>{
         // 执行渲染函数,获取组件要渲染的内容, 即 render 函数返回的 虚拟 DOM, 将其 this 设置为 state, 这就是 vue 中可以通过 this 访问 data 中的状态的原理
        const subTree = render.call(renderContext,renderContext)
        if(!instance.isMounted){
          beforeMount && beforeMount.call(renderContext)
          // 调用 patch 函数来挂载组件所描述的内容 即 subTree
          patch(null,subTree,container,anchor)
          instance.isMounted = true
          mounted && mounted.call(renderContext)
        } else {
          beforeUpdated && beforeUpdated.call(renderContext)
          patch(instance.subTree,subTree,container,anchor)
          updated && updated.call(renderContext)
        }
        instance.subTree = subTree
      },{
        // 指定该副作用函数的调度器 为 queueJob
        scheduler: queueJob
      })
  }

  function render(vnode,container){
    if(vnode){
      // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
      patch(container._vnode,vnode,container)
    }else{
      if(container._vnode){
        // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
        unmount(container._vnode)
      }
    }
    // 把 vnode 存储到 container._vnode 下, 即后续渲染中的旧 vnode
    container._vnode = vnode
  }
  return {
    render
  }
}

// 使用 在创建 renderer 时传入配置项 这个配置项主要用于指定是在 DOM 还是在其他端渲染, createRenderer方法不考虑在什么端使用
const renderer = createRenderer({
  // 用于创建元素
  createElement(tag){
    return document.createElement(tag)
  },
  // 用于设置元素的文本节点
  setElementText(el,text){
    el.textContent = text
  },
  // 用于在给定的 parent 下添加指定元素
  insert(el,parent,anchor = null){
    parent.insertBefore(el,anchor)
  },
  /**
 * 属性设置的相关操作封装到 patchProps 中,作为渲染器的选项传递
 * @param {*} el 容器元素
 * @param {*} key 需要设置给容器的属性
 * @param {*} prevValue 上一个属性
 * @param {*} nextValue 将要设置的属性
 */
  patchProps(el,key,prevValue,nextValue){
    console.log('patchProps',el,'--',key,'--',prevValue,'--',nextValue);
    // 对 class 进行处理
    if(key === 'class'){
      el.className = changeClass(nextValue) || ''
    }else if(shouldSetAsProps(el,key,nextValue)){
      // 获取该 DOM Properties 的类型
      const type = typeof el[key];
      // 如果 type 是布尔值类型, value 是空字符串,则将值矫正为 true(这种情况会发生在比如属性是 disable的时候)
      if(type === 'boolean' && nextValue === ''){
        el[key] = true
      }else{
        el[key] = nextValue
      }
    }else{
      // 如果要设置的属性没有对应的 DOM Properties ,则直接使用 setAttribute 设置属性
      el.setAttribute(key,nextValue)
    }
  }
})

// 对 class 进行序列化处理

function changeClass(value){
  let res = ''
  function classNameChange(value){
    if(typeof value === 'string'){
      res += value + ' '
    }else if(Object.prototype.toString.call(value) === '[object Object]'){
      Object.keys(value).forEach(item=>{
        console.log('item',item);
        value[item] ? (res += item + ' ') : ''
      })
    }else if(Array.isArray(value)){
      value.forEach(item=>classNameChange(item))
    }
    return res
  }
  return classNameChange(value).trim()
}

// demo
const vondes = {
  type:'p',
  props:{
    class:[
      'black',
      {
        'reds':true,
        'blues':false
      },
      'sdfas'
    ],
    id:'100'
  },
  children:'text'
}
renderer.render(vondes,document.querySelector('#app'))
console.log(changeClass(vondes.props.class));


// 调度器, 组件自身的响应式数据发生变化是, 因为 effect 的执行是同步的, 所以需要将 相关联的副作用进行缓存, 可以对 任务进行去重, 且避免多次执行副作用函数带来的开销
// 缓存任务队列, 使用 set 数据结构
const queue = new Set()
// 一个标志, 代表是否正在刷新任务队列
let isFlushing = false
// 创建一个立即 resolve 的 Promise
const p = Promise.resolve()
// 调度器的主要函数, 用来将一个任务 添加到缓存队列中, 并且开始刷新队列
function queueJob(job){
  // 将 job 添加到任务队列 queue中
  queue.add(job)
  // 如果还没有开始刷新队列,则刷之
  if(!isFlushing){
    // 将该标志设置为 true , 以避免重复刷新
    isFlushing = true
    // 在微任务中 刷新缓冲队列
    p.then(()=>{
      try{
        // 执行任务队列中的任务
        queue.forEach(job=>job())
      } finally {
        // 重置状态
        isFlushing = false
        queue.clear = 0
      }
    })
  }
}