>  기사  >  웹 프론트엔드  >  vue3의 렌더링 시스템에 대한 심층 분석

vue3의 렌더링 시스템에 대한 심층 분석

青灯夜游
青灯夜游앞으로
2021-12-29 10:44:183002검색

이 글은 vue3의 렌더링 시스템에 대한 심층적인 분석을 제공할 것입니다. 도움이 되길 바랍니다!

vue3의 렌더링 시스템에 대한 심층 분석

마라톤은 세계에서 가장 긴 육상 경기(총 42.195km)라는 사실은 모두가 알고 있는 사실입니다. 모든 스포츠 중에서 육체적으로 가장 힘든 행사이기도 합니다. 자신의 의지를 시험합니다. 마라톤 전체를 계속해서 달릴 수 있다면, 또 무엇을 지속할 수 없습니까?

마찬가지로 우리가 몇몇 훌륭한 클래스 라이브러리의 소스 코드를 공부할 때, 소스 코드 수준의 마라톤에 참여하는 것처럼 전체 과정이 지루합니다. 그래서 오늘은 Vue.js 3.0 렌더링 시스템의 소스 코드 분석 버전에 대한 마라톤을 진행하도록 안내하겠습니다. 전체 프로세스 동안 저자는 귀하를 위한 공급 스테이션(흐름도)도 제공할 것입니다. 편의. .

Thinking

오늘 기사를 시작하기 전에 다음 사항에 대해 생각해 보세요.

  • vue 파일이 DOM 노드로 변환되어 To로 렌더링되는 방법 브라우저? vue文件是如何转换成DOM节点,并渲染到浏览器上的?

  • 数据更新时,整个的更新流程又是怎么样的?

vuejs有两个阶段:编译时运行时

编译时

我们平常开发时写的.vue文件是无法直接运行在浏览器中的,所以在webpack编译阶段,需要通过vue-loader.vue文件编译生成对应的js代码,vue组件对应的template模板会被编译器转化为render函数

运行时

接下来,当编译后的代码真正运行在浏览器时,便会执行render函数并返回VNode,也就是所谓的虚拟DOM,最后将VNode渲染成真实的DOM节点

vue3의 렌더링 시스템에 대한 심층 분석

了解完vue组件渲染的思路后,接下来让我们从Vue.js 3.0(后续简称vue3)的源码出发,深入了解vue组件的整个渲染流程是怎么样的?

准备

本文主要是分析vue3的渲染系统,为了方便调试,我们直接通过引入vue.js文件的方式进行源码调试分析。

  • vue3源码下载

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

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
  • 启动开发环境

npm run serve
  • 测试代码

<!-- 调试代码目录:/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>

创建应用

从上面的测试代码,我们会发现vue3vue2的挂载方式是不同的,vue3是通过createApp这个入口函数进行应用的创建。接下来我们来看下createApp的具体实现:

// 入口文件: /vue-next/packages/runtime-dom/src/index.ts
const createApp = ((...args) => {
  console.log(&#39;createApp入参:&#39;, ...args);
  // 创建应用
  const app = ensureRenderer().createApp(...args);
  const { mount } = app;
  // 重写mount
  app.mount = (containerOrSelector) => {
    // ...
  };
  return app;
});

ensureRenderer

首先通过ensureRenderer创建web端的渲染器,我们来看下具体实现:

// 更新属性的方法
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)));
}

在这里可以看出,通过延迟创建渲染器,当我们只依赖响应式包的情况下,可以通过tree-shaking移除渲染相关的代码,大大减少包的体积。

createRenderer

通过ensureRenderer可以看出,真正的入口是这个createRenderer方法:

// /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 = () => {}  // 核心diff过程
  const processElement = () => {} // 处理element
  const mountElement = () => {} // 挂载element
  const mountChildren = () => {} // 挂载子节点
  const processFragment = () => {} // 处理fragment节点
  const processComponent = () => {} // 处理组件
  const mountComponent = () => {} // 挂载组件
  const setupRenderEffect = () => {}  // 运行带副作用的render函数
  const render = () => {} // 渲染挂载流程
  // ...
  
  // =======================
  // 2000+行的内敛函数
  // =======================
  
  return {
    render,
    hydrate, // 服务端渲染相关
    createApp: createAppAPI(render, hydrate)
  }
}

接下来我们先跳过这些内敛函数的实现(后面的渲染流程用到时,我们再具体分析),来看下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;
  };
}

可以看出,createAppAPI返回的createApp函数才是真正创建应用的入口。在createApp里会创建vue应用的上下文,同时初始化app,并绑定应用上下文到app实例上,最后返回app

这里有个值得注意的点:app对象上的usemixincomponentdirective方法都返回了app 데이터가 업데이트될 때 전체

