Home  >  Article  >  Web Front-end  >  An in-depth analysis of the rendering system in vue3

An in-depth analysis of the rendering system in vue3

青灯夜游
青灯夜游forward
2021-12-29 10:44:183022browse

This article will give you an in-depth analysis of the rendering system in vue3. I hope it will be helpful to you!

An in-depth analysis of the rendering system in vue3

When it comes to marathon, everyone knows that marathon is the longest track and field event in the world (the total distance is 42.195 kilometers). It is the most physically demanding and the most tiring of all sports. A project to practice one's will. If you can persist in running the entire marathon, then what else can't you persist in?

Similarly, when we study the source code of some excellent class libraries, the whole process is boring, just like participating in a source code-level marathon. So today I will take you to run a marathon about the source code analysis version of the Vue.js 3.0 rendering system. During the whole process, the author will also provide you with a supply station (Flowchart) for everyone’s convenience.

Thinking

Before starting today’s article, you can think about it:

  • # How are ##vue files converted into DOM nodes and rendered to the browser?

  • When data is updated, what does the entire

    update process look like?

vuejs has two stages: compile time and runtime.

When compiling

The

.vue file we write during normal development cannot be run directly in the browser , so in the webpack compilation phase, the .vue file needs to be compiled through vue-loader to generate the corresponding jsCode, the template template corresponding to the vue component will be compiler converted into a render function.

Runtime

Next, when the compiled code actually runs in the browser, the

render function# will be executed. ##And return VNode, which is the so-called virtual DOM, and finally render VNode into a real DOM node.

An in-depth analysis of the rendering system in vue3After understanding the idea of ​​

vue

component rendering, let us start from Vue.js 3.0 (hereinafter referred to as ## Starting from the source code of #vue3), let’s have an in-depth understanding of the whole rendering process of the vue component.

Preparation

This article mainly analyzes the rendering system of

vue3
. In order to facilitate debugging, we directly pass Introduce the

vue.js file for source code debugging and analysis.

    vue3
  • Source code download

    # 源码地址(推荐ssh方式下载)
    https://github.com/vuejs/vue-next
    # 或者下载笔者做笔记用的版本
    https://github.com/AsyncGuo/vue-next/tree/vue3_notes

Generate
    vue.global .js
  • File

    npm run dev
    # bundles .../vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js...
    # created packages/vue/dist/vue.global.js in 2.8s

Start the development environment
  • npm run serve

Test code
  • <!-- 调试代码目录:/packages/vue/examples/test.html -->
    
    <script src="./../dist/vue.global.js"></script>
    
    <div id="app">
      <div>static node</div>
      <div>{{title}}</div>
      <button @click="add">click</button>
      <Item :msg="title"/>
    </div>
    
    <script>
      const Item = {
        props: [&#39;msg&#39;],
        template: `<div>{{ msg }}</div>`
      }
      const app = Vue.createApp({
        components: {
          Item
        },
        setup() {
          return {
            title: Vue.ref(0)
          }
        },
        methods: {
          add() {
            this.title += 1
          }
        },
      })
    
      app.mount(&#39;#app&#39;)
    </script>

Create application

From the above test code, we will find that vue3

and # The mounting method of ##vue2

is different. vue3 creates applications through the entry function createApp. Next, let’s take a look at the specific implementation of createApp: <pre class="brush:js;toolbar:false;">// 入口文件: /vue-next/packages/runtime-dom/src/index.ts const createApp = ((...args) =&gt; { console.log(&amp;#39;createApp入参:&amp;#39;, ...args); // 创建应用 const app = ensureRenderer().createApp(...args); const { mount } = app; // 重写mount app.mount = (containerOrSelector) =&gt; { // ... }; return app; });</pre>ensureRenderer

First pass ensureRendererCreate the renderer on the web side, let's take a look at the specific implementation:

// 更新属性的方法
const patchProp = () => {
	// ...
}
// 操作DOM的方法
const nodeOps = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },
  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },
  ...
}
// web端的渲染器所需的参数设置
const rendererOptions = extend({ patchProp }, nodeOps);
let renderer;
// 延迟创建renderer
function ensureRenderer() {
  return (renderer || (renderer = createRenderer(rendererOptions)));
}

