核心是将组件变成 vnode -> 再将 vnode 变成真实 dom -> 插入到页面上

应用初始化

  1. <body>
  2. <div id="app"></div>
  3. <script>
  4. let { createApp } = VueRuntimeDOM;
  5. let App = {
  6. render() {
  7. console.log('render');
  8. }
  9. }
  10. let app = createApp(App, { name: 'hh', age: 12 })
  11. app.mount('#app')
  12. </script>
  13. </body>

调用createApp 会返回一个app对象,有个mount 方法

createApp 简化版源码

ackages/runtime-dom/src/index.ts

  1. // packages/runtime-dom/src/index.ts
  2. // 定义渲染器
  3. let renderer;
  4. // 创建一个渲染器
  5. function ensureRenderer() {
  6. return renderer || (renderer = createRenderer(rendererOptions))
  7. }
  8. export const createApp = ((...args) => {
  9. // 创建 app 对象
  10. const app = ensureRenderer().createApp(...args)
  11. const { mount } = app
  12. // 重写 mount方法
  13. app.mount = (containerOrSelector) => {
  14. // 拿到容器元素
  15. const container = normalizeContainer(containerOrSelector)
  16. if (!container) return
  17. // 挂载之前清空容器
  18. container.innerHTML = ''
  19. // 调用原始 mount方法
  20. mount(container);
  21. //将组建渲染成dom元素进行挂载,这是 runtime-core 要做的事情
  22. }
  23. return app
  24. })

调app.mount 方法首先要清空容器里面的东西。
createApp 方法中主要做了两件事:

  1. 创建 app 对象
  2. 重写 app.mount 方法

创建 app 对象

  1. // packages/runtime-core/src/renderer.ts
  2. // 创建渲染器
  3. export function createRenderer(options) {
  4. return baseCreateRenderer(options)
  5. }
  6. // 创建不同平台渲染器的函数,在其内部都会调用 baseCreateRenderer
  7. function baseCreateRenderer(options, createHydrationFns) {
  8. // 一系列内部函数
  9. const render = (vnode, container) => {
  10. // 组件渲染的核心逻辑
  11. }
  12. // 返回渲染器对象
  13. return {
  14. render,
  15. hydrate,
  16. createApp: createAppAPI(render, hydrate)
  17. }
  18. }

可以看出渲染器最终由 baseCreateRenderer 函数生成,是一个包含 rendercreateApp 函数的 JS 对象。其中 createApp 函数是由 createAppAPI 函数返回的。那 createAPI 接收的参数有哪些呢?为了寻求答案,我们需要看一下 createAppAPI 做了什么事情。
packages/runtime-core/src/apiCreateApp.ts

  1. export function createAppAPI(render, hydrate){
  2. // 渲染方法
  3. return function createApp(rootComponent, rootProps = null) {
  4. const context = createAppContext()
  5. const installedPlugins = new Set()
  6. let isMounted = false
  7. const app = {
  8. mount(
  9. rootContainer,
  10. isHydrate,
  11. isSVG
  12. ) {
  13. // 根据组建创建虚拟节点
  14. const vnode = createVNode(
  15. rootComponent,
  16. rootProps
  17. )
  18. vnode.appContext = context
  19. // 将虚拟节点和容器获取到后调用render方法渲染
  20. render(vnode, rootContainer)
  21. }
  22. }
  23. }
  24. }

createAppAPI 创建了createApp 方法,并根据传入的 根组件和根组件属性生成了 虚拟dom,并用传入的 render 方法渲染虚拟dom。

重写app.mount 方法

  1. // packages/runtime-dom/src/index.ts
  2. const { mount } = app
  3. app.mount = (containerOrSelector): any => {
  4. // 1. 标准化容器(将传入的 DOM 对象或者节点选择器统一为 DOM 对象)
  5. const container = normalizeContainer(containerOrSelector)
  6. if (!container) return
  7. const component = app._component
  8. // 2. 标准化组件(如果根组件不是函数,并且没有 render 函数和 template 模板,则把根组件 innerHTML 作为 template)
  9. if (!isFunction(component) && !component.render && !component.template) {
  10. component.template = container.innerHTML
  11. }
  12. // 3. 挂载前清空容器的内容
  13. container.innerHTML = ''
  14. // 4. 执行渲染器创建 app 对象时定义的 mount 方法(在后文中称之为「标准 mount 函数」)来渲染根组件
  15. const proxy = mount(container)
  16. return proxy
  17. }

浏览器平台 app.mount 方法重写主要做了 4 件事情:

  1. 标准化容器
  2. 标准化组件
  3. 挂载前清空容器的内容
  4. 执行标准 mount 函数渲染组件

此时可能会有人思考一个问题:为什么要重写app.mount 呢?答案是因为 Vue.js 需要支持跨平台渲染。
支持跨平台渲染的思路:不同的平台具有不同的渲染器,不同的渲染器中会调用标准的 baseCreateRenderer 来保证核心(标准)的渲染流程是一致的。

以浏览器端和服务端渲染的代码实现为例:
yuque_diagram.jpg

标准 mount 方法

  1. // packages/runtime-core/src/apiCreateApp.ts
  2. // createAppAPI 函数内部返回的 createApp 函数中定义了 app 对象,mount 函数是 app 对象的方法之一
  3. mount(rootContainer, isHydrate) {
  4. // 1. 创建根组件的 vnode
  5. const vnode = createVNode(rootComponent, rootProps)
  6. // 2. 利用函数参数传入的渲染器渲染 vnode
  7. render(vnode, rootContainer)
  8. app._container = rootContainer
  9. return vnode.component.proxy
  10. },

