Scope: this browser teaching implementation runs only trusted templates that you write; dynamic function compilation is subject to CSP. It omits a full keyed diff, reactive props, Map/Set support and a deduplicating scheduler. Component templates use one root node.
template compiler enhanced version, added:
v-if(compiled to ternary/short circuit)v-for(compiled to.map(), supportsitem in items/(item, index) in items):prop/v-bind:propexpressions (compiled to dynamic props)@event/v-onand event modifiers (.stop.prevent.self)- Generate simple
patchFlaganddynamicPropsmetadata; mount sets all attributes, while later updates usePROPSto narrow attribute updates.TEXTis metadata only here.
This implementation remains short and runnable (open directly in the browser), making it easy for you to experiment, extend, and learn by comparing it with real Vue compilation ideas.The following is a complete single file implementation. Save the entire HTML as mini-vue-compiler.html and open it in the browser to run the demo.
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>mini-vue extended template compiler (v-if / v-for / :prop / @event.mod)</title>
<style>button{margin:4px}</style>
</head>
<body>
<div id="app"></div>
<script>
/* ---------------------------
Minimal reactivity (reactive/ref/effect)
--------------------------- */
let activeEffect = null
const targetMap = new WeakMap()
function effect(fn, options = {}) {
const runner = () => {
if (!runner.active) return
for (const dep of runner.deps) dep.delete(runner)
runner.deps = []
const parent = activeEffect
try {
activeEffect = runner
return fn()
} finally {
activeEffect = parent
}
}
runner.active = true
runner.deps = []
runner.scheduler = options.scheduler
if (!options.lazy) runner()
return runner
}
function track(target, key){
if (!activeEffect) return
let m = targetMap.get(target)
if (!m) targetMap.set(target, m = new Map())
let dep = m.get(key)
if (!dep) m.set(key, dep = new Set())
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
function trigger(target, key){
const m = targetMap.get(target); if (!m) return
const dep = m.get(key); if (!dep) return
const effects = new Set(dep)
effects.forEach(e => e.scheduler ? e.scheduler(e) : e())
}
function reactive(obj){
if (typeof obj !== 'object' || obj === null) return obj
return new Proxy(obj, {
get(t,k,r){ const v = Reflect.get(t,k,r); track(t,k); return (typeof v==='object' && v!==null)? reactive(v) : v },
set(t,k,v,r){ const old = t[k]; const res = Reflect.set(t,k,v,r); if (old !== v) trigger(t,k); return res },
deleteProperty(t,k){ const had = k in t; const res = Reflect.deleteProperty(t,k); if (had && res) trigger(t,k); return res}
})
}
function ref(val){
const r = { __isRef: true,
get value(){ track(r,'value'); return val },
set value(v){ val = v; trigger(r,'value') }
}
return r
}
/* ---------------------------
VNode / h / TEXT symbol
--------------------------- */
const TEXT = Symbol('text')
function h(type, props = null, children = null) {
return { type, props, children, el: null, patchFlag: 0, dynamicProps: null }
}
function createTextVNode(text){
return { type: TEXT, props: null, children: String(text), el: null, patchFlag: 0, dynamicProps: null }
}
/* ---------------------------
Compiler (extended)
- supports: {{}} interpolation, :prop, @event.mod, v-if, v-for
- generates render function string using `with(_ctx){ ... }`
- outputs patchFlag & dynamicProps for runtime optimization
--------------------------- */
const PatchFlags = {
TEXT: 1 << 0,
PROPS: 1 << 1,
FULL_PROPS: 1 << 2
}
function compileTemplate(template) {
const tpl = document.createElement('template')
tpl.innerHTML = template.trim()
function parseNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const raw = node.textContent
const tokens = []
let last = 0
const re = /{{([^}]+)}}/g
let m
while ((m = re.exec(raw)) !== null) {
if (m.index > last) tokens.push(JSON.stringify(raw.slice(last, m.index)))
tokens.push('(' + m[1].trim() + ')')
last = m.index + m[0].length
}
if (last < raw.length) tokens.push(JSON.stringify(raw.slice(last)))
if (tokens.length === 0) return null
return { type: 'text', code: tokens.join(' + '), dynamic: tokens.some(t => t.startsWith('(')) }
}
if (node.nodeType === Node.ELEMENT_NODE) {
const tag = node.tagName.toLowerCase()
// attributes
const attrs = []
const dynamicProps = []
const eventProps = []
const children = []
for (let i = 0; i < node.attributes.length; i++) {
const a = node.attributes[i]
const name = a.name
const value = a.value
if (name === 'v-if') {
attrs.push({ kind:'v-if', exp: value })
} else if (name === 'v-for') {
// parse: (item, idx) in items OR item in items
const inMatch = value.match(/^\s*(?:\(([^)]+)\)|([^]+?))\s+in\s+(.+)\s*$/)
if (!inMatch) throw new Error('v-for expression invalid: ' + value)
const rawVars = inMatch[1] || inMatch[2]
const vars = rawVars.split(',').map(s => s.trim())
attrs.push({ kind:'v-for', source: inMatch[3].trim(), alias: vars })
} else if (name.startsWith(':') || name.startsWith('v-bind:')) {
const propName = name.startsWith(':') ? name.slice(1) : name.slice(7)
attrs.push({ kind:'bind', prop: propName, exp: value })
dynamicProps.push(propName)
} else if (name.startsWith('@') || name.startsWith('v-on:')) {
const evtNameRaw = name.startsWith('@') ? name.slice(1) : name.slice(5)
const parts = evtNameRaw.split('.')
const evt = parts.shift()
const mods = parts // modifiers like stop, prevent, self
attrs.push({ kind:'on', event: evt, exp: value, mods })
eventProps.push('on' + capitalize(evt))
dynamicProps.push('on' + capitalize(evt))
} else {
// static prop
attrs.push({ kind:'static', prop: name, value: value })
}
}
node.childNodes.forEach(n => {
const parsed = parseNode(n)
if (parsed) children.push(parsed)
})
return { type: 'element', tag, attrs, children, dynamicProps: dynamicProps, eventProps }
}
return null
}
const body = []
tpl.content.childNodes.forEach(n => { const p = parseNode(n); if (p) body.push(p) })
function genNode(n) {
if (n.type === 'text') {
// produce text vnode or text expression
const code = `createTextVNode(${n.code})`
return { code, dynamic: n.dynamic, isArray: false }
}
if (n.type === 'element') {
// handle v-if / v-for on this element
const vIfAttr = n.attrs.find(a => a.kind === 'v-if')
const vForAttr = n.attrs.find(a => a.kind === 'v-for')
// generate children code
const childrenCodes = n.children.map(c => genNode(c))
const childrenCode = '[' + childrenCodes.map(c => c.code).join(', ') + '].flat(Infinity).filter(Boolean)'
// props code: static + dynamic
const staticProps = n.attrs.filter(a=>a.kind==='static').map(a => `${JSON.stringify(a.prop)}: ${JSON.stringify(a.value)}`)
const bindProps = n.attrs.filter(a=>a.kind==='bind').map(a => `${JSON.stringify(a.prop)}: ${a.exp}`)
const onProps = n.attrs.filter(a=>a.kind==='on').map(a => {
const evt = a.event
const mods = a.mods || []
const wrapperParts = []
if (mods.includes('stop')) wrapperParts.push('e.stopPropagation()')
if (mods.includes('prevent')) wrapperParts.push('e.preventDefault()')
if (mods.includes('self')) {
// self: only call if e.target === e.currentTarget
wrapperParts.push('if(e.target !== e.currentTarget) return')
}
// call handler from context
const expression = a.exp.trim()
const method = /^[$A-Z_a-z][\w$]*(?:\.[\w$]+)*$/.test(expression)
wrapperParts.push(method ? `return (${expression})(e)` : `return (${expression})`)
const fnCode = `(e)=>{ const $event = e; ${wrapperParts.join(';')} }`
return `${JSON.stringify('on' + capitalize(evt))}: ${fnCode}`
})
const propsParts = []
if (staticProps.length) propsParts.push(...staticProps)
if (bindProps.length) propsParts.push(...bindProps)
if (onProps.length) propsParts.push(...onProps)
const propsObj = propsParts.length ? `{ ${propsParts.join(', ')} }` : 'null'
// compute patchFlag and dynamicProps list
const hasDynamicProps = bindProps.length + onProps.length > 0
const patchFlags = (childrenCodes.some(c=>c.dynamic) ? PatchFlags.TEXT : 0) | (hasDynamicProps ? PatchFlags.PROPS : 0)
const dynamicList = hasDynamicProps ? JSON.stringify( n.dynamicProps.length ? n.dynamicProps : [] ) : 'null'
const vnodeCode = `(() => { const vnode = h(${JSON.stringify(n.tag)}, ${propsObj}, ${childrenCode}); vnode.patchFlag = ${patchFlags}; vnode.dynamicProps = ${dynamicList}; return vnode })()`
// v-if
if (vIfAttr) {
return { code: `( (${vIfAttr.exp}) ? ${vnodeCode} : null )`, dynamic: true, isArray: false }
}
// v-for
if (vForAttr) {
const aliasVars = vForAttr.alias // array of names
const item = aliasVars[0] || '$item'
const index = aliasVars[1] || '$index'
const source = vForAttr.source
// create map expression; ensure each iteration returns vnode or null
const mapCode = `(${source}) ? (${source}).map((${item}, ${index}) => { return ${vnodeCode} }) : []`
return { code: mapCode, dynamic: true, isArray: true }
}
return { code: vnodeCode, dynamic: hasDynamicProps || childrenCodes.some(c=>c.dynamic), isArray: false }
}
return { code: 'null', dynamic: false, isArray: false }
}
// build root code (single node or array)
const rootNodes = body.map(n => genNode(n))
const rootCode = rootNodes.length === 1 ? rootNodes[0].code : '[' + rootNodes.map(n=>n.code).join(', ') + ']'
const fnCode = `
return function render(_ctx) {
const runtime = this && this.__runtimeHelpers || runtimeHelpers;
const { h, createTextVNode } = runtime;
with(_ctx){
return ${rootCode};
}
}
`
const factory = new Function('runtimeHelpers', fnCode)
return (helpers) => factory(helpers)
}
/* ---------------------------
Renderer with patchFlag-aware setProps
--------------------------- */
function createRenderer() {
function resolveComponents(vnode, components = {}) {
if (Array.isArray(vnode)) {
return vnode.flat(Infinity).filter(Boolean).map(v => resolveComponents(v, components))
}
if (!vnode) return null
if (typeof vnode.type === 'string') {
const name = Object.keys(components).find(key => key.toLowerCase() === vnode.type.toLowerCase())
if (name) vnode.type = components[name]
}
if (Array.isArray(vnode.children)) vnode.children = resolveComponents(vnode.children, components)
return vnode
}
function setProps(el, oldProps = {}, newProps = {}, keys = null) {
for (const key of keys || new Set([...Object.keys(oldProps), ...Object.keys(newProps)])) {
const value = newProps[key]
if (oldProps[key] === value || key === 'key') continue
if (key.startsWith('on')) el[key.toLowerCase()] = typeof value === 'function' ? value : null
else if (value == null || value === false) el.removeAttribute(key)
else el.setAttribute(key, value === true ? '' : value)
}
}
function unmount(vnode) {
if (!vnode) return
if (Array.isArray(vnode)) { vnode.forEach(unmount); return }
if (vnode.component) {
const runner = vnode.component.update
runner.active = false
for (const dep of runner.deps) dep.delete(runner)
unmount(vnode.component.subTree)
} else {
if (Array.isArray(vnode.children)) vnode.children.forEach(unmount)
if (vnode.el) vnode.el.remove()
}
}
function patchChildren(c1, c2, el) {
if (Array.isArray(c2)) {
if (Array.isArray(c1)) {
for (let i = 0; i < Math.max(c1.length, c2.length); i++) patch(c1[i], c2[i], el)
} else {
el.textContent = ''
c2.forEach(c => patch(null, c, el))
}
} else {
if (Array.isArray(c1)) c1.forEach(unmount)
const text = c2 == null ? '' : String(c2)
if (el.textContent !== text) el.textContent = text
}
}
function mountComponent(vnode, container, anchor) {
const Comp = vnode.type
const instance = vnode.component = {
props: { ...vnode.props }, setupState: {}, subTree: null, update: null
}
const result = Comp.setup ? Comp.setup(instance.props, { emit() {} }) : {}
if (typeof result === 'function') instance.render = result
else instance.setupState = result || {}
if (!instance.render) instance.render = Comp.render || compileTemplate(Comp.template)({ h, createTextVNode })
instance.update = effect(() => {
const nextTree = resolveComponents(instance.render(instance.setupState), Comp.components)
patch(instance.subTree, nextTree, container, anchor)
instance.subTree = nextTree
vnode.el = nextTree && nextTree.el
}, { scheduler: runner => Promise.resolve().then(runner) })
}
function patch(n1, n2, container, anchor = null) {
if (n1 === n2) return
if (!n2) { unmount(n1); return }
if (Array.isArray(n2)) {
const before = Array.isArray(n1) ? n1 : []
if (n1 && !Array.isArray(n1)) unmount(n1)
for (let i = 0; i < Math.max(before.length, n2.length); i++) patch(before[i], n2[i], container, anchor)
return
}
if (n1 && n1.type !== n2.type) {
anchor = n1.el ? n1.el.nextSibling : anchor
unmount(n1)
n1 = null
}
if (n2.type === TEXT) {
if (!n1) {
n2.el = document.createTextNode(n2.children)
container.insertBefore(n2.el, anchor)
} else {
n2.el = n1.el
if (n1.children !== n2.children) n2.el.textContent = n2.children
}
} else if (typeof n2.type === 'string') {
if (!n1) {
const el = n2.el = document.createElement(n2.type)
setProps(el, {}, n2.props || {})
patchChildren(null, n2.children, el)
container.insertBefore(el, anchor)
} else {
n2.el = n1.el
setProps(n2.el, n1.props || {}, n2.props || {}, n2.patchFlag & 2 ? n2.dynamicProps : null)
patchChildren(n1.children, n2.children, n2.el)
}
} else {
if (!n1) mountComponent(n2, container, anchor)
else {
const instance = n2.component = n1.component
for (const key of Object.keys(instance.props)) {
if (!(key in (n2.props || {}))) delete instance.props[key]
}
Object.assign(instance.props, n2.props || {})
instance.update()
n2.el = instance.subTree && instance.subTree.el
}
}
}
return {
patch,
createApp: root => ({
mount(selector) { patch(null, h(root), document.querySelector(selector)) }
})
}
}
const { createApp, patch } = createRenderer()
const runtimeHelpers = { h, createTextVNode }
/* ---------------------------
Demo components + template showcasing features
--------------------------- */
const ItemList = {
template: `
<div>
<h3>Items (v-for with v-if example)</h3>
<ul>
<li v-for="(it, idx) in items" :key="it.id">
<span>{{ idx }} - {{ it.text }}</span>
<button @click.stop="remove(it.id)">remove</button>
</li>
</ul>
<div v-if="items.length === 0">No items</div>
</div>
`,
setup() {
const state = reactive({ items: [ {id:1,text:'one'}, {id:2,text:'two'} ] })
function remove(id) { state.items = state.items.filter(i=>i.id!==id) }
return { get items() { return state.items }, remove }
}
}
const App = {
template: `
<div>
<h1>mini-vue extended compiler demo</h1>
<div :data-test="msg" @click.self="onRootClick" style="padding:6px;border:1px solid #ddd">
<div>{{ msg }}</div>
<button @click.prevent="inc">inc</button>
<button @click="toggle">toggle child</button>
<div v-if="show">
<ItemList></ItemList>
</div>
<div>
<h3>v-for simple numbers</h3>
<div v-for="n in nums">{{ n }}</div>
</div>
</div>
</div>
`,
components: { ItemList },
setup() {
const state = reactive({ count:0, show:true, nums: [1,2,3] })
function inc(e){ state.count++ ; console.log('inc click', e && e.type) }
function toggle(){ state.show = !state.show }
function onRootClick(){ console.log('root clicked (self)') }
return {
get msg() { return 'hello mini-vue: ' + state.count },
get show() { return state.show },
get nums() { return state.nums },
inc, toggle, onRootClick
}
}
}
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1) }
createApp(App).mount('#app')
</script>
</body>
</html>Description (key points review)
- v-if: It is converted into a conditional expression
cond ? vnode : nullat compile time, and returnsnulldirectly during runtime (no rendering) to avoid unnecessary patches. - v-for: compiled to
source.map((item, index) => { return vnode }); child arrays are flattened. The renderer compares positions and unmounts surplus nodes; keyed node moves are not implemented. - :prop/v-bind: Recognized as dynamic props, the compiler will put these prop expressions into the props object and record the
dynamicPropslist (used for patchFlag optimization). - @event.mod: Supports
.stop,.prevent,.self(can be extended), and will wrap modifier logic in the generated event processing function. - patchFlags:
PROPSnarrows updates to thedynamicPropslist; initial mount still sets all attributes.TEXTis not yet used to skip other node comparisons. - Implementation limitation: For brevity, the compiler uses the browser DOM parser (
template.innerHTML) to do the AST, and expressions are executed viawith(_ctx){ ... }- this is stricter, safer, and generates precompiled functions in a real compiler.Our goal is "teaching and experimentation", not production safety or complete implementation.