It can be seen here that by delaying the creation of the renderer, when we only rely on the responsive package, we can pass tree-shaking

Remove rendering-related code, greatly reducing the size of the package.

createRenderer

It can be seen from ensureRenderer that the real entrance is this

createRenderer

Method: <pre class="brush:js;toolbar:false;">// /vue-next/packages/runtime-core/src/renderer.ts export function createRenderer(options) { return baseCreateRenderer(options) } function baseCreateRenderer(options, createHydrationFns) { // 通用的DOM操作方法 const { insert: hostInsert, remove: hostRemove, ... } = options // ======================= // 渲染的核心流程 // 通过闭包缓存内敛函数 // ======================= const patch = () =&gt; {} // 核心diff过程 const processElement = () =&gt; {} // 处理element const mountElement = () =&gt; {} // 挂载element const mountChildren = () =&gt; {} // 挂载子节点 const processFragment = () =&gt; {} // 处理fragment节点 const processComponent = () =&gt; {} // 处理组件 const mountComponent = () =&gt; {} // 挂载组件 const setupRenderEffect = () =&gt; {} // 运行带副作用的render函数 const render = () =&gt; {} // 渲染挂载流程 // ... // ======================= // 2000+行的内敛函数 // ======================= return { render, hydrate, // 服务端渲染相关 createApp: createAppAPI(render, hydrate) } }</pre> Next, let’s skip the implementation of these introverted functions (we will analyze it in detail when used in the subsequent rendering process) and look at the specific implementation of createAppAPI :

createAppAPI

function createAppAPI(render, hydrate) {
  // 真正创建app的入口
  return function createApp(rootComponent, rootProps = null) {
    // 创建vue应用上下文
    const context = createAppContext();
    // 已安装的vue插件
    const installedPlugins = new Set();
    let isMounted = false;
    const app = (context.app = {
      _uid: uid++,
      _component: rootComponent, // 根组件
      use(plugin, ...options) {
        // ...
      	return app
      },
      mixin(mixin) {},
      component(name, component) {},
      directive(name, directive) {},
      mount(rootContainer) {},
      unmount() {},
      provide(key, value) {}
    });
    return app;
  };
}
It can be seen that the createApp function returned by

createAppAPI

is Reallycreate the entrance to the application. In createApp, the vue application context will be created, app will be initialized at the same time, and the application context will be bound to the app instance, and finally return app. There is something worth noting here: use

,
mixin

, component on the app object and directive methods both return app application instances, developers can chain call.

