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 的渲染行为。

相关文章
|
3月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
538 0
|
6月前
|
存储 数据采集 供应链
属性描述符初探——Vue实现数据劫持的基础
属性描述符还有很多内容可以挖掘,比如defineProperty与Proxy的区别,比如vue2与vue3实现数据劫持的方式有什么不同,实现效果有哪些差异等,这篇博文只是入门,以后有时间再深挖。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
10月前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
11月前
|
监控 JavaScript 开发者
在 Vue 中,子组件为何不可以修改父组件传递的 Prop,如果修改了,Vue 是如何监控到属性的修改并给出警告的
在 Vue 中,子组件不能直接修改父组件传递的 Prop,以确保数据流的单向性和可预测性。如果子组件尝试修改 Prop,Vue 会通过响应式系统检测到这一变化,并在控制台发出警告,提示开发者避免这种操作。
|
11月前
|
JavaScript 前端开发 开发者
VUE学习一:初识VUE、指令、动态绑定、计算属性
这篇文章主要介绍了Vue.js的基础知识,包括初识Vue、指令如v-for、v-on的使用、动态属性绑定(v-bind)、计算属性等概念与实践示例。
124 1
|
11月前
|
缓存 JavaScript Serverless
vue中computed计算属性、watch侦听器、methods方法的区别以及用法
vue中computed计算属性、watch侦听器、methods方法的区别以及用法
608 0
|
11月前
|
缓存 JavaScript 前端开发
深入理解Vue.js中的计算属性与侦听属性
【10月更文挑战第5天】深入理解Vue.js中的计算属性与侦听属性
170 0
|
9天前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
108 2
|
3月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
4月前
|
JavaScript 数据可视化 前端开发
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
540 77