vuejs对象更新渲染_Vue.js 3.0 组件是如何渲染为 DOM 的?

题图 来自 Vue.js 官网

本文主要是讲述 Vue.js 3.0 中一个组件是如何转变为页面中真实 DOM 节点的。对于任何一个基于 Vue.js 的应用来说,一切的故事都要从应用初始化(通常会命名为 APP 的根组件挂载到 HTML 页面 DOM 节点上)说起。所以,我们可以从应用的根组件为切入点。

应用初始化

在 Vue.js 3.0 中,初始化一个应用的方式和 Vue.js 2.x 有差别但是差别不大(本质上都是把 App 组件挂载到 id 为 app 的 DOM 节点上),在 Vue.js 3.0 中用法如下:

import { createApp } from 'vue'

import App from './app'

const app = createApp(App)

app.mount('#app')

createApp 简化版源码

// packages/runtime-dom/src/index.ts

// 创建应用

const createApp = ((...args) => {

// 1. 创建 app 对象

const app = ensureRenderer().createApp(...args)

const { mount } = app

// 2. 重写 mount 方法

app.mount = (containerOrSelector) => {

// ...

}

return app

})

createApp 方法中主要做了两件事:

创建 app 对象

重写 app.mount 方法

接下来会分别看一下这两个过程都做了什么事情。

创建 app 对象

从 ensureRenderer() 着手。在 Vue.js 3.0 中有一个「渲染器」的概念,我们先对渲染器有一个初步的印象:渲染器可以用于跨平台渲染,是一个包含了平台渲染核心逻辑的 JavaScript 对象。接下来,我们通过简化版源码来验证这个结论:

// packages/runtime-dom/src/index.ts

// 定义渲染器变量

let renderer

// 创建一个渲染器对象

// 惰性创建渲染器(当用户只依赖响应式包的时候可以通过 tree-shaking 的方式移除核心渲染逻辑相关的代码)

function ensureRenderer() {

return renderer || (renderer = createRenderer(rendererOptions))

}

// packages/runtime-core/src/renderer.ts

export function createRenderer(options) {

return baseCreateRenderer(options)

}

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

// packages/runtime-core/src/apiCreateApp.ts

// 接收一个渲染器 render 作为参数,接收一个可选参数 hydrate,返回一个用于创建 app 的函数