업데이트 프로세스🎜는 어떻게 되나요? 🎜🎜vuejs에는 🎜컴파일 시간🎜과 🎜런타임🎜이라는 두 가지 단계가 있습니다. 🎜🎜🎜🎜컴파일하는 동안🎜🎜🎜🎜우리가 개발하는 동안 일반적으로 작성하는 .vue 파일은 브라우저에서 직접 실행할 수 없으므로 webpack🎜컴파일 단계🎜 동안, 해당 js 코드를 생성하려면 vue-loader를 통해 .vue 파일을 컴파일해야 하며, vue 코드>템플릿 템플릿에 해당하는 구성 요소는 🎜컴파일러🎜에 의해 렌더링 함수로 변환됩니다. 🎜🎜🎜🎜Runtime🎜🎜🎜🎜다음으로, 컴파일된 코드가 실제로 브라우저에서 실행되면 render function이 실행되고 VNode가 반환됩니다. 소위 Virtual DOM은 마침내 VNode를 실제 DOM 노드로 렌더링합니다. 🎜🎜vue3의 렌더링 시스템에 대한 심층 분석🎜🎜이해하세요 vue 컴포넌트 렌더링 아이디어를 마친 후 🎜Vue.js 3.0🎜(이하 vue3)의 소스 코드에서 시작하여 -vue에 대한 심층적인 이해 code> 컴포넌트의 🎜전체 렌더링 프로세스🎜는 무엇인가요? 🎜🎜🎜🎜준비🎜🎜🎜
🎜이 글에서는 주로 vue3의 렌더링 시스템을 분석하기 위해 vue.js 파일을 직접 소개합니다. . 소스 코드 디버깅 분석. 🎜
🎜🎜🎜vue3 소스 코드 다운로드🎜
// 一直use一直爽
createApp(App).use(Router).use(Vuex).component(&#39;component&#39;,{}).mount("#app")
🎜🎜🎜vue.global.js 파일 생성🎜
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;
});
🎜🎜🎜개발 환경 시작🎜
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;
  };
}
🎜🎜🎜코드 테스트🎜
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()
}
🎜🎜🎜앱 만들기🎜🎜🎜🎜 위의 테스트 코드에서 vue3vue2의 마운트 방법이 다르다는 것을 알 수 있습니다. vue3createApp의 입력 기능을 사용합니다. 애플리케이션을 생성합니다. 다음으로, createApp의 구체적인 구현을 살펴보겠습니다. 🎜
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;
}
🎜🎜ensureRenderer🎜🎜🎜🎜첫 번째 패스 ensureRenderer code> 웹 측에서 렌더러를 생성하려면 구체적인 구현을 살펴보겠습니다. 🎜<pre class="brush:js;toolbar:false;">// 实际调用的render方法即为baseCreateRenderer方法中缓存的render方法 function baseCreateRenderer() { const render = (vnode, container) =&gt; { if (vnode == null) { if (container._vnode) { // 卸载组件 unmount() } } else { // 正常挂载 patch(container._vnode || null, vnode, container) } } }</pre><blockquote>🎜여기에서 볼 수 있듯이 렌더러 생성을 지연함으로써 반응형 패키지에만 의존하는 경우 다음을 수행할 수 있습니다. 🎜tree-shaking🎜을 통해 옮겨보세요. 렌더링 관련 코드를 제외하면 패키지 크기가 대폭 줄어듭니다. 🎜</blockquote>🎜🎜<span style="font-size: 16px;">createRenderer🎜🎜🎜🎜 <code>ensureRenderer에서 볼 수 있듯이 실제 입구는 이 createRenderer입니다. code >Method: 🎜<pre class="brush:js;toolbar:false;">const patch = ( n1, // 旧的vnode n2, // 新的vnode container, // 挂载的容器 ... ) =&gt; { // ... 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 &amp; 1 /* ELEMENT */) { // 处理DOM元素 processElement(n1, n2, container, ...); } else if (shapeFlag &amp; 6 /* COMPONENT */) { // 处理组件 processComponent(n1, n2, container, ...); } else if (shapeFlag &amp; 64 /* TELEPORT */) { type.process(n1, n2, container, ...); } else if (shapeFlag &amp; 128 /* SUSPENSE */) { type.process(n1, n2, container, ...); } } }</pre>🎜 다음으로 이러한 내향적인 함수의 구현을 건너뛰고(후속 렌더링 프로세스에서 사용할 때 자세히 분석하겠습니다) <code>createAppAPI의 구체적인 구현을 살펴보겠습니다. 🎜 🎜🎜 createAppAPI🎜🎜🎜
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);
  }
};
🎜createAppAPI에서 반환된 createApp 함수가 실제임을 알 수 있습니다🎜 신청서 작성🎜 입구. createApp에서는 vue 애플리케이션의 컨텍스트가 생성되고, app은 초기화되고 애플리케이션 컨텍스트는 에 바인딩됩니다. >app 인스턴스를 켜고 마지막으로 app으로 돌아갑니다. 🎜
🎜여기서 주목할 만한 사항이 있습니다: app 개체의 use, mixin, Component directivedirective 메서드는 app 애플리케이션 인스턴스를 반환하며 개발자는 🎜체인 호출🎜을 할 수 있습니다. 🎜
// 一直use一直爽
createApp(App).use(Router).use(Vuex).component(&#39;component&#39;,{}).mount("#app")

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

vue3의 렌더링 시스템에 대한 심층 분석

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

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

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

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

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结构:

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内容:

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的内容有发生什么变化:

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的内容:

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也就是在最开始创建渲染器时传入的参数对应的处理方法,也就完成整个跨平台的初次渲染流程

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方法的内容:

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的内容,验证一下我们的分析:

vue3의 렌더링 시스템에 대한 심층 분석

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

1vue3의 렌더링 시스템에 대한 심층 분석

派发更新

分析完依赖收集阶段,我们再看下,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()
    }
  }
}

1vue3의 렌더링 시스템에 대한 심층 분석

总结

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

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

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

1vue3의 렌더링 시스템에 대한 심층 분석

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

위 내용은 vue3의 렌더링 시스템에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제