• vue3源码分析——实现element属性更新,child更新


    引言

    <<往期回顾>>

    1. vue3源码分析——实现组件通信provide,inject
    2. vue3源码分析——实现createRenderer,增加runtime-test

    本期来实现, vue3更新流程,更新元素的props,以及更新元素的child,所有的源码请查看

    正文

    上期文章增加了runtime-test的测试子包,接下来的所有代码都会基于该库来进行测试,vue3是怎么做到element的更新呢,更新的流程是咋样的呢?请看下面流程图

    image.png

    分析

    在上面流程图中,如果在setup中有一个对象obj,并且赋值为 ref({a:1}),然后通过某种方式重新赋值为2,就会触发更新流程;

    1. set操作中,都会进行trigger;
    2. trigger 后则是执行对于的run方法;
    3. 最后是这个run是通过effect来进行收集

    attention!!!🎉🎉🎉

    effect 来收集的run函数是在哪里收集,收集的是啥呢?

    effect收集依赖肯定是在mountElement里面,但是具体在哪里呢?在mountElement中,里面有三个函数

    • createComponentInstance:创建实例
    • setupComponent: 设置组件的状态,设置render函数
    • setupRenderEffect: 对组件render函数进行依赖收集

    看到上面三个函数,想必大家都知道是在哪个函数进行effect了吧!😊😊😊

    编码流程

    // 改造setupRenderEffect函数之前,需要在实例上加点东西,判断是否完成挂载,如果完成挂载则是更新操作,还有则需要拿到当前的组件的children tree
    
    export function createComponentInstance(vnode, parent) {
      const instance = {
       ...其他属性
        // 是否挂载
        isMounted: false,
        // 当前的组件树
        subtree: {}
      }
    }
    
     function setupRenderEffect(instance: any, vnode: any, container: any) {
     //  添加effect函数
        effect(() => {
          if (!instance.isMounted) {
            // 获取到vnode的子组件,传入proxy进去
            const { proxy } = instance
            const subtree = instance.render.call(proxy)
            instance.subtree = subtree
    
            // 遍历children
            patch(null, subtree, container, instance)
    
            // 赋值vnode.el,上面执行render的时候,vnode.el是null
            vnode.el = subtree.el
    
            // 渲染完成
            instance.isMounted = true
          } else {
            // 更新操作
            // 获取到vnode的子组件,传入proxy进去
            const { proxy } = instance
            const preSubtree = instance.subtree
            const nextSubtree = instance.render.call(proxy)
            // 遍历children
            patch(preSubtree, nextSubtree, container, instance)
            instance.subtree = nextSubtree
          }
        })
      }
    
    
    
    • 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

    上面就是关键的更新元素的步骤,接下来从TDD的开发模式,来实现element属性的更新和element元素的更新

    属性更新

    属性更新,毫无疑问的是,元素中的属性进行更新,新增,修改和删除等!

    测试用例

    test('test update props', () => {
        const app = createApp({
          name: 'App',
          setup() {
            const props = ref({
              foo: 'foo',
              bar: 'bar',
              baz: 'baz'
            })
    
            const changeFoo = () => {
              props.value.foo = 'foo1'
            }
    
            const changeBarToUndefined = () => {
              props.value.bar = undefined
            }
    
            const deleteBaz = () => {
              props.value = {
                foo: 'foo',
                bar: 'bar'
              }
            }
            return {
              props,
              deleteBaz,
              changeFoo,
              changeBarToUndefined,
            }
          },
          render() {
            return h('div', { class: 'container', ...this.props }, [h('button', { onClick: this.changeFoo, id: 'changeFoo' }, 'changeFoo'), h('button', { onClick: this.changeBarToUndefined, id: 'changeBarToUndefined' }, 'changeBarToUndefined'), h('button', { onClick: this.deleteBaz, id: 'deleteBaz' }, 'deleteBaz')])
          }
        })
    
        const appDoc = document.querySelector('#app')
        app.mount(appDoc)
        // 默认挂载
        expect(appDoc?.innerHTML).toBe('<div class="container" foo="foo" bar="bar" baz="baz">省略button</div>')
    
        // 删除属性
        const deleteBtn = appDoc?.querySelector('#deleteBaz') as HTMLElement;
        deleteBtn?.click();
        expect(appDoc?.innerHTML).toBe('<div class="container" foo="foo" bar="bar">省略button</div>')
    
        // 更新属性
        const changeFooBtn = appDoc?.querySelector('#changeFoo') as HTMLElement;
        changeFooBtn?.click();
        expect(appDoc?.innerHTML).toBe('<div class="container" foo="foo1" bar="bar">省略button</div>')
    
        // 属性置undefined
        const changeBarToUndefinedBtn = appDoc?.querySelector('#changeBarToUndefined') as HTMLElement;
        changeBarToUndefinedBtn?.click();
        expect(appDoc?.innerHTML).toBe('<div class="container" foo="foo1">省略button</div>')
      })
    
    
    • 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

    分析

    通过上面的需求,分析以下内容:

    • 删除属性

    image.png

    • 更新属性

    image.png

    • 将属性设置为null,undefined

    image.png

    问题解决:

    1. 在processElement中,存入老节点则需要进行更新操作
    2. 更新分为三种情况

    编码

     function processElement(n1, n2, container: any, parentComponent) {
        // 判断是挂载还是更新
        if (n1) {
          // 拿到新旧属性
          const oldProps = n1.props
          const newProps = n2.props
          // 可能新的点击没有el
          const el = (n2.el = n1.el)
          // 更新属性
          patchProps(el, oldProps, newProps)
    
        } else {
          // 挂载
          mountElement(n2, container, parentComponent)
        }
      }
      
      // 更新属性
     function patchProps(el, oldProps, newProps) {
     // 属性相同不进行更新
        if (oldProps === newProps) {
          return
        }
        // 遍历新的属性
        for (let key in newProps) {
        // 如果存在与旧属性中,说明属性发生变化,需要进行修改操作
          if (key in oldProps) {
            // 需要进行更新操作
            hostPatchProps(el, key, oldProps[key], newProps[key])
          }
        }
    
        // 新属性里面没有旧属性,则删除
        for (let key in oldProps) {
          if (key in newProps) {
            continue
          } else {
            hostPatchProps(el, key, oldProps[key], null)
          }
        }
      }
      
      // 对比新老节点
      function patchProps(el, key, oldValue, newValue) {
        // 新值没有,则移除
        if (newValue === null || newValue === undefined) {
          el.removeAttribute(key)
        } else {
        // 重新赋值
          el.setAttribute(key, newValue)
        }
    }
    
    
    • 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

    完成上面的编码,对应的测试用例也是可以通过的

    更新children

    children的更新里面包含diff算法哦!

    在设计h()函数中,有三个属性,第一个是type,第二个是属性,第三个则是children,children的类型有两种,一种是数组,另一种则是文本. 那么针对这两种情况,都需要分情况讨论,则会存在4种情况:

    • array —> text
    • text —> array
    • text —> text
    • array —> array: 这里需要使用diff算法

    由于测试用例比较占用文本,本个篇幅则省略测试用例,有需要的同学请查看源码获取

    更新array—> text

    老的节点是array,新节点是text,是不是需要把老的先删除,然后在给当前节点进行赋值哇!

    // 在processElement 种更新属性下面,加入一个新的方法,更新children
     function patchChildren(oldVNodes, newVNodes, container, parentComponent) {
        // 总共有4种情况来更新children
        // 1. children从array变成text
        const oldChildren = oldVNodes.children
        const newChildren = newVNodes.children
        const oldShapeflag = oldVNodes.shapeflag
        const newShapeflag = newVNodes.shapeflag
        if(Array.isArray(oldChildren) && typeof newChildren === string){
         // 删除老节点
         oldChildren.forEach(child=> {
           const parent = child.parentNode;
           if(parent){
              parent.removeChild(child)
           }
         })
         // 添加文本节点
         container.textContent = newChildren
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    更新 text —> array

    更新这个节点则是先把老的节点给删除,然后在挂载新的节点

    // 接着上面的判断
    else if(typeof oldChildren === 'string' && Array.isArray(newChildren)){
     // 删除老节点
      container.textContent = ''
      // 挂载新的节点’
      newChildren.forEach(child => {
         patch(null, child, container, parentComponent)
      })
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    更新 text —> text

    更新文本节点则更简单,直接判断赋值即可

    // 接着上面的判断
    else if(typeof oldChildren === 'string' && typeof newChildren === 'string' && oldChildren !== newChildren){
     // 重新赋值
      container.textContent = newChildren
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面这么写代码是不是有点小重复哇,这是为了方便大家的理解,优化好的代码已经在github等你了哦

    更新 array —> array

    本文篇幅有限,diff算法就留给下篇文章吧

    总结

    本文主要实现了vue3 element的更新,更新主要是在mountElement种的setupRenderEffect中来收集整个render函数的依赖,当reder函数中的响应式数据发生变化,则调用当前的run函数来触发更新操作! 然后还实现了vue3中的属性的更新,属性主要有三种情况: 两者都存在,执行修改;老的存在,新的不存在,执行删除;老的被设置成null或者undefined也需要执行删除。,最后还实现了vue中更新children,主要是针对 text_children和array_child的两两相互更新,最后还差一个都是数组的没有实现,加油!👍👍👍

  • 相关阅读:
    Python tkinter -- 第11章滚动条
    2024华为OD机试真题-伐木工-(C++/Python)-C卷D卷-200分
    Springboot整合JPA多数据源(Oracle+Mysql)
    XTTS系列之三:中转空间的选择和优化
    3.无重复字符的最长子串
    【测开求职】面试题:HR面相关的开放性问题
    Git、Github、Gitee、GitLab学习笔记
    Sample Average Approximation,SAA
    简明docker安装
    Python性能测试框架:Locust实战教程
  • 原文地址:https://blog.csdn.net/qq_41499782/article/details/125505140