vue3组件渲染流程(简易)

633 阅读9分钟

前言

  • runtime-core: 与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
  • runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
  • vue3的runtime-core源码地址
  • vue3的runtime-dom源码地址
  • 浏览器的渲染组件的流程(如下图所示)
  • 宏观(Vue3是Monorepo管理项目):
  • 根据平台的不同, 提供的操作方法不通过
  • 平台模块包裹着核心编译模块, 编译时去调用响应模块
  • 微观:
  • 描述以示例为准
  • runtime-dom
  • 该模块提供浏览器的针对元素, 文本, 事件等 增删改查
  • 1.通过createApp(App, {})(runtime-dom的API)将组件选项和属性选项传入到runtime-core模块
  • 2.也将浏览器增删改查的选项传入到runtime-core模块
  • 3.并将挂载元素的id 通过app.mount('#app')将挂载根元素传入到runtime-core模块
  • runtime-core
  • 1.将组件的选项和属性选项创建虚拟dom(vnode)const vnode = createVNode(rootComponent, rootProps)
  • 2.渲染renderrender(vnode, '根元素')
  • 3.通过patch去比对vnode, 老的没传就是生成(递归操作)
  • 4.如果是元素,文本 生成元素, 添加元素, 生成文本, 添加文本
  • 5.如果是组件, 生成组件实例createComponentInstance
  • 6.处理数据挂载到实例上setupComponent 如state props attrs render(着重处理)...
  • 并对vue2.0API做了兼容处理(未写 循环合并)源码对应componentOptions.ts -> applyOptions方法
  • 7.创建effect函数(引入reactivity模块的API) 执行render函数setupRenderEffect

浏览器编译结构图


     +--------> 模板complier过程(本章不考虑, 后面的章节细讲) ?
     |         
+----+----+    
|         |
|   vue   |
|         |
+----+----+   +---------------------+    +----------------------+    +-------------------+
    |         |                     |    |                      |    |                   |
    +-------->|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
              |                     |    |                      |    |                   |
              +---------------------+    +----------------------+    +-------------------+

组件初始化渲染流程图

图片替换文本

示例

<script src="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js"></script>
<div id="app"></div>
<script>
  let { createApp, reactive, h} = VueRuntimeDOM

  let App = {
    setup(props, context) {
      let state = reactive({ name:'张三' })
      let fn = function () {
        console.log('点击事件')
        state.name = '李四'
      }
          
        
      return () => {
        return h('div', {onClick:fn}, state.name)
      }

    },
  }

  let app = createApp(App, { name: 'xxxx', age: 30 })
  app.mount('#app')
  console.log("🚀", app)
</script>

图片替换文本

shared

index.ts

// 是不是个对象
export const isObject = (value) => typeof value == 'object' && value !== null
// 合并对象
export const extend = Object.assign
// 是不是数组
export const isArray = Array.isArray
// 是不是函数
export const isFunction = (value) => typeof value == 'function'
// 是不是数字
export const isNumber = (value) => typeof value == 'number'
// 是不是字符
export const isString = (value) => typeof value === 'string'
// 是不是正整数
export const isIntegerKey = (key) => parseInt(key) + '' === key


// 是不是自己的属性
let hasOwnpRroperty = Object.prototype.hasOwnProperty
export const hasOwn = (target, key) => hasOwnpRroperty.call(target, key)

// 是不是同一个值
export const hasChanged = (oldValue,value) => oldValue !== value

shapeFlag.ts

/**
 * @description 通过位运算 来做类型判断(做权限和类型 位运算是最佳实践)
 * @description | 有一个是1 就是1
 * @description & 全部都是1 才是1
 */