1.创建 vnode

packages/runtime-core/src/vnode.ts

  1. function _createVNode(
  2. type,
  3. props = null,
  4. children = null,
  5. patchFlag = 0,
  6. dynamicProps = null,
  7. isBlockNode = false
  8. ) {
  9. // 根据type 来区分是组件还是普通元素
  10. // 将 vnode 类型信息编码为位图
  11. const shapeFlag = isString(type)
  12. ? ShapeFlags.ELEMENT // 是个字符串 就认为他是个元素
  13. : __FEATURE_SUSPENSE__ && isSuspense(type)
  14. ? ShapeFlags.SUSPENSE
  15. : isTeleport(type)
  16. ? ShapeFlags.TELEPORT
  17. : isObject(type) // 是对象就是个组件
  18. ? ShapeFlags.STATEFUL_COMPONENT // 带有状态的组件
  19. : isFunction(type) // 是函数就是函数式组件
  20. ? ShapeFlags.FUNCTIONAL_COMPONENT
  21. : 0 // 其他情况就啥也不是
  22. // 一个对象描述对应的内容,虚拟节点有跨平台的能力
  23. const vnode = {
  24. __v_isVNode: true, // 他是一个 vnode 节点
  25. __v_skip: true,
  26. type,
  27. props,
  28. key: props && normalizeKey(props), // diff 算法会用到 key
  29. ref: props && normalizeRef(props),
  30. scopeId: currentScopeId,
  31. slotScopeIds: null,
  32. children: null, // 虚拟节点的儿子 ,有三种情况,是个插槽、文本、数组
  33. component: null, // 存放组件对应实例
  34. suspense: null,
  35. ssContent: null,
  36. ssFallback: null,
  37. dirs: null,
  38. transition: null,
  39. el: null, // 后面会将虚拟节点和真实节点对应起来
  40. anchor: null,
  41. target: null,
  42. targetAnchor: null,
  43. staticCount: 0,
  44. shapeFlag, // 描述自己的类型
  45. patchFlag,
  46. dynamicProps,
  47. dynamicChildren: null,
  48. appContext: null
  49. }
  50. // 标准化儿子
  51. normalizeChildren(vnode, children)
  52. return vnode
  53. }
  54. export function normalizeChildren(vnode, children) {
  55. let type = 0
  56. const { shapeFlag } = vnode
  57. // 如果儿子没有 不处理
  58. if (children == null) {
  59. children = null
  60. } else if (isArray(children)) { // 儿子是数组
  61. type = ShapeFlags.ARRAY_CHILDREN // 更新类型
  62. }else {
  63. type = ShapeFlags.TEXT_CHILDREN
  64. }
  65. vnode.children = children
  66. vnode.shapeFlag |= type // 标识出自己的类型和儿子的类型
  67. }

createVNode 做了 4 件事:

  1. 对 VNodeTypes 或 ClassComponent 类型的 type 进行各种标准化处理
  2. 将 vnode 类型信息编码为位图
  3. 创建 vnode 对象
  4. 标准化子节点 children

创建好vnode 之后就是 渲染 vnode 了

2.render 渲染 vnode

渲染 vnode 就是用 渲染器返回的 render 方法进行的。将一个虚拟节点挂载到真实 dom 上

  1. return {
  2. render,
  3. hydrate,
  4. createApp: createAppAPI(render, hydrate)
  5. }
  1. // 根据不同的 虚拟dom 节点 创建对应的真实元素
  2. const render = (vnode, container) => {
  3. if (vnode == null) {
  4. // 如果 vnode 为 null,但是容器中有 vnode,则销毁组件
  5. if (container._vnode) {
  6. unmount(container._vnode, null, null, true)
  7. }
  8. } else {
  9. // 创建或更新组件 初次渲染 老的 vnode 没有
  10. patch(container._vnode || null, vnode, container)
  11. }
  12. // packages/runtime-core/src/scheduler.ts
  13. flushPostFlushCbs()
  14. // 缓存 vnode 节点(标识该 vnode 已经完成渲染)
  15. container._vnode = vnode
  16. }

render 做的事情是:如果传入的 vnode 为空,则销毁组件,否则就创建或者更新组件。

patch

核心是判断虚拟节点的类型,然后根据不同的类型 处理,渲染组件、渲染元素、渲染文本等

  1. const patch = (
  2. n1, // 老的虚拟节点
  3. n2, // 新的虚拟节点
  4. container,
  5. anchor = null,
  6. parentComponent = null,
  7. parentSuspense = null,
  8. isSVG = false,
  9. slotScopeIds = null,
  10. optimized = false
  11. ) => {
  12. // 1. 如果是更新 vnode 并且新旧 vnode 类型不一致,则销毁旧的 vnode
  13. if (n1 && !isSameVNodeType(n1, n2)) {
  14. anchor = getNextHostNode(n1)
  15. unmount(n1, parentComponent, parentSuspense, true)
  16. n1 = null
  17. }
  18. const { type, ref, shapeFlag } = n2
  19. // 2. 处理不同类型节点的渲染
  20. switch (type) {
  21. case Text:
  22. processText(n1, n2, container, anchor)
  23. break
  24. case Comment:
  25. processCommentNode(n1, n2, container, anchor)
  26. break
  27. case Static:
  28. if (n1 == null) {
  29. mountStaticNode(n2, container, anchor, isSVG)
  30. } else if (__DEV__) {
  31. patchStaticNode(n1, n2, container, isSVG)
  32. }
  33. break
  34. case Fragment:
  35. processFragment(
  36. n1,
  37. n2,
  38. container,
  39. anchor,
  40. parentComponent,
  41. parentSuspense,
  42. isSVG,
  43. slotScopeIds,
  44. optimized
  45. )
  46. break
  47. default:
  48. // 元素
  49. if (shapeFlag & ShapeFlags.ELEMENT) {
  50. processElement(
  51. n1,
  52. n2,
  53. container,
  54. anchor,
  55. parentComponent,
  56. parentSuspense,
  57. isSVG,
  58. slotScopeIds,
  59. optimized
  60. )
  61. } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件
  62. processComponent( // 处理组件
  63. n1,
  64. n2,
  65. container,
  66. anchor,
  67. parentComponent,
  68. parentSuspense,
  69. isSVG,
  70. slotScopeIds,
  71. optimized
  72. )
  73. }
  74. }
  75. }

