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

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

引言


<<往期回顾>>


1.vue3源码分析——实现组件通信provide,inject

2.vue3源码分析——实现createRenderer,增加runtime-test


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


正文


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


052dc60c48ad2a3f3f528bdcf4019e40.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
      }
    })
  }


上面就是关键的更新元素的步骤,接下来从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>')
  })


分析


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


  • 删除属性


c15ced3ddbf5bde42f7cb993054e67f0.png


  • 更新属性


e041877b2e195d3f104c58dbba4f4b36.png


  • 将属性设置为null,undefined


375e122a9f2f44b5d36429eb621e9a37.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)
    }
}


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


更新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
    }
}


更新 text —> array


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


// 接着上面的判断
else if(typeof oldChildren === 'string' && Array.isArray(newChildren)){
 // 删除老节点
  container.textContent = ''
  // 挂载新的节点’
  newChildren.forEach(child => {
     patch(null, child, container, parentComponent)
  })
}


更新 text —> text


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


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


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


更新 array —> array


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


总结


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

相关文章
|
2月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
143 64
|
2月前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
115 60
|
10天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
38 3
|
2月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
39 8
|
2月前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
33 1
|
2月前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
42 1
|
2月前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
JavaScript
vue element学习
Vue element 先clonehttps://github.com/ElementUI/element-cooking-starter.git的例子在目录下运行 npm install 安装所需要的nodejs包再运行 npm run dev 可以看到 element的简单例子element...
1614 0
|
4天前
|
JavaScript
vue使用iconfont图标
vue使用iconfont图标
38 1
|
14天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。

热门文章

最新文章