Vue3源码梳理:运行时的设计方案概况
2023-12-17 13:44:03
关于运行时和demo简单示例
-
运行时,简单理解,就是把vnode渲染到页面中
<div id='app'></div> <script> const { render, h } = Vue const vnode = h('div', { class: 'test', }, 'hello render') const container = document.querySelector('#app') render(vnode, container) </script>
-
整个runtime包含两个环节
- 1.利用h函数生成vnode
- 2.利用render函数把vnode渲染到指定位置
-
所以,我们的目标是
- 理解vnode作用,为何要创建vnode
- 创建vnode参数是干嘛的,为何要传递这些参数
-
在理解这些之前, 我们需要了解
- HTML DOM 节点树 与 虚拟 DOM 树
- 这两者的区别
HTML DOM 节点树 与 虚拟 DOM 树
1 )两个概念
- html dom 节点树
- 虚拟 dom 树
<div>
<h1> hello h1</h1>
<!-- 哈哈 -->
hello div
</div>
- 浏览器会把它们通过一个dom树来表示
- dom树的解释:https://zh.javascript.info/dom-nodes
- 上述dom树的示例包含
- 标签点击、注释节点、文本节点
2 )关于虚拟DOM
- 官方关于虚拟dom的解释:https://cn.vuejs.org/guide/extras/rendering-mechanism.html#virtual-dom
- 虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念
- 意为将目标所需的 UI 通过数据结构“虚拟”地表示出来
- 保存在内存中,然后将真实的 DOM 与之保持同步
- 这个概念是由 React 率先开拓,随后在许多不同的框架中都有不同的实现,当然也包括 Vue。
- 虚拟dom是一种理念,期望通过js对象描述一个div节点
- 所以说,与其说虚拟 DOM 是一种具体的技术,不如说是一种模式,所以并没有一个标准的实现
3 ) 区别示例
拿文本节点来说
html dom 节点树表示
<div>text</div>
虚拟dom表示
const vnode = {
type: 'div',
children: 'text'
}
总结
- 在运行时 runtime ,渲染器 renderer 会遍历整个虚拟dom树,并据此结构构建真实的dom树
- 这个过程我们可以把它叫做挂载 mount
- 在这个 vnode 对象发生变化时候,我们会对比 旧的 VNode 和 新的 VNode 之间的区别
- 找出它们之间的区别,并应用这其中的变化到真实的dom上,这个过程叫做更新 patch
关于挂载和更新
简化版的demo
<div id='app'></div>
<script>
// <div>hello render</div>
const vnode = {
type: 'div',
children: 'hello render'
}
const vnode2 = {
type: 'div',
children: 'hello patch'
}
function render(oldVNode, newVNode, container) {
// 第一次属于挂载,old不存在
if(!oldVNode) {
mount(newVNode, container)
} else {
patch(oldVNode, newVNode, container)
}
}
// 挂载方法
function mount(vnode, container) {
const ele = document.createElement(vnode.type) // 1. 创建当前节点
ele.innerText = vnode.children // 2.插入具体节点
container.appendChild(ele) // 3. 将创建的节点存放到容器中
}
// 卸载操作
function unmount(container) {
container.innerHTML = ''
}
// 更新操作
function patch(oldVNode, newVNode, container) {
// 1. 卸载
unmount(container)
// 2. 重新渲染
const ele = document.createElement(newVNode.type)
ele.innerText = newVNode.children
container.appendChild(ele)
}
// 初始化时去挂载
render(null, vnode, document.querySelector('#app'))
// 延迟两秒进行更新
setTimeout(() => {
render(vnode, vnode2, document.querySelector('#app'))
}, 2000)
</script>
- 以上是挂载、更新的逻辑,是一个精简版的更新操作
- vue本质上也是这类操作(删除旧节点,挂载新节点),但是性能更优,实现更复杂
h函数 和 render函数
在vue中的vnode对象实际上属性很多,我们精简一下
{
// 是否是一个vnode对象
"__v_isVNode": true,
// 当前节点类型
"type": "div",
// 当前节点的属性
"props": {"class": "test"}
// 它的子节点
"children": "hello render"
}
- h函数本质上就是一个生成vnode的函数
- https://cn.vuejs.org/api/render-function.html#h
官方示例
import { h } from 'vue'
// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })
// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])
- 官方文档上提供了各种各样的使用方式
- 注意,除了 type 外,其他参数都是可选的
- h 函数最多可接收三个参数
- type: string | Component: 既可以是字符串(原生标记),也可以是一个Vue组件的定义
- props?: object | null: 要传递的 prop
- children?: Children | Slot | Slots: 子节点
2 ) render 函数
-
https://cn.vuejs.org/api/options-rendering.html#render
render(vnode, container)
-
vnode 虚拟dom树
-
container: 承载的容器,真实节点的渲染节点位置
-
通过render函数,我们可以通过编程形式来把虚拟dom转换成真实dom挂载到指定的容器盒子上
核心设计原则
1 ) 概述
- vue源码中包含两块
runtime-core
runtime-dom
- vue为什么要这么划分
- 为什么不像是reactivity,都组织到一起
- vue挂载和更新的逻辑处理是什么
2 ) vue为何分开设计
-
runtime-core 是运行时核心代码
- 只放核心逻辑,不会放置宿主环境下的相关操作
- 当当前的vue需要在浏览器端运行时,它就可以把操作dom的一些逻辑作为参数传递到render里面
- 比如在 baseCreateRenderer 方法中的options,里面可以解构出很多 API
- 这些 API 都是宿主环境传过来的函数
- 假如当前 Vue 需要在浏览器上渲染,就把自己的一些 API 传递过来,类似于接口对接的方式,满足不同宿主平台的调用
- 以此来满足不同平台的不同挂载渲染场景
-
runtime-dom 是浏览器渲染的核心逻辑,多是一个浏览器相关基本操作
- 这里的很多API都会被作为参数,传递到
runtime-core
包中使用 - 实现了 渲染 和 宿主平台 两者的解耦
- 这里宿主平台可以是浏览器,可以是类浏览器环境
- 一般基于vue渲染都是spa,服务端渲染 ssr
- 除了这些渲染情况,还有weex, uniapp等都会用到vue的渲染服务
- 这里的很多API都会被作为参数,传递到
-
所以
- 分包的原因是:
- 针对不同的宿主环境使用不同的API
- 分包的原因是:
-
挂载和更新的逻辑处理
-
baseCreateRenderer
最终返回了一个对象{render, hydrate, createApp}
,这个对象里有 render 函数 -
render函数有三个参数:
vnode
,container
,isSVG
这第三个参数不用管const render: RootRenderFunction = (vnode, container, isSVG) => { // 不存在vnode if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) } } else { // 存在vnode更新 patch(container._vnode || null, vnode, container, null, null, null, isSVG) } flushPostFlushCbs() container._vnode = vnode }
-
进入patch函数
const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren ) => { if (n1 === n2) { return } // patching & not same type, unmount old tree if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null } if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } 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) { 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) } }
-
里面有一个switch,根据当前vnode的type来划分vnode的类型进行各自的处理
-
挂载的操作,本质上都是依赖patch函数来执行的,内部根据type来匹配各类挂载流程
-
所以,整个挂载的操作,本质上是依赖于patch函数来执行的,内部基于type来执行不同类型节点的挂载
-
整个render的大致逻辑如下
- baseCreateRenderer 这个函数
- 包含核心的render方法
- render方法的渲染会在vnode存在的时候使用patch函数
- patch函数会根据当前节点的vnode类型来选择不用的节点挂载
- 而每一种类型的挂载节点都类似处理:
- 旧节点不存在时,进行挂载;
- 旧节点存在进行更新
- baseCreateRenderer 这个函数
-
文章来源:https://blog.csdn.net/Tyro_java/article/details/135043267
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!