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.parentNodeif (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] = cachedlet first, lastwhile (true) {let node = cachedFirst.cloneNode(true)if (!first) first = nodeparent.insertBefore(node, anchor)if (cachedFirst === cachedLast) {last = nodebreak}cachedFirst = cachedFirst.nextSibling!}return [first, last] as any}// <parent> before | first ... last | anchor </parent>const before = anchor ? anchor.previousSibling : parent.lastChildif (anchor) {let insertionPointlet usingTempInsertionPoint = falseif (anchor instanceof Element) {insertionPoint = anchor} else {// insertAdjacentHTML only works for elements but the anchor is not an// element...usingTempInsertionPoint = trueinsertionPoint = 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 [// firstbefore ? before.nextSibling : parent.firstChild,// lastanchor ? anchor.previousSibling : parent.lastChild]}}
pathProps 属性操作
属性操作有个 对比的过程
export const patchProp: DOMRendererOptions['patchProp'] = (el, // 元素key, // 属性prevValue, // 前一个值nextValue, // 新的值isSVG = false,prevChildren,parentComponent,parentSuspense,unmountChildren) => {switch (key) {// specialcase 'class':patchClass(el, nextValue, isSVG) // 那最新的属性覆盖掉旧的breakcase 'style': // {style:{color: 'red'}} -> {style:{background: 'red'}} 删掉之前的patchStyle(el, prevValue, nextValue)breakdefault:// 如果不是事件 才是属性if (isOn(key)) { // 如果是 以 on 开头的就是事件,onClick,onChange// ignore v-model listenersif (!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)._vtcif (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.displaystyle.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 {// 新的有 需要赋值到stylefor (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) {// patchexistingInvoker.value = nextValue} else {const [name, options] = parseName(rawName)if (nextValue) { // 以前没有绑定过 现在要绑定// addconst invoker = (invokers[rawName] = createInvoker(nextValue, instance)) // 创建事件addEventListener(el, name, invoker, options) // 绑定事件} else if (existingInvoker) { // 以前绑定了 现在没有 需要移除事件// removeremoveEventListener(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 事件,
如果存在就给他赋新的回调,
如果不存在,就说明之前没有绑定过现在需要创建一个新的事件然后绑定,
如果之前就绑定了,现在没有,就需要移除事件并删除缓存,