// 一直use一直爽
createApp(App).use(Router).use(Vuex).component(&#39;component&#39;,{}).mount("#app")

到此app应用实例已经创建好了~,打印查看下创建的app应用:

An in-depth analysis of the rendering system in vue3

总结一下创建app应用实例的过程:

  • 创建web端对应的渲染器(延迟创建,tree-shaking

  • 执行baseCreateRenderer方法(通过闭包缓存内敛函数,后续挂载阶段的主流程

  • 执行createAppAPI方法(1. 创建应用上下文;2. 创建app并返回

An in-depth analysis of the rendering system in vue3

挂载阶段

接下来,当我们执行app.mount时,便会开始挂载组件。而我们调用的app.mount则是重写后的mount方法:

const createApp = ((...args) => {
  // ...
  const { mount } = app; // 缓存原始的mount方法
  // 重写mount
  app.mount = (containerOrSelector) => {
    // 获取容器
    const container = normalizeContainer(containerOrSelector);
    if (!container) return;
    const component = app._component;
    // 判断如果传入的根组件不是函数&根组件没有render函数&没有template,就把容器的内容设置为根组件的template
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML;
    }
    // 清空容器内容
    container.innerHTML = &#39;&#39;;
    // 执行缓存的mount方法
    const proxy = mount(container, false, container);
    return proxy;
  };
  return app;
});

执行完web端重写的mount方法后,才是真正挂载组件的开始,即调用createAppAPI返回的app应用上的mount方法:

function createAppAPI(render, hydrate) {
  // 真正创建app的入口
  return function createApp(rootComponent, rootProps = null) {
    // ...
    const app = (context.app = {
      // 挂载根组件
      mount(rootContainer, isHydrate, isSVG) {
        if (!isMounted) {
          // 创建根组件对应的vnode
          const vnode = createVNode(rootComponent, rootProps);
          // 根级vnode存在应用上下文
          vnode.appContext = context;
          // 将虚拟vnode节点渲染成真实节点,并挂载
          render(vnode, rootContainer, isSVG);
          isMounted = true;
          // 记录应用的根组件容器
          app._container = rootContainer;
          rootContainer.__vue_app__ = app;
          app._instance = vnode.component;
          return vnode.component.proxy;
        }
      }
    });
    return app;
  };
}

总结一下,mount方法主要做了什么呢?

  • 创建根组件对应的vnode

  • 根组件vnode绑定应用上下文context

  • 渲染vnode成真实节点,并挂载

  • 记录挂载状态

细心的同学可能已经发现了,这里的mount方法是一个标准的跨平台渲染流程,抽象vnode,然后通过rootContainer实现特定平台的渲染,例如在浏览器环境下,它就是一个DOM对象,在其他平台就是其他特定的值。这也就是为什么我们在调用runtime-dom包的creataApp方法时,重写mount方法,完善不同平台的渲染逻辑。

创建vnode

提到vnode,可能更多人会和高性能联想到一起,误以为vnode的性能就一定比手动操作DOM的高,其实不然。vnode的底层同样是要操作DOM,相反如果vnodepatch过程过长,同样会导致页面的卡顿。 而vnode的提出则是对原生DOM的抽象,在跨平台设计的处理上会起到一定的抽象化。例如:服务端渲染、小程序端渲染、weex平台...

接下来,我们来看下创建vnode的过程:

function _createVNode(
  type,
  props,
  children,
  patchFlag,
  ...
): VNode {
  // 规范化class & style
  // 例如:class=[]、class={}、style=[]等格式,需规范化
  if (props) {
    // ...
  }

  // 获取vnode类型
	const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
        ? 128 /* SUSPENSE */
        : isTeleport(type)
            ? 64 /* TELEPORT */
            : isObject(type)
                ? 4 /* STATEFUL_COMPONENT */
                : isFunction(type)
                    ? 2 /* FUNCTIONAL_COMPONENT */
                    : 0;

  return createBaseVNode()
}
function createBaseVNode(
	type, 
  props = null, 
  children = null,
  ...
) {
  // vnode的默认结构
  const vnode = {
    __v_isVNode: true, // 是否为vnode
    __v_skip: true, // 跳过响应式数据化
    type, // 创建vnode的第一个参数
    props, // DOM参数
    children,
    component: null, // 组件实例(instance),通过createComponentInstance创建
    shapeFlag, // 类型标记,在patch阶段,通过匹配shapeFlag进行相应的渲染过程
    ...
  };
  
  // 标准化子节点
  if (needFullChildrenNormalization) {
  	normalizeChildren(vnode, children);
  }
  
  // 收集动态子代节点或子代block到父级block tree
  if (isBlockTreeEnabled > 0 &&
      !isBlockNode &&
      currentBlock &&
      (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
      vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {
    currentBlock.push(vnode);
  }
  return vnode;
}

通过上面的代码,我们可以总结一下,创建vnode阶段都做了什么:

  • 规范化class & style(例如:class=[]、class={}、style=[]等格式)

  • 标记vnode的类型shapeFlag,即根组件对应的vnode类型(type即为根组件rootComponent,此时根组件为对象格式,所以shapeFlag即为4)

  • 标准化子节点(初始化时,children为空)

  • 收集动态子代节点或子代block到父级block tree(这里便是vue3引入的新概念:block tree,篇幅有限,本文就不展开陈述了)

这里,我们可以打印查看一下此时根组件对应的vnode结构:

An in-depth analysis of the rendering system in vue3

渲染vnode

通过createVNode获取到根组件对应的vnode,然后执行render方法,而这里的render函数便是baseCreateRenderer通过闭包缓存的render函数:

// 实际调用的render方法即为baseCreateRenderer方法中缓存的render方法
function baseCreateRenderer() {
  const render = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
        // 卸载组件
        unmount()
      }
    } else {
      // 正常挂载
      patch(container._vnode || null, vnode, container)
    }
  }
}
  • 当传入的vnodenull&存在老的vnode,则进行卸载组件

  • 否则,正常挂载

  • 挂载完成后,批量执行组件生命周期

  • 绑定vnode到容器上,以便后续更新阶段通过新旧vnode进行patch

⚠️:接下来,整个渲染过程将会在baseCreateRenderer这个核心函数的内敛函数中执行~

patch

接下来,我们来看下render过程中的patch函数的实现:

const patch = (
  n1,	// 旧的vnode
  n2,	// 新的vnode
  container, // 挂载的容器
  ...
) => {
  // ...
  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:
      // 处理文本
      processText(n1, n2, container, anchor)
      break
    case Comment:
      // 注释节点
      processCommentNode(n1, n2, container, anchor)
      break
    case Static:
      // 静态节点
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG)
      }
      break
    case Fragment:
      // fragment节点
      processFragment(n1, n2, container, ...)
      break
    default:
      if (shapeFlag & 1 /* ELEMENT */) {	// 处理DOM元素
        processElement(n1, n2, container, ...);
      }
      else if (shapeFlag & 6 /* COMPONENT */) {	// 处理组件
        processComponent(n1, n2, container, ...);
      }
      else if (shapeFlag & 64 /* TELEPORT */) {
        type.process(n1, n2, container, ...);
      }
      else if (shapeFlag & 128 /* SUSPENSE */) {
        type.process(n1, n2, container, ...);
      }
  }
}

