Vue.js 设计与实现—渲染器学习笔记(一)

136 阅读12分钟

Vue.js 设计与实现—渲染器学习笔记(一)

什么是渲染器

渲染器的作用是把虚拟 DOM 渲染为特定平台上的真实元素。例如在浏览器平台,渲染器会把虚拟 DOM 渲染为真实 DOM

什么是虚拟 DOM(vdom) & 虚拟节点(vnode)

虚拟 DOM 通常使用 virtual DOM 表示,简写 vdom。虚拟 DOM 和真实 DOM 结构是一样的,都是由一组节点组成的树形结构,而虚拟节点使用 vnode 表示,其中任何一个 vnode 都可以代表一颗子树,因此 vnode 和 vdom 可以替换使用。

// 这是文本子节点
let vnode = {
	type: 'div',
	children: '文本子节点',
}
// 这是没有子节点
vnode = {
	type: 'div',
	children: null,
}
// 这是一组子节点
vnode = {
	type: 'div',
	children: [
		{
			type: 'p',
			children: 'p1',
		},
		{
			type: 'p',
			children: 'p2',
		},
	],
}

挂载

渲染器把 vdom 渲染为真实 DOM 的过程叫做挂载。

function createRenderer() {
	/**
	 * 渲染
	 * @param {*} vnode 新的vnode
	 * @param {*} container 挂载点
	 */
	function render(vnode, container) {
		// 渲染
	}
	function hydrate() {
		// 同构渲染下
		// 激活已有的 DOM 元素
	}
	return {
		render,
		hydrate,
	}
}

为什么需要 createRenderer 函数?

渲染器和渲染不同,渲染器不仅可以用来渲染,还可以用来激活已有的 DOM 元素,这通常发生在同构渲染的情况下。

render 函数的实现

function createRenderer() {
	/**
	 * 渲染
	 * @param {*} vnode 新的vnode
	 * @param {*} container 挂载点
	 */
	function render(vnode, container) {
		if (vnode) {
			// patch函数用于挂载/更新DOM
			patch(container._vnode, vnode, container)
		} else {
			if (container._vnode) {
				// 存在旧的vnode,则清空DOM
				container.innerHTML = ''
			}
		}
		container._vnode = vnode
	}
	return {
		render,
	}
}

patch 函数的实现

function createRenderer() {
	function render(vnode, container) {
		// ...
	}
	/**
	 * 挂载/更新
	 * @param {*} n1 旧vnode
	 * @param {*} n2 新vnode
	 * @param {*} container 挂载点
	 */
	function patch(n1, n2, container) {
		if (!n1) {
			// 挂载
			mountElement(n2, container)
		} else {
			// 更新
		}
	}
	return {
		render,
	}
}

mountElement 函数的实现