export const enum ShapeFlags {
  ELEMENT = 1,                                  // 1    00000001  ->  1 * 2^0
  FUNCTIONAL_COMPONENT = 1 << 1,                // 2    00000010  ->  1 * 2^1 + 0 * 2^0
  STATEFUL_COMPONENT = 1 << 2,                  // 4    00000100  ->  1 * 2^2 + 0 * 2^1 + 0 * 2^0
  TEXT_CHILDREN = 1 << 3,                       // 8
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

/**
 * @description ShapeFlags.COMPONENT
 * @description 用|运算(有一个是1 就是1)
 *   00000100      ->      ShapeFlags.STATEFUL_COMPONENT
 *   00000010      ->      ShapeFlags.FUNCTIONAL_COMPONENT
 *  ----------
 *   00000110      ->      结果
 * 
 * @description 判断 ShapeFlags.ELEMENT 是不是组件
 * @description 用&运算(全部都是1 才是1)
 *   00000001      ->      ShapeFlags.ELEMENT  
 *   00000110      ->      ShapeFlags.COMPONENT
 *  ----------
 *   00000000      ->      结果(不是组件)
 */

runtime-dom

modules/attrs.ts

/**
 * @description attr的生成与比对
 * @param el 		元素dom
 * @param key 		键
 * @param value 	新值
 */
export const patchAttr = (el, key, value) =>{
  	if(value == null){
  	    el.removeAttribute(key)
  	}else {
  	    el.setAttribute(key, value)
  	}
}

modules/class.ts

/**
 * @description   class 生成与比对
 * @param el      元素dom
 * @param value   新值
 */
export const patchClass = (el, value) =>{
  if (value == null) {
    value = ''
  }
  el.className = value
}

modules/event.ts

/**
 * @description event 生成与比对
 * @param el 		  元素dom
 * @param key 		键
 * @param value 	新值
 */
export const patchEvent = (el, key, value) => {
  // 对函数进行缓存
  const invokers = el._vei || (el._vei = {})
  
  const exists = invokers[key]
  if (value && exists) {
    exists.value = value
  } else {
    // onClick -> click
    const eventName = key.slice(2).toLowerCase()

    if (value) {
      let invoker = invokers[key] = createInvoker(value)
      el.addEventListener(eventName,invoker)
    } else {
      // 移除事件 清除缓存
      el.removeEventListener(eventName,exists)
      invokers[key] = undefined
    }
  }

}

/**
 * @description 创建事件
 * @description onClick="fn"
 * @description 考虑情况: fn变化时, 通过什么方式去处理
 * @param value 函数事件
 */
function createInvoker(value) {
  const invoker = (e) =>{ invoker.value(e) }
  // 为了能随时更改value属性
  invoker.value = value
  return invoker
}

modules/style.ts

/**
 * @description style的生成与比对
 * @param {Element} el 		  元素dom
 * @param {Object}  prev 	  老值
 * @param {Object}  next 	  新值
 */
export const patchStyle = (el, prev, next) => {
  const style = el.style
  if(next == null){
    el.removeAttribute('style')

  }else{
    // 老的有 新的没有 
    if (prev) {
      for(let key in prev){
        if (next[key] == null) {
          style[key] = ''
        }
      }
    }

    // 新的里面需要赋值到style上
    for (let key in next) {
      style[key] = next[key]
    }

  }

}

patchProp.ts

import { patchAttr } from "./modules/attr"
import { patchClass } from "./modules/class"
import { patchEvent } from "./modules/events"
import { patchStyle } from "./modules/style"

export const patchProp = (el, key, prevValue, nextValue) => {
  switch (key) {
    case 'class':
      patchClass(el, nextValue)
      break;
    case 'style':
      patchStyle(el, prevValue, nextValue)
      break;
    default:
      // 是不是事件onClick
      if (/^on[^a-z]/) {
        patchEvent(el, key, nextValue)
      } else {
        patchAttr(el, key, nextValue)
      }
      break;
  }
}

nodeOps.ts

/**
 * @description 元素      生成 移除 移动 查询
 * @description 元素文本  生成 修改
 * @description 文本      生成 修改
 */
export const nodeOps = {
  /**
   * @desc 元素
   */
  createElement: tagName => document.createElement(tagName),

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },

  insert: (child, parent, anchor = null) => {
    // 如果参照物为空 则相当于appendChild
    parent.insertBefore(child, anchor)
  },

  querySelector: selector => document.querySelector(selector),

  setElementText: (el, text) => el.textContent = text,

  /**
   * @desc 文本操作 创建文本
   */ 
  createText: text => document.createTextNode(text),

  setText: (node, text) => node.nodeValue = text

  // 获取紧跟之后的节点
  nextSibling: (node) => node.nextSibling
}