patch 函数做了 2 件事情:

  1. 如果是更新 vnode 并且新旧 vnode 类型不一致,则销毁旧的 vnode
  2. 处理不同类型节点的渲染

在 patch 函数的多个参数中,我们优先关注前 3 个参数:

  1. n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次新建(挂载)的过程
  2. n2 表示新的 vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑
  3. container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面

1.处理组件的渲染

processComponent

  1. // packages/runtime-core/src/renderer.ts
  2. const processComponent = (
  3. n1: VNode | null,
  4. n2: VNode,
  5. container: RendererElement,
  6. anchor: RendererNode | null,
  7. parentComponent: ComponentInternalInstance | null,
  8. parentSuspense: SuspenseBoundary | null,
  9. isSVG: boolean,
  10. slotScopeIds: string[] | null,
  11. optimized: boolean
  12. ) => {
  13. n2.slotScopeIds = slotScopeIds
  14. if (n1 == null) { // 组件没有上一次的虚拟节点就是 初始化流程
  15. if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
  16. ;(parentComponent!.ctx as KeepAliveContext).activate(
  17. n2,
  18. container,
  19. anchor,
  20. isSVG,
  21. optimized
  22. )
  23. } else { // 组件更新流程
  24. mountComponent( // 挂载组件
  25. n2,
  26. container,
  27. anchor,
  28. parentComponent,
  29. parentSuspense,
  30. isSVG,
  31. optimized
  32. )
  33. }
  34. } else {
  35. updateComponent(n1, n2, optimized)
  36. }
  37. }

核心 mountComponent

  1. // packages/runtime-core/src/renderer.ts
  2. /**
  3. * let { createApp } = VueRuntimeDOM;
  4. let App = {
  5. setup(props,context){
  6. return
  7. },
  8. render(proxy) {
  9. console.log('render');
  10. }
  11. }
  12. let app = createApp(App, { name: 'hh', age: 12 })
  13. app.mount('#app')
  14. * 挂载组件 最核心的就是 调用 setup 拿到返回值(如果有 setup),或者获取render 函数返回的结果来进行渲染
  15. * 1.创建组件实例
  16. * 2.将需要的数据解析到实例上
  17. * 3.创建一个 effect 让render 函数执行
  18. */
  19. const mountComponent = (
  20. initialVNode,
  21. container,
  22. anchor,
  23. parentComponent,
  24. parentSuspense,
  25. isSVG,
  26. optimized
  27. ) => {
  28. // 2.x compat may pre-creaate the component instance before actually
  29. // mounting
  30. const compatMountInstance =
  31. __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  32. // 1.根据vnode创建一个组件实例挂载到当前vnode 的component 属性上
  33. const instance =
  34. compatMountInstance ||
  35. (initialVNode.component = createComponentInstance(
  36. initialVNode,
  37. parentComponent,
  38. parentSuspense
  39. ))
  40. // 2.需要的数据解析到实例上
  41. setupComponent(instance)
  42. // 3.创建一个 effect 让 render 函数执行
  43. setupRenderEffect(
  44. instance,
  45. initialVNode,
  46. container,
  47. anchor,
  48. parentSuspense,
  49. isSVG,
  50. optimized
  51. )
  52. }

做了三件事:

  • 1.创建组件实例
  • 2.将需要的数据解析到实例上
  • 3.创建一个 effect 让 render 函数执行,当属性更新了组件会重新渲染
  • 4.拿到render方法返回的结果(即 h 函数的返回结果,也是一个 vnode),再次走渲染流程(调用patch)