function createRenderer() {
	function render(vnode, container) {
		// ...
	}
	function patch(n1, n2, container) {
		// ...
	}
	/**
	 * 挂载
	 * @param {*} vnode
	 * @param {*} container
	 */
	function mountElement(vnode, container) {
		const el = document.createElement(vnode.type)
		if (typeof vnode.children === 'string') {
			el.textContent = vnode.children
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		container.insertBefore(el, null)
	}
	return {
		render,
	}
}

我们的目标是设计一个通用渲染器,所以我们要将依赖于浏览器特有的 API 进行抽离。

function createRenderer(options) {
	// 抽离浏览器特有的API
	const { createElement, setElementText, insert } = options
	function render(vnode, container) {
		// ...
	}
	function patch(n1, n2, container) {
		// ...
	}
	function mountElement(vnode, container) {
		const el = createElement(vnode.type)
		if (typeof vnode.children === 'string') {
			setElementText(el, vnode.children)
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		insert(el, container)
	}
	return {
		render,
	}
}
const renderer = createRenderer({
	/**
	 * 创建真实DOM
	 * @param {*} tag tagName
	 * @returns
	 */
	createElement(tag) {
		return document.createElement(tag)
	},
	/**
	 * 设置DOM的文本
	 * @param {*} el DOM节点
	 * @param {*} text
	 */
	setElementText(el, text) {
		el.textContent = text
	},
	/**
	 * 在给定的parent下添加指定DOM节点
	 * @param {*} el
	 * @param {*} parent
	 * @param {*} anchor
	 */
	insert(el, parent, anchor) {
		parent.insertBefore(el, anchor)
	},
})

处理元素属性

let vnode = {
	type: 'div',
	props: {
		id: 'test',
	},
	children: '文本子节点',
}

function createRenderer(options) {
	// 抽离浏览器特有的API
	const { createElement, setElementText, insert } = options
	function render(vnode, container) {
		// ...
	}
	function patch(n1, n2, container) {
		// ...
	}
	function mountElement(vnode, container) {
		const el = createElement(vnode.type)
		if (typeof vnode.children === 'string') {
			setElementText(el, vnode.children)
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		// 处理元素属性
		if (vnode.props) {
			for (const key in vnode.props) {
				// 先用最简单的方式:el.setAttribute
				el.setAttribute(key, vnode.props[key])
			}
		}
		insert(el, container)
	}
	return {
		render,
	}
}

HTML Attributs 和 DOM Properties

<!-- id type value就是HTML Attributs -->
<input id="my-input" type="text" value="foo" />
// 打印出来的就是DOM Properties
console.dir(document.querySelector('#my-input'))
  • HTML Attributs 和 DOM Properties 的属性名并不是一一对应的,比如 HTML Attributs class 其对应的 DOM Properties 是 className。
  • 不是所有的 DOM Properties 都有对应的 HTML Attributs。比如使用 el.textContent 给元素设置文本内容,但是 HTML Attributs 没有对应的属性。
  • HTML Attributs 的作用是设置 DOM Properties 的初始值。

正确的设置元素属性

let vnode = {
	type: 'button',
	props: {
		disabled: false,
	},
	children: '这是一个按钮',
}

实际渲染出来,按钮被禁用了。因为 el.setAttribute 函数,总是会被字符串化,结果为 el.setAttribute('disabled', 'false')。为了解决这个问题,需要在渲染器中做特殊处理

  • 若属性存在于 DOM Properties,优先设置元素的 DOM Properties。
  • 若属性不存在于 DOM Properties,则设置 setAttribute。
  • 若 DOM 类型为 boolean,且值为空字符串,手动修正为 true。处理如button这种情况。
function createRenderer(options) {
	// 抽离浏览器特有的API
	const { createElement, setElementText, insert } = options
	function render(vnode, container) {
		// ...
	}
	function patch(n1, n2, container) {
		// ...
	}
	function mountElement(vnode, container) {
		const el = createElement(vnode.type)
		if (typeof vnode.children === 'string') {
			setElementText(el, vnode.children)
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		if (vnode.props) {
			for (const key in vnode.props) {
				if (key in el) {
					// 如果属性存在于DOM Properties
					const type = typeof el[key]
					const value = vnode.props[key]
					// 手动修正
					if (type === 'boolean' && value === '') {
						el[key] = true
					} else {
						el[key] = value
					}
				} else {
					// 属性不存在与DOM Properties
					el.setAttribute(key, vnode.props[key])
				}
			}
		}
		insert(el, container)
	}
	return {
		render,
	}
}

处理特殊属性,只能用 setAttribute

有一些 DOM Properties 是只读的,例如form 属性对应的 DOM Properties 是 el.form,是只读的,只能通过 setAttribute 函数设置 form 属性。

function shouldSetAsProps(el, key) {
	if (key === 'form' && el.tagName === 'INPUT') return false
	return key in el
}
function createRenderer(options) {
	// 抽离浏览器特有的API
	const { createElement, setElementText, insert } = options
	function render(vnode, container) {
		// ...
	}
	function patch(n1, n2, container) {
		// ...
	}
	function mountElement(vnode, container) {
		const el = createElement(vnode.type)
		if (typeof vnode.children === 'string') {
			setElementText(el, vnode.children)
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		if (vnode.props) {
			for (const key in vnode.props) {
				if (shouldSetAsProps(el, key)) {
					const type = typeof el[key]
					const value = vnode.props[key]
					if (type === 'boolean' && value === '') {
						el[key] = true
					} else {
						el[key] = value
					}
				} else {
					el.setAttribute(key, vnode.props[key])
				}
			}
		}
		insert(el, container)
	}
	return {
		render,
	}
}

抽离属性处理方法

function shouldSetAsProps(el, key) {
	if (key === 'form' && el.tagName === 'INPUT') return false
	return key in el
}
function createRenderer(options) {
	// 抽离浏览器特有的API
	const { createElement, setElementText, insert, patchProps } = options
	function render(vnode, container) {
		// ...
	}
	function patch(n1, n2, container) {
		// ...
	}
	function mountElement(vnode, container) {
		const el = createElement(vnode.type)
		if (typeof vnode.children === 'string') {
			setElementText(el, vnode.children)
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		if (vnode.props) {
			for (const key in vnode.props) {
				patchProps(el, key)
			}
		}
		insert(el, container)
	}
	return {
		render,
	}
}
const renderer = createRenderer({
	/**
	 * 创建真实DOM
	 * @param {*} tag tagName
	 * @returns
	 */
	createElement(tag) {
		return document.createElement(tag)
	},
	/**
	 * 设置DOM的文本
	 * @param {*} el DOM节点
	 * @param {*} text
	 */
	setElementText(el, text) {
		el.textContent = text
	},
	/**
	 * 在给定的parent下添加指定DOM节点
	 * @param {*} el
	 * @param {*} parent
	 * @param {*} anchor
	 */
	insert(el, parent, anchor) {
		parent.insertBefore(el, anchor)
	},
	/**
	 * 挂载props
	 * @param {*} el
	 * @param {*} key
	 */
	patchProps(el, key) {
		if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			const value = vnode.props[key]
			if (type === 'boolean' && value === '') {
				el[key] = true
			} else {
				el[key] = value
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
})

class 属性设置

vue 中 class 属性的设置有三种方式

let vnode = {
	type: 'div',
	props: {
		class: 'foo bar',
	},
}
vnode = {
	type: 'div',
	props: {
		class: { foo: true, bar: false },
	},
}
vnode = {
	type: 'div',
	props: {
		class: ['foo bar', { baz: true }],
	},
}

序列化(规范化)class

我们需要对这三种方式进行序列化处理,转化为字符串

function normalizerClass(_classes) {
	let classes = ''
	if (typeof _classes === 'string') {
		classes = `${_classes} `
	} else if (typeof _classes === 'object') {
		if (Array.isArray(_classes)) {
			_classes.forEach(_class => {
				classes += normalizerClass(_class)
			})
		} else {
			for (const classKey in _classes) {
				if (_classes[classKey]) {
					classes += `${classKey} `
				}
			}
		}
	}
	return classes
}

设置 class 属性

作者对比了 3 种设置 class 的方式(el.className,el.setAttribute,el.classList)的性能,发现 el.className 性能最佳。

function createRenderer(options) {
	const { createElement, setElementText, insert, patchProps } = options
	// ...
	function mountElement(vnode, container) {
		const el = createElement(vnode.type)
		if (typeof vnode.children === 'string') {
			setElementText(el, vnode.children)
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		if (vnode.props) {
			for (const key in vnode.props) {
				// 传入属性值
				patchProps(el, key, vnode.props[key])
			}
		}
		insert(el, container)
	}
	return {
		render,
	}
}
const renderer = createRenderer({
	// ...
	patchProps(el, key, value) {
		if (key === 'class') {
			el.className = value || ''
		}
		if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			const value = vnode.props[key]
			if (type === 'boolean' && value === '') {
				el[key] = true
			} else {
				el[key] = value
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
	// ...
})
let vnode = {
	type: 'input',
	props: {
		id: 'form1',
		form: 'form1',
		class: normalizerClass(['foo', { bar: false }]).trim(),
	},
}

处理事件

const renderer = createRenderer({
	// ...
	patchProps(el, key, value) {
		if (/^on/.test(key)) {
			const eventName = key.slice(2).toLowerCase()
			el.addEventListener(eventName, value)
		} else if (key === 'class') {
			el.className = value || ''
		}
		if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			const value = vnode.props[key]
			if (type === 'boolean' && value === '') {
				el[key] = true
			} else {
				el[key] = value
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
})

更新节点

更新操作包括更新 props更新子节点

function createRenderer(options) {
	// ...
	function patch(n1, n2, container) {
		if (!n1) {
			// 挂载
			mountElement(n2, container)
		} else {
			// 更新
			patchElement(n1, n2)
		}
	}
	/**
	 * 更新
	 * @param {*} n1 旧的vnode
	 * @param {*} n2 新的vnode
	 */
	function patchElement(n1, n2) {
		// 更新props
		patchProps(el, key, preValue, nextValue)
		// 更新子节点
		patchChildren(n1, n2)
	}
}

更新 props

function createRenderer(options) {
	const { createElement, setElementText, insert, patchProps } = options
	// ...
	function patch(n1, n2, container) {
		if (!n1) {
			// 挂载
			mountElement(n2, container)
		} else {
			// 更新
			patchElement(n1, n2)
		}
	}
	function mountElement(vnode, container) {
		// 更新
		// 让vnode.el引用真实DOM元素,以便后续更新(patchElement)使用
		const el = (vnode.el = createElement(vnode.type))
		if (typeof vnode.children === 'string') {
			setElementText(el, vnode.children)
		} else if (Array.isArray(vnode.children)) {
			vnode.children.forEach(child => {
				patch(null, child, el)
			})
		}
		if (vnode.props) {
			for (const key in vnode.props) {
				// 更新:挂载props
				patchProps(el, key, null, vnode.props[key])
			}
		}
		insert(el, container)
	}
	/**
	 * 更新
	 * @param {*} n1 旧的vnode
	 * @param {*} n2 新的vnode
	 */
	function patchElement(n1, n2) {
		// 更新props
		const el = (n2.el = n1.el)
		const oldProps = n1.props || {}
		const newProps = n2.props || {}
		for (const key in newProps) {
			if (newProps[key] !== oldProps[key]) {
				// 更新props
				patchProps(el, key, oldProps[key], newProps[key])
			}
		}
		for (const key in oldProps) {
			// 新vnode.props中删掉的属性
			if (!(key in newProps)) {
				patchProps(el, key, oldProps[key], null)
			}
		}
		// 更新子节点
		patchChildren(n1, n2, el)
	}
	return {
		render,
	}
}
const renderer = createRenderer({
	// ...
	/**
	 * 挂载/更新props
	 * @param {*} el
	 * @param {*} key
	 */
	patchProps(el, key, preValue, nextValue) {
		if (/^on/.test(key)) {
			const eventName = key.slice(2).toLowerCase()
			el.addEventListener(eventName, nextValue)
		} else if (key === 'class') {
			el.className = nextValue || ''
		} else if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			if (type === 'boolean' && nextValue === '') {
				el[key] = true
			} else {
				el[key] = nextValue
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
	// ...
})

更新事件

const renderer = createRenderer({
	// ...
	/**
	 * 挂载/更新props
	 * @param {*} el
	 * @param {*} key
	 */
	patchProps(el, key, preValue, nextValue) {
		if (/^on/.test(key)) {
			const eventName = key.slice(2).toLowerCase()
			// 更新事件
			preValue && el.removeEventListener(eventName, preValue)
			el.addEventListener(eventName, nextValue)
		} else if (key === 'class') {
			el.className = nextValue || ''
		} else if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			if (type === 'boolean' && nextValue === '') {
				el[key] = true
			} else {
				el[key] = nextValue
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
	// ...
})

以这种简单粗暴的方式能够达到目的,每次都需要 removeEventListener 函数,操作起来性能不佳。
更新后,

const renderer = createRenderer({
	// ...
	patchProps(el, key, preValue, nextValue) {
		if (/^on/.test(key)) {
			let invoker = el._vei
			const eventName = key.slice(2).toLowerCase()
			if (nextValue) {
				if (!invoker) {
					// 初次绑定事件
					invoker = el._vei = e => {
						invoker.value(e)
					}
					invoker.value = nextValue
					el.addEventListener(eventName, invoker)
				} else {
					// 更新事件
					invoker.value = nextValue
				}
			} else if (invoker) {
				// 新vnode.props中删掉的事件
				preValue && el.removeEventListener(eventName, invoker)
			}
		} else if (key === 'class') {
			el.className = nextValue || ''
		} else if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			if (type === 'boolean' && nextValue === '') {
				el[key] = true
			} else {
				el[key] = nextValue
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
})
// 一个元素可以绑定多个事件,此时会出现事件覆盖的bug
let vnode = {
	type: 'div',
	props: {
		id: 'test',
		class: normalizerClass(['foo', { bar: false }]).trim(),
		onClick: e => {
			alert('点击事件')
		},
		onContextmenu: e => {
			e.preventDefault()
			alert('鼠标右击事件')
		},
	},
	children: 'test',
}
renderer.render(vnode, document.querySelector('#app'))

由于一个元素可以绑定多个事件,为了避免事件覆盖,需要将js el._vei 的数据结构设置为对象,它的键是事件名称,值是对应的事件处理函数。

const renderer = createRenderer({
	// ...
	patchProps(el, key, preValue, nextValue) {
		if (/^on/.test(key)) {
			// 更新
			const invokers = el._vei || (el._vei = {})
			let invoker = invokers[key]
			const eventName = key.slice(2).toLowerCase()
			if (nextValue) {
				if (!invoker) {
					// 初次绑定事件
					invoker = el._vei = e => {
						invoker.value(e)
					}
					invoker.value = nextValue
					el.addEventListener(eventName, invoker)
				} else {
					// 更新事件
					invoker.value = nextValue
				}
			} else if (invoker) {
				// 新vnode.props中删掉的事件
				preValue && el.removeEventListener(eventName, invoker)
			}
		} else if (key === 'class') {
			el.className = nextValue || ''
		} else if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			if (type === 'boolean' && nextValue === '') {
				el[key] = true
			} else {
				el[key] = nextValue
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
})
// 一个元素绑定多个同一类型的事件处理函数(用数组表达),此时会出现事件没有触发的bug
let vnode = {
	type: 'div',
	props: {
		id: 'test',
		class: normalizerClass(['foo', { bar: false }]).trim(),
		onClick: [
			e => {
				alert('点击事件')
			},
			e => {
				alert('点击事件111')
			},
		],
	},
	children: 'test',
}

一个元素绑定多个同一类型的事件处理函数(用数组表达)

const renderer = createRenderer({
	// ...
	patchProps(el, key, preValue, nextValue) {
		if (/^on/.test(key)) {
			const invokers = el._vei || (el._vei = {})
			let invoker = invokers[key]
			const eventName = key.slice(2).toLowerCase()
			if (nextValue) {
				if (!invoker) {
					// 初次绑定事件
					invoker = el._vei = e => {
						// 更新
						if (Array.isArray(invoker.value)) {
							invoker.value.forEach(fn => fn(e))
						} else {
							invoker.value(e)
						}
					}
					invoker.value = nextValue
					el.addEventListener(eventName, invoker)
				} else {
					// 更新事件
					invoker.value = nextValue
				}
			} else if (invoker) {
				// 新vnode.props中删掉的事件
				preValue && el.removeEventListener(eventName, invoker)
			}
		} else if (key === 'class') {
			el.className = nextValue || ''
		} else if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			if (type === 'boolean' && nextValue === '') {
				el[key] = true
			} else {
				el[key] = nextValue
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
})
// 事件冒泡与更新时机 按理说下面这边代码不会执行父元素的事件处理函数,实则不然
import { effect, ref } from 'vue'
let isTrue = ref(false)
effect(() => {
	let vnode = {
		type: 'div',
		props: {
			id: 'test',
			class: normalizerClass(['foo', { bar: false }]).trim(),
			onClick: isTrue.value
				? e => {
						alert('父元素点击')
				  }
				: null,
		},
		children: [
			{
				type: 'p',
				props: {
					onClick: e => {
						isTrue.value = true
						alert('子元素点击')
					},
				},
				children: 'test',
			},
		],
	}
	renderer.render(vnode, document.querySelector('#app'))
})

事件冒泡与更新时机
只需要屏蔽掉所有绑定时间(attached)晚于事件触发时间(e.timeStamp)的所有事件。

const renderer = createRenderer({
	// ...
	patchProps(el, key, preValue, nextValue) {
		if (/^on/.test(key)) {
			const invokers = el._vei || (el._vei = {})
			let invoker = invokers[key]
			const eventName = key.slice(2).toLowerCase()
			if (nextValue) {
				if (!invoker) {
					// 初次绑定事件
					invoker = el._vei = e => {
						// 事件触发时间早于事件绑定时间,则不执行事件处理函数
						if (e.timeStamp < invoker.attached) return // 新增
						if (Array.isArray(invoker.value)) {
							invoker.value.forEach(fn => fn(e))
						} else {
							invoker.value(e)
						}
					}
					invoker.value = nextValue
					// 新增
					invoker.attached = performance.now()
					el.addEventListener(eventName, invoker)
				} else {
					// 更新事件
					invoker.value = nextValue
				}
			} else if (invoker) {
				// 新vnode.props中删掉的事件
				preValue && el.removeEventListener(eventName, invoker)
			}
		} else if (key === 'class') {
			el.className = nextValue || ''
		} else if (shouldSetAsProps(el, key)) {
			const type = typeof el[key]
			if (type === 'boolean' && nextValue === '') {
				el[key] = true
			} else {
				el[key] = nextValue
			}
		} else {
			el.setAttribute(key, vnode.props[key])
		}
	},
})

更新子节点

卸载相关代码

function createRenderer(options) {
	const { createElement, setElementText, insert, patchProps } = options
	/**
	 * 更新子节点
	 * @param {*} n1 旧vnode 文本子节点/没有子节点/一组子节点
	 * @param {*} n2 新vnode 文本子节点/没有子节点/一组子节点
	 * @param {*} container 挂载点
	 */
	function patchChildren(n1, n2, container) {
		// 文本子节点
		if (typeof n2.children === 'string') {
			if (Array.isArray(n1.children)) {
				// 卸载旧的一组子节点
				n1.children.forEach(vnode => unmount(vnode))
			}
			setElementText(container, n2.children)
		} else if (Array.isArray(n2.children)) {
			// 一组子节点
			if (Array.isArray(n1.children)) {
				// TODO:diff算法
				// 这里先简单处理,直接卸载所有旧子节点,挂载新子节点
				n1.children.forEach(vnode => unmount(vnode))
				n2.children.forEach(vnode => patch(null, vnode, container))
			} else {
				setElementText(container, '')
				// 挂载新的一组节点
				n2.children.forEach(vnode => patch(null, vnode, container))
			}
		} else if (!n2.children) {
			// 没有子节点
			if (Array.isArray(n1.children)) {
				n1.children.forEach(vnode => unmount(vnode))
			} else if (typeof n1.children === 'string') {
				setElementText(container, '')
			}
		}
	}
}

区分vnode类型

新旧节点的类型都不同时,就没有更新(更新的时候,dom节点取的是旧的dom)的必要了,更新会出问题,这时应该直接卸载旧节点,挂载新节点。

function createRenderer(options) {
	// ...
	function patch(n1, n2, container) {
		// 新增
		if (n1 && n1.type !== n2.type) {
			// unmount稍后实现
			unmount(n1)
			n1 = null
		}
		const { type } = n2
		// 描述的就是普通标签
		if (typeof type === 'string') {
			if (!n1) {
				// 挂载
				mountElement(n2, container)
			} else {
				// 更新
				patchElement(n1, n2)
			}
		} else if (typeof type === 'object') {
			// 描述的是组件
		} else if (type === 'xxx') {
			// 处理其它类型
		}
	}
	// ...
}

卸载

开始的时候,我们直接container.innerHTML = ''进行卸载,这么做不严谨,主要原因有:

  • 容器的内容有组件渲染时,卸载时,应该调用组件卸载相关的生命周期函数。
  • 还有些元素存在自定义指令,卸载时,应该执行自定义指令的unbind钩子。
  • 使用innerHTML清空,不会移除绑定在DOM上的事件处理函数。
function createRenderer(options) {
	// ...
	function render(vnode, container) {
		if (vnode) {
			patch(container._vnode, vnode, container)
		} else {
			if (container._vnode) {
				// 更新
				unmount(container._vnode)
			}
		}
		container._vnode = vnode
	}
	function unmount(vnode) {
		const parent = vnode.el.parentNode
		if (parent) {
			parent.removeChild(vnode.el)
		}
	}
	// ...
}

抽离与浏览器平台相关API

function createRenderer(options) {
	const { createElement, setElementText, insert, patchProps, removeChild } = options
	function unmount(vnode) {
		removeChild(vnode)
	}
}
const renderer = createRenderer({
	// ...
	removeChild(vnode) {
		const parent = vnode.el.parentNode
		if (parent) {
			parent.removeChild(vnode.el)
		}
	},
})

文本节点和注释节点

vnode.type属性代表一个vnode的类型,如果是字符串,则描述的是普通标签,并且值就是标签的名称。但是文本节点和注释节点,没有标签,因此需要创造一个唯一的标识,来表示属性值:

const Text = Symbol()
const TextVnode = {
	type: Text,
	children: '我是文本节点',
}
const Comment = Symbol()
const CommentVnode = {
	type: Text,
	children: '我是注释节点',
}
function createRenderer(options) {
	// ...
	function patch(n1, n2, container) {
		if (n1 && n1.type !== n2.type) {
			unmount(n1)
			n1 = null
		}
		const { type } = n2
		// 描述的就是普通标签
		if (typeof type === 'string') {
			if (!n1) {
				// 挂载
				mountElement(n2, container)
			} else {
				// 更新
				patchElement(n1, n2)
			}
		} else if (typeof type === 'object') {
			// 描述的是组件
		} else if (type === Text) {
			if (!n1) {
				const el = (n2.el = document.createTextNode(n2.children))
				insert(el, container)
			} else {
				const el = (n2.el = n1.el)
				if (n2.children !== n1.children) {
					el.nodeValue = n2.children
				}
			}
		} else if (type === Comment) {
			if (!n1) {
				const el = (n2.el = document.createComment(n2.children))
				insert(el, container)
			} else {
				const el = (n2.el = n1.el)
				if (n2.children !== n1.children) {
					el.nodeValue = n2.children
				}
			}
		}
	}
	// ...
}

抽离浏览器特有的API

function createRenderer(options) {
	const {
		createElement,
		setElementText,
		insert,
		patchProps,
		removeChild,
		createText,
		createComment,
		setText,
	} = options
	// ...
	function patch(n1, n2, container) {
		if (n1 && n1.type !== n2.type) {
			unmount(n1)
			n1 = null
		}
		const { type } = n2
		// 描述的就是普通标签
		if (typeof type === 'string') {
			if (!n1) {
				// 挂载
				mountElement(n2, container)
			} else {
				// 更新
				patchElement(n1, n2)
			}
		} else if (typeof type === 'object') {
			// 描述的是组件
		} else if (type === Text) {
			if (!n1) {
				const el = (n2.el = createText(n2.children))
				insert(el, container)
			} else {
				const el = (n2.el = n1.el)
				if (n2.children !== n1.children) {
					setText(el, n2.children)
				}
			}
		} else if (type === Comment) {
			if (!n1) {
				const el = (n2.el = createComment(n2.children))
				insert(el, container)
			} else {
				const el = (n2.el = n1.el)
				if (n2.children !== n1.children) {
					setText(el, n2.children)
				}
			}
		}
	}
	// ...
}
const renderer = createRenderer({
	// ...
	// 创建文本节点
	createText(text) {
		return document.createTextNode(text)
	},
	// 创建注释节点
	createComment(text) {
		return document.createComment(text)
	},
	// 设置nodeValue
	setText(el, text) {
		el.nodeValue = text
	},
	// ...
})

Fragment

vue2中不允许有多个根节点,vue3新增了Fragment,可以表示多个根节点。也需要创建一个唯一的标识。

const Fragment = Symbol()
const FragmentVnode = {
	type: Fragment,
	children: [
		{
			type: 'li',
			children: '1',
		},
		{
			type: 'li',
			children: '2',
		},
		{
			type: 'li',
			children: '3',
		},
	],
}

function createRenderer(options) {
	const {
		createElement,
		setElementText,
		insert,
		patchProps,
		removeChild,
		createText,
		createComment,
		setText,
	} = options
	// ...
	function patch(n1, n2, container) {
		if (n1 && n1.type !== n2.type) {
			unmount(n1)
			n1 = null
		}
		const { type } = n2
		// 描述的就是普通标签
		if (typeof type === 'string') {
			if (!n1) {
				// 挂载
				mountElement(n2, container)
			} else {
				// 更新
				patchElement(n1, n2)
			}
		} else if (typeof type === 'object') {
			// 描述的是组件
		} else if (type === Text) {
			if (!n1) {
				const el = (n2.el = createText(n2.children))
				insert(el, container)
			} else {
				const el = (n2.el = n1.el)
				if (n2.children !== n1.children) {
					setText(el, n2.children)
				}
			}
		} else if (type === Comment) {
			if (!n1) {
				const el = (n2.el = createComment(n2.children))
				insert(el, container)
			} else {
				const el = (n2.el = n1.el)
				if (n2.children !== n1.children) {
					setText(el, n2.children)
				}
			}
		} else if (type === Fragment) {
			// 新增
			if (!n1) {
				n2.children.forEach(vnode => patch(null, vnode, container))
			} else {
				patchChildren(n1, n2, container)
			}
		}
	}
	function unmount(vnode) {
		if (vnode.type === Fragment) { // 新增
			// Fragment只会渲染子节点,所以只需要卸载子节点
			vnode.children.forEach(vnode => unmount(vnode))
			return
		}
		removeChild(vnode)
	}
}