分析patch函数,我们会发现patch函数会通过判断typeshapeFlag的不同来走不同的处理逻辑,今天我们主要分析组件类型普通DOM元素的处理。

processComponent

初始化渲染时,typeobject并且shapeFlag对应的值为4(位运算4 & 6),即对应processComponent组件的处理方法:

const processComponent = (n1, n2, container, ...) => {
  if (n1 == null) {
    if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
      // 激活组件(已缓存的组件)
      parentComponent.ctx.activate(n2, container, ...);
    }
    else {
      // 挂载组件
      mountComponent(n2, container, ...);
    }
  }
  else {
    // 更新组件
    updateComponent(n1, n2, optimized);
  }
};

如果n1null,则执行挂载组件;否则更新组件

mountComponent

接下来我们继续看挂载组件的mountComponent函数的实现:

const mountComponent = (initialVNode, container, ...) => {
  // 1. 创建组件实例
  const instance = (
    // 这个时候就把组件实例挂载到了组件vnode的component属性上了
    initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)
  );
  // 2. 设置组件实例
  setupComponent(instance);
  // 3. 设置并运行带有副作用的渲染函数
  setupRenderEffect(instance, initialVNode, container,...);
};

省略掉无关主流程的代码后,可以看到,mountComponent函数主要做了三件事:

  • 创建组件实例

function createComponentInstance(vnode, parent, suspense) {
  const type = vnode.type;
  // 绑定应用上下文
  const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
  // 组件实例的默认值
  const instance = {
    uid: uid$1++, //组件唯一id
    vnode,	// 当前组件的vnode
    type, // vnode节点类型
    parent, // 父组件的实例instance
    appContext, // 应用上下文
    root: null, // 根实例
    next: null, // 当前组件mounted时,为null,将设置为instance.vnode,下次update时,将执行updateComponentPreRender
    subTree: null,	// 组件的渲染vnode,由组件的render函数生成,创建后同步
    update: null,	// 组件内容挂载或更新到视图的执行回调,创建后同步
    scope: new EffectScope(true /* detached */),
    render: null, // 组件的render函数,在setupStatefulComponent阶段赋值
    proxy: null,	// 是一个proxy代理ctx字段,内部使用this时,指向它
    // local resovled assets
    // resolved props and emits options
    // emit
    // props default value
    // inheritAttrs
    // state
    // suspense related
    // lifecycle hooks
  };
  {
    instance.ctx = createDevRenderContext(instance);
  }
  instance.root = parent ? parent.root : instance;
  instance.emit = emit.bind(null, instance);
  return instance;
}

createComponentInstance函数主要是初始化组件实例并返回,打印查看下根组件对应的instance内容:

An in-depth analysis of the rendering system in vue3

  • 设置组件实例