index.ts

import { extend } from "@vue/shared/src"
import { nodeOps } from "./nodeOps"
import { patchProp } from "./patchProp"
import { createRenderer } from '@vue/runtime-core'
export * from '@vue/runtime-core'

const rendererOptions = extend({ patchProp }, nodeOps)

export function createApp(rootComponent, rootProps = {}) {
  const app = createRenderer(rendererOptions).createApp(rootComponent,rootProps)
  
  let { mount } = app
  app.mount = function (container) {
    container = nodeOps.querySelector(container)
    container.innerHTML = ''

    mount(container)
  }

  return app
}

runtime-core

index.ts

export * from '@vue/reactivity'

export {
  createRenderer
} from './renderer'

export {
  h
} from './h'

renderer.ts

import { effect } from "@vue/reactivity/src"
import { ShapeFlags } from "@vue/shared/src"
import { createAppAPI } from "./apiCreateApp"
import { createComponentInstance, setupComponent } from "./component"
import { queueJob } from "./scheduler"
import { normalizeVNode ,Text} from "./vnode"

/**
 * @description 创建一个渲染器
 * @param rendererOptions 针对浏览器的一些元素 文本的增删改查
 */
export function createRenderer(rendererOptions) {
  /**
   * @desc 浏览器的操作方法
   */
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    nextSibling: hostNextSibling,
  } = rendererOptions

  /**
   * @desc 创建组件的effect函数 执行render函数
   */
  const setupRenderEffect = (instance, container) => {
    instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        let proxyToUse = instance.proxy
        let subTree = instance.subTree = instance.render.call(proxyToUse, proxyToUse)

        // 用render函数的返回值 继续渲染
        patch(null, subTree, container)
        instance.isMounted = true
      } else {
        // diff...
      }
    }, {
      scheduler: queueJob
    })

  }

  /**
   * @desc 挂载组件
   * @param initialVNode 初始化vnode
   * @param container    dom
   */
  const mountComponent = (initialVNode, container) => {
    // 1.先有实例
    const instance = (initialVNode.component = createComponentInstance(initialVNode))

    // 2.需要的数据解析到实例上 如 state props attrs render...
    setupComponent(instance)

    // 3.创建effect函数 执行render函数
    setupRenderEffect(instance, container)
  }

  /**
   * @desc 加工组件
   */
  const processComponent = (n1, n2, container) => {
    if (n1 == null) {
      // 初次渲染
      mountComponent(n2, container)
    } else {
      // update...
    }
  }

  /**
   * @desc 挂载孩子
   */
  const mountChildren = (children, container) => {
    for (let i = 0; i < children.length; i++) {
      // 孩子有可能存在的文本数组 如['1', 'a']
      // 创建文本的虚拟节点
      // patch时 创建文本节点 将文本节点插入到元素中
      let child = normalizeVNode(children[i])
      patch(null, child, container)
    }
  }

  /**
   * @desc 挂载元素
   */
  const mountElement = (vnode, container) => {
    const { props, shapeFlag, type, children } = vnode
    let el = (vnode.el = hostCreateElement(type))

    // 属性生成
    if (props) {
      for (const key in props) {
          hostPatchProp(el, key, null, props[key]);
      }
    }

    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      hostSetElementText(el, children)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(children, el)
    }

    hostInsert(el, container)
  }

  /**
   * @desc 加工元素
   */
  const processElement = (n1, n2, container) => {
    if (n1 == null) {
      mountElement(n2, container)
    } else {
      // update...
    }
  }

  /**
   * @desc 加工文本
   */
  const processText = (n1, n2, container) => {
    if (n1 == null) {
      hostInsert((n2.el = hostCreateText(n2.children)), container)
    }
  }

  /**
   * @desc 比对
   * @param n1          老的vnode
   * @param n2          新的vnode
   * @param container   dom
   */
  const patch = (n1, n2, container) => {
    const { shapeFlag, type } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container)
        break;
    
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(n1, n2, container)
        } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
          processComponent(n1, n2, container)
        }

        break;
    }
  }

  /**
   * @desc 渲染
   * @param vnode     vnode
   * @param container dom
   */  
  const render = (vnode, container) => {
    // 核心 根据vnode -> dom
    patch(null, vnode, container)
  }

  return {
    createApp: createAppAPI(render)
  }

}

