runtime-dom 是为了解决平台差异的 (浏览器的)
核心是提供 domAPI方法,操作节点、属性的更新
节点操作:增删改查
属性操作:添加、删除、更新、样式、类、事件、其他属性

nodeOps 节点操作

  1. export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  2. /**
  3. * 元素插入
  4. * @param child 要插入的元素
  5. * @param parent 插到哪个里面去
  6. * @param anchor 当前参照物 如果为空,则相当于 appendChild
  7. */
  8. insert: (child, parent, anchor) => {
  9. parent.insertBefore(child, anchor || null)
  10. },
  11. /**
  12. * 元素删除
  13. * 通过儿子找到父亲删除
  14. * @param child
  15. */
  16. remove: child => {
  17. const parent = child.parentNode
  18. if (parent) {
  19. parent.removeChild(child)
  20. }
  21. },
  22. /**
  23. * 元素增加
  24. * 创建节点,不同平台创建元素的方式不同
  25. * @param tag
  26. * @param isSVG
  27. * @param is
  28. * @param props
  29. * @returns
  30. */
  31. createElement: (tag, isSVG, is, props): Element => {
  32. const el = isSVG
  33. ? doc.createElementNS(svgNS, tag)
  34. : doc.createElement(tag, is ? { is } : undefined)
  35. if (tag === 'select' && props && props.multiple != null) {
  36. ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
  37. }
  38. return el
  39. },
  40. /**
  41. * 元素查找
  42. * @param selector
  43. * @returns
  44. */
  45. querySelector: selector => doc.querySelector(selector),
  46. /**
  47. * 给元素设置文本
  48. * @param el
  49. * @param text
  50. */
  51. setElementText: (el, text) => {
  52. el.textContent = text
  53. },
  54. /**
  55. * 文本操作
  56. * 创建文本
  57. * @param text
  58. * @returns
  59. */
  60. createText: text => doc.createTextNode(text),
  61. createComment: text => doc.createComment(text),
  62. /**
  63. * 给节点设置文本
  64. * @param node
  65. * @param text
  66. */
  67. setText: (node, text) => {
  68. node.nodeValue = text
  69. },
  70. /**
  71. * 获取父节点
  72. * @param node
  73. * @returns
  74. */
  75. parentNode: node => node.parentNode as Element | null,
  76. nextSibling: node => node.nextSibling,
  77. setScopeId(el, id) {
  78. el.setAttribute(id, '')
  79. },
  80. cloneNode(el) {
  81. const cloned = el.cloneNode(true)
  82. // #3072
  83. // - in `patchDOMProp`, we store the actual value in the `el._value` property.
  84. // - normally, elements using `:value` bindings will not be hoisted, but if
  85. // the bound value is a constant, e.g. `:value="true"` - they do get
  86. // hoisted.
  87. // - in production, hoisted nodes are cloned when subsequent inserts, but
  88. // cloneNode() does not copy the custom property we attached.
  89. // - This may need to account for other custom DOM properties we attach to
  90. // elements in addition to `_value` in the future.
  91. if (`_value` in el) {
  92. ;(cloned as any)._value = (el as any)._value
  93. }
  94. return cloned
  95. },
  96. // __UNSAFE__
  97. // Reason: insertAdjacentHTML.
  98. // Static content here can only come from compiled templates.
  99. // As long as the user only uses trusted templates, this is safe.
  100. insertStaticContent(content, parent, anchor, isSVG, cached) {
  101. if (cached) {
  102. let [cachedFirst, cachedLast] = cached
  103. let first, last
  104. while (true) {
  105. let node = cachedFirst.cloneNode(true)
  106. if (!first) first = node
  107. parent.insertBefore(node, anchor)
  108. if (cachedFirst === cachedLast) {
  109. last = node
  110. break
  111. }
  112. cachedFirst = cachedFirst.nextSibling!
  113. }
  114. return [first, last] as any
  115. }
  116. // <parent> before | first ... last | anchor </parent>
  117. const before = anchor ? anchor.previousSibling : parent.lastChild
  118. if (anchor) {
  119. let insertionPoint
  120. let usingTempInsertionPoint = false
  121. if (anchor instanceof Element) {
  122. insertionPoint = anchor
  123. } else {
  124. // insertAdjacentHTML only works for elements but the anchor is not an
  125. // element...
  126. usingTempInsertionPoint = true
  127. insertionPoint = isSVG
  128. ? doc.createElementNS(svgNS, 'g')
  129. : doc.createElement('div')
  130. parent.insertBefore(insertionPoint, anchor)
  131. }
  132. insertionPoint.insertAdjacentHTML('beforebegin', content)
  133. if (usingTempInsertionPoint) {
  134. parent.removeChild(insertionPoint)
  135. }
  136. } else {
  137. parent.insertAdjacentHTML('beforeend', content)
  138. }
  139. return [
  140. // first
  141. before ? before.nextSibling : parent.firstChild,
  142. // last
  143. anchor ? anchor.previousSibling : parent.lastChild
  144. ]
  145. }
  146. }

pathProps 属性操作

