runtime-dom 是为了解决平台差异的 (浏览器的)
核心是提供 domAPI方法,操作节点、属性的更新
节点操作:增删改查
属性操作:添加、删除、更新、样式、类、事件、其他属性
nodeOps 节点操作
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
/**
* 元素插入
* @param child 要插入的元素
* @param parent 插到哪个里面去
* @param anchor 当前参照物 如果为空,则相当于 appendChild
*/
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
/**
* 元素删除
* 通过儿子找到父亲删除
* @param child
*/
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
/**
* 元素增加
* 创建节点,不同平台创建元素的方式不同
* @param tag
* @param isSVG
* @param is
* @param props
* @returns
*/
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
/**
* 元素查找
* @param selector
* @returns
*/
querySelector: selector => doc.querySelector(selector),
/**
* 给元素设置文本
* @param el
* @param text
*/
setElementText: (el, text) => {
el.textContent = text
},
/**
* 文本操作
* 创建文本
* @param text
* @returns
*/
createText: text => doc.createTextNode(text),
createComment: text => doc.createComment(text),
/**
* 给节点设置文本
* @param node
* @param text
*/
setText: (node, text) => {
node.nodeValue = text
},
/**
* 获取父节点
* @param node
* @returns
*/
parentNode: node => node.parentNode as Element | null,
nextSibling: node => node.nextSibling,
setScopeId(el, id) {
el.setAttribute(id, '')
},
cloneNode(el) {
const cloned = el.cloneNode(true)
// #3072
// - in `patchDOMProp`, we store the actual value in the `el._value` property.
// - normally, elements using `:value` bindings will not be hoisted, but if
// the bound value is a constant, e.g. `:value="true"` - they do get
// hoisted.
// - in production, hoisted nodes are cloned when subsequent inserts, but
// cloneNode() does not copy the custom property we attached.
// - This may need to account for other custom DOM properties we attach to
// elements in addition to `_value` in the future.
if (`_value` in el) {
;(cloned as any)._value = (el as any)._value
}
return cloned
},
// __UNSAFE__
// Reason: insertAdjacentHTML.
// Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe.
insertStaticContent(content, parent, anchor, isSVG, cached) {
if (cached) {
let [cachedFirst, cachedLast] = cached
let first, last
while (true) {
let node = cachedFirst.cloneNode(true)
if (!first) first = node
parent.insertBefore(node, anchor)
if (cachedFirst === cachedLast) {
last = node
break
}
cachedFirst = cachedFirst.nextSibling!
}
return [first, last] as any
}
// <parent> before | first ... last | anchor </parent>
const before = anchor ? anchor.previousSibling : parent.lastChild
if (anchor) {
let insertionPoint
let usingTempInsertionPoint = false
if (anchor instanceof Element) {
insertionPoint = anchor
} else {
// insertAdjacentHTML only works for elements but the anchor is not an
// element...
usingTempInsertionPoint = true
insertionPoint = isSVG
? doc.createElementNS(svgNS, 'g')
: doc.createElement('div')
parent.insertBefore(insertionPoint, anchor)
}
insertionPoint.insertAdjacentHTML('beforebegin', content)
if (usingTempInsertionPoint) {
parent.removeChild(insertionPoint)
}
} else {
parent.insertAdjacentHTML('beforeend', content)
}
return [
// first
before ? before.nextSibling : parent.firstChild,
// last
anchor ? anchor.previousSibling : parent.lastChild
]
}
}
pathProps 属性操作
属性操作有个 对比的过程
export const patchProp: DOMRendererOptions['patchProp'] = (
el, // 元素
key, // 属性
prevValue, // 前一个值
nextValue, // 新的值
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
switch (key) {
// special
case 'class':
patchClass(el, nextValue, isSVG) // 那最新的属性覆盖掉旧的
break
case 'style': // {style:{color: 'red'}} -> {style:{background: 'red'}} 删掉之前的
patchStyle(el, prevValue, nextValue)
break
default:
// 如果不是事件 才是属性
if (isOn(key)) { // 如果是 以 on 开头的就是事件,onClick,onChange
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent) // 添加、删除、修改
}
} else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
)
} else {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value
// store value as dom properties since non-string values will be
// stringified.
if (key === 'true-value') {
;(el as any)._trueValue = nextValue
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
patchAttr(el, key, nextValue, isSVG, parentComponent)
}
break
}
}
这里面需要分好几种情况,有操作className、style、事件、其他属性等
对比class
export function patchClass(el: Element, value: string | null, isSVG: boolean) {
if (value == null) { // 说明 之前有 现在没有就设置为 空
value = ''
}
if (isSVG) {
el.setAttribute('class', value)
} else {
// directly setting className should be faster than setAttribute in theory
// if this is an element during a transition, take the temporary transition
// classes into account.
const transitionClasses = (el as ElementWithTransition)._vtc
if (transitionClasses) {
value = (value
? [value, ...transitionClasses]
: [...transitionClasses]
).join(' ')
}
// 否则就设置 className 的值
el.className = value
}
}
之前有现在没有就将 class 设置为 空
否则就重置 class
对比style
export function patchStyle(el: Element, prev: Style, next: Style) {
const style = (el as HTMLElement).style // 获取样式
if (!next) { // 如果新传入的没有样式 直接删掉
el.removeAttribute('style')
} else if (isString(next)) {
if (prev !== next) {
const current = style.display
style.cssText = next
// indicates that the `display` of the element is controlled by `v-show`,
// so we always keep the current `display` value regardless of the `style` value,
// thus handing over control to `v-show`.
if ('_vod' in el) {
style.display = current
}
}
} else {
// 新的有 需要赋值到style
for (const key in next) {
setStyle(style, key, next[key])
}
// 老的有 新的没有
if (prev && !isString(prev)) { // {style:{color: 'red'}} -> {style:{background: 'red'}}
for (const key in prev) {
if (next[key] == null) { // 老的有 新的没有 需要删除
setStyle(style, key, '')
}
}
}
}
}
样式可以设置好几个,以对象的形式管理 {style:{color: ‘red’}} -> {style:{background: ‘red’}}
老的有新的没有就删除,新的有老的没有,就赋值到style。
对比事件:
export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// vei = vue event invokers vue 事件调用 el._vei 对事件进行缓存
const invokers = el._vei || (el._vei = {}) // 元素上所有的事件调用都绑定在 _vei 上
const existingInvoker = invokers[rawName]
// 看当前事件是否已经存在缓存中
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = nextValue
} else {
const [name, options] = parseName(rawName)
if (nextValue) { // 以前没有绑定过 现在要绑定
// add
const invoker = (invokers[rawName] = createInvoker(nextValue, instance)) // 创建事件
addEventListener(el, name, invoker, options) // 绑定事件
} else if (existingInvoker) { // 以前绑定了 现在没有 需要移除事件
// remove
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined
}
}
}
// 创建一个事件
function createInvoker(
initialValue: EventValue,
instance: ComponentInternalInstance | null
) {
const invoker: Invoker = (e: Event) => {
const timeStamp = e.timeStamp || _getNow()
if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
callWithAsyncErrorHandling(
patchStopImmediatePropagation(e, invoker.value),
instance,
ErrorCodes.NATIVE_EVENT_HANDLER,
[e]
)
}
}
invoker.value = initialValue // 为了随时能更改 value 属性
invoker.attached = getNow()
return invoker
}
事件比较复杂,需要在 当前元素上对事件进行缓存,每次新增一个事件的时候看这个事件是否存在在缓存中,比如:click 事件,
如果存在就给他赋新的回调,
如果不存在,就说明之前没有绑定过现在需要创建一个新的事件然后绑定,
如果之前就绑定了,现在没有,就需要移除事件并删除缓存,