function setupComponent(instance, isSSR = false) {
  const { props, children } = instance.vnode;
  // 判断是否为状态组件
  const isStateful = isStatefulComponent(instance);
  // 初始化组件属性、slots
  initProps(instance, props, isStateful, isSSR);
  initSlots(instance, children);
  
  // 当状态组件时,挂载setup信息
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined;
  return setupResult;
}

setupComponent的逻辑也很简单,首先初始化组件propsslots挂载到组件实例instance上,然后根据组件类型vnode.shapeFlag===4,判断是否挂载setup信息(也就是vue3的composition api)。

function setupStatefulComponent(instance, isSSR) {
  const Component = instance.type;
  // 创建渲染上下文的属性访问缓存
  instance.accessCache = Object.create(null);
  // 创建渲染上下文代理
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
  const { setup } = Component;
  // 判断组件是否存在setup
  if (setup) {
    // 判断setup是否有参数,有的话,创建setup上下文并挂载组件实例
    // 例如:setup(props) => {}
    const setupContext = (instance.setupContext =
                          setup.length > 1 ? createSetupContext(instance) : null);
    // 执行setup函数
    const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props) , setupContext]);
    handleSetupResult(instance, setupResult, isSSR);
  }
  else {
    finishComponentSetup(instance, isSSR);
  }
}

判断组件是否设置了setup函数:

  • 若设置了setup函数,则执行setup函数,并判断其返回值的类型。若返回值类型为函数时,则设置组件实例render的值为setupResult,否则作为组件实例setupState的值

function handleSetupResult(instance, setupResult, isSSR) {
  // 判断setup返回值类型
  if (isFunction(setupResult)) {
   	// 返回值为函数时,则当作组件实例的render方法
    instance.render = setupResult;
  }
  else if (isObject(setupResult)) {
    // 返回值为对象时,则当作组件实例的setupState
    instance.setupState = proxyRefs(setupResult);
  }
  else if (setupResult !== undefined) {
    warn$1(`setup() should return an object. Received: ${setupResult === null ? &#39;null&#39; : typeof setupResult}`);
  }
  finishComponentSetup(instance, isSSR);
}
  • 设置组件实例的render方法,分析finishComponentSetup函数,render函数有三种设置方式:

  • setup返回值为函数类型,则instance.render = setupResult

  • 若组件存在render方法,则instance.render = component.render

  • 若组件存在template模板,则instance.render = compile(template)

组件实例的render优化级:instance.render = setup() || component.render || compile(template)

function finishComponentSetup(instance, ...) {
  const Component = instance.type;
  // 绑定render方法到组件实例上
  if (!instance.render) {
    if (compile && !Component.render) {
      const template = Component.template;
      if (template) {
        // 通过编译器编译template,生成render函数
        Component.render = compile(template, ...);
      }
    }
    instance.render = (Component.render || NOOP);
  }
  // support for 2.x options
  ...
}

设置完组件后,我们可以再查看下instance的内容有发生什么变化:

An in-depth analysis of the rendering system in vue3

这个时候组件实例instancedataproxyrendersetupState已经绑定上了初始值。

  • 设置并运行带有副作用的渲染函数

const setupRenderEffect = (instance, initialVNode, container, ...) => {
  // 创建响应式的副作用函数
  const componentUpdateFn = () => {
    // 首次渲染
    if (!instance.isMounted) {
      // 渲染组件生成子树vnode
      const subTree = (instance.subTree = renderComponentRoot(instance));
      patch(null, subTree, container, ...);
      initialVNode.el = subTree.el;
      instance.isMounted = true;
    }
    else {
      // 更新
    }
  };
 	// 创建渲染effcet
  const effect = new ReactiveEffect(
    componentUpdateFn, 
    () => queueJob(instance.update), 
    instance.scope // track it in component&#39;s effect scope
 	);
  const update = (instance.update = effect.run.bind(effect));
  update.id = instance.uid;
  update();
};

接下来继续执行setupRenderEffect函数,首先会创建渲染effect响应式系统还包括其他副作用:computed effectwatch effect),并绑定副作用执行函数到组件实例的update属性上(更新流程会再次触发update函数),并立即执行update函数,触发首次更新