1.创建组件实例
1.createComponentInstance

  1. // packages/runtime-core/src/components.ts
  2. export function createComponentInstance(
  3. vnode,
  4. parent,
  5. suspense
  6. ) {
  7. const type = vnode.type
  8. // inherit parent app context - or - if root, adopt from root vnode
  9. const appContext =
  10. (parent ? parent.appContext : vnode.appContext) || emptyAppContext
  11. // 组件实例
  12. const instance = {
  13. uid: uid++,
  14. vnode,
  15. type,
  16. parent,
  17. appContext,
  18. root, // to be immediately set
  19. next: null,
  20. subTree, // will be set synchronously right after creation
  21. update, // will be set synchronously right after creation
  22. render: null,
  23. proxy: null,
  24. exposed: null,
  25. exposeProxy: null,
  26. withProxy: null,
  27. effects: null,
  28. provides: parent ? parent.provides : Object.create(appContext.provides),
  29. accessCache,
  30. renderCache: [],
  31. // local resovled assets
  32. components: null,
  33. directives: null,
  34. // resolved props and emits options
  35. propsOptions: normalizePropsOptions(type, appContext),
  36. emitsOptions: normalizeEmitsOptions(type, appContext),
  37. // emit
  38. emit: null, // to be set immediately
  39. emitted: null,
  40. // props default value
  41. propsDefaults: EMPTY_OBJ,
  42. // inheritAttrs
  43. inheritAttrs: type.inheritAttrs,
  44. // state
  45. ctx: EMPTY_OBJ, // 组件上下文
  46. data: EMPTY_OBJ,
  47. props: EMPTY_OBJ,
  48. attrs: EMPTY_OBJ,
  49. slots: EMPTY_OBJ,
  50. refs: EMPTY_OBJ,
  51. setupState: EMPTY_OBJ, // setup 的返回值
  52. setupContext: null,
  53. // suspense related
  54. suspense,
  55. suspenseId: suspense ? suspense.pendingId : 0,
  56. asyncDep: null,
  57. asyncResolved: false,
  58. // lifecycle hooks
  59. // not using enums here because it results in computed properties
  60. isMounted: false, // 组件是否挂载过
  61. isUnmounted: false,
  62. isDeactivated: false,
  63. bc: null,
  64. c: null,
  65. bm: null,
  66. m: null,
  67. bu: null,
  68. u: null,
  69. um: null,
  70. bum: null,
  71. da: null,
  72. a: null,
  73. rtg: null,
  74. rtc: null,
  75. ec: null,
  76. sp: null
  77. }
  78. instance.ctx = { _: instance } // 将instance放到了上下文里
  79. instance.root = parent ? parent.root : instance
  80. instance.emit = emit.bind(null, instance)
  81. return instance
  82. }

将 instance 赋值给他的 ctx 属性,之后访问的时候通过 instance.ctx._, 而不用通过 instance.data 、instance.props ….
2.setupComponent将需要的数据解析到实例上

  1. // packages/runtime-core/src/components.ts
  2. export function setupComponent(
  3. instance,
  4. isSSR = false
  5. ) {
  6. isInSSRComponentSetup = isSSR
  7. const { props, children } = instance.vnode // 拿出虚拟节点的 props、children
  8. // 当前组件是不是有状态的组件,函数组件
  9. const isStateful = instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  10. // 根据props 解析出 props 和 attrs,将其放到 instance 上
  11. initProps(instance, props, isStateful, isSSR) // instance.props = props
  12. initSlots(instance, children) // 插槽的解析 instance.children = children
  13. // 获取 setup 的返回值 调用当前实例的setup方法,用setup的返回值填充setupState 和对应的render方法
  14. const setupResult = isStateful
  15. ? setupStatefulComponent(instance, isSSR)
  16. : undefined
  17. isInSSRComponentSetup = false
  18. return setupResult
  19. }

setupStatefulComponent

  1. // packages/runtime-core/src/components.ts
  2. // 调用setup
  3. function setupStatefulComponent(
  4. instance,
  5. isSSR
  6. ) {
  7. // 拿到组件
  8. const Component = instance.type
  9. instance.accessCache = Object.create(null)
  10. // 1. 1.代理 传递给 render 函数的参数 代理了之后就不用通过 instance.props, instance.data,intance.attrs这样访问了
  11. instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
  12. // 2. 拿到组件的 setup方法
  13. const { setup } = Component
  14. if (setup) {
  15. // 创建 setup 的上下文参数 setup(instance.props, setupContext)
  16. const setupContext = (instance.setupContext =
  17. setup.length > 1 ? createSetupContext(instance) : null)
  18. currentInstance = instance
  19. // 执行setup
  20. const setupResult = setup(instance.props, setupContext)
  21. handleSetupResult(setupResult)
  22. // 开发时会使用这些属性
  23. Component.render(instance.proxy)
  24. }else { // 没有setup 调用finishComponentSetup
  25. finishComponentSetup(instance, isSSR)
  26. }
  27. }
  28. // 创建开发时需要用到的 props 和 context
  29. function createSetupContext(instance) {
  30. const expose = exposed => {
  31. instance.exposed = exposed || {}
  32. }
  33. // 开发时使用的四个参数 setup(props, context)
  34. return {
  35. attrs: instance.attrs,
  36. slots: instance.slots,
  37. emit: instance.emit,
  38. expose
  39. }
  40. }

setupStatefulComponent 做了三件事:

  • 代理 传递给 render 函数的参数
  • 拿到组件的 setup方法,创建 setup 的上下文参数 setup(instance.props, setupContext)
  • 如果 setup 存在就执行setup,如果setup 不存在就调用 组件的 render 方法

1.代理

  1. // packages/runtime-core/src/components.ts
  2. export const PublicInstanceProxyHandlers = {
  3. get({ _: instance }, key) {
  4. const {
  5. ctx,
  6. setupState,
  7. data,
  8. props,
  9. accessCache,
  10. type,
  11. appContext
  12. } = instance
  13. if (
  14. hasOwn(setupState, key)
  15. ) {
  16. return setupState[key]
  17. }else if (hasOwn(setupState, key)) {
  18. return setupState[key]
  19. } else if (hasOwn(data, key)) {
  20. return data[key]
  21. }
  22. },
  23. set({ _: instance },key,value) {
  24. const { data, setupState, ctx } = instance
  25. if (hasOwn(setupState, key)) {
  26. setupState[key] = value
  27. } else if (hasOwn(data, key)) {
  28. data[key] = value
  29. } else if (hasOwn(instance.props, key)) {
  30. __DEV__ &&
  31. warn(
  32. `Attempting to mutate prop "${key}". Props are readonly.`,
  33. instance
  34. )
  35. return false
  36. }
  37. if (key[0] === '$' && key.slice(1) in instance) {
  38. __DEV__ &&
  39. warn(
  40. `Attempting to mutate public property "${key}". ` +
  41. `Properties starting with $ are reserved and readonly.`,
  42. instance
  43. )
  44. return false
  45. } else {
  46. if (__DEV__ && key in instance.appContext.config.globalProperties) {
  47. Object.defineProperty(ctx, key, {
  48. enumerable: true,
  49. configurable: true,
  50. value
  51. })
  52. } else {
  53. ctx[key] = value
  54. }
  55. }
  56. return true
  57. }
  58. }

