• Mini-Vue之渲染系统实现(render)及diff算法


    Vue源码之三大核心系统

    事实上,Vue源码包含三大核心:

    • Compiler模块:模板编译系统;
    • Runtime模块:也可以称之为Render(渲染)模块,真正的渲染模块;
    • Reactivity模块:响应式系统。

    在下载后的Vue源码的package文件中,可看到这些模块:

    在这里插入图片描述

    大概流程就是通过开发者编写的template模板,经过编译系统后生成VNode,然后再通过渲染系统来生成真实DOM,最后,通过响应式系统对数据进行监听,当数据发生改变时,页面会通过diff算法对比VNode的变化,然后以最大粒度复用代码,保证性能相对来说消耗最小。


    渲染系统实现

    该模块主要包含三个功能:

    • h函数,用于返回一个VNode对象;
    • mount函数,用于将VNode挂载到DOM上,
    • patch函数,用于对两个VNode进行对比,决定如何处理新的VNode

    标题已经说得很清楚了,只是一个迷你版的Vue,所以断然不会和真正的Vue源码一样写那么多Edge Case(边际判断),主要是从本篇文章中学到这个流程,然后做到看源代码时不至于一头雾水。

    h函数-生成VNode

    上一篇文章我已经介绍过,VNode本质上就是一个JavaScript对象。你可能会好奇为什么要经过VNode这一层,直接渲染不是更加方便快捷吗?首先,VNode可以对真实的元素节点进行抽象,因此很多直接操作DOM存在的限制,比如diff比对,clone操作在转换为一个JavaScript对象后,就变得更加简单了。此外,鉴于目前跨端开发的火爆,编写一套代码可以在多终端的需求越来越高涨,你肯定不想你写的代码只能在浏览器上运行吧?毕竟对大多数人来说,个人手机比PC的优先级要高太多了。

    话不多说,下面开始上代码。

    h函数主要的作用就是生成VNode,所以它的实现比较简单。

    const h = (tag, props, children) => {
      return {
        tag,
        props,
        children
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    也许你没有看过我上一篇文章,所以我在这里再解释一下,tag表示标签名;props表示标签上所加的各种属性,如css样式、方法绑定;最后的children则可能是文本,也可能是另一个对象。

    Mount函数-挂载VNode

    熟悉Vue开发的肯定知道mount函数是干嘛的,要是连这个都不知道,那也没必要来看源码了。

    mount函数的实现可以分为散步:

    1. 根据tag,创建HTML元素,并且存储到VNode的el中;
    2. 处理props,如果是以on开头,那么则是事件监听,普通属性则直接通过setAttribute添加;
    3. 处理子节点,如果是字符穿,则直接设置文本内容,如果是数组节点,那么遍历调用mount
    const mount = (vnode, container) => {
        // 1.创建出真实的元素,并且在vnode上保留el
        const el = vnode.el = document.createElement(vnode.tag)
    
        // 2.处理props
        if (vnode.props) {
            for (const key in vnode.props) {
                const value = vnode.props[key]
    
                // 如果监听的是事件
                if (key.startsWith('on')) {
                    el.addEventListener(key.slice(2).toLowerCase(), value)
                } else {  // 是其它属性
                    el.setAttribute(key, value)
                }
            }
        }
    
        // 3.处理children
        if (vnode.children) {
            if (typeof vnode.children === 'string') {
                el.textContent = vnode.children  // textContent是内置属性,给元素添加文本节点
            } else {
                vnode.children.forEach(item => {
                    mount(item, el)
                })
            }
        }
    
        // 4.将el挂载到container上
        container.appendChild(el)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    Patch函数-对比两个VNode

    • n1和n2是不同类型的节点
      • 找到n1的el父节点,删除n1节点
      • 挂载n2节点到n1的el的父节点上
    • 相同节点
      • 处理props
        • 将新节点的props全部挂载到el上;
        • 删除不需要的旧节点的属性
      • 处理children
        • 新节点是字符串,直接调用方法改变文本
        • 新节点不是字符串
          • 旧节点是一个字符串
            • 将textContent置空
            • 遍历新节点,挂载到el上
          • 旧节点也是数组类型
            • 取出数组的最小长度
            • 遍历所有节点,新旧节点进行patch操作
            • 如果新节点更长,则挂载
            • 如果旧节点更长,则卸载(删除)
    const patch = (n1, n2) => {
        // 如果 n1 和 n2 是不同类型的节点
        if (n1.tag !== n2.tag) {
            const n1ElParent = n1.el.parentElement // 拿到conatiner
            n1ElParent.removeChild(n1.el)
            mount(n2, n1ElParent)
        } else {
            // 1.取出element对象,并且在n2对象中保存,这里要明白,n2中是不包含el节点的,具体看上述h函数的实现
            const el = n2.el = n1.el
    
            // 2.处理props
            const oldProps = n1.props || {}
            const newProps = n2.props || {}
            // 2.1 遍历新节点的所有props属性,进行比较
            for (const key in newProps) {
                const oldValue = oldProps[key]
                const newValue = newProps[key]
                if (newValue !== oldValue) {
                    if (key.startsWith('on')) {
                        el.addEventListener(key.slice(2).toLowerCase(), newValue)
                    } else {  // 是其它属性
                        el.setAttribute(key, newValue)
                    }
                }
            }
            // 2.2 删除旧的props
            for (const key in oldProps) {
                // 看到这里你可能会疑惑,为什么要把这个判断提上来,其实这里如果不提出来的话,就会导致两次添加事件监听,导致事件一直不会停止,如果你不相信的话,可以把这个代码放下去,然后写一个点击+1的小demo,那么你就会理解了
                if (key.startsWith('on')) {
                    const value = oldProps[key]
                    el.removeEventListener(key.slice(2).toLowerCase(), value)
                }
                if (!(key in newProps)) {
                    el.removeAttribute(key)
                }
            }
    
            // 3.处理children
            const oldChildren = n1.children || []
            const newChidlren = n2.children || []
    
            if (typeof newChidlren === "string") { // 情况一: newChildren本身是一个string
                // 边界情况 (edge case)
                if (typeof oldChildren === "string") {
                    if (newChidlren !== oldChildren) {
                        el.textContent = newChidlren
                    }
                } else {
                    el.innerHTML = newChidlren;
                }
            } else { // 情况二: newChildren本身是一个数组
                if (typeof oldChildren === "string") {
                    el.innerHTML = "";
                    newChidlren.forEach(item => {
                        mount(item, el);
                    })
                } else {
                    // oldChildren: [v1, v2, v3, v8, v9]
                    // newChildren: [v1, v5, v6]
                    // 1.前面有相同节点的原生进行patch操作
                    const commonLength = Math.min(oldChildren.length, newChidlren.length);
                    for (let i = 0; i < commonLength; i++) {
                        patch(oldChildren[i], newChidlren[i]);
                    }
    
                    // 2.newChildren.length > oldChildren.length
                    if (newChidlren.length > oldChildren.length) {
                        newChidlren.slice(oldChildren.length).forEach(item => {
                            mount(item, el);
                        })
                    }
    
                    // 3.newChildren.length < oldChildren.length
                    if (newChidlren.length < oldChildren.length) {
                        oldChildren.slice(newChidlren.length).forEach(item => {
                            el.removeChild(item.el);
                        })
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    至此,Mini-Vue从编译后的生成的VNode到挂载节点、生成DOM并进行新旧VNode对比的算法部分就告一段了。

  • 相关阅读:
    docker-io, docker-ce, docker-ee 区别
    一文搞懂系列之SpringMVC开发步骤及项目框架注册
    magento2 跨域处理
    时间,空间复杂度讲解——夯实根基
    使用python实现http协议的方法
    继承和组合
    手把手带你学python自动化测试(一)——自动化测试环境搭建
    使用内存映射加快PyTorch数据集的读取
    element ui 的tree单选,控制setCheckedNodes传入data即可
    循环分支、字符串习题(水仙花数、字符串左旋)
  • 原文地址:https://blog.csdn.net/weixin_49172439/article/details/126394407