Vue3 组件渲染流程

88 阅读4分钟

应用实例

每个 Vue 应用都是通过createApp 函数创建一个新的 应用实例,我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

import { createApp } from 'vue'
const app = createApp({
  data() {
    return {
      count: 0
    }
  }
})
app.mount('#app')

挂载应用

createApp 会返回一个拥有mount方法的实例,应用实例必须在调用了 mount 方法后才会渲染出来。 mount挂载组件时需要用根组件生成虚拟节点vnode,然后使用render方法开始渲染。

function createApp(rootComponent) {
  const app = {
    _component: rootComponent,
    mount(rootContainer) {
      const vnode = createVNode(rootComponent);
      render(vnode, rootContainer);
    }
  };

  return app;
}

虚拟DOM渲染成真实DOM - patch

虚拟DOM渲染成真实DOM是由patch方法完成的,patch会对比新旧vnode并更新真实DOM。

patch 需要分情况处理:

  • 文本节点,通过DOM API更新
    • 创建文本节点 createTextNode
    • 设置节点值 nodeValue
    • 插入节点 insertBefore
  • fragment 类型,只渲染子节点
    • 递归patch子节点 mountChildren
  • element 类型 processElement,节点,通过DOM API更新
    • 首次渲染 mountElement
      • 创建元素 createElement
      • 设置文本 textContent
      • 递归patch子节点 mountChildren
      • 设置属性 patchProp
      • 插入元素 insertBefore
    • 更新 updateElement
      • patchProp,遍历新属性找到新增或变化的属性、遍历旧属性找到需移除的属性
        • 增删属性 setAttribute/removeAttribute
        • on开头的属性需要增删事件处理器 addEventListener/removeEventListener
      • patchChildren
  • 组件类型 processComponent,渲染组件
    • createComponentInstance 创建组件实例
    • setupComponent 加工组件数据
      • 初始化props、slots
      • 调用 setup 处理返回结果
    • setupRenderEffect 建立响应式渲染 ReactiveEffect
      • 建立组件更新函数 componentUpdateFn
        • 调用渲染函数 render 生成子节点,递归patch子节点
      • 收集渲染函数的响应式依赖,依赖改变时用微任务执行更新函数

patch 代码描述:

const render = (vnode, container) => {
  patch(null, vnode, container);
};

function patch(n1, n2, container = null, anchor = null, parentComponent = null) {
  // 基于 n2 的类型来判断
  // 因为 n2 是新的 vnode
  const { type, shapeFlag } = n2;
  switch (type) {
    case Text:
      processText(n1, n2, container);
      break;
    // 其中还有几个类型比如: static fragment comment
    case Fragment:
      processFragment(n1, n2, container);
      break;
    default:
      // 这里就基于 shapeFlag 来处理
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container, anchor, parentComponent);
      } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
        processComponent(n1, n2, container, parentComponent);
      }
  }
}

function processComponent(n1, n2, container, parentComponent) {
  if (!n1) {
    mountComponent(n2, container, parentComponent);
  } else {
    updateComponent(n1, n2);
  }
}

渲染组件

组件渲染分为首次渲染和更新,分别由mountComponent、updateComponent实现。

  • 首次渲染组件
    • createComponentInstance 创建组件实例
      • type 创建实例时被赋值为vnode.type, 组件对象会在创建虚拟dom时被存入vnode.type
      • proxy
    • setupComponent 加工组件数据
      • 初始化props,赋值实例 props 属性
      • 初始化slots,赋值实例 slots 属性,将vnode.children 存到instance.slots
      • 初始化组件数据setupStatefulComponent,调用 setup 处理返回结果
        • 赋值实例的 proxy 属性,代理instance.ctx
    • setupRenderEffect 建立响应式渲染 ReactiveEffect
      • 建立组件更新函数 componentUpdateFn
      • 赋值instance.update
  • 组件更新
    • 判断是否需要更新
      • 若是则调用instance.update更新

mountComponent 代码描述:

function mountComponent(initialVNode, container, parentComponent) {
  const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent));
  setupComponent(instance);
  setupRenderEffect(instance, initialVNode, container);
}