function renderComponentRoot(instance) {
  const { proxy, withProxy, render, ... } = instance;
  let result;
  try {
    const proxyToUse = withProxy || proxy;
    // 执行实例的render方法,返回vnode,然后再标准化vnode
    // 执行render方法时,会调用proxyToUse,即会触发PublicInstanceProxyHandlers的get
    result = normalizeVNode(render.call(proxyToUse, proxyToUse, ...));
  }
  
  return result;
}

此时,renderComponentRoot函数会执行实例的render方法,即setupComponent阶段绑定在实例render方法上的函数,同时标准化render返回的vnode并返回,作为子树vnode

同样我们可以打印查看一下子树vnode的内容:

An in-depth analysis of the rendering system in vue3

此时,可能有些同学开始疑惑了,为什么会有两颗vnode树呢?这两颗vnode树又有什么区别呢?

  • initialVNode

    initialVNode就是组件的vnode,即描述整个组件对象的,组件vnode会定义一些和组件相关的属性:dataprops生命周期等。通过渲染组件vnode,生成子树vnode

  • sub tree

    子树vnode是通过组件vnoderender方法生成的,其实也就是对组件模板template的描述,即真正要渲染到浏览器的DOM vnode

生成subTree后,接下来就继续通过patch方法,把subTree节点挂载到container上。 接下来,我们继续往下分析,大家可以看下上面subTree的截图:subTreetype值为Fragment,回忆下patch方法的实现:

const patch = (
  n1,	// 旧的vnode
  n2,	// 新的vnode
  container, // 挂载的容器
  ...
) => {
  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Fragment:
      // fragment节点
      processFragment(n1, n2, container, ...)
      break
    default:
      // ...
  }
}

Fragment也就是vue3提到的新特性之一,在vue2中,是不支持多根节点组件,而vue3则是正式支持的。细想一下,其实还是单个根节点组件,只是vue3的底层用Fragment包裹了一层。我们再看下processFragment的实现:

const processFragment = (n1, n2, container, ...) => {
  // 创建碎片开始、结束的文本节点
  const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(&#39;&#39;));
  const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(&#39;&#39;));
  
  if (n1 == null) {
    hostInsert(fragmentStartAnchor, container, anchor);
    hostInsert(fragmentEndAnchor, container, anchor);
    // 挂载子节点数组
    mountChildren(n2.children, container, ...);
  } else {
    // 更新
  }
};

接下来继续挂载子节点数组:

const mountChildren = (children, container, ...) => {
  for (let i = start; i < children.length; i++) {
    const child = (children[i] = optimized
                   ? cloneIfMounted(children[i])
                   : normalizeVNode(children[i]));
    patch(null, child, container, ...);
  }
};

遍历子节点,patch每个子节点,根据child节点的type递归处理。接下来,我们主要看下typeELEMENT类型的DOM元素,即processElement

const processElement = (n1, n2, container, ...) => {
  if (n1 == null) {
    // 挂载DOM元素
    mountElement(n2, container,...)
  } else {
    // 更新
  }
}
const mountElement = (vnode, container, ...) => {
  let el;
  let vnodeHook;
  const { type, props, shapeFlag, ... } = vnode;
  {
    // 创建DOM节点,并绑定到当前vnode的el上
    el = vnode.el = hostCreateElement(vnode.type, ...);
  }
  // 插入父级节点
  hostInsert(el, container, anchor);
};

创建DOM节点,并挂载到vnode.el上,然后把DOM节点挂载到container中,继续递归其他vnode的处理,最后挂载整个vnode到浏览器视图中,至此完成vue3的首次渲染整个流程。mountElement方法中提到到hostCreateElementhostInsert也就是在最开始创建渲染器时传入的参数对应的处理方法,也就完成整个跨平台的初次渲染流程

An in-depth analysis of the rendering system in vue3

更新流程

分析完vue3首次渲染的整个流程后,那么在数据更新后,vue3又是怎么更新渲染呢?接下来分析更新流程阶段就要涉及到vue3响应式系统的知识了(由于篇幅有限,我们不会展开更多响应式的知识,期待后续篇章的更加详细的分析)。

依赖收集

回忆下在首次渲染时的设置组件实例setupComponent阶段会创建渲染上下文代理,而在生成subTree阶段,会通过renderComponentRoot函数,执行组件vnoderender方法,同时会触发渲染上下文代理PublicInstanceProxyHandlersget,从而实现依赖收集。

function setupStatefulComponent(instance, isSSR) {
  ...
  // 创建渲染上下文代理
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
}
function renderComponentRoot(instance) {
  const proxyToUse = withProxy || proxy;
  // 执行render方法时,会调用proxyToUse,即会触发PublicInstanceProxyHandlers的get
  result = normalizeVNode(
    render.call(proxyToUse, proxyToUse, ...)
  );
  return result;
}

我们可以查看下此时组件vnoderender方法的内容:

An in-depth analysis of the rendering system in vue3

或者打印查看render方法内容:

(function anonymous(
) {
const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue

const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "static node", -1 /* HOISTED */)
const _hoisted_2 = ["onClick"]

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    const _component_item = _resolveComponent("item")

    return (_openBlock(), _createElementBlock(_Fragment, null, [
      _hoisted_1,
      _createElementVNode("div", null, _toDisplayString(title), 1 /* TEXT */),
      _createElementVNode("button", { onClick: add }, "click", 8 /* PROPS */, _hoisted_2),
      _createVNode(_component_item, { msg: title }, null, 8 /* PROPS */, ["msg"])
    ], 64 /* STABLE_FRAGMENT */))
  }
}
})

