622 lines
23 KiB
Markdown
622 lines
23 KiB
Markdown
# 29|运行时:Vue在浏览器里是怎么跑起来的?
|
||
|
||
你好,我是大圣。
|
||
|
||
上一讲我们学习了Vue响应式的大致原理,响应式就是可以把普通的JavaScript对象包裹成响应式对象,这样,我们对对象做的修改,响应式都能够监听到,并且执行effect内部注册的函数来执行数据修改之后的效果。
|
||
|
||
那今天我就跟你聊一下Vue在浏览器里是如何运行的,照例我们还是对着Vue 3的源码来学习,不过源码复杂,为了帮助你理解主要逻辑,我会直接把源码简化再演示,当然怎么简化源码的一些小技巧也会顺便分享给你。
|
||
|
||
好了废话不多说,我们马上开始。前端框架需要处理的最核心的两个流程,就是首次渲染和数据更新后的渲染。先来看首次渲染的源码。演示代码会用Vue 3的实际代码,你也可以在 [weiyouyi](https://github.com/shengxinjing/weiyouyi/blob/main/src/runtime-core/apiCreateApp.js#L4) 项目中看到我们课程的mini版本代码。
|
||
|
||
## 首次渲染
|
||
|
||
我们知道,想要启动一个Vue项目,只需要从Vue中引入createApp,传入App组件,并且调用createApp返回的App实例的mount方法,就实现了项目的启动。这个时候Vue也完成了首次渲染,代码逻辑如下:
|
||
|
||
![](https://static001.geekbang.org/resource/image/39/7c/3974d85351462f5190363869a39b1f7c.png?wh=1622x786)
|
||
|
||
所以createApp就是项目的初始化渲染入口。
|
||
|
||
但是这段简单的代码是怎么完成初始化渲染的呢?我们可以在Vue中的runtime-dom中看到createApp的定义,你可以打开 [GitHub链接](https://github.com/vuejs/vue-next/blob/master/packages/runtime-dom/src/index.ts#L66)查看。
|
||
|
||
这里就有一个看代码的小技巧,分享给你,我们首次查看源码的时候,可以先把一些无用的信息删除,方便自己梳理主体的逻辑。看Vue代码,和今天主题无关的无用信息有哪些,\_\_COMPAT\_\_代码是用来兼容Vue 2的,\_\_DEV\_\_代码是用来调试的,我们可以把这些代码删除之后,得到下面的简化版createApp源码。
|
||
|
||
再看思路就比较清晰了。我们使用ensureRenderer返回的对象去创建app,并且重写了app.mount方法;在mount方法内部,我们查找mount传递的DOM元素,并且调用ensureRenderer返回的mount方法,进行初始化渲染。如下图所示:
|
||
|
||
![](https://static001.geekbang.org/resource/image/70/82/7073e9c5b18e105a499e30208bd0c582.jpg?wh=2440x896)
|
||
|
||
之前我们讲过要会TypeScript,这时你就能感受到TypeScript的好处了,现在即使我们不知道app.mount是什么逻辑,也能知道这个函数的参数只能是Element、ShadowRoot或者string三者之一,也就很好理解内部的normalizeContainer就是把你传递的参数统一变为浏览器的DOM元素,Typescript类型带来的好处,我们在读源码的时候会一直感受得到。
|
||
|
||
```javascript
|
||
export const createApp = ((...args) => {
|
||
const app = ensureRenderer().createApp(...args)
|
||
const { mount } = app
|
||
// 重写mount
|
||
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
|
||
const container = normalizeContainer(containerOrSelector)
|
||
if (!container) return
|
||
|
||
const component = app._component
|
||
if (!isFunction(component) && !component.render && !component.template) {
|
||
component.template = container.innerHTML
|
||
}
|
||
container.innerHTML = ''
|
||
const proxy = mount(container, false, container instanceof SVGElement)
|
||
if (container instanceof Element) {
|
||
container.removeAttribute('v-cloak')
|
||
container.setAttribute('data-v-app', '')
|
||
}
|
||
return proxy
|
||
}
|
||
return app
|
||
})
|
||
function normalizeContainer(container){
|
||
if (isString(container)) {
|
||
const res = document.querySelector(container)
|
||
}
|
||
return container
|
||
}
|
||
|
||
```
|
||
|
||
我们继续深入了解ensureRenderer方法,以及ensureRenderer方法返回的createApp方法。
|
||
|
||
**这里ensureRenderer函数,内部通过createRenderer函数,创建了一个浏览器的渲染器,并且缓存了渲染器renderer**,这种使用闭包做缓存的方式,你在日常开发中也可以借鉴这种思路。
|
||
|
||
createRenderer函数,我们在自定义渲染器那一讲里学到过,传递的rendererOptions就是浏览器里面标签的增删改查API:
|
||
|
||
```javascript
|
||
// 浏览器dom操作
|
||
import { nodeOps } from './nodeOps'
|
||
// 浏览器dom属性更新
|
||
import { patchProp } from './patchProp'
|
||
import { createRenderer } from '@vue/runtime-core'
|
||
const rendererOptions = extend({ patchProp }, nodeOps)
|
||
|
||
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
|
||
|
||
function ensureRenderer() {
|
||
return (
|
||
renderer ||
|
||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
|
||
)
|
||
}
|
||
|
||
```
|
||
|
||
可以看到,createRenderer函数传递的参数是nodeOps和patchProp的合并对象。
|
||
|
||
我们继续进入nodeOps和pathProp也可以看到下面的代码,写了很多方法。通过ensureRenderer存储这些操作方法后,createApp内部就可以脱离具体的渲染平台了,这也是Vue 3实现跨端的核心逻辑:
|
||
|
||
```javascript
|
||
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
|
||
insert: (child, parent, anchor) => {
|
||
parent.insertBefore(child, anchor || null)
|
||
},
|
||
remove: child => {
|
||
const parent = child.parentNode
|
||
if (parent) {
|
||
parent.removeChild(child)
|
||
}
|
||
},
|
||
createElement: (tag, isSVG, is, props): Element => {
|
||
const el = isSVG
|
||
? doc.createElementNS(svgNS, tag)
|
||
: doc.createElement(tag, is ? { is } : undefined)
|
||
|
||
if (tag === 'select' && props && props.multiple != null) {
|
||
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
|
||
}
|
||
return el
|
||
},
|
||
|
||
createText: text => doc.createTextNode(text),
|
||
|
||
createComment: text => doc.createComment(text),
|
||
|
||
setText: (node, text) => {
|
||
node.nodeValue = text
|
||
},
|
||
|
||
setElementText: (el, text) => {
|
||
el.textContent = text
|
||
},
|
||
parentNode: node => node.parentNode as Element | null,
|
||
nextSibling: node => node.nextSibling,
|
||
querySelector: selector => doc.querySelector(selector),
|
||
...
|
||
}
|
||
|
||
```
|
||
|
||
然后我们就需要进入到rumtime-core模块去看下createRenderer是如何工作的。你可以在这个[GitHub链接](https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/renderer.ts#L290)内看到createRenderer的代码逻辑。当然源码比较复杂,我们照样需要简化一下。
|
||
|
||
createRenderer是调用baseCreateRenderer创建的,baseCreateRenderer函数内部有十几个函数,代码行数合计2000行左右,这也是我们学习Vue源码最复杂的一个函数了。按前面简化源码的思路,先把工具函数的实现折叠起来,精简之后代码主要逻辑其实很简单。
|
||
|
||
我们一起来看。
|
||
|
||
首先获取了平台上所有的insert、remove函数,这些函数都是nodeOps传递进来的,然后定义了一些列patch、mount、unmount函数,通过名字我们不难猜出,这就是Vue中更新、渲染组件的工具函数,比如mountElement就是渲染DOM元素、mountComponent就是渲染组件updateComponent就是更新组件。这部分的简化代码,你也可以在[weiyouyi](https://github.com/shengxinjing/weiyouyi/blob/main/src/runtime-core/renderer.js)项目中查看。
|
||
|
||
```javascript
|
||
export function createRenderer<
|
||
HostNode = RendererNode,
|
||
HostElement = RendererElement
|
||
>(options: RendererOptions<HostNode, HostElement>) {
|
||
return baseCreateRenderer<HostNode, HostElement>(options)
|
||
}
|
||
|
||
function baseCreateRenderer(){
|
||
const {
|
||
insert: hostInsert,
|
||
remove: hostRemove,
|
||
patchProp: hostPatchProp,
|
||
createElement: hostCreateElement,
|
||
createText: hostCreateText,
|
||
createComment: hostCreateComment,
|
||
setText: hostSetText,
|
||
setElementText: hostSetElementText,
|
||
parentNode: hostParentNode,
|
||
nextSibling: hostNextSibling,
|
||
setScopeId: hostSetScopeId = NOOP,
|
||
cloneNode: hostCloneNode,
|
||
insertStaticContent: hostInsertStaticContent
|
||
} = options
|
||
const patch = ()=>... //一个函数
|
||
const processText = ()=>...
|
||
const processCommentNode = ()=>...
|
||
const processElement = ()=>...
|
||
const mountElement = ()=>...
|
||
const mountChildren = ()=>...
|
||
const patchElement = ()=>...
|
||
const patchBlockChildren = ()=>...
|
||
const patchProps = ()=>...
|
||
const processComponent = ()=>...
|
||
const mountComponent = ()=>...
|
||
const updateComponent = ()=>...
|
||
const setupRenderEffect = ()=>...
|
||
const patchChildren = ()=>...
|
||
const patchKeyedChildren = ()=>...
|
||
const unmount = ()=>...
|
||
const unmountComponent = ()=>...
|
||
const unmountComponent = ()=>...
|
||
const unmountComponent = ()=>...
|
||
const unmountComponent = ()=>...
|
||
const render: RootRenderFunction = (vnode, container, isSVG) => {
|
||
if (vnode == null) {
|
||
if (container._vnode) {
|
||
unmount(container._vnode, null, null, true)
|
||
}
|
||
} else {
|
||
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
|
||
}
|
||
flushPostFlushCbs()
|
||
container._vnode = vnode
|
||
}
|
||
return {
|
||
render,
|
||
hydrate,
|
||
createApp: createAppAPI(render, hydrate)
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
整个createApp函数的执行逻辑如下图所示:
|
||
|
||
![](https://static001.geekbang.org/resource/image/cf/7b/cfcbf6cd3f3195518f9e0e407338a37b.jpg?wh=2526x2208)
|
||
|
||
最后返回的createApp方法,实际上是createAPI的返回值,并且给createAPI传递了render方法。render方法内部很简单,就是判断container容器上有没有\_vnode属性,如果有的话就执行unmout方法,没有的话就执行patch方法,最后把vnode信息存储在container.\_vnode上。
|
||
|
||
那createAppAPI又做了什么呢?我们继续进入createAppAPI源码,看下面的代码。内部创建了一个app对象,app上注册了我们熟悉的use、component和mount等方法:
|
||
|
||
```javascript
|
||
export function createAppAPI<HostElement>(
|
||
render: RootRenderFunction,
|
||
hydrate?: RootHydrateFunction
|
||
): CreateAppFunction<HostElement> {
|
||
return function createApp(rootComponent, rootProps = null) {
|
||
const context = createAppContext()
|
||
let isMounted = false
|
||
|
||
const app: App = (context.app = {
|
||
_context: context,
|
||
_instance: null,
|
||
use(plugin: Plugin, ...options: any[]) ,
|
||
component(name: string, component?: Component): any {
|
||
if (!component) {
|
||
return context.components[name]
|
||
}
|
||
context.components[name] = component
|
||
return app
|
||
},
|
||
directive(name: string, directive?: Directive)
|
||
mount(
|
||
rootContainer: HostElement,
|
||
isHydrate?: boolean,
|
||
isSVG?: boolean
|
||
): any {
|
||
if (!isMounted) {
|
||
const vnode = createVNode(
|
||
rootComponent as ConcreteComponent,
|
||
rootProps
|
||
)
|
||
vnode.appContext = context
|
||
// 核心的逻辑
|
||
if (isHydrate && hydrate) {
|
||
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
|
||
} else {
|
||
render(vnode, rootContainer, isSVG)
|
||
}
|
||
return getExposeProxy(vnode.component!) || vnode.component!.proxy
|
||
}
|
||
},
|
||
|
||
provide(key, value) {
|
||
context.provides[key as string] = value
|
||
return app
|
||
}
|
||
})
|
||
|
||
return app
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
可以看到mount内部执行的是传递进来的render方法,也就是上面的render方法。container 就是我们app.mount中传递的DOM元素,对DOM元素进行处理之后,执行patch函数实现整个应用的加载。
|
||
|
||
所以我们的下一个任务就是需要搞清楚patch函数的执行逻辑。
|
||
|
||
### patch 函数
|
||
|
||
patch传递的是container.\_vnode,也就是上一次渲染缓存的vnode、本次渲染组件的vnode,以及容器container。
|
||
|
||
下面就是patch函数的代码,核心代码我添加了注释。其中n1是上次渲染的虚拟DOM,n2是下次要渲染的虚拟DOM。
|
||
|
||
首先可以把n1和n2做一次判断,如果虚拟DOM的节点类型不同,就直接unmount之前的节点。因为比如之前是Button组件,现在要渲染Container组件,就没有计算diff的必要,直接把Button组件销毁再渲染Container即可。
|
||
|
||
如果n1和n2类型相同,比如都是Button组件或者都是div标签,我们需要判断具体的类型再去执行不同的函数,比如processText、processFragment、processElement以及processComponent等函数。
|
||
|
||
看第55行,这里的ShapeFlags用到了位运算的知识,我们后面会通过刷算法题的方式介绍,暂时我们只需要知道,ShapeFlags可以帮助我们快速判断需要操作的类型就可以了。
|
||
|
||
```javascript
|
||
const patch: PatchFn = (
|
||
n1,
|
||
n2,
|
||
container,
|
||
anchor = null,
|
||
parentComponent = null,
|
||
parentSuspense = null,
|
||
isSVG = false,
|
||
slotScopeIds = null,
|
||
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
|
||
) => {
|
||
// 两次虚拟dom完全一样 啥也不用干
|
||
if (n1 === n2) {
|
||
return
|
||
}
|
||
// 虚拟dom节点类型不一样, unmount老的虚拟dom,并且n1赋值null
|
||
if (n1 && !isSameVNodeType(n1, n2)) {
|
||
anchor = getNextHostNode(n1)
|
||
unmount(n1, parentComponent, parentSuspense, true)
|
||
n1 = null
|
||
}
|
||
// n2是要渲染的虚拟dom,我们获取type,ref和shapeFlag
|
||
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)
|
||
} else if (__DEV__) {
|
||
patchStaticNode(n1, n2, container, isSVG)
|
||
}
|
||
break
|
||
case Fragment:
|
||
processFragment(
|
||
n1,
|
||
n2,
|
||
container,
|
||
anchor,
|
||
parentComponent,
|
||
parentSuspense,
|
||
isSVG,
|
||
slotScopeIds,
|
||
optimized
|
||
)
|
||
break
|
||
default:
|
||
// 运运算判断操作类型
|
||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||
// html标签
|
||
processElement(
|
||
n1,
|
||
n2,
|
||
container,
|
||
anchor,
|
||
parentComponent,
|
||
parentSuspense,
|
||
isSVG,
|
||
slotScopeIds,
|
||
optimized
|
||
)
|
||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||
// 组件
|
||
processComponent(
|
||
n1,
|
||
n2,
|
||
container,
|
||
anchor,
|
||
parentComponent,
|
||
parentSuspense,
|
||
isSVG,
|
||
slotScopeIds,
|
||
optimized
|
||
)
|
||
} else if (shapeFlag & ShapeFlags.TELEPORT) {
|
||
;(type as typeof TeleportImpl).process(
|
||
n1 as TeleportVNode,
|
||
n2 as TeleportVNode,
|
||
container,
|
||
anchor,
|
||
parentComponent,
|
||
parentSuspense,
|
||
isSVG,
|
||
slotScopeIds,
|
||
optimized,
|
||
internals
|
||
)
|
||
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
|
||
;(type as typeof SuspenseImpl).process(
|
||
n1,
|
||
n2,
|
||
container,
|
||
anchor,
|
||
parentComponent,
|
||
parentSuspense,
|
||
isSVG,
|
||
slotScopeIds,
|
||
optimized,
|
||
internals
|
||
)
|
||
} else if (__DEV__) {
|
||
warn('Invalid VNode type:', type, `(${typeof type})`)
|
||
}
|
||
}
|
||
|
||
// set ref
|
||
if (ref != null && parentComponent) {
|
||
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
代码的整体执行逻辑如下图所示:![](https://static001.geekbang.org/resource/image/c5/a8/c5c55f140c4573b698265c99bc9cf8a8.jpg?wh=1699x778)
|
||
|
||
我们首次渲染的App是一个组件,所以要执行的就是processComponent方法。
|
||
|
||
### processComponent方法
|
||
|
||
那我们继续进入到processComponent代码内部,看下面的代码。首次渲染的时候,n1就是null,所以会执行mountComponent;如果是更新组件的时候,n1就是上次渲染的vdom,需要执行updateComponent。
|
||
|
||
```javascript
|
||
const processComponent = (
|
||
n1: VNode | null,
|
||
n2: VNode,
|
||
container: RendererElement,
|
||
anchor: RendererNode | null,
|
||
parentComponent: ComponentInternalInstance | null,
|
||
parentSuspense: SuspenseBoundary | null,
|
||
isSVG: boolean,
|
||
slotScopeIds: string[] | null,
|
||
optimized: boolean
|
||
) => {
|
||
n2.slotScopeIds = slotScopeIds
|
||
if (n1 == null) {
|
||
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
|
||
;(parentComponent!.ctx as KeepAliveContext).activate(
|
||
n2,
|
||
container,
|
||
anchor,
|
||
isSVG,
|
||
optimized
|
||
)
|
||
} else {
|
||
mountComponent(
|
||
n2,
|
||
container,
|
||
anchor,
|
||
parentComponent,
|
||
parentSuspense,
|
||
isSVG,
|
||
optimized
|
||
)
|
||
}
|
||
} else {
|
||
updateComponent(n1, n2, optimized)
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
updateComponent是虚拟DOM的逻辑,我们会在下一讲详细剖析,这一讲主要讲首次渲染的过程。
|
||
|
||
所以我们进入mountComponent函数中,可以看到mountComponent函数内部会对组件的类型进行一系列的判断,还有一些对Vue 2的兼容代码,核心的渲染逻辑就是setupComponent函数和setupRenderEffect函数。
|
||
|
||
```javascript
|
||
import {setupComponent} from './component'
|
||
const mountComponent: MountComponentFn = (
|
||
) => {
|
||
// 2.x compat may pre-creaate the component instance before actually
|
||
// mounting
|
||
const compatMountInstance =
|
||
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
|
||
const instance: ComponentInternalInstance =
|
||
compatMountInstance ||
|
||
(initialVNode.component = createComponentInstance(
|
||
initialVNode,
|
||
parentComponent,
|
||
parentSuspense
|
||
))
|
||
|
||
// resolve props and slots for setup context
|
||
if (!(__COMPAT__ && compatMountInstance)) {
|
||
|
||
setupComponent(instance)
|
||
|
||
}
|
||
(
|
||
instance,
|
||
initialVNode,
|
||
container,
|
||
anchor,
|
||
parentSuspense,
|
||
isSVG,
|
||
optimized
|
||
)
|
||
|
||
if (__DEV__) {
|
||
popWarningContext()
|
||
endMeasure(instance, `mount`)
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
setupComponent和setupRenderEffect,它俩又做了点什么呢?可以参考下面的示意图这两个实现组件首次渲染的函数:
|
||
![](https://static001.geekbang.org/resource/image/d4/51/d4b431396eb7ef90e9ab0e1021f46051.jpg?wh=3213x1529)
|
||
|
||
### setupComponent
|
||
|
||
首先看setupComponent,要完成的就是执行我们写的setup函数。
|
||
|
||
可以看到,内部先初始化了props和slots,并且执行setupStatefulComponent创建组件,而这个函数内部从component中获取setup属性,也就是script setup内部实现的函数,就进入到我们组件内部的reactive、ref等函数实现的逻辑了。
|
||
|
||
```javascript
|
||
export function setupComponent(
|
||
instance: ComponentInternalInstance,
|
||
isSSR = false
|
||
) {
|
||
isInSSRComponentSetup = isSSR
|
||
|
||
const { props, children } = instance.vnode
|
||
const isStateful = isStatefulComponent(instance)
|
||
initProps(instance, props, isStateful, isSSR)
|
||
initSlots(instance, children)
|
||
|
||
const setupResult = isStateful
|
||
? setupStatefulComponent(instance, isSSR)
|
||
: undefined
|
||
isInSSRComponentSetup = false
|
||
return setupResult
|
||
}
|
||
|
||
function setupStatefulComponent(
|
||
instance: ComponentInternalInstance,
|
||
isSSR: boolean
|
||
) {
|
||
const Component = instance.type as ComponentOptions
|
||
// 执行setup
|
||
const { setup } = Component
|
||
if (setup) {
|
||
const setupContext = (instance.setupContext =
|
||
setup.length > 1 ? createSetupContext(instance) : null)
|
||
|
||
setCurrentInstance(instance)
|
||
pauseTracking()
|
||
const setupResult = callWithErrorHandling(
|
||
setup,
|
||
instance,
|
||
ErrorCodes.SETUP_FUNCTION,
|
||
[instance.props, setupContext]
|
||
)
|
||
if (isPromise(setupResult)) {
|
||
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
|
||
} else {
|
||
handleSetupResult(instance, setupResult, isSSR)
|
||
}
|
||
} else {
|
||
finishComponentSetup(instance, isSSR)
|
||
}
|
||
}
|
||
|
||
export function callWithErrorHandling(
|
||
fn: Function,
|
||
instance: ComponentInternalInstance | null,
|
||
type: ErrorTypes,
|
||
args?: unknown[]
|
||
) {
|
||
let res
|
||
try {
|
||
res = args ? fn(...args) : fn()
|
||
} catch (err) {
|
||
handleError(err, instance, type)
|
||
}
|
||
return res
|
||
}
|
||
|
||
```
|
||
|
||
### setupRenderEffect
|
||
|
||
另一个setupRenderEffect函数,就是为了后续数据修改注册的函数,我们先梳理一下核心的实现逻辑。
|
||
|
||
组件首次加载会调用patch函数去初始化子组件,注意setupRenderEffect本身就是在patch函数内部执行的,所以这里就会递归整个虚拟DOM树,然后触发生命周期mounted,完成这个组件的初始化。
|
||
|
||
页面首次更新结束后,setupRenderEffect不仅实现了组件的递归渲染,还注册了组件的更新机制。
|
||
|
||
在下面的核心代码中,我们通过ReactiveEffect创建了effect函数,这个概念上一讲我们手写过,然后执行instance.update赋值为effect.run方法,这样结合setup内部的ref和reactive绑定的数据,数据修改之后,就会触发update方法的执行,内部就会componentUpdateFn,内部进行递归的patch调用执行每个组件内部的update方法实现组件的更新。
|
||
|
||
```javascript
|
||
if (!instance.isMounted) {
|
||
patch(
|
||
null,
|
||
subTree,
|
||
container,
|
||
anchor,
|
||
instance,
|
||
parentSuspense,
|
||
isSVG
|
||
)
|
||
}else{
|
||
// updateComponent
|
||
}
|
||
// create reactive effect for rendering
|
||
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) as SchedulerJob)
|
||
update.id = instance.uid
|
||
|
||
update()
|
||
|
||
```
|
||
|
||
这样我们就实现了整个Vue的渲染和更新流程。
|
||
|
||
## 总结
|
||
|
||
今天要学的内容就聊完了,我们来总结一下学到的内容吧,今天我们分析了Vue 3在浏览器中执行的全流程,你可以配合Vue在浏览器中执行的流程图来复习。
|
||
![](https://static001.geekbang.org/resource/image/5f/f7/5f2527dd6eb75120bc3644cdfa5636f7.jpg?wh=6962x3378)
|
||
|
||
Vue通过createApp创建应用,并且执行返回的mount方法实现在浏览器中的挂载,在createApp中,通过传递浏览器平台的操作方法nodeOps创建了浏览器的渲染器renderer。
|
||
|
||
首次执行Vue项目的时候,通过patch实现组件的渲染,patch函数内部根据节点的不同类型,去分别执行processElement、processComponent、processText等方法去递归处理不同类型的节点,最终通过setupComponent执行组件的setup函数,setupRenderEffect中使用响应式的effect函数监听数据的变化。
|
||
|
||
你可以先看我们实现的迷你版本项目weiyouyi,然后再去看Vue 3中实际的代码,可以学习代码中很多优秀的设计思路,比如createRenderer中使用闭包作为缓存、使用位运算来提高组件类型的判断效率等。学习优秀框架中的代码设计,这对我们日常开发项目的代码质量也有很好的提高作用。
|
||
|
||
## 思考题
|
||
|
||
最后留一个思考题,mount函数中除了render函数,还有一个hydrate的函数调用,这个函数式干什么用的呢?欢迎在评论区分享你的答案,我们下一讲再见。
|
||
|