export function createAppAPI(render, hydrate) {

// createApp 接收两个参数:根组件对象和根组件的prop

return function createApp(rootComponent, rootProps = null) {

const context = createAppContext()

const app = (context.app = {

_uid: uid++,

_component: rootComponent,

_props: rootProps,

_container: null,

_context: context,

version,

get config() {},

set config(v) {},

use(plugin: Plugin, ...options: any[]) {},

mixin(mixin: ComponentOptions) {},

component(name: string, component?: Component): any {},

directive(name: string, directive?: Directive) {},

mount(rootContainer: HostElement, isHydrate?: boolean): any {

// 创建根组件的 vnode

const vnode = createVNode(rootComponent, rootProps)

// 利用函数参数传入的渲染器渲染 vnode

render(vnode, rootContainer)

app._container = rootContainer

return vnode.component.proxy

},

unmount() {},

provide(key, value) {}

}

return app

}

}

渲染器对象的 createApp 方法接收两个参数:根组件对象和根组件的prop。这和应用初始化 demo 中 createApp(App) 的使用方式是吻合的。还可以看到的是:createApp 返回的 app 对象在最初定义时包含了 _uid 、 use 、 mixin 、 component 、mount 等属性。

此时,我们可以得出结论:在应用层调用的 createApp 方法内部,首先会生成一个渲染器,然后调用渲染器的 createApp 方法创建 app 对象。app 对象中具有一系列我们在日常开发应用时已经很熟悉的属性。

在应用层调用的 createApp 方法内部创建好 app 对象后,接下来便是对 app.mount 方法重写。

重写 app.mount 方法

先看一下简化版的 app.mount 源码:

// packages/runtime-dom/src/index.ts

const { mount } = app

app.mount = (containerOrSelector) => {

// 1. 标准化容器(将传入的 DOM 对象或者节点选择器统一为 DOM 对象)

const container = normalizeContainer(containerOrSelector)

if (!container) return

const component = app._component

// 2. 标准化组件(如果根组件不是函数,并且没有 render 函数和 template 模板,则把根组件 innerHTML 作为 template)

if (!isFunction(component) && !component.render && !component.template) {

component.template = container.innerHTML

}

// 3. 挂载前清空容器的内容

container.innerHTML = ''

// 4. 执行渲染器创建 app 对象时定义的 mount 方法(在后文中称之为「标准 mount 函数」)来渲染根组件

const proxy = mount(container)

return proxy

}

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

标准化容器

标准化组件

挂载前清空容器的内容

执行标准 mount 函数渲染组件

此时可能会有人思考一个问题:为什么要重写app.mount 呢?答案是因为 Vue.js 需要支持跨平台渲染。

支持跨平台渲染的思路: 不同的平台具有不同的渲染器,不同的渲染器中会调用标准的 baseCreateRenderer 来保证核心(标准)的渲染流程是一致的。

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

createApp 流程图

在分别了解了 创建 app 对象和重写 app.mount 过程后,我们来以整体的视角看一下 createApp 函数的实现:

目前为止,只是对应用的初始化有了一个初步的印象,但是还没有涉及到具体的组件渲染过程。可以看到根组件的渲染是在标准 mount 函数中进行的。所以接下来需要去深入了解标准 mount 函数。

标准 mount 函数

简化版源码

// packages/runtime-core/src/apiCreateApp.ts

// createAppAPI 函数内部返回的 createApp 函数中定义了 app 对象,mount 函数是 app 对象的方法之一

mount(rootContainer, isHydrate) {

// 1. 创建根组件的 vnode

const vnode = createVNode(rootComponent, rootProps)

// 2. 利用函数参数传入的渲染器渲染 vnode

render(vnode, rootContainer)

app._container = rootContainer

return vnode.component.proxy

}

createVNode 方法做了两件事:

基于根组件「创建 vnode」

在根组件容器中「渲染 vnode」

vnode 大致可以理解为 Virtual DOM(虚拟 DOM)概念的一个具体实现,是用普通的 JS 对象来描述 DOM 对象。因为不是真实的 DOM 对象,所以叫做 Virtual DOM。

我们来一起看一下创建 vnode 和渲染 vnode 的具体过程。

创建 vnode

简化版源码(已经把分支逻辑拿掉)

// packages/runtime-core/src/vnode.ts

function _createVNode(type, props, children, patchFlag, dynamicProps, isBlockNode = false) {

// 1. 对 VNodeTypes 或 ClassComponent 类型的 type 进行各种标准化处理:规范化 vnode、规范化 component、规范化 CSS 类和样式

// 2. 将 vnode 类型信息编码为位图

const shapeFlag = isString(type)

? ShapeFlags.ELEMENT

: __FEATURE_SUSPENSE__ && isSuspense(type)

? ShapeFlags.SUSPENSE

: isTeleport(type)

? ShapeFlags.TELEPORT

: isObject(type)

? ShapeFlags.STATEFUL_COMPONENT

: isFunction(type)

? ShapeFlags.FUNCTIONAL_COMPONENT

: 0

// 3. 创建 vnode 对象

const vnode = {

__v_isVNode: true,

[ReactiveFlags.SKIP]: true,

type, // 把函数入参 type 赋值给 vnode

props,

children: null,

component: null,

staticCount: 0,

shapeFlag, // 把 vnode 类型信息赋值给 vnode

// 还有很多属性

}

// 4. 标准化子节点 children

normalizeChildren(vnode, children)

return vnode

}

createVNode 做了 4 件事:

对 VNodeTypes 或 ClassComponent 类型的 type 进行各种标准化处理

将 vnode 类型信息编码为位图

创建 vnode 对象

标准化子节点 children

细心的同学会发现:在标准 mount 函数中执行 createVNode(rootComponent, rootProps) 时,参数是根组件 rootComponent 和根组件属性 rootProps,但是在 _createVNode 在定义时函数签名的前两个参数确实 type 和 props。rootComponent 与 type 的关系是什么呢?函数名为什么差了一个 _ 呢?

首先函数名的差异,是由于在定义函数时,基于代码运行环境做了一个判断:

export const createVNode = __DEV__

? createVNodeWithArgsTransform

: _createVNode

其次,rootComponent 与 type 的关系我们可以从 type 的类型定义中得到答案:

function _createVNode(

type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,

props: (Data & VNodeProps) | null = null

): VNode { }

当 createVNode把这 4 件事情做好后,会返回已经创建好 vnode,接下来做的事情是渲染 vnode。

渲染 vnode

即使不看具体源码实现,我们其实大致可以用一句话总结出渲染 vnode 过程做了什么事情:把 vnode 转化为真实 DOM。

前文我们提过,渲染器是一个包含了平台渲染核心逻辑的 JavaScript 对象。渲染 vnode 正是通过调用渲染器的 render 方法做的。

// 返回渲染器对象

return {

render,

hydrate,

createApp: createAppAPI(render, hydrate)

}

我们来看一下 render 函数的定义(简化版源码):

// packages/runtime-core/src/renderer.ts

const render = (vnode, container) => {

if (vnode == null) {

// 如果 vnode 为 null,但是容器中有 vnode,则销毁组件

if (container._vnode) {

unmount(container._vnode, null, null, true)

}

} else {

// 创建或更新组件

patch(container._vnode || null, vnode, container)

}

// packages/runtime-core/src/scheduler.ts

flushPostFlushCbs()

// 缓存 vnode 节点(标识该 vnode 已经完成渲染)

container._vnode = vnode

}

抽象来看,render 做的事情是:如果传入的 vnode 为空,则销毁组件,否则就创建或者更新组件。其中有两个关键函数:patch 和 unmount(patch、unmount 和 render 都是在baseCreateRenderer函数内部的方法)。

可以从 patch 着手,看一下是如何将 vnode 转化为 DOM 的。

patch

// packages/runtime-core/src/renderer.ts

const patch = (

n1,

n2,

container,

anchor = null,

parentComponent = null,

parentSuspense = null,

isSVG = false,

optimized = false

) => {

// 1. 如果是更新 vnode 并且新旧 vnode 类型不一致,则销毁旧的 vnode

if (n1 && !isSameVNodeType(n1, n2)) {

anchor = getNextHostNode(n1)

unmount(n1, parentComponent, parentSuspense, true)

n1 = null

}

// 2. 处理不同类型节点的渲染

const { type, ref, shapeFlag } = n2

switch (type) {

case Text:

// 处理文本节点

processText(n1, n2, container, anchor)

break

case Comment:

// 处理注释节点

break

case Static:

// 处理静态节点

break

case Fragment:

// 处理 Fragment 元素(https://v3.vuejs.org/guide/migration/fragments.html#fragments)

break

default:

if (shapeFlag & ShapeFlags.ELEMENT) {

// 处理普通 DOM 元素

} else if (shapeFlag & ShapeFlags.COMPONENT) {

// 处理组件

} else if (shapeFlag & ShapeFlags.TELEPORT) {

// 处理 TELEPORT

} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {

// 处理 SUSPENSE

} else if (__DEV__) {

warn('Invalid VNode type:', type, `(${typeof type})`)

}

}

}

patch 函数做了 2 件事情:

如果是更新 vnode 并且新旧 vnode 类型不一致,则销毁旧的 vnode

处理不同类型节点的渲染

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

n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次新建(挂载)的过程

n2 表示新的 vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑

container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面

以新建文本 DOM 节点为例,此时 n1 为 null,n2 类型为 Text,所以会走分支逻辑:

processText(n1, n2, container, anchor) 。processText 内部会去调用 hostCreateText 和 hostSetText。

hostCreateText 和 hostSetText 是从 baseCreateRenderer 函数入参 options 中解析出来的方法:

// packages/runtime-core/src/renderer.ts

const {

insert: hostInsert,

remove: hostRemove,

patchProp: hostPatchProp,

forcePatchProp: hostForcePatchProp,

createElement: hostCreateElement,

createText: hostCreateText,

createComment: hostCreateComment,

setText: hostSetText,

setElementText: hostSetElementText,

parentNode: hostParentNode,

nextSibling: hostNextSibling,

setScopeId: hostSetScopeId = NOOP,

cloneNode: hostCloneNode,

insertStaticContent: hostInsertStaticContent

} = options

来看看 options 是怎么来的:

// packages/runtime-core/src/renderer.ts

// 在调用 baseCreateRenderer 时,传入了渲染参数

function baseCreateRenderer(options) { }

还记得前文提到的我们在哪里调用了 baseCreateRenderer 吗?

// packages/runtime-dom/src/index.ts

// 创建应用

const createApp = ((...args) => {

// 1. 创建 app 对象

const app = ensureRenderer().createApp(...args)

return app

})

// packages/runtime-dom/src/index.ts

const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)