取值的时候走 get,赋值的时候走set

  1. props:{a: 1}
  2. data: {c: 3}
  3. setupState: {b: 2}
  4. let { createApp } = VueRuntimeDOM;
  5. let App = {
  6. setup(props,context){
  7. return
  8. },
  9. render(proxy) {
  10. console.log(proxy.a, proxy.b, proxy.c); // 1, 2, 3
  11. }
  12. }

2.创建 setup 的上下文参数

  1. // packages/runtime-core/src/components.ts
  2. export function createSetupContext(instance) {
  3. const expose = exposed => {
  4. instance.exposed = exposed || {}
  5. }
  6. return { // 开发时使用的四个参数
  7. attrs: instance.attrs,
  8. slots: instance.slots,
  9. emit: instance.emit,
  10. expose
  11. }
  12. }

这几个参数是开发时会用到的参数 setup(props, context)

3.如果没有setup就调用finishComponentSetup获取render 函数。

  1. // packages/runtime-core/src/components.ts
  2. export function finishComponentSetup(
  3. instance,
  4. isSSR,
  5. skipOptions
  6. ) {
  7. const Component = instance.type
  8. // template / render function normalization
  9. if (__NODE_JS__ && isSSR) {
  10. instance.render = (instance.render ||
  11. Component.render ||
  12. NOOP)
  13. } else if (!instance.render) { // 如果没 render 就编译 template
  14. // could be set from setup()
  15. if (compile && !Component.render) {
  16. const template =
  17. (__COMPAT__ &&
  18. instance.vnode.props &&
  19. instance.vnode.props['inline-template']) ||
  20. Component.template
  21. if (template) {
  22. if (__DEV__) {
  23. startMeasure(instance, `compile`)
  24. }
  25. const { isCustomElement, compilerOptions } = instance.appContext.config
  26. const {
  27. delimiters,
  28. compilerOptions: componentCompilerOptions
  29. } = Component
  30. const finalCompilerOptions = extend(
  31. extend(
  32. {
  33. isCustomElement,
  34. delimiters
  35. },
  36. compilerOptions
  37. ),
  38. componentCompilerOptions
  39. )
  40. if (__COMPAT__) {
  41. // pass runtime compat config into the compiler
  42. finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
  43. if (Component.compatConfig) {
  44. extend(finalCompilerOptions.compatConfig, Component.compatConfig)
  45. }
  46. }
  47. Component.render = compile(template, finalCompilerOptions)
  48. if (__DEV__) {
  49. endMeasure(instance, `compile`)
  50. }
  51. }
  52. }
  53. // 将生成的 render 函数 挂载到实例上
  54. instance.render = (Component.render || NOOP)
  55. }
  56. }

这个方法会取组件上的 render 函数,如果没有render就对 template 模板进行编译,生成 render 函数,然后放在实例上
4.如果有 setup 就处理setup 的返回值

  1. // packages/runtime-core/src/components.ts
  2. export function handleSetupResult(
  3. instance,
  4. setupResult,
  5. isSSR
  6. ) {
  7. if (isFunction(setupResult)) { // 如果结果是函数,就作为组件的 render
  8. if (__NODE_JS__ && (instance.type).__ssrInlineRender) {
  9. instance.ssrRender = setupResult
  10. } else {
  11. instance.render = setupResult
  12. }
  13. } else if (isObject(setupResult)) { // 如果是对象 就赋值给 setupState
  14. instance.setupState = setupResult
  15. }
  16. finishComponentSetup(instance, isSSR)
  17. }

如果setup 的返回值是一个 函数就直接作为组件的 render ,如果是对象就赋值给实例的 setupState,

如果既有 render 函数,setup又返回了 函数就会优先使用 返回值作为 render 函数。

  1. let { createApp } = VueRuntimeDOM;
  2. let App = {
  3. setup(props,context){
  4. return (proxy) => {
  5. console.log('setup render')
  6. }
  7. },
  8. render(proxy) {
  9. console.log(proxy.a, proxy.b, proxy.c);
  10. console.log('component render')
  11. }
  12. }
  1. let { createApp } = VueRuntimeDOM;
  2. let App = {
  3. setup(props,context){
  4. // return (proxy) => {
  5. // console.log('setup render')
  6. // }
  7. return {
  8. a: 1,
  9. b: 2,
  10. }
  11. },
  12. render(proxy) {
  13. console.log(proxy.a, proxy.b, proxy.c);
  14. console.log('component render')
  15. }
  16. }