function createComponentInstance(vnode, parent) {
  const instance = {
    type: vnode.type,
    vnode,
    props: {},
    attrs: {},
    slots: {},
    parent,
    provides: parent ? parent.provides : {},
    // ...
    setupState: {},
  };
  instance.ctx = {
    _: instance
  };
  instance.emit = emit.bind(null, instance);
  return instance;
}

function updateComponent(n1, n2, container) {
  const instance = (n2.component = n1.component);
  if (shouldUpdateComponent(n1, n2)) {
    instance.next = n2;
    instance.update();
  } else {
    n2.component = n1.component;
    n2.el = n1.el;
    instance.vnode = n2;
  }
}

建立响应式渲染 setupRenderEffect

组件数据改变后需要调用组件update方法重新渲染,需要用响应式副作用建立响应式渲染,即用effect包裹更新函数,当更新函数内的依赖改变时重新调用更新函数。

建立响应式渲染:

  • 建立组件更新函数 componentUpdateFn
    • 调用渲染函数 render 生成子节点,递归patch子节点
  • 建立响应式更新 instance.update,收集渲染函数的响应式依赖,依赖改变时用微任务执行更新函数

setupRenderEffect 代码描述:

function setupRenderEffect(instance, initialVNode, container) {
  function componentUpdateFn() {
    if (!instance.isMounted) {
      const proxyToUse = instance.proxy;
      const subTree = (instance.subTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse)));
      patch(null, subTree, container, null, instance);
      initialVNode.el = subTree.el;
      instance.isMounted = true;
    } else {
      const { next, vnode } = instance;
      if (next) {
        next.el = vnode.el;
        updateComponentPreRender(instance, next);
      }
      const proxyToUse = instance.proxy;
      const nextTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse));
      const prevTree = instance.subTree;
      instance.subTree = nextTree;
      patch(prevTree, nextTree, prevTree.el, null, instance);
    }
  }
  instance.update = effect(componentUpdateFn, {
    scheduler: () => {
      queueJob(instance.update);
    }
  });
}
function updateComponentPreRender(instance, nextVNode) {
  nextVNode.component = instance;
  instance.vnode = nextVNode;
  instance.next = null;
  const { props } = nextVNode;
  instance.props = props;
}

Template 中 ref 的解包

在模板template或者渲染函数render中使用ref变量时会自动解包,可以直接使用ref而不需要通过.value访问ref的值。

ref 的解包实现流程:

  • 调用setup获取setup返回的数据
  • 对setupState做代理,在代理中对属性进行unRef解包
function mountComponent(initialVNode, container, parentComponent) {
  const instance = createComponentInstance(initialVNode, parentComponent);
  // ...
  const { setup } = instance.type;
  const setupContext = {/* ... */};
  const setupResult = setup && setup(instance.props, setupContext);
  instance.setupState = proxyRefs(setupResult);
  instance.render.call(instance.setupState, instance.setupState);
}

function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    get(target, key, receiver) {
      return unRef(Reflect.get(target, key, receiver));
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      if (isRef(oldValue) && !isRef(value)) {
        return (target[key].value = value);
      } else {
        return Reflect.set(target, key, value, receiver);
      }
    }
  });
}

Veux4 Store 响应式的实现

  • Vuex 是以插件的形式在 Vue 中使用的,在 createApp 时调用 install 安装
  • Store 类的 install,通过 app.provide 和 config.globalProperties.$store 提供 store 实例
  • Vuex4 中的 state 是通过 reactive API 去创建的响应式数据
  • useStore 用 inject 获取 provide 时存入的 store
import { inject } from 'vue';

class Store {
  constructor(options = {}) {
    this._committing = false;
    this._actions = Object.create(null);
    this._actionSubscribers = [];
    this._mutations = Object.create(null);
    this._wrappedGetters = Object.create(null);
    this._modules = new ModuleCollection(options);
    this._modulesNamespaceMap = Object.create(null);
    this._subscribers = [];
    this._makeLocalGettersCache = Object.create(null);
    // ...
    const state = this._modules.root.state;
    resetStoreState(this, state);
  }

  install(app, injectKey) {
    app.provide(injectKey || storeKey, this);
    app.config.globalProperties.$store = this;
  }
}

function resetStoreState(store, state, hot) {
  store._state = reactive({
    data: state
  });
}

export const storeKey = 'store';

export function useStore(key = null) {
  return inject(key !== null ? key : storeKey);
}