apiCreateApp.ts

import { createVNode } from "./vnode"

export function createAppAPI(render) {
  return function createApp(rootComponent, rootProps) {
    const app = {
      _props: rootProps,
      _component: rootComponent,
      _container: null,
      vnode: null,
      mount(container) {

        // 创建虚拟节点
        const vnode = createVNode(rootComponent, rootProps)
        app.vnode = vnode
        // 渲染render
        render(vnode, container)

        app._container = container
      }
    }

    return app

  }

}

vnode.ts

import { isArray, isObject, isString, ShapeFlags } from "@vue/shared/src"

/**
 * @description  创建虚拟dom
 * @param type      可能是组件Object, 也可能是字符串h('div', {}, 'children')
 * @param props     属性
 * @param children  孩子
 */
export const createVNode = (type, props, children = null) => {
  const shapeFlag = isString(type) ?
  ShapeFlags.ELEMENT : isObject(type) ?
  ShapeFlags.STATEFUL_COMPONENT : 0
  
  const vnode = {
    __v_isVnode: true,        // 是一个vnode节点
    type,                     // 对象就是组件的options选项 字符如div...
    props,
    children,                    
    component: null,          // 存储组件对应的实例
    el: null,                 // 将虚拟节点与真实节点对应
    key: props && props.key,  // diff算法 会用到key
    shapeFlag                 // 判断自己类型 和 孩子的类型
  }

  normalizeChildren(vnode, children)

  return vnode
}

/**
 * @description 将父级的shapeFlag与孩子 位算|
 * @description 目的: 判断自己类型 和 孩子的类型
 */
function normalizeChildren(vnode, children) {
  let type = 0

  if (children == null) {

  } else if (isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN
  } else {
    type = ShapeFlags.TEXT_CHILDREN
  }

  vnode.shapeFlag |= type
}

/**
 * @description 创建文本的虚拟节点
 * @description 针对如下传递的children情况
 * @description h('div', {}, ['文本1', '文本2'])
 * @param Text  创建一个文本类型
 */
export const Text = Symbol('Text')

export function normalizeVNode(child) {
  if (isObject(child)) return child

  return createVNode(Text, null, String(child))
}

/**
 * @description 判断该对象是否是虚拟节点
 * @param vnode 虚拟节点
 */
export function isVnode(vnode){
  return vnode.__v_isVnode
}

component.ts

import { isFunction, isObject, ShapeFlags } from "@vue/shared/src"
import { PublicInstanceProxyHandlers } from "./componentPublicInstance"

/**
 * @description 创建组件实例
 * @param vnode 组件的vnode
 */
export function createComponentInstance(vnode) {
  const instance = {
    vnode,
    type: vnode.type,   // 组件的options
    props: {},
    attrs: {},
    slots: {},
    ctx: {},
    data: {},
    setupState: {},     // setup返回的对象
    proxy: null,        // render函数的参数代理
    render: null,       // render函数
    subTree: null,      // render函数 返回的结果就是subTree(h)
    isMounted: false    // 组件是否挂载
  }
  // 用于render函数的代理
  instance.ctx = { _: instance }

  return instance
}

/**
 * @description 将需要的数据解析到实例上 如 state props attrs render...
 * @param instance 组件的实例
 */
export function setupComponent(instance) {
  const { props, children } = instance.vnode

  // 根据props解析出 props和attrs 将其放到instance上 源码initProps()
  instance.props = props

  // 插槽的解析 源码initSlot()
  instance.children = children

  // 当前组件是不是有状态的组件
  let isStateful = instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  if (isStateful) { setupStatefulComponent(instance) }

}

/**
 * @description 调用当前实例的setup方法 用setup的返回值填充setupState 和 对应的render方法
 * @param instance 组件实例
 */
function setupStatefulComponent(instance) {
  // 1.代理 传递给render函数的参数
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

  // 2.获取组件的类型 拿到组件的setup方法
  let Component = instance.type
  let { setup } = Component

  if (setup) {
    let setupContext = createSetupContext(instance)
    const setupResult = setup(instance.props, setupContext)

    handleSetupResult(instance, setupResult)
  } else {

    finishComponentSetup(instance)
  }
}

/**
 * @description setup返回的结果处理 可能是对象(setupState) 或者是render函数
 * @param instance     组件实例
 * @param setupResult  setup返回的结果
 */
function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    instance.render = setupResult
  } else if (isObject(setupResult)) {
    instance.setupState = setupResult
  }

  finishComponentSetup(instance)
}

/**
 * @description 完成组件的render函数
 * @param instance 组件实例
 */
function finishComponentSetup(instance) {
  let Component = instance.type

  if (!instance.render) {
    // 对template模板进行编译 产生render函数
    // 需要将生成render函数放在实例上
    if(!Component.render && Component.template) {
      // complier...
    }

    instance.render = Component.render
  }

  // 对vue2.0API做了兼容处理 就是循环合并
  // 源码componentOptions.ts -> applyOptions
}

/**
 * @description 创建setup参数 上下文
 */
function createSetupContext(instance) {
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: () => {},
    expose: () => {}
  }
}

componentPublicInstance.ts

import { hasOwn } from "@vue/shared/src"

/**
 * @description 用来代理 render函数 获取变量
 */
export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    const { setupState, props, data } = instance

    // 不能访问$ 开头的变量
    if (key[0] == '$') return

    if (hasOwn(setupState, key)) {
      return setupState[key]
    } else if (hasOwn(props, key)) {
      return props[key]
    } else if (hasOwn(data, key)) {
      return data[key]
    }
  },

  set({ _: instance }, key, value) {
    const { setupState, props, data } = instance

    if (hasOwn(setupState, key)) {
      setupState[key] = value
    } else if (hasOwn(props, key)) {
      props[key] = value
    } else if (hasOwn(data, key)) {
      data[key] = value
    }
    return true
  }

}

h.ts

import { isArray, isObject } from "@vue/shared/src"
import { createVNode, isVnode } from "./vnode"

/**
 * @description h函数 -> vnode
 * @param type              类型
 * @param propsOrChildren   属性或者孩子
 * @param children          孩子
 * @description 用户使用规则如下
 * h('div',{a:1})
 * h('div',h('span'))
 * h('div','a')
 * h('div',[h('span'),h('span')])
 * h('div',{},'helloworld')
 * h('div',{},h('span'))
 * h('div',null,h('span'),h('span'))
 * h('div',null,'a','b','c')
 * @description 返回的结果
 * createVNode(type, props, children)
 */
export function h(type, propsOrChildren, children) {
  const l = arguments.length

  if (l == 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      if (isVnode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }

      return createVNode(type, propsOrChildren)
    } else {

      return createVNode(type, null, propsOrChildren)
    }

  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments,2)
    } else if (l === 3 && isVnode(children)) {
      children = [children]
    }

    return createVNode(type, propsOrChildren, children)
  }

}

scheduler.ts

/**
 * @description 执行队列
 * @param queue           执行栈
 * @param isFlushPending  是否刷新(防抖)
 */
let queue = []
let isFlushPending = false

export function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

function queueFlush() {
  if (!isFlushPending) {
    isFlushPending = true
    Promise.resolve().then(flushJobs)
  }
}

function flushJobs() {
  // 先刷新父 在刷新子
  queue.sort((a, b) => a.id - b.id)
  
  for(let i=0; i < queue.length; i++) {
    const job = queue[i]
    job()
  }
  
  // 置空
  isFlushPending = false
  queue.length = 0

}