仔细观察render的第一个参数_ctx,即传入的渲染上下文代理proxy,当访问title字段时,就会触发PublicInstanceProxyHandlersget方法,那PublicInstanceProxyHandlers的逻辑又是怎么呢?

// 代理渲染上下文的handler实现
const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    const { ctx, setupState, data, props, accessCache, type, appContext } = instance;
    let normalizedProps;
    // key值不以$开头的属性
    if (key[0] !== &#39;$&#39;) {
      // 优先从缓存中判断当前属性需要从哪里获取
      // 性能优化:缓存属性应该根据哪种类型获取,避免每次都触发hasOwn的开销
      const n = accessCache[key];
      if (n !== undefined) {
        switch (n) {
          case 0 /* SETUP */:
            return setupState[key];
          case 1 /* DATA */:
            return data[key];
          case 3 /* CONTEXT */:
            return ctx[key];
          case 2 /* PROPS */:
            return props[key];
            // default: just fallthrough
        }
      }
      // 获取属性值的顺序:setupState => data => props => ctx => 取值失败
      else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache[key] = 0 /* SETUP */;
        return setupState[key];
      }
      else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache[key] = 1 /* DATA */;
        return data[key];
      }
      else if (
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)) {
        accessCache[key] = 2 /* PROPS */;
        return props[key];
      }
      else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache[key] = 3 /* CONTEXT */;
        return ctx[key];
      }
      else if (shouldCacheAccess) {
        accessCache[key] = 4 /* OTHER */;
      }
    }
  },
  set() {},
  has() {}
};

接下来我们以keytitle的例子简单介绍下get的逻辑:

  • 首先判断key值是否已$开头,明显title走否的逻辑

  • 再看accessCache缓存中是否存在

    性能优化:缓存属性应该根据哪种类型获取,避免每次都触发**hasOwn**的开销

  • 最后再按照顺序获取:setupState => data => props => ctxPublicInstanceProxyHandlerssethas的处理逻辑,同样以这个顺序处理

  • 若存在时,先设置缓存accessCache,再从setupState中获取title对应的值

重点来了,当访问setupState.title时,触发proxyget的流程会有两个阶段:

  • 首先触发setupState对应的proxyget,然后获取title的值,判断其是否为Ref

  • 是:继续获取ref.value,即触发ref类型的依赖收集流程

  • 否:直接返回,即为普通数据类型,不进行依赖收集

// 设置组件实例时会设置setupState的代理prxoy
// 设置流程:setupComponent=>setupStatefulComponent=>handleSetupResult
instance.setupState = proxyRefs(setupResult)

export function proxyRefs(objectWithRefs) {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, {
        get: (target, key, receiver) => {
          return unref(Reflect.get(target, key, receiver))
        },
        set: (target, key, value, receiver) => {}
      })
}