image.png
3.创建一个 effect 让 render 执行 (组件的渲染流程)
因为是组件,所以在 render 执行的时候会创建组件的子树 vnode,然后递归调用patch渲染。

  1. setup(props,context){
  2. return (proxy) => {
  3. return h('div', {}, 'hello world')
  4. }
  5. }
  1. // packages/runtime-core/src/redenerer.ts
  2. // 创建一个 effect 让 render 执行
  3. const setupRenderEffect = (
  4. instance,
  5. initialVNode,
  6. container,
  7. anchor,
  8. parentSuspense,
  9. isSVG,
  10. optimized
  11. ) => {
  12. // create reactive effect for rendering
  13. instance.update = effect(function componentEffect() {
  14. // 没有被挂载就是初次渲染
  15. if (!instance.isMounted) {
  16. let proxyToUse = instance.proxy;
  17. // 执行render 他的返回值是 h 函数的执行结果, 就是组件的渲染内容
  18. /**
  19. * setup(props,context){
  20. * return (proxy) => {
  21. * return h('div', {}, 'hello world')
  22. * }
  23. * }
  24. */
  25. let subTree = instance.subTree = instance.render.call(proxyToUse,proxyToUse);
  26. // 用render 函数的返回值 继续渲染子树
  27. patch(
  28. null,
  29. subTree,
  30. container,
  31. anchor,
  32. instance,
  33. parentSuspense,
  34. isSVG
  35. )
  36. instance.isMounted = true
  37. }

执行render 的时候会返回h 函数的执行结果,这个结果就是组件要渲染内容的vnode,最后调用 patch 函数将这个 vnode渲染到页面。这时渲染走的是元素的渲染逻辑。

  1. /**
  2. * h('div', {a: 1}) 有属性没儿子
  3. * h('div', {}, 'children') // 儿子是字符串
  4. * h('div',{}, h('span')) // 儿子是个对象
  5. * h('div', h('span')) 不写属性
  6. * h('div', [h('span'), h('span')])
  7. * h('div', null, h('span'), h('span'))
  8. * h('div', null, 'a', 'b', 'c')
  9. * @param type
  10. * @param propsOrChildren
  11. * @param children
  12. * @returns
  13. */
  14. export function h(type, propsOrChildren, children) {
  15. const l = arguments.length
  16. if (l === 2) { // 类型+ 属性 或者 类型+children
  17. if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
  18. // single vnode without props
  19. if (isVNode(propsOrChildren)) {
  20. return createVNode(type, null, [propsOrChildren]) // 孩子
  21. }
  22. // props without children
  23. return createVNode(type, propsOrChildren) // 属性
  24. } else {
  25. // omit props 如果第二个参数不是对象一定是孩子
  26. return createVNode(type, null, propsOrChildren)
  27. }
  28. } else {
  29. if (l > 3) {
  30. // 除了前两个 后面都是孩子
  31. children = Array.prototype.slice.call(arguments, 2)
  32. } else if (l === 3 && isVNode(children)) {
  33. children = [children]
  34. }
  35. return createVNode(type, propsOrChildren, children)
  36. }
  37. }

h 函数可能的写法有好几种,都考虑到了,最终都是通过 createVNode 生成 虚拟节点。
4.拿到render方法返回的结果(即 h 函数的返回结果,也是一个 vnode),再次走渲染流程(调用patch)
看下面。

2.处理元素的渲染

上面组件的渲染处理完就该处理组件下面的元素了。

  1. // 处理元素的渲染
  2. const processElement = (
  3. n1,
  4. n2,
  5. container,
  6. anchor,
  7. parentComponent,
  8. parentSuspense,
  9. isSVG,
  10. slotScopeIds,
  11. optimized
  12. ) => {
  13. isSVG = isSVG || (n2.type) === 'svg'
  14. // 初次渲染
  15. if (n1 == null) {
  16. mountElement(
  17. n2,
  18. container,
  19. anchor,
  20. parentComponent,
  21. parentSuspense,
  22. isSVG,
  23. slotScopeIds,
  24. optimized
  25. )
  26. } else {
  27. // 更新
  28. }

初次渲染

  1. // 渲染元素是递归渲染 先渲染最外层的 div 在渲染它下面的 孩子
  2. const mountElement = (
  3. vnode,
  4. container,
  5. anchor,
  6. parentComponent,
  7. parentSuspense,
  8. isSVG,
  9. slotScopeIds,
  10. optimized
  11. ) => {
  12. let el
  13. let vnodeHook
  14. const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
  15. if (
  16. !__DEV__ &&
  17. vnode.el &&
  18. hostCloneNode !== undefined &&
  19. patchFlag === PatchFlags.HOISTED
  20. ) {
  21. el = vnode.el = hostCloneNode(vnode.el)
  22. } else {
  23. // 创建真实 dom 并赋值给 vnode.el
  24. el = vnode.el = hostCreateElement(
  25. vnode.type,
  26. isSVG,
  27. props && props.is,
  28. props
  29. )
  30. // 判断有儿子的类型 是文本类型
  31. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  32. hostSetElementText(el, vnode.children)
  33. } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  34. // 如果是数组 h('div',{}, ['hello', 'leah']) 不能直接插入,插入会覆盖,需要转换成 vnode 再循环调用patch插入到页面
  35. mountChildren(
  36. vnode.children,
  37. el, // 挂载容器
  38. null,
  39. parentComponent,
  40. parentSuspense,
  41. isSVG && type !== 'foreignObject',
  42. slotScopeIds,
  43. optimized || !!vnode.dynamicChildren
  44. )
  45. }
  46. // props 如果有属性就处理属性 {style:{color: 'red'}}
  47. if (props) {
  48. for (const key in props) {
  49. if (!isReservedProp(key)) {
  50. hostPatchProp(
  51. el,
  52. key,
  53. null,
  54. props[key],
  55. isSVG,
  56. vnode.children,
  57. parentComponent,
  58. parentSuspense,
  59. unmountChildren
  60. )
  61. }
  62. }
  63. }
  64. }
  65. // 插入到容器中
  66. hostInsert(el, container, anchor)
  67. }

我们可以看到创建元素dom 的时候调用了 hostCreateElement ,设置文本节点调用了 hostSetElementText,还有设置属性的方法 hostPatchProp这些方法是从 baseCreateRenderer 函数入参 options 中解析出来的方法:

  1. // packages/runtime-core/src/renderer.ts
  2. const {
  3. insert: hostInsert,
  4. remove: hostRemove,
  5. patchProp: hostPatchProp,
  6. forcePatchProp: hostForcePatchProp,
  7. createElement: hostCreateElement,
  8. createText: hostCreateText,
  9. createComment: hostCreateComment,
  10. setText: hostSetText,
  11. setElementText: hostSetElementText,
  12. parentNode: hostParentNode,
  13. nextSibling: hostNextSibling,
  14. setScopeId: hostSetScopeId = NOOP,
  15. cloneNode: hostCloneNode,
  16. insertStaticContent: hostInsertStaticContent
  17. } = options

上面的options对应如下:

  1. // packages/runtime-dom/src/nodeOps.ts
  2. export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  3. /**
  4. * 元素插入
  5. * @param child 要插入的元素
  6. * @param parent 插到哪个里面去
  7. * @param anchor 当前参照物 如果为空,则相当于 appendChild
  8. */
  9. insert: (child, parent, anchor) => {
  10. parent.insertBefore(child, anchor || null)
  11. },
  12. /**
  13. * 元素删除
  14. * 通过儿子找到父亲删除
  15. * @param child
  16. */
  17. remove: child => {
  18. const parent = child.parentNode
  19. if (parent) {
  20. parent.removeChild(child)
  21. }
  22. },
  23. /**
  24. * 元素增加
  25. * 创建节点,不同平台创建元素的方式不同
  26. * @param tag
  27. * @param isSVG
  28. * @param is
  29. * @param props
  30. * @returns
  31. */
  32. createElement: (tag, isSVG, is, props): Element => {
  33. const el = isSVG
  34. ? doc.createElementNS(svgNS, tag)
  35. : doc.createElement(tag, is ? { is } : undefined)
  36. if (tag === 'select' && props && props.multiple != null) {
  37. ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
  38. }
  39. return el
  40. },
  41. /**
  42. * 元素查找
  43. * @param selector
  44. * @returns
  45. */
  46. querySelector: selector => doc.querySelector(selector),
  47. /**
  48. * 给元素设置文本
  49. * @param el
  50. * @param text
  51. */
  52. setElementText: (el, text) => {
  53. el.textContent = text
  54. },
  55. /**
  56. * 文本操作
  57. * 创建文本
  58. * @param text
  59. * @returns
  60. */
  61. createText: text => doc.createTextNode(text),
  62. createComment: text => doc.createComment(text),
  63. /**
  64. * 给节点设置文本
  65. * @param node
  66. * @param text
  67. */
  68. setText: (node, text) => {
  69. node.nodeValue = text
  70. },
  71. /**
  72. * 获取父节点
  73. * @param node
  74. * @returns
  75. */
  76. parentNode: node => node.parentNode as Element | null,
  77. nextSibling: node => node.nextSibling,
  78. setScopeId(el, id) {
  79. el.setAttribute(id, '')
  80. },
  81. cloneNode(el) {
  82. const cloned = el.cloneNode(true)
  83. // #3072
  84. // - in `patchDOMProp`, we store the actual value in the `el._value` property.
  85. // - normally, elements using `:value` bindings will not be hoisted, but if
  86. // the bound value is a constant, e.g. `:value="true"` - they do get
  87. // hoisted.
  88. // - in production, hoisted nodes are cloned when subsequent inserts, but
  89. // cloneNode() does not copy the custom property we attached.
  90. // - This may need to account for other custom DOM properties we attach to
  91. // elements in addition to `_value` in the future.
  92. if (`_value` in el) {
  93. ;(cloned as any)._value = (el as any)._value
  94. }
  95. return cloned
  96. },
  97. // __UNSAFE__
  98. // Reason: insertAdjacentHTML.
  99. // Static content here can only come from compiled templates.
  100. // As long as the user only uses trusted templates, this is safe.
  101. insertStaticContent(content, parent, anchor, isSVG, cached) {
  102. if (cached) {
  103. let [cachedFirst, cachedLast] = cached
  104. let first, last
  105. while (true) {
  106. let node = cachedFirst.cloneNode(true)
  107. if (!first) first = node
  108. parent.insertBefore(node, anchor)
  109. if (cachedFirst === cachedLast) {
  110. last = node
  111. break
  112. }
  113. cachedFirst = cachedFirst.nextSibling!
  114. }
  115. return [first, last] as any
  116. }
  117. // <parent> before | first ... last | anchor </parent>
  118. const before = anchor ? anchor.previousSibling : parent.lastChild
  119. if (anchor) {
  120. let insertionPoint
  121. let usingTempInsertionPoint = false
  122. if (anchor instanceof Element) {
  123. insertionPoint = anchor
  124. } else {
  125. // insertAdjacentHTML only works for elements but the anchor is not an
  126. // element...
  127. usingTempInsertionPoint = true
  128. insertionPoint = isSVG
  129. ? doc.createElementNS(svgNS, 'g')
  130. : doc.createElement('div')
  131. parent.insertBefore(insertionPoint, anchor)
  132. }
  133. insertionPoint.insertAdjacentHTML('beforebegin', content)
  134. if (usingTempInsertionPoint) {
  135. parent.removeChild(insertionPoint)
  136. }
  137. } else {
  138. parent.insertAdjacentHTML('beforeend', content)
  139. }
  140. return [
  141. // first
  142. before ? before.nextSibling : parent.firstChild,
  143. // last
  144. anchor ? anchor.previousSibling : parent.lastChild
  145. ]
  146. }
  147. }
  148. packages/runtime-dom/src/patchProp.ts
  149. export const patchProp: DOMRendererOptions['patchProp'] = (
  150. el, // 元素
  151. key, // 属性
  152. prevValue, // 前一个值
  153. nextValue,
  154. isSVG = false,
  155. prevChildren,
  156. parentComponent,
  157. parentSuspense,
  158. unmountChildren
  159. ) => {
  160. switch (key) {
  161. // special
  162. case 'class':
  163. patchClass(el, nextValue, isSVG) // 那最新的属性覆盖掉旧的
  164. break
  165. case 'style': // {style:{color: 'red'}} -> {style:{background: 'red'}} 删掉之前的
  166. patchStyle(el, prevValue, nextValue)
  167. break
  168. default:
  169. // 如果不是事件 才是属性
  170. if (isOn(key)) { // 如果是 以 on 开头的就是事件,onClick,onChange
  171. // ignore v-model listeners
  172. if (!isModelListener(key)) {
  173. patchEvent(el, key, prevValue, nextValue, parentComponent) // 添加、删除、修改
  174. }
  175. } else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
  176. patchDOMProp(
  177. el,
  178. key,
  179. nextValue,
  180. prevChildren,
  181. parentComponent,
  182. parentSuspense,
  183. unmountChildren
  184. )
  185. } else {
  186. // special case for <input v-model type="checkbox"> with
  187. // :true-value & :false-value
  188. // store value as dom properties since non-string values will be
  189. // stringified.
  190. if (key === 'true-value') {
  191. ;(el as any)._trueValue = nextValue
  192. } else if (key === 'false-value') {
  193. ;(el as any)._falseValue = nextValue
  194. }
  195. patchAttr(el, key, nextValue, isSVG, parentComponent)
  196. }
  197. break
  198. }
  199. }

可以看到这些方法就是最终调用 document 上的方法将 vnode 转换成 真实 DOM渲染到页面上的。

渲染元素是递归渲染 先渲染最外层的 div 在渲染它下面的 孩子.
通过 document.createElement 创建真实dom元素,并赋值给 vnode.el , 然后判断儿子的类型是不是文本,如果是文本 直接给元素设置文本 el.textContent = text ,如果是数组类型的文本,不能直接插入到页面,那样插入会覆盖,需要遍历将文本转换成 vnode 再调用patch插入到页面。代码如下:

  1. // 将文本创建成 虚拟节点
  2. const mountChildren = (
  3. children,
  4. container,
  5. anchor,
  6. parentComponent,
  7. parentSuspense,
  8. isSVG,
  9. slotScopeIds,
  10. optimized,
  11. start = 0
  12. ) => {
  13. for (let i = start; i < children.length; i++) {
  14. const child = (children[i] = optimized
  15. ? cloneIfMounted(children[i])
  16. : normalizeVNode(children[i]))
  17. // 渲染文本
  18. patch(
  19. null,
  20. child,
  21. container,
  22. anchor,
  23. parentComponent,
  24. parentSuspense,
  25. isSVG,
  26. slotScopeIds,
  27. optimized
  28. )
  29. }
  30. }
  31. // 将儿子转成 vnode 然后调用 patch渲染
  32. export function normalizeVNode(child) {
  33. if (child == null || typeof child === 'boolean') {
  34. // empty placeholder
  35. return createVNode(Comment)
  36. } else if (isArray(child)) {
  37. // fragment
  38. return createVNode(
  39. Fragment, // type
  40. null, // props
  41. child.slice() // children
  42. )
  43. } else if (typeof child === 'object') {
  44. // 是 object 直接返回
  45. return cloneIfMounted(child)
  46. } else {
  47. // 字符串 或者 numbers
  48. return createVNode(Text, null, String(child))
  49. }
  50. }

文本转换成了vnode之后就需要将 文本类型的 vnode 插入到页面中,我们来看一下是如何处理文本节点的渲染

3.处理文本节点的渲染

以新建文本 DOM 节点为例,此时 n1 为 null,n2 类型为 Text,所以会走分支逻辑:processText(n1, n2, container, anchor)processText 内部会去调用 hostCreateTexthostSetText

  1. const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  2. // 如果老的vnode 不存在就是第一次创建
  3. if (n1 == null) {
  4. hostInsert(
  5. (n2.el = hostCreateText(n2.children as string)),
  6. container,
  7. anchor
  8. )
  9. } else {
  10. const el = (n2.el = n1.el!)
  11. if (n2.children !== n1.children) {
  12. hostSetText(el, n2.children as string)
  13. }
  14. }
  15. }

hostCreateTexthostSetText 也是从 baseCreateRenderer 函数入参 options 中解析出来的方法。
首先将文本节点转换成 dom元素,然后插入到页面中。

组件渲染流程图

组件渲染流程.png

image.png

首先处理组件的挂载,走 processComponent ,根据传入的参数,如果有 n1 就是组件更新,走 updateComponent,如果没有n1 就是组件初次渲染,走 mountComponent。
mountComponent 第一步创建组件实例instance,第二步给组件实例增添属性,第三步设置渲染副作用函数effect,这样当组件更新之后也能重新渲染,调用组件的render 方法,拿到 render 函数返回的子树虚拟节点subTree,继续走 patch 渲染子树。判断这个子树是元素还是组件,如果是元素就走 processElement,如果是组件就走 processComponent。