深入解析下vue3中的渲染系统

本篇文章给大家深入解析一下vue3中的渲染系统,希望对大家有所帮助!

深入解析下vue3中的渲染系统

提到马拉松,大家都知道马拉松是世界上最长的田径项目(全程42.195公里),是所有体育运动中体力消耗最大,同时也是最磨练一个人的意志的项目。如果说你可以坚持跑完整个马拉松,那还有什么是你不可以坚持下来的呢。

同样,在我们学习研究一些优秀类库的源码时,整个过程也是枯燥乏味,亦如参加一场源码级的马拉松。那今天笔者就带着大家一起跑一场关于Vue.js 3.0渲染系统的源码解析版的马拉松,在整个过程中,笔者也会给大家提供补给站(流程图),方便大家阅读。

思考

立即学习“前端免费学习笔记(深入)”;

在开始今天的文章之前,大家可以想一下:

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源码下载

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

登录后复制

生成vue.global.js文件

  1. 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

登录后复制

启动开发环境

  1. npm run serve

登录后复制

测试代码

  1.   
    static node
      
    {{title}}
        
    const Item = { props: ['msg'], template: `
    {{ msg }}` } const app = Vue.createApp({ components: { Item }, setup() { return { title: Vue.ref(0) } }, methods: { add() { this.title += 1 } }, }) app.mount('#app')
  2. 登录后复制

  3. 创建应用

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

  5. // 入口文件: /vue-next/packages/runtime-dom/src/index.tsconst createApp = ((...args) => {  console.log('createApp入参:', ...args);  // 创建应用  const app = ensureRenderer().createApp(...args);  const { mount } = app;  // 重写mount  app.mount = (containerOrSelector) => {    // ...  };  return app;});
  6. 登录后复制

  7. ensureRenderer

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

  9. // 更新属性的方法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;// 延迟创建rendererfunction ensureRenderer() {  return (renderer || (renderer = createRenderer(rendererOptions)));}
  10. 登录后复制

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

  12. createRenderer

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

  14. // /vue-next/packages/runtime-core/src/renderer.tsexport 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)  }}
  15. 登录后复制

  16. 接下来我们先跳过这些内敛函数的实现(后面的渲染流程用到时,我们再具体分析),来看下createAppAPI的具体实现:

  17. createAppAPI

  18. 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;  };}
  19. 登录后复制

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

  21. 这里有个值得注意的点:app对象上的usemixincomponentdirective方法都返回了app应用实例,开发者可以链式调用

  22. // 一直use一直爽createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")
  23. 登录后复制

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

  25. 深入解析下vue3中的渲染系统

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

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

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

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

  30. 深入解析下vue3中的渲染系统

  31. 挂载阶段

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

  33. 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 = '';    // 执行缓存的mount方法    const proxy = mount(container, false, container);    return proxy;  };  return app;});
  34. 登录后复制

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

  36. 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;  };}
  37. 登录后复制

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

  39. 创建根组件对应的vnode

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

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

  42. 记录挂载状态

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

  44. 创建vnode

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

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

  47. 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()}
  48. 登录后复制

  49. 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;}
  50. 登录后复制

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

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

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

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

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

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

  57. 深入解析下vue3中的渲染系统

  58. 渲染vnode

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

  60. // 实际调用的render方法即为baseCreateRenderer方法中缓存的render方法function baseCreateRenderer() {  const render = (vnode, container) => {    if (vnode == null) {      if (container._vnode) {        // 卸载组件        unmount()      }    } else {      // 正常挂载      patch(container._vnode || null, vnode, container)    }  }}
  61. 登录后复制

  62. 当传入的vnodenull&存在老的vnode,则进行卸载组件

  63. 否则,正常挂载

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

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

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

  67. patch

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

  69. 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, ...);      }  }}
  70. 登录后复制

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

  72. processComponent

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

  74. 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);  }};
  75. 登录后复制

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

  77. mountComponent

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

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

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

  82. 创建组件实例

  83. 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;}
  84. 登录后复制

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

  86. 深入解析下vue3中的渲染系统

  87. 设置组件实例

  88. 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;}
  89. 登录后复制

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

  91. 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);  }}
  92. 登录后复制

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

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

  95. 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 ? 'null' : typeof setupResult}`);  }  finishComponentSetup(instance, isSSR);}
  96. 登录后复制

  97. 设置组件实例的render方法,分析finishComponentSetup函数,render函数有三种设置方式:

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

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

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

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

  102. 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  ...}
  103. 登录后复制

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

  105. 深入解析下vue3中的渲染系统

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

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

  108. 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's effect scope );  const update = (instance.update = effect.run.bind(effect));  update.id = instance.uid;  update();};
  109. 登录后复制

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

  111. 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;}
  112. 登录后复制

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

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

  115. 深入解析下vue3中的渲染系统

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

  117. initialVNode

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

  119. sub tree

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

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

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

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

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

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

  128. const mountChildren = (children, container, ...) => {  for (let i = start; i 

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

    const processElement = (n1, n2, container, ...) => {  if (n1 == null) {    // 挂载DOM元素    mountElement(n2, container,...)  } else {    // 更新  }}
  129. 登录后复制

  130. 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);};
  131. 登录后复制

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

  133. 深入解析下vue3中的渲染系统

  134. 更新流程

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

  136. 依赖收集

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

  138. function setupStatefulComponent(instance, isSSR) {  ...  // 创建渲染上下文代理  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));}
  139. 登录后复制

  140. function renderComponentRoot(instance) {  const proxyToUse = withProxy || proxy;  // 执行render方法时,会调用proxyToUse,即会触发PublicInstanceProxyHandlers的get  result = normalizeVNode(    render.call(proxyToUse, proxyToUse, ...)  );  return result;}
  141. 登录后复制

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

  143. 深入解析下vue3中的渲染系统

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

  145. (function anonymous() {const _Vue = Vueconst { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vueconst _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 */))  }}})
  146. 登录后复制

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

  148. // 代理渲染上下文的handler实现const PublicInstanceProxyHandlers = {  get({ _: instance }, key) {    const { ctx, setupState, data, props, accessCache, type, appContext } = instance;    let normalizedProps;    // key值不以$开头的属性    if (key[0] !== '$') {      // 优先从缓存中判断当前属性需要从哪里获取      // 性能优化:缓存属性应该根据哪种类型获取,避免每次都触发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() {}};
  149. 登录后复制

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

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

  152. 再看accessCache缓存中是否存在

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

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

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

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

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

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

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

  160. // 设置组件实例时会设置setupState的代理prxoy// 设置流程:setupComponent=>setupStatefulComponent=>handleSetupResultinstance.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}
  161. 登录后复制

  162. 访问ref.value时,触发ref的依赖收集。那我们先来分析Vue.ref()的实现逻辑又是什么呢?

  163. // 调用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)  }}
  164. 登录后复制

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

  166. 回忆下setupRenderEffect阶段的实现:

  167. 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 {}  }}
  168. 登录后复制

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

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

  171. 深入解析下vue3中的渲染系统

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

  173. 深入解析下vue3中的渲染系统

  174. 派发更新

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

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

  177. // 代理渲染上下文的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;  }};
  178. 登录后复制

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

  180. const shallowUnwrapHandlers: ProxyHandler = {  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)    }  }}
  181. 登录后复制

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

  183. 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()    }  }}
  184. 登录后复制

  185. 深入解析下vue3中的渲染系统

  186. 总结

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

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

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

  190. 深入解析下vue3中的渲染系统

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

  192. 以上就是深入解析下vue3中的渲染系统的详细内容,更多请关注【创想鸟】其它相关文章!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
编程技术

vue3项目keepAlive使用方法详解

2025-4-1 17:21:12

编程技术

认识一下vue中的$attrs和$listeners属性,聊聊用法

2025-4-1 17:21:27

0 条回复 A文章作者 M管理员
欢迎您,新朋友,感谢参与互动!
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
私信列表
搜索