Vue3中虚拟dom原理介绍

本文涉及的产品
语种识别,语种识别 100万字符
图片翻译,图片翻译 100张
文档翻译,文档翻译 1千页
简介: 虚拟DOM本章将从零介绍Vue中的虚拟DOM,从渲染函数带到mount函数以及最后的patch函数也都有具体的代码实现。致谢Vue Mastery非常好的课程,可以转载,但请声明源链接:文章源链接justin3go.com(有些latex公式某些平台不能渲染可查看这个网站)
published: true
date: 2022-2-3
tags: '前端框架 Vue'


虚拟DOM

本章将从零介绍Vue中的虚拟DOM,从渲染函数带到mount函数以及最后的patch函数也都有具体的代码实现。

致谢Vue Mastery非常好的课程,可以转载,但请声明源链接:文章源链接justin3go.com(有些latex公式某些平台不能渲染可查看这个网站)

虚拟DOM层的一些好处


  • 它让组件的渲染逻辑完全从真实DOM中解耦
  • 更直接地去重用框架的运行在其他环境中
  • Vue运行第三方开发人员创建自定义渲染解决方案,目标不仅仅是浏览器,也包括IOS和Android等原生环境
  • 也可以使用API创建自定义渲染器直接渲染到WebGL,而不是DOM节点
  • 提供了以编程方式构造、检查、克隆以及操作所需的DOM操作的能力

渲染函数


模板会完成你要做的事,在99%的情况下你只需写出HTML就好了,有时候需要做一些更可控的事情,这种情况下,需要编写一个渲染函数,所以渲染函数是什么样子的呢?

Vue2 API

// 这是组件定义中的一个选项,相比于提供一个template选项,你可以为组件提供一个渲染函数,在Vue2中,你会得到h参数直接作为渲染函数的参数,可以用它创造vnode
render(h) {
    // vnode接收的第一个参数是type
    return h('div', {
        // 第二个参数是一个对象,包含vnode上的所有数据或属性;
        // Vue2中的API比较冗长,必须指明传递给节点的绑定类型,如果要绑定属性,你必须把它嵌套在attrs对象下,如果要绑定时事件侦听器,你得把它绑定在on下面
        attrs: {
            id: 'foo'
        },
        on: {
            click: this.onClick
        }
        // 第三个参数是这个vnode的子节点,直接传递一个字符串是一个方便的API去表明此节点只包含文本子节点,但它也可以是数组,包含跟多的子节点,这个数组可以嵌套跟多的嵌套h调用
    }, 'hello')
}


Vue3 API

  • Flat props structure(扁平的props结构)
  • Globally importedhhelper(全局导入h)
  • 因为Vue2的h需要连续传递,所以设置为全局变量
import {h} from 'vue'
render(){
    // 当你调用h时,第二个参数现在总是一个扁平的对象,你可以直接给它传递一个属性;
    // 任何带on的都会自动绑定为一个监听器,所以不必考虑太多嵌套的问题
    // 大多数时候你也不必考虑是应将其作为attribute绑定,还是DOM属性绑定,因为Vue将智能地找到最好方法
    // 实际上,检查这个key是否存在,在原生DOM中作为属性,如果存在,我们会将其设置为property,如果不存在,我们将其设置为一个attribute
    return h('div', {
        id: 'foo'
        onClick: this.onClick
    }, 'hello')
}


什么时候去使用渲染函数


静态结构的写法

import {h} from 'vue'
const App = {
    render(){
        return h('div', {
            id: 'hello'
        })
    }
}


上述代码最终会得到类似于以下的代码:
<div id=hello></div>
在最终的dom里面

然后,你可以给它嵌套更多的嵌套子元素

const App = {
    render(){
        return h('div', {
            id: 'hello'
        }, [
            h('span', 'world')
        ])
    }
}


上述代码最终会得到类似于以下的代码:
<div id=hello><span>world</span></div>
在最终的dom里面


使用v-if

// 使用是三目表达式或者普通的if-else,是一样的
const App = {
    render(){
        // v-if="ok"
        return this.ok
            ? h('div', {id: 'hello'}, [h('span', 'world')])
        :this.otherCondition
            ?h('p', 'other branch')
        :h('span')
    }
}