export function unref(ref) {
  return isRef(ref) ? ref.value : ref
}
  • 访问ref.value时,触发ref的依赖收集。那我们先来分析Vue.ref()的实现逻辑又是什么呢?

// 调用Vue.ref(0),从而触发createRef的流程
// 省略其他无关代码
function ref(value) {
  return createRef(value, false)
}
function createRef(rawValue) {
  return new RefImpl(rawValue, false)
}
// ref的实现
class RefImpl {
  constructor(value) {
    this._rawValue = toRaw(value)
    this._value = toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }
}
function trackRefValue(ref) {
  if (isTracking()) {
    if (!ref.dep) {
      ref.dep = new Set()
    }
    // 添加副作用,进行依赖收集
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

分析ref的实现,会发现当访问ref.value时,会触发RefImpl实例的value方法,从而触发trackRefValue,进行依赖收集dep.add(activeEffect)。那这时的activeEffect又是谁呢?

回忆下setupRenderEffect阶段的实现:

const setupRenderEffect = (instance, initialVNode, container, ...) => {
  // 创建响应式的副作用函数
  const componentUpdateFn = () => {};
 	// 创建渲染effcet
  const effect = new ReactiveEffect(
    componentUpdateFn, 
    () => queueJob(instance.update), 
    instance.scope
 	);
  const update = (instance.update = effect.run.bind(effect));
  update();
};

// 创建effect类的实现
class ReactiveEffect {
  run() {
    try {
      effectStack.push((activeEffect = this))
      // ...
      return this.fn()
    } finally {}
  }
}

当执行update函数时(即渲染effect实例的run方法),从而设置全局activeEffect当前渲染effect,也就是说此时dep.add(activeEffect)收集的activeEffect就是这个渲染effect,从而实现了依赖收集。

我们可以打印一下setupState的内容,验证一下我们的分析:

An in-depth analysis of the rendering system in vue3

通过截图,我们可以看到此时title收集的副作用就是渲染effect,细心的同学就发现了截图中的fn方法就是componentUpdateFn函数,执行fn()继续挂载children

1An in-depth analysis of the rendering system in vue3

派发更新

分析完依赖收集阶段,我们再看下,vue3又是如何进行派发更新呢?

当我们点击按钮执行this.title += 1时,同样会触发PublicInstanceProxyHandlersset方法,而set的触发顺序同样和get一致:setupState=>data=>其他不允许修改的判断(例如:props$开头的保留字段

// 代理渲染上下文的handler实现
const PublicInstanceProxyHandlers = {
  set({ _: instance }, key, value) {
    const { data, setupState, ctx } = instance;
    // 1. 更新setupState的属性值
    if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
      setupState[key] = value;
    }
    // 2. 更新data的属性值
    else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
      data[key] = value;
    }
    // ...
    return true;
  }
};

设置setupState[key]从而继续触发setupStateset方法:

const shallowUnwrapHandlers: ProxyHandler<any> = {
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    // oldValue为ref类型&value不是ref时执行
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      // 否则,直接返回
      return Reflect.set(target, key, value, receiver)
    }
  }
}

当设置oldValue.value的值时继续触发refset方法,判断ref是否存在dep,执行副作用effect.run(),从而派发更新,完成更新流程。

class RefImpl{
  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

// 判断ref是否存在依赖,从而派发更新
function triggerRefValue(ref) {
  ref = toRaw(ref)
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}
// 派发更新
function triggerEffects(dep) {
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      // 执行副作用
      effect.run()
    }
  }
}

1An in-depth analysis of the rendering system in vue3

总结

综上,我们分析完了vue3的整个渲染过程更新流程,当然我们只是从主要的渲染流程分析,完整的渲染过程的复杂度不止于此,比如基于block tree的优化实现,patch阶段的diff优化以及在更新流程中的响应式阶段的优化又是怎样的等细节。

本文的初衷便是给大家提供分析vue3整个渲染过程的轮廓,有了整体的印象,再去分析了解更加细节的点的时候,也会更有思路和方向。

最后,附一张完整的渲染流程图,与君共享。

1An in-depth analysis of the rendering system in vue3

【相关推荐:《vue.js教程》】

The above is the detailed content of An in-depth analysis of the rendering system in vue3. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete