]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: get oldVal from metadata
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Thu, 25 Jan 2024 08:38:56 +0000 (16:38 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Thu, 25 Jan 2024 09:00:19 +0000 (17:00 +0800)
13 files changed:
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
packages/compiler-vapor/src/generators/html.ts
packages/compiler-vapor/src/generators/prop.ts
packages/compiler-vapor/src/generators/text.ts
packages/runtime-vapor/__tests__/component.spec.ts
packages/runtime-vapor/__tests__/if.spec.ts
packages/runtime-vapor/src/dom.ts
packages/runtime-vapor/src/dom/patchProp.ts

index 05b85c443e26a761ccbad5827b1f37aa2c11c650..2ce2fddd02e11c7a34e38b4a2144f00080d8d4e1 100644 (file)
@@ -10,7 +10,7 @@ export function render(_ctx) {
   const n1 = _createTextNode(_ctx.count)
   _insert(n1, n3, n2)
   _renderEffect(() => {
-    _setText(n1, undefined, _ctx.count)
+    _setText(n1, _ctx.count)
   })
   return n0
 }"
@@ -130,10 +130,10 @@ export function render(_ctx) {
   const n1 = _createTextNode(_ctx.bar)
   _append(n2, n1)
   _renderEffect(() => {
-    _setText(n1, undefined, _ctx.bar)
+    _setText(n1, _ctx.bar)
   })
   _renderEffect(() => {
-    _setDynamicProp(n2, "id", undefined, _ctx.foo)
+    _setDynamicProp(n2, "id", _ctx.foo)
   })
   return n0
 }"
@@ -149,10 +149,10 @@ export function render(_ctx) {
   const n1 = _createTextNode(_ctx.bar)
   _append(n2, n1)
   _renderEffect(() => {
-    _setText(n1, undefined, _ctx.bar)
+    _setText(n1, _ctx.bar)
   })
   _renderEffect(() => {
-    _setDynamicProp(n2, "id", undefined, _ctx.foo)
+    _setDynamicProp(n2, "id", _ctx.foo)
   })
   return n0
 }"
@@ -169,10 +169,10 @@ export function render(_ctx) {
   const n2 = _createTextNode(2)
   _append(n0, n1, n2)
   _renderEffect(() => {
-    _setText(n1, undefined, 1)
+    _setText(n1, 1)
   })
   _renderEffect(() => {
-    _setText(n2, undefined, 2)
+    _setText(n2, 2)
   })
   return n0
 }"
@@ -193,10 +193,10 @@ export function render(_ctx) {
   _append(n4, n3)
   _on(n4, "click", (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
   _renderEffect(() => {
-    _setText(n1, undefined, _ctx.count)
-    _setText(n2, undefined, _ctx.count)
-    _setText(n3, undefined, _ctx.count)
-    _setDynamicProp(n4, "id", undefined, _ctx.count)
+    _setText(n1, _ctx.count)
+    _setText(n2, _ctx.count)
+    _setText(n3, _ctx.count)
+    _setDynamicProp(n4, "id", _ctx.count)
   })
   return n0
 }"
@@ -210,7 +210,7 @@ exports[`compile > expression parsing > interpolation 1`] = `
   const n1 = _createTextNode(a + b.value)
   _append(n0, n1)
   _renderEffect(() => {
-    _setText(n1, undefined, a + b.value)
+    _setText(n1, a + b.value)
   })
   return n0
 })()"
@@ -222,7 +222,7 @@ exports[`compile > expression parsing > v-bind 1`] = `
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, key.value+1, undefined, _unref(foo)[key.value+1]())
+    _setDynamicProp(n1, key.value+1, _unref(foo)[key.value+1]())
   })
   return n0
 })()"
@@ -258,28 +258,28 @@ export function render(_ctx) {
   _insert([n5, n6], n0, n10)
   _append(n0, n7, n8)
   _renderEffect(() => {
-    _setText(n1, undefined, 1)
+    _setText(n1, 1)
   })
   _renderEffect(() => {
-    _setText(n2, undefined, 2)
+    _setText(n2, 2)
   })
   _renderEffect(() => {
-    _setText(n3, undefined, 4)
+    _setText(n3, 4)
   })
   _renderEffect(() => {
-    _setText(n4, undefined, 5)
+    _setText(n4, 5)
   })
   _renderEffect(() => {
-    _setText(n5, undefined, 7)
+    _setText(n5, 7)
   })
   _renderEffect(() => {
-    _setText(n6, undefined, 8)
+    _setText(n6, 8)
   })
   _renderEffect(() => {
-    _setText(n7, undefined, 'A')
+    _setText(n7, 'A')
   })
   _renderEffect(() => {
-    _setText(n8, undefined, 'B')
+    _setText(n8, 'B')
   })
   return n0
 }"