使用v-for

import {h} from 'vue'
const App = this: {
    render()
    // v-for
    return this.list.map(item => {
        return h('div', {key: item.id}, item.text)
    })
}


处理插槽

import {h} from 'vue'
const App = {
    render(){
        const slot = this.$slot.default
        ?this.$slots.default()
        :[]
    }
}


例子

假设我们有一个堆栈组件,一些用户界面库可能会有这种情况吗,堆栈组件时布局组件

<Stack size="4">
        <div>hello</div>
    <Stack size="4">
        <div>hello</div>
        <div>hello</div>
    </Stack>
</Stack>
<div class="stack">
    <div class="mt-4">
        <div>hello</div>
    </div>
    <div class="mt-4">
        <div class="stack">
            <div class="mt-4">
                <div>hello</div>                 
            </div> 
        </div>
    </div>
</div>
<script>
import {h} from 'vue'
const Stack = {
    render(){
        const slot = this.$slots.default
        ?this.$slots.default()
        :[]
    // 所有内容放进stack类中
    return h('div', { class: 'stack'}, slot.map(child =>{
        return h('div', {class: `mt-${this.$props.size}`},[
            child
        ])
    }))
        
    }
}
</script>


实际使用:

<script src="https://unpkg.com/vue@next"></script>
<style>
    .mt-4{
        margin:10px;   
    }
</style>
<div id="app">
</div>
<script>
const {h,createApp} = Vue
// 使用渲染函数生成的Stack组件
const Stack = {
    render(){
        const slot = this.$slots.default
        ?this.$slots.default()
        :[]
    // 所有内容放进stack类中
    return h('div', { class: 'stack'}, slot.map(child =>{
        return h('div', {class: `mt-${this.$props.size}`},[
            child
        ])
    }))
        
    }
}
// 使用Stack组件
const App = {
    components: {
        Stack
    },    
    template: `    
    <Stack size="4">
        <div>hello</div>
        <Stack size="4">
            <div>hello</div>
            <div>hello</div>
        </Stack>
    </Stack>`
}
createApp(App).mount('#app')
</script>


效果:

经验:什么时候使用render

  • 当你意识到你想表达的逻辑使用JavaScript更容易表达,而不是模板语法
  • 当你创作可重用的功能组件时更常见,要跨多个应用程序共享或者组织内部共享
  • 你主要在编写特性组件,模板通常是有效的方式
  • 模板的好处是更简单,优化通过编译器优化,尤其当你有很多标记的时候
  • 它更容易让设计师接管组件并用CSS设置样式

创造一个mount函数


一些假设让例子更简单:

  • 一切都是一个元素
  • 调用参数总是一样的顺序(tag, props, children),所以下面有如果你没有任何的属性,你需要在那里传入null参数
<div id="app">
    
</div>
<script>
function h(tag, props, children){
    return {
        tag,
        props,
        children
    }
}
// mount会接收我们所说的vnode,contianer是DOM元素
function mount(vnode, container){
    // 中间的vnode.el是为了后续实现patchh
    const el = vnode.el= document.createElement(vnode.tag)  // 这给了我们实际的节点对应于虚拟节点
    // props: 如果有,我们需要迭代这些属性把它们分别放在元素上作为DOM的property或attribute
    if(vnode.props){
        // 这里为了简单,就假设一切都是attribute
        for (const key in vnode.props){
            const value= vnode.props[key]
            el.setAttribute(key, value)
        }
    }
    
    // children: 假设这个参数是一个虚拟节点数组或者是一个字符串
    if(vnode.children){
        if(typeof vnode.children === 'string'){
            el.textContent = vnode.children
        }else{
            vnode.children.forEach(child => {
                mount(child, el)
            })
        }
    }
    // 把它插入容器
    container.appendChild(el)
}
    
