vue3 源码学习,实现一个 mini-vue(九):构建 renderer 渲染器之 ELEMENT 节点的各种属性挂载

简介: vue3 源码学习,实现一个 mini-vue(九):构建 renderer 渲染器之 ELEMENT 节点的各种属性挂载

highlight: vs2015

theme: juejin

前言

原文来自 我的个人博客

在前几章中,我们实现了 ELEMENT 节点的挂载、更新以及删除等操作。

但是我们的代码现在还只能挂载 class 属性,而不能挂载其他属性。本章我们就来实现一下其他属性的挂载( style 属性,事件 属性)

1. 源码阅读:vue3 是如何挂载其他属性的

我们从下面的测试实例开始阅读源码:

<script>
  const { h, render } = Vue

  const vnode = h('textarea', {
    class: 'test-class',
    value: 'textarea value',
    type: 'text'
  })
  // 挂载
  render(vnode, document.querySelector('#app'))
</script>

在这个测试实例中,我们为 textarea 挂载了三个属性 classvaluetype,根据之前的源码阅读我们知道,属性的挂载是在 packages/runtime-dom/src/patchProp.ts 中的 patchProp 方法处进行的。

所以我们可以直接在这里进行 debugger,因为我们设置了三个属性,所以会 执行三次,我们一个一个来看:

  1. 第一次进入 patchProp

image.png

  1. 可以看到,代码首先会执行 patchClass,而在 patchClass 中最终会执行 el.className = value。至此 class 设置完成。
  2. 第二次进入 patchProp

image.png

  1. 可以看到代码前三个 if 都会跳过,而在第四个 if 时,会执行 shouldSetAsProp(el, key, nextValue, isSVG),其实这个方法会返回 false,最终还是执行 else 中的代码,在 else 中最终会执行 patchAttr 方法:

image.png

  1. patchAttr 最终执行 el.setAttribute(key, isBoolean ? '' : value) 设置 type
  2. 至此 type 设置完成
  3. 第三次进入 patchProp

image.png

  1. 可以看到第三次进入最后执行的是 patchDOMProp,这个方法最后是通过 执行 el[key] = value 设置 value,完成 value 属性的设置的
  2. 至此 value 设置完成
  3. 至此三个属性全部设置完成。

总结:

由以上代码可知:

  1. 针对于三个属性,vue 通过了 三种不同的方式 来进行了设置:
  2. class 属性:通过 el.className 设定
  3. textareatype 属性:通过 el.setAttribute 设定
  4. textareavalue 属性:通过 el[key] = value 设定

至于 vue 为什么要通过三种不同的形式挂载属性,主要有以下两点原因:

  1. 首先 HTML AttributesDOM Properties 想要成功的进行各种属性的设置,就需要 针对不同属性通过不同方式 完成,例如:
// 修改 class
el.setAttribute('class', 'm-class') // 成功
el['class'] = 'm-class' // 失败
el.className = 'm-class' // 成功

