Vue3中虚拟dom原理介绍

本文涉及的产品
文本翻译,文本翻译 100万字符
图片翻译,图片翻译 100张
语种识别,语种识别 100万字符
简介: 虚拟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作为节点位置的提示



目录
相关文章
|
18天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
15天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
35 7
|
19天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
33 9
|
16天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
38 3
|
15天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
36 1
|
15天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
36 1
|
18天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
18天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
21天前
|
API
vue3知识点:reactive对比ref
vue3知识点:reactive对比ref
28 3
|
21天前
|
JavaScript API
vue3知识点:ref函数
vue3知识点:ref函数
30 2