function ensureRenderer() {

return renderer || (renderer = createRenderer(rendererOptions))

}

// packages/runtime-core/src/renderer.ts

export function createRenderer(options) {

return baseCreateRenderer(options)

}

可以看到在创建渲染器时,我们调用了 baseCreateRenderer 并传入了 options。options 的值为 extend({ patchProp, forcePatchProp }, nodeOps)。

我们如果知道了 nodeOps 中的 createText、setText 等方法做了什么事情,就清楚了某一个确定类型的 vnode 是如何转变为 DOM 的。先看一下 nodeOps 的定义:

// packages/runtime-dom/src/nodeOps.ts

export const nodeOps = {

createText: text => doc.createTextNode(text),

setText: (node, text) => {},

// 其他方法

}

此时已经非常接近问题的答案了,关键是看一下 doc 变量是什么:

const doc = (typeof document !== 'undefined' ? document : null) as Document

至此,我们知道了答案:先把组件转化为 vnode,针对特定类型的 vnode 执行不同的渲染逻辑,最终调用 document 上的方法将 vnode 渲染成 DOM。

抽象一下,从组件到渲染生成 DOM 需要经历 3 个过程:创建 vnode - 渲染 vnode - 生成 DOM。

在渲染 vnode 部分,我们以一个简单的 Text 类型的 vnode 为例来找到了答案。其实在 baseCreateRenderer 中有 30+ 个函数来处理不同类型的 vnode 的渲染。比如:用来处理组件类型的 processComponent 函数、用来处理普通 DOM 元素类型的processElement 函数等。由于 vnode 是一个树形数据结构,在处理过程中还应用到了递归思想。建议感兴趣的同学自行查看。