上面同样是修改 class,通过 HTML Attributes 的方式使用 setAttribute 就可以成功,通过 el['class'] 就会失败,因为在 DOM Properties 中,修改 class 要通过 el['className']`

  1. 还有出于性能的考虑,比如 classNamesetAttribute('class', '')className 的性能会更高

2. 代码实现:区分处理 ELEMENT 节点的各种属性挂载

  1. packages/runtime-dom/src/patchProp.ts 中,增加新的判断条件:
export const patchProp = (el, key, prevValue, nextValue) => {
  ...
  else if (shouldSetAsProp(el, key)) {
    // 通过 DOM Properties 指定
    patchDOMProp(el, key, nextValue)
  } else {
    // 其他属性
    patchAttr(el, key, nextValue)
  }
}
  1. packages/runtime-dom/src/patchProp.ts 中,创建 shouldSetAsProp 方法:
/**
 * 判断指定元素的指定属性是否可以通过 DOM Properties 指定
 */
function shouldSetAsProp(el: Element, key: string) {
  // #1787, #2840 表单元素的表单属性是只读的,必须设置为属性 attribute
  if (key === 'form') {
    return false
  }

  // #1526 <input list> 必须设置为属性 attribute
  if (key === 'list' && el.tagName === 'INPUT') {
    return false
  }

  // #2766 <textarea type> 必须设置为属性 attribute
  if (key === 'type' && el.tagName === 'TEXTAREA') {
    return false
  }

  return key in el
}
  1. packages/runtime-dom/src/modules/props.ts 中,增加 patchDOMProp 方法:
/**
 * 通过 DOM Properties 指定属性
 */
export function patchDOMProp(el: any, key: string, value: any) {
  try {
    el[key] = value
  } catch (e: any) {}
}
  1. packages/runtime-dom/src/modules/attrs.ts 中,增加 patchAttr 方法:
/**
 * 通过 setAttribute 设置属性
 */
export function patchAttr(el: Element, key: string, value: any) {
  if (value == null) {
    el.removeAttribute(key)
  } else {
    el.setAttribute(key, value)
  }
}

至此,代码完成。

创建测试实例 packages/vue/examples/runtime/render-element-props.html

<script>
  const { h, render } = Vue

  const vnode = h('textarea', {
    class: 'test-class',
    value: 'textarea value',
    type: 'text'
  })
  // 挂载
  render(vnode, document.querySelector('#app'))
</script>

测试渲染成功。

3. 源码阅读:style 属性的挂载和更新

创建测试实例阅读源码:

<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      style: {
        color: 'red'
      }
    },
    '你好,世界'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  setTimeout(() => {
    const vnode2 = h(
      'div',
      {
        style: {
          fontSize: '32px'
        }
      },
      '你好,世界'
    )
    // 挂载
    render(vnode2, document.querySelector('#app'))
  }, 2000)
</script>

我们继续在 patchProp 方法中,跟踪源码实现:

  1. 第一次进入 patchProp,执行 挂载 操作:

image.png

  1. 可以看到在 patchProp 方法中会进入 patchStyle 方法,而 patchStyle 经过判断会进入 setStyle ,我们进入 setStyle 方法:

image.png

  1. setStyle 中,最后执行 style[prefixed as any] = val ,直接为 style 对象进行赋值操作,至此 style 属性 挂载完成
  2. 接下来延迟两秒之后就开始 style更新操作
  3. 忽略掉相同的挂载逻辑,代码执行到 patchStyle 方法下:

image.png

  1. 可以看到此时 会执行 setStyle(style,key,''),再次进入 setStyle,此时 val'',最后会执行 style['color'] = '',完成 清理旧样式 操作。
  2. 至此 更新 操作完成

总结:

由以上代码可知:

  1. 整个 style 赋值的逻辑还是比较简单的
  2. 不考虑边缘情况 的前提下,vue 只是对 style 进行了 缓存赋值 两个操作
  3. 缓存是通过 prefixCache = {} 进行
  4. 赋值则是直接通过 style[xxx] = val 进行

4. 代码实现:style 属性的更新和挂载

  1. packages/runtime-dom/src/patchProp.ts 中,处理 style 情况:
/**
* 为 prop 进行打补丁操作
*/
export const patchProp = (el, key, prevValue, nextValue) => {
  ......
  else if (key === 'style') {
    // style
    patchStyle(el, prevValue, nextValue)
  } 
  ......
}
  1. packages/runtime-dom/src/modules/style.ts 中,新建 patchStyle 方法:
import { isString } from '@vue/shared'

/**
 * 为 style 属性进行打补丁
 */
export function patchStyle(el: Element, prev, next) {
  // 获取 style 对象
  const style = (el as HTMLElement).style
  // 判断新的样式是否为纯字符串
  const isCssString = isString(next)
  if (next && !isCssString) {
    // 赋值新样式
    for (const key in next) {
      setStyle(style, key, next[key])
    }
    // 清理旧样式
    if (prev && !isString(prev)) {
      for (const key in prev) {
        if (next[key] == null) {
          setStyle(style, key, '')
        }
      }
    }
  }
}

/**
 * 赋值样式
 */
function setStyle(
  style: CSSStyleDeclaration,
  name: string,
  val: string | string[]
) {
  style[name] = val
}

代码完成

创建测试实例 packages/vue/examples/runtime/render-element-style.html

<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      style: {
        color: 'red'
      }
    },
    '你好,世界'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  setTimeout(() => {
    const vnode2 = h(
      'div',
      {
        style: {
          fontSize: '32px'
        }
      },
      '你好,世界'
    )
    // 挂载
    render(vnode2, document.querySelector('#app'))
  }, 2000)
</script>

效果:

页面刷新,两秒钟后样式更新。

3.gif

5. 源码阅读:事件的挂载和更新

我们通过如下测试用例来阅读 vue 源码:

<script>
  const { h, render } = Vue

  const vnode = h(
    'button',
    {
      onClick() {
        alert('点击')
      }
    },
    '点击'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  setTimeout(() => {
    const vnode2 = h(
      'button',
      {
        onDblclick() {
          alert('双击')
        }
      },
      '双击'
    )
    // 挂载
    render(vnode2, document.querySelector('#app'))
  }, 2000)
</script>

上面代码很简单就是页面刚渲染时挂载 点击事件,两秒钟之后更新为 双击事件

  1. 我们依然来到 patchProps 方法:

image.png

  1. 此时会进入 patchEvent 方法中:

image.png

  1. patchEvent 中,

    1. 首先创建了一个 invokers 对象并绑定到了 el._wei 上,这是个用于缓存的对象,我们可以不用管,他目前只是一个空对象。
    2. 然后又执行 existingInvoker = invokers[rawName]rawName 此时为 'onClick' 这就是想从缓存中取出之前已经缓存过得 onClick 事件函数,我们目前没缓存过,所以是 undefined,所以程序会执行下面的 else
    3. 可以看到最后会执行 addEventListener 的方法,这个方法就是最终挂载事件的方法。但是我们会有个疑问这个 invoker 是什么东西呢?我们代码进入 82createInvoker:

image.png

  1. 由上图我们知道 invoker 就是一个函数,它的 value 属性是当前 onClick 函数
  2. 创建完 invoker 对象后,会执行 invokers[rawName],也就是缓存下来。
  3. 至此,支持事件 挂载 完成
  4. 等待两秒之后,执行 更新 操作:
  5. 第二次 进入 patchEvent,会再次挂载 onDblclick 事件与 第一次 相同,此时的 invokers 值为:

image.png

  1. 但是,到这还没完,我们知道 属性的挂载 其实是在 packages/runtime-core/src/renderer.ts 中的 patchProps 中进行的,观察内部方法,我们可以发现 内部进行了两次 for 循环

image.png

  1. 所以此时还会执行下面的 for 循环来卸载之前的 onClick 事件,我们 第三次 进入到 patchEvent 方法中:

image.png

  1. 这次因为 nextValuenull 且 存在 existingInvoker,所以会执行最后的 removeEventListener 即卸载 onClick 事件,最后执行 invokers[rawName] = undefined,删除 onClick 事件的缓存。
  2. 至此 卸载旧事件 完成

总结:

  1. 我们一共三次进入 patchEvent 方法

    1. 第一次进入为 挂载 onClick 行为
    2. 第二次进入为 挂载 onDblclick 行为
    3. 第三次进入为 卸载 onClick 行为
  2. 挂载事件,通过 el.addEventListener 完成
  3. 卸载事件,通过 el.removeEventListener 完成
  4. 除此之外,还有一个 _veiinvokers 对象 和 invoker 函数,我们说两个东西需要重点关注,那么这两个对象有什么意义呢?

深入事件更新

patchEvent 方法中有一行代码是我们没有讲到的,那就是:

// patch
existingInvoker.value = nextValue

这行代码是用来更新事件的,vue 通过这种方式而不是调用 addEventListenerremoveEventListener 解决了频繁的删除、新增事件时非常消耗性能的问题。

6. 代码实现:事件的挂载和更新

  1. packages/runtime-dom/src/patchProp.ts 中,增加 patchEvent 事件处理
} else if (isOn(key)) {
  // 事件
  patchEvent(el, key, prevValue, nextValue)
}
  1. packages/runtime-dom/src/modules/events.ts 中,增加 patchEventparseNamecreateInvoker 方法:
/**
 * 为 event 事件进行打补丁
 */
export function patchEvent(
  el: Element & { _vei?: object },
  rawName: string,
  prevValue,
  nextValue
) {
  // vei = vue event invokers
  const invokers = el._vei || (el._vei = {})
  // 是否存在缓存事件
  const existingInvoker = invokers[rawName]
  // 如果当前事件存在缓存,并且存在新的事件行为,则判定为更新操作。直接更新 invoker 的 value 即可
  if (nextValue && existingInvoker) {
    // patch
    existingInvoker.value = nextValue
  } else {
    // 获取用于 addEventListener || removeEventListener 的事件名
    const name = parseName(rawName)
    if (nextValue) {
      // add
      const invoker = (invokers[rawName] = createInvoker(nextValue))
      el.addEventListener(name, invoker)
    } else if (existingInvoker) {
      // remove
      el.removeEventListener(name, existingInvoker)
      // 删除缓存
      invokers[rawName] = undefined
    }
  }
}

/**
 * 直接返回剔除 on,其余转化为小写的事件名即可
 */
function parseName(name: string) {
  return name.slice(2).toLowerCase()
}

/**
 * 生成 invoker 函数
 */
function createInvoker(initialValue) {
  const invoker = (e: Event) => {
    invoker.value && invoker.value()
  }
  // value 为真实的事件行为
  invoker.value = initialValue
  return invoker
}
  1. 支持事件的打补丁处理完成。

可以创建如下测试实例 packages/vue/examples/runtime/render-element-event.html

<script>
  const { h, render } = Vue

  const vnode = h(
    'button',
    {
      onClick() {
        alert('点击')
      }
    },
    '点击'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  setTimeout(() => {
    const vnode2 = h(
      'button',
      {
        onDblclick() {
          alert('双击')
        }
      },
      '双击'
    )
    // 挂载
    render(vnode2, document.querySelector('#app'))
  }, 2000)
</script>

效果:

4.gif

7. 渲染器模块的局部总结

目前我们已经完成了针对于 ELEMENT 的:

  1. 挂载
  2. 更新
  3. 卸载
  4. patch props 打补丁

    1. class
    2. style
    3. event
    4. attr

等行为的处理。

针对于 挂载、更新、卸载 而言,我们主要使用了 packages/runtime-dom/src/nodeOps.ts 中的浏览器兼容方法进行的实现,比如:

  1. doc.createElement
  2. parent.removeChild

等等。

而对于 patch props 的操作而言,因为 HTML AttributesDOM Properties 不同的问题,所以我们需要针对不同的 props 进行分开的处理。

而最后的 event,本身并不复杂,但是 vei 的更新思路也是非常值得学习的一种事件更新方案。

至此,针对于 ELEMENT 的处理终于完成啦~

接下来是 TextComment 以及 Component 的渲染行为。

相关文章
|
1月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
131 64
|
5天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
25 3
|
1月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
35 8
|
1月前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
32 1
|
1月前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
39 1
|
10天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
1月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
44 1
vue学习第一章
|
1月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
31 1
|
1月前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
38 1
vue学习第四章
|
1月前
|
JavaScript 前端开发 算法
vue学习第7章(循环)
欢迎来到瑞雨溪的博客,一名热爱JavaScript和Vue的大一学生。本文介绍了Vue中的v-for指令,包括遍历数组和对象、使用key以及数组的响应式方法等内容,并附有综合练习实例。关注我,将持续更新更多优质文章!🎉🎉🎉
28 1
vue学习第7章(循环)

热门文章

最新文章