const vdom = h('div', {class: 'red'},[
    h('span', null, ['hello'])
])
mount(vdom, document.getElementById())
// n1是旧的虚拟DOM,之前的快照,n2是新的虚拟DOM,是我们现在想要展示在界面的部分
// patch需要找出最小数量它需要执行的DOM操作
function patch(n1, n2){
    ...    
}
const vdom2 = h('div', {class: 'green'},[
    h('span', null, ['changed'])
])
patch(vdom, vdom2)
</script>


我们渲染了原始组件,把它变成了虚拟DOM,当一个响应式属性被更新的时候,触发了重新渲染,重新生成了另一个表示形式的虚拟DOM,然后新旧比较。

创建patch函数


<div id="app">
    
</div>
<script>
function h(tag, props, children){
    return {
        tag,
        props,
        children
    }
}
function mount(vnode, container){
    const el = vnode.el= document.createElement(vnode.tag)
    if(vnode.props){
        for (const key in vnode.props){
            const value= vnode.props[key]
            el.setAttribute(key, value)
        }
    }
    if(vnode.children){
        if(typeof vnode.children === 'string'){
            el.textContent = vnode.children
        }else{
            vnode.children.forEach(child => {
                mount(child, el)
            })
        }
    }
    container.appendChild(el)
}
    
const vdom = h('div', {class: 'red'},[
    h('span', null, ['hello'])
])
mount(vdom, document.getElementById())
// n1是旧的虚拟DOM,之前的快照,n2是新的虚拟DOM,是我们现在想要展示在界面的部分
// patch需要找出最小数量它需要执行的DOM操作
function patch(n1, n2){
    // 这里仅讨论相同类型需要做的工作
    if(n1.tag === n2.tag){
        // 中间这部是为了在以后的更新中成为未来的快照
        const el = n2.el = n1.el
        // props 
        // 这里不讨论n1,n2的props是否为空的四种分支情况
        const oldProps = n1.props || {}
        const newProps = n2.props || {}
        for(const key in newProps){
            const oldValue = oldProps[key]
            const newValue = newProps[key]
            // 只有在实际变化后才会调用,以最小化实际DOM API的调用
            if(newValue !== oldValue){
                // 旧的没有,set会添加,旧的有key,set会替换
                el.setAttribute(key, newValue)
            }
        }
        // 接下来讨论key不在newProps中的时候
        for (const key in oldProps){
            if(!(key in oldProps)){
                el.removeAttribute(key)
            }
        }
        
        // children
        const oldChildren= n1.children
        const newChildren = n2.children
        if(typeof newChildren === 'string'){
            if(typeof oldChildren === 'string'){
                if(newChildren !== oldChildren){
                    el.textContent = newChildren
                }
            }else{
                // 使用文本直接覆盖现有的DOM节点并丢弃它们
                el.textContent = newChildren
            }
        }else{  // newC是arr的情况
            if(typeof oldChildren === 'string'){
                el.innreHTML = ''  // 清理,然后这个元素变为空元素
                // 加入
                newChildren,forEach(child => {
                    mount(child, el)
                })
            }else{  // 都是数组的情况
                const commonLength = Math.min(oldChildren.length, newChildren.length)
                for (let i = 0; i < commonLength; i++){
                    patch(oldChildren[i], newChildren[i])
                }
                if(newChildren.length > oldChildren.length){
                    mount(child, el)
                }else if(newwChildren.length < oldChildren.length){
                    oldChildren.slice(newChildren.length).forEach(child => {
                        el.removeChild(child.)
                    })
                }
            }
        }
    }else{
        // repalce
    }
}
const vdom2 = h('div', {class: 'green'},[
    h('span', null, ['changed'])
])
patch(vdom, vdom2)
</script>


props:

可以看到,patch函数做了相当大的工作,遍历了两个对象,但是有了编译器,给了我们很多的提示,完全跳过这一部分是可能的;

children:

Vue内部比较数组的一种模式

  • 键模式:当你使用v-for并提供一个key,key作为节点位置的提示



目录
相关文章
|
2月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
520 139
|
2月前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
235 1
|
7月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
994 5
|
3月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
399 11
|
2月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
270 0
|
4月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
451 1
|
4月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
265 0
|
5月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
137 0
|
7月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
615 17
|
8月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点

热门文章

最新文章