总结

最后,我们来做个总结:

在 Vue.js 中, vnode 是对抽象事物的描述。

从组件到渲染生成 DOM 需要经历 3 个过程:创建 vnode - 渲染 vnode - 生成 DOM。

组件是如何转变为 DOM 的:先把组件转化为 vnode,针对特定类型的 vnode 执行不同的渲染逻辑,最终调用 document 上的方法将 vnode 渲染成 DOM。

渲染器是一个包含了平台渲染核心逻辑的 JavaScript 对象,可以用于跨平台渲染。

渲染器对象中的 createApp 方法,创建了一个具有 mount 方法的 app 实例。app.mount 方法中先是用根组件创建了 vnode,然后调用渲染器对象中的 render 方法去渲染 vnode,最终通过 DOM API 将 vnode 转化为 DOM。

附录

Vue.js 中使用了哪些 DOM 的方法:

createElement

createElementNS

createTextNode

createComment

querySelector

insertBefore

insert

removeChild

setAttribute

cloneNode

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue.js 中,可以通过按钮点击来实现组件之间的页面跳转或替换。有几种方式可以实现这一功能,以下是其中两种常见的方法: 1. 使用 Vue Router:Vue Router 是 Vue.js 官方提供的路由管理插件。首先,你需要安装和配置 Vue Router。然后,在你的按钮点击事件中,使用 `router.push()` 方法实现页面跳转。例如: ```javascript <template> <div> <button @click="goToAnotherPage">跳转到另一个页面</button> </div> </template> <script> import { mapActions } from 'vuex'; export default { methods: { ...mapActions(['goToAnotherPage']), }, }; </script> ``` ```javascript // 在路由配置文件中 import VueRouter from 'vue-router'; import AnotherPage from './components/AnotherPage.vue'; const routes = [ { path: '/another-page', component: AnotherPage }, // 其他路由配置... ]; const router = new VueRouter({ routes, }); export default router; ``` 2. 使用条件渲染:在主页面组件中,使用条件渲染来控制要显示的组件。通过点击按钮,改变条件渲染的变量,从而切换到不同的组件。例如: ```javascript <template> <div> <button @click="toggleComponent">切换组件</button> <component v-if="showComponent" :is="currentComponent"></component> <component v-else :is="anotherComponent"></component> </div> </template> <script> import AnotherComponent from './components/AnotherComponent.vue'; export default { data() { return { showComponent: true, }; }, computed: { currentComponent() { return this.showComponent ? 'MainComponent' : 'AnotherComponent'; }, }, components: { AnotherComponent, }, methods: { toggleComponent() { this.showComponent = !this.showComponent; }, }, }; </script> ``` 以上是两种常见的实现方式,你可以根据具体的需求选择适合你的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值