index 1484e4ea74312aea8e025a672b7ed3c00a3c58b7..efbe8477bb673b5d0c5d7e95ec5cfdf40783b8cc 100644 (file)
@@ -8,7 +8,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setAttr(n1, "foo-bar", undefined, _ctx.id)
+    _setAttr(n1, "foo-bar", _ctx.id)
   })
   return n0
 }"
@@ -22,7 +22,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setAttr(n1, "foo-bar", undefined, _ctx.fooBar)
+    _setAttr(n1, "foo-bar", _ctx.fooBar)
   })
   return n0
 }"
@@ -36,7 +36,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, "fooBar", undefined, _ctx.id)
+    _setDynamicProp(n1, "fooBar", _ctx.id)
   })
   return n0
 }"
@@ -50,7 +50,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, _camelize(_ctx.foo), undefined, _ctx.id)
+    _setDynamicProp(n1, _camelize(_ctx.foo), _ctx.id)
   })
   return n0
 }"
@@ -64,7 +64,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, "fooBar", undefined, _ctx.fooBar)
+    _setDynamicProp(n1, "fooBar", _ctx.fooBar)
   })
   return n0
 }"
@@ -78,7 +78,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDOMProp(n1, "fooBar", undefined, _ctx.fooBar)
+    _setDOMProp(n1, "fooBar", _ctx.fooBar)
   })
   return n0
 }"
@@ -92,7 +92,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDOMProp(n1, "fooBar", undefined, _ctx.id)
+    _setDOMProp(n1, "fooBar", _ctx.id)
   })
   return n0
 }"
@@ -106,7 +106,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDOMProp(n1, "fooBar", undefined, _ctx.id)
+    _setDOMProp(n1, "fooBar", _ctx.id)
   })
   return n0
 }"
@@ -120,7 +120,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, \`.\${_ctx.fooBar}\`, undefined, _ctx.id)
+    _setDynamicProp(n1, \`.\${_ctx.fooBar}\`, _ctx.id)
   })
   return n0
 }"
@@ -134,7 +134,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDOMProp(n1, "fooBar", undefined, _ctx.fooBar)
+    _setDOMProp(n1, "fooBar", _ctx.fooBar)
   })
   return n0
 }"
@@ -148,7 +148,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, "id", undefined, _ctx.id)
+    _setDynamicProp(n1, "id", _ctx.id)
   })
   return n0
 }"
@@ -162,7 +162,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, _ctx.id, undefined, _ctx.id)
+    _setDynamicProp(n1, _ctx.id, _ctx.id)
   })
   return n0
 }"
@@ -176,7 +176,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, "camel-case", undefined, _ctx.camelCase)
+    _setDynamicProp(n1, "camel-case", _ctx.camelCase)
   })
   return n0
 }"
@@ -190,7 +190,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setDynamicProp(n1, "id", undefined, _ctx.id)
+    _setDynamicProp(n1, "id", _ctx.id)
   })
   return n0
 }"
index 865e46319ea43cacfb7aa6590a6de28fe42e8779..ce999b4d5bc666d4e72d86c7305a36e13a82fe44 100644 (file)
@@ -8,7 +8,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setHtml(n1, undefined, _ctx.code)
+    _setHtml(n1, _ctx.code)
   })
   return n0
 }"
@@ -22,7 +22,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setHtml(n1, undefined, _ctx.test)
+    _setHtml(n1, _ctx.test)
   })
   return n0
 }"
@@ -35,7 +35,7 @@ export function render(_ctx) {
   const t0 = _template("<div></div>")
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
-  _setHtml(n1, undefined, "")
+  _setHtml(n1, "")
   return n0
 }"
 `;
index 4fb9d979d51c24aff5f2378358206e96c29e97b2..e85387cd74826e2fe3c8aec43aad16e77da2ce47 100644 (file)
@@ -7,7 +7,7 @@ export function render(_ctx) {
   const t0 = _template("<div></div>")
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
-  _setDynamicProp(n1, "id", undefined, _ctx.foo)
+  _setDynamicProp(n1, "id", _ctx.foo)
   return n0
 }"
 `;
@@ -20,8 +20,8 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n3, { 1: [n2],}],} = _children(n0)
   const n1 = _createTextNode(_ctx.msg)
-  _setText(n1, undefined, _ctx.msg)
-  _setClass(n2, "class", undefined, _ctx.clz)
+  _setText(n1, _ctx.msg)
+  _setClass(n2, _ctx.clz)
   _prepend(n3, n1)
   return n0
 }"
@@ -44,7 +44,7 @@ export function render(_ctx) {
   const t0 = _template("<div><div></div></div>")
   const n0 = t0()
   const { 0: [, { 0: [n1],}],} = _children(n0)
-  _setDynamicProp(n1, "id", undefined, _ctx.foo)
+  _setDynamicProp(n1, "id", _ctx.foo)
   return n0
 }"
 `;
index 0ae00ce44c78c23958e6bb16c52e9b257211f14d..0de524c455507295c7f285f7bec8ed6282f4fb5c 100644 (file)
@@ -8,7 +8,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setText(n1, undefined, _ctx.str)
+    _setText(n1, _ctx.str)
   })
   return n0
 }"
@@ -22,7 +22,7 @@ export function render(_ctx) {
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
   _renderEffect(() => {
-    _setText(n1, undefined, _ctx.test)
+    _setText(n1, _ctx.test)
   })
   return n0
 }"
@@ -35,7 +35,7 @@ export function render(_ctx) {
   const t0 = _template("<div></div>")
   const n0 = t0()
   const { 0: [n1],} = _children(n0)
-  _setText(n1, undefined, "")
+  _setText(n1, "")
   return n0
 }"
 `;
index b8f5d335ddd844dbb3cece7f495b2ab0b95100e3..6381f18305dd3ec85c168dd1d30bbd01fdeb30d7 100644 (file)
@@ -83,7 +83,7 @@ describe('compiler v-bind', () => {
     })
 
     expect(code).matchSnapshot()
-    expect(code).contains('_setDynamicProp(n1, "id", undefined, _ctx.id)')
+    expect(code).contains('_setDynamicProp(n1, "id", _ctx.id)')
   })
 
   test('no expression', () => {
@@ -110,7 +110,7 @@ describe('compiler v-bind', () => {
     })
 
     expect(code).matchSnapshot()
-    expect(code).contains('_setDynamicProp(n1, "id", undefined, _ctx.id)')
+    expect(code).contains('_setDynamicProp(n1, "id", _ctx.id)')
   })
 
   test('no expression (shorthand)', () => {
@@ -129,9 +129,7 @@ describe('compiler v-bind', () => {
     })
 
     expect(code).matchSnapshot()
-    expect(code).contains(
-      '_setDynamicProp(n1, "camel-case", undefined, _ctx.camelCase)',
-    )
+    expect(code).contains('_setDynamicProp(n1, "camel-case", _ctx.camelCase)')
   })
 
   test('dynamic arg', () => {
@@ -152,7 +150,7 @@ describe('compiler v-bind', () => {
     })
 
     expect(code).matchSnapshot()
-    expect(code).contains('_setDynamicProp(n1, _ctx.id, undefined, _ctx.id)')
+    expect(code).contains('_setDynamicProp(n1, _ctx.id, _ctx.id)')
   })
 
   test('should error if empty expression', () => {
@@ -194,7 +192,7 @@ describe('compiler v-bind', () => {
     })
 
     expect(code).matchSnapshot()
-    expect(code).contains('_setDynamicProp(n1, "fooBar", undefined, _ctx.id)')
+    expect(code).contains('_setDynamicProp(n1, "fooBar", _ctx.id)')
   })
 
   test('.camel modifier w/ no expression', () => {
@@ -215,9 +213,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains(
-      '_setDynamicProp(n1, "fooBar", undefined, _ctx.fooBar)',
-    )
+    expect(code).contains('_setDynamicProp(n1, "fooBar", _ctx.fooBar)')
   })
 
   test('.camel modifier w/ dynamic arg', () => {
@@ -238,9 +234,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains(
-      `_setDynamicProp(n1, _camelize(_ctx.foo), undefined, _ctx.id)`,
-    )
+    expect(code).contains(`_setDynamicProp(n1, _camelize(_ctx.foo), _ctx.id)`)
   })
 
   test.todo('.camel modifier w/ dynamic arg + prefixIdentifiers')
@@ -263,7 +257,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains('_setDOMProp(n1, "fooBar", undefined, _ctx.id)')
+    expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.id)')
   })
 
   test('.prop modifier w/ no expression', () => {
@@ -284,7 +278,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains('_setDOMProp(n1, "fooBar", undefined, _ctx.fooBar)')
+    expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.fooBar)')
   })
 
   test('.prop modifier w/ dynamic arg', () => {
@@ -305,9 +299,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains(
-      '_setDynamicProp(n1, `.${_ctx.fooBar}`, undefined, _ctx.id)',
-    )
+    expect(code).contains('_setDynamicProp(n1, `.${_ctx.fooBar}`, _ctx.id)')
   })
 
   test.todo('.prop modifier w/ dynamic arg + prefixIdentifiers')
@@ -330,7 +322,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains('_setDOMProp(n1, "fooBar", undefined, _ctx.id)')
+    expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.id)')
   })
 
   test('.prop modifier (shortband) w/ no expression', () => {
@@ -351,7 +343,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains('_setDOMProp(n1, "fooBar", undefined, _ctx.fooBar)')
+    expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.fooBar)')
   })
 
   test('.attr modifier', () => {
@@ -372,7 +364,7 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains('_setAttr(n1, "foo-bar", undefined, _ctx.id)')
+    expect(code).contains('_setAttr(n1, "foo-bar", _ctx.id)')
   })
 
   test('.attr modifier w/ no expression', () => {
@@ -393,6 +385,6 @@ describe('compiler v-bind', () => {
 
     expect(code).matchSnapshot()
     expect(code).contains('renderEffect')
-    expect(code).contains('_setAttr(n1, "foo-bar", undefined, _ctx.fooBar)')
+    expect(code).contains('_setAttr(n1, "foo-bar", _ctx.fooBar)')
   })
 })
index 503d56a22f78c0a423c915573419612513f4e710..1a26f4bf2d3f60d2a0c2e7f110add180bb61afd3 100644 (file)
@@ -5,7 +5,7 @@ import { genExpression } from './expression'
 export function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) {
   const { newline, pushFnCall, vaporHelper } = context
   newline()
-  pushFnCall(vaporHelper('setHtml'), `n${oper.element}`, 'undefined', () =>
+  pushFnCall(vaporHelper('setHtml'), `n${oper.element}`, () =>
     genExpression(oper.value, context),
   )
 }
index b48d0773bf0ecbfdd56d3f42d00e47786567ee8a..7fb40fedf76e3e6840ff142a9a5b34d6eafd32e2 100644 (file)
@@ -40,7 +40,6 @@ export function genSetProp(oper: SetPropIRNode, context: CodegenContext) {
                 expr()
               }
             },
-        'undefined',
         () => genExpression(oper.value, context),
       )
       return
@@ -62,7 +61,6 @@ export function genSetProp(oper: SetPropIRNode, context: CodegenContext) {
         genExpression(oper.key, context)
       }
     },
-    'undefined',
     () => genExpression(oper.value, context),
   )
 }
index c1d708aa674222b433fc808428f8c78b29c750ed..4c6cc02cf29e2a093c6fe0a1549a71620e891df6 100644 (file)
@@ -5,7 +5,7 @@ import { genExpression } from './expression'
 export function genSetText(oper: SetTextIRNode, context: CodegenContext) {
   const { pushFnCall, newline, vaporHelper } = context
   newline()
-  pushFnCall(vaporHelper('setText'), `n${oper.element}`, 'undefined', () =>
+  pushFnCall(vaporHelper('setText'), `n${oper.element}`, () =>
     genExpression(oper.value, context),
   )
 }
index cda2b8359f0e66ed4c0c118402ccf6472176172d..f7199e2d12be3ee7436ec7ae2d07d8ba151f0047 100644 (file)
@@ -34,7 +34,7 @@ describe('component', () => {
           0: [n1],
         } = children(n0)
         watchEffect(() => {
-          setText(n1, void 0, count.value)
+          setText(n1, count.value)
         })
         return n0
       },
index 790e36cc4deee12364bb390decdb3aed0180a95d..0910a6a92f070b588b1afb410b8711fb398ff0bd 100644 (file)
@@ -69,7 +69,7 @@ describe('createIf', () => {
                   0: [n3],
                 } = children(n2)
                 renderEffect(() => {
-                  setText(n3, void 0, counter.value)
+                  setText(n3, counter.value)
                 })
                 return n2
               })),
index 44540940eabc74465d938b636ddae76d292c8de1..7f8a76e2fea5c91f1e9624dc09035bc67ea8814e 100644 (file)
@@ -49,18 +49,6 @@ export function remove(block: Block, parent: ParentNode) {
   }
 }
 
-export function setText(el: Node, oldVal: any, newVal: any) {
-  if ((newVal = toDisplayString(newVal)) !== oldVal) {
-    el.textContent = newVal
-  }
-}
-
-export function setHtml(el: Element, oldVal: any, newVal: any) {
-  if (newVal !== oldVal) {
-    el.innerHTML = newVal
-  }
-}
-
 type Children = Record<number, [ChildNode, Children]>
 export function children(n: Node): Children {
   const result: Children = {}
index 97eb8cd3f13a511ebe15279aa96fd3ad110567ea..d877afbd3f0f98c4ac2d7ee0fe05efef1fbaaa51 100644 (file)
@@ -3,79 +3,95 @@ import {
   isString,
   normalizeClass,
   normalizeStyle,
+  toDisplayString,
 } from '@vue/shared'
 import { currentInstance } from '../component'
 
-export function setClass(el: Element, oldVal: any, newVal: any) {
-  if ((newVal = normalizeClass(newVal)) !== oldVal && (newVal || oldVal)) {
-    recordPropMetadata(el, 'class', newVal)
-    el.className = newVal
+export function setClass(el: Element, value: any) {
+  const prev = recordPropMetadata(el, 'class', (value = normalizeClass(value)))
+  if (value !== prev && (value || prev)) {
+    el.className = value
   }
 }
 
-export function setStyle(el: HTMLElement, oldVal: any, newVal: any) {
-  if ((newVal = normalizeStyle(newVal)) !== oldVal && (newVal || oldVal)) {
-    recordPropMetadata(el, 'style', newVal)
-    if (typeof newVal === 'string') {
-      el.style.cssText = newVal
+export function setStyle(el: HTMLElement, value: any) {
+  const prev = recordPropMetadata(el, 'style', (value = normalizeStyle(value)))
+  if (value !== prev && (value || prev)) {
+    if (typeof value === 'string') {
+      el.style.cssText = value
     } else {
       // TODO
     }
   }
 }
 
-export function setAttr(el: Element, key: string, oldVal: any, newVal: any) {
-  if (newVal !== oldVal) {
-    recordPropMetadata(el, key, newVal)
-    if (newVal != null) {
-      el.setAttribute(key, newVal)
+export function setAttr(el: Element, key: string, value: any) {
+  const oldVal = recordPropMetadata(el, key, value)
+  if (value !== oldVal) {
+    if (value != null) {
+      el.setAttribute(key, value)
     } else {
       el.removeAttribute(key)
     }
   }
 }
 
-export function setDOMProp(el: any, key: string, oldVal: any, newVal: any) {
+export function setDOMProp(el: any, key: string, value: any) {
+  const oldVal = recordPropMetadata(el, key, value)
   // TODO special checks
-  if (newVal !== oldVal) {
-    recordPropMetadata(el, key, newVal)
-    el[key] = newVal
+  if (value !== oldVal) {
+    el[key] = value
   }
 }
 
-export function setDynamicProp(
-  el: Element,
-  key: string,
-  oldVal: any,
-  newVal: any,
-) {
+export function setDynamicProp(el: Element, key: string, value: any) {
   // TODO
   const isSVG = false
   if (key === 'class') {
-    setClass(el, oldVal, newVal)
+    setClass(el, value)
   } else if (key === 'style') {
-    setStyle(el as HTMLElement, oldVal, newVal)
+    setStyle(el as HTMLElement, value)
   } else if (
     key[0] === '.'
       ? ((key = key.slice(1)), true)
       : key[0] === '^'
         ? ((key = key.slice(1)), false)
-        : shouldSetAsProp(el, key, newVal, isSVG)
+        : shouldSetAsProp(el, key, value, isSVG)
   ) {
-    setDOMProp(el, key, oldVal, newVal)
+    setDOMProp(el, key, value)
   } else {
     // TODO special case for <input v-model type="checkbox">
-    setAttr(el, key, oldVal, newVal)
+    setAttr(el, key, value)
   }
 }
 
-export function recordPropMetadata(el: Node, key: string, value: any) {
+export function recordPropMetadata(el: Node, key: string, value: any): any {
   if (currentInstance) {
     let metadata = currentInstance.metadata.get(el)
     if (!metadata) {
       currentInstance.metadata.set(el, (metadata = { props: {} }))
     }
+    const prev = metadata.props[key]
     metadata.props[key] = value
+    return prev
+  }
+}
+
+export function setText(el: Node, value: any) {
+  const oldVal = recordPropMetadata(
+    el,
+    'textContent',
+    (value = toDisplayString(value)),
+  )
+  if (value !== oldVal) {
+    el.textContent = value
+  }
+}
+
+export function setHtml(el: Element, value: any) {
+  const oldVal = recordPropMetadata(el, 'innerHTML', value)
+  if (value !== oldVal) {
+    el.innerHTML = value
   }
 }