属性操作有个 对比的过程

  1. export const patchProp: DOMRendererOptions['patchProp'] = (
  2. el, // 元素
  3. key, // 属性
  4. prevValue, // 前一个值
  5. nextValue, // 新的值
  6. isSVG = false,
  7. prevChildren,
  8. parentComponent,
  9. parentSuspense,
  10. unmountChildren
  11. ) => {
  12. switch (key) {
  13. // special
  14. case 'class':
  15. patchClass(el, nextValue, isSVG) // 那最新的属性覆盖掉旧的
  16. break
  17. case 'style': // {style:{color: 'red'}} -> {style:{background: 'red'}} 删掉之前的
  18. patchStyle(el, prevValue, nextValue)
  19. break
  20. default:
  21. // 如果不是事件 才是属性
  22. if (isOn(key)) { // 如果是 以 on 开头的就是事件,onClick,onChange
  23. // ignore v-model listeners
  24. if (!isModelListener(key)) {
  25. patchEvent(el, key, prevValue, nextValue, parentComponent) // 添加、删除、修改
  26. }
  27. } else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
  28. patchDOMProp(
  29. el,
  30. key,
  31. nextValue,
  32. prevChildren,
  33. parentComponent,
  34. parentSuspense,
  35. unmountChildren
  36. )
  37. } else {
  38. // special case for <input v-model type="checkbox"> with
  39. // :true-value & :false-value
  40. // store value as dom properties since non-string values will be
  41. // stringified.
  42. if (key === 'true-value') {
  43. ;(el as any)._trueValue = nextValue
  44. } else if (key === 'false-value') {
  45. ;(el as any)._falseValue = nextValue
  46. }
  47. patchAttr(el, key, nextValue, isSVG, parentComponent)
  48. }
  49. break
  50. }
  51. }

这里面需要分好几种情况,有操作className、style、事件、其他属性等

对比class

  1. export function patchClass(el: Element, value: string | null, isSVG: boolean) {
  2. if (value == null) { // 说明 之前有 现在没有就设置为 空
  3. value = ''
  4. }
  5. if (isSVG) {
  6. el.setAttribute('class', value)
  7. } else {
  8. // directly setting className should be faster than setAttribute in theory
  9. // if this is an element during a transition, take the temporary transition
  10. // classes into account.
  11. const transitionClasses = (el as ElementWithTransition)._vtc
  12. if (transitionClasses) {
  13. value = (value
  14. ? [value, ...transitionClasses]
  15. : [...transitionClasses]
  16. ).join(' ')
  17. }
  18. // 否则就设置 className 的值
  19. el.className = value
  20. }
  21. }

之前有现在没有就将 class 设置为 空
否则就重置 class

对比style

  1. export function patchStyle(el: Element, prev: Style, next: Style) {
  2. const style = (el as HTMLElement).style // 获取样式
  3. if (!next) { // 如果新传入的没有样式 直接删掉
  4. el.removeAttribute('style')
  5. } else if (isString(next)) {
  6. if (prev !== next) {
  7. const current = style.display
  8. style.cssText = next
  9. // indicates that the `display` of the element is controlled by `v-show`,
  10. // so we always keep the current `display` value regardless of the `style` value,
  11. // thus handing over control to `v-show`.
  12. if ('_vod' in el) {
  13. style.display = current
  14. }
  15. }
  16. } else {
  17. // 新的有 需要赋值到style
  18. for (const key in next) {
  19. setStyle(style, key, next[key])
  20. }
  21. // 老的有 新的没有
  22. if (prev && !isString(prev)) { // {style:{color: 'red'}} -> {style:{background: 'red'}}
  23. for (const key in prev) {
  24. if (next[key] == null) { // 老的有 新的没有 需要删除
  25. setStyle(style, key, '')
  26. }
  27. }
  28. }
  29. }
  30. }

样式可以设置好几个,以对象的形式管理 {style:{color: ‘red’}} -> {style:{background: ‘red’}}
老的有新的没有就删除,新的有老的没有,就赋值到style。

对比事件:

  1. export function patchEvent(
  2. el: Element & { _vei?: Record<string, Invoker | undefined> },
  3. rawName: string,
  4. prevValue: EventValue | null,
  5. nextValue: EventValue | null,
  6. instance: ComponentInternalInstance | null = null
  7. ) {
  8. // vei = vue event invokers vue 事件调用 el._vei 对事件进行缓存
  9. const invokers = el._vei || (el._vei = {}) // 元素上所有的事件调用都绑定在 _vei 上
  10. const existingInvoker = invokers[rawName]
  11. // 看当前事件是否已经存在缓存中
  12. if (nextValue && existingInvoker) {
  13. // patch
  14. existingInvoker.value = nextValue
  15. } else {
  16. const [name, options] = parseName(rawName)
  17. if (nextValue) { // 以前没有绑定过 现在要绑定
  18. // add
  19. const invoker = (invokers[rawName] = createInvoker(nextValue, instance)) // 创建事件
  20. addEventListener(el, name, invoker, options) // 绑定事件
  21. } else if (existingInvoker) { // 以前绑定了 现在没有 需要移除事件
  22. // remove
  23. removeEventListener(el, name, existingInvoker, options)
  24. invokers[rawName] = undefined
  25. }
  26. }
  27. }
  1. // 创建一个事件
  2. function createInvoker(
  3. initialValue: EventValue,
  4. instance: ComponentInternalInstance | null
  5. ) {
  6. const invoker: Invoker = (e: Event) => {
  7. const timeStamp = e.timeStamp || _getNow()
  8. if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
  9. callWithAsyncErrorHandling(
  10. patchStopImmediatePropagation(e, invoker.value),
  11. instance,
  12. ErrorCodes.NATIVE_EVENT_HANDLER,
  13. [e]
  14. )
  15. }
  16. }
  17. invoker.value = initialValue // 为了随时能更改 value 属性
  18. invoker.attached = getNow()
  19. return invoker
  20. }

事件比较复杂,需要在 当前元素上对事件进行缓存,每次新增一个事件的时候看这个事件是否存在在缓存中,比如:click 事件,
如果存在就给他赋新的回调,
如果不存在,就说明之前没有绑定过现在需要创建一个新的事件然后绑定,
如果之前就绑定了,现在没有,就需要移除事件并删除缓存,