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)
}
}