渲染器用来执行渲染任务,渲染真实的节点
渲染器是框架跨平台能力的关键
渲染器和渲染是完全不同的两个概念,渲染器的作用是将元素渲染为特定平台上的真实元素,浏览器平台,渲染器将虚拟 dom 渲染为真实的 dom
借组 effect 实现响应式渲染
/**
* 浏览器端的渲染器
* @params domString
* @params container HTMLELEMENT
* @return null
*/
function renderer(domString, container) {
container.innerHTML = domString
}
// https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js
const { effect, ref } = window.VueReactivity
function renderer(domString, container) {
container.innerHTML = domString
}
const count = ref(0)
// 借助effect 才能实现 响应式渲染
effect(() => {
renderer(`<h1>${count.value}</h1>`, document.getElementById('app'))
})
setTimeout(() => {
count.value++
}, 1000)
createRenderer() 封装渲染器
/**
* 创建渲染器,渲染器包含很多,比如renderer,createApp
* @returns
*/
function createRenderer() {
function patch(n1, n2, container) {
/**
* 旧节点不存在,意味着 挂载,调用 mountElement 函数完成渲染
*/
if (!n1) {
mountElement(n2, container)
}
}
/**
* 挂载元素抽离为单独的Api,目的是为了跨平台渲染
*/
function mountElement(vnode, container) {
const el = (vnode.el = document.createElement(vnode.type))
if (typeof vnode.children === 'string') {
/**
* vnode.children 是字符串 就直接作为 el的文本节点
*/
el.innerText = vnode.children
}
// 将元素添加到容器中
container.appendChild(el)
}
/**
* 浏览器端的渲染器, 将元素渲染到节点
* @param {*} domString
* @param {HTMLElement} container
*/
function renderer(vnode, container) {
if (vnode) {
// 新 node 存在,将新、旧节点同时传递给patch函数,进行打补丁
// 首次渲染也是一次特殊的patch,首次渲染时旧节点不存在
patch(container._vnode, vnode, container)
} else {
// 旧节点存在,新节点不存在,说明是卸载操作
if (container._vnode) {
container.innerHTML = ''
}
}
container._vnode = vnode // 保存节点,作为后续渲染的旧节点
}
function hydrate(vnode, container) {}
return {
renderer,
hydrate,
}
}
const { renderer } = createRenderer()
createRenderer({createElement,setElementText}) 传递 options 实现渲染器与浏览器解耦
// 通过参数传递 createElement 方法,使得渲染器与浏览器解耦,实现跨平台的一步
const brower = {
createElement(el) {
return document.createElement(el)
},
setElementText(el, text) {
el.innerText = text
},
insert(el, parent, auchor = null) {
parent.insertBefore(el, anchor)
},
}
// 传递 options 实现 渲染器与浏览器环境解耦,实现跨平台
function createRenderer(options = brower) {
const { createElement, setElementText, insert } = options
// xxx 其他代码
}
给元素设置 HTML attributes || Dom Properties
function mountElement() {
// ... 省略部分代码
/**
* 判断 key 是否是 dom attributes
* @param {HTMLElement} el
* @param {string} key
* @returns
*/
function shouldSetAsProps(el, key) {
/**
* 特殊处理部分属性,比如 form属性,是只读的,不能通过 el.form 来修改,只能通过el.setAttribute()来设置
*/
if (key === 'form' && el.tagName === 'INPUT') return false
return key in el
}
if (vnode.props) {
/**
* 设置属性
*/
if (vnode.props) {
for (let key in vnode.props) {
/**
* 思路:
* 1. 先设置 dom Properties el[key] = value, 并且要将 '' 矫正为true
* 2. dom Properties 不存在 就通过 el.setAttribute(key,value) 来设置属性
*
* 使用 shouldSetAsProps(el,key) 函数来代替 el in key 判断 是否应该作为 dom Properties
*/
if (shouldSetAsProps(el, key)) {
/**
* dom Properties
* const node = {
* type: 'button',
* props:{
* disabled: '' // 这里就等价于 disabled: true
* }
* }
*/
const type = typeof el[key]
const value = vnode.props[key]
/**
* 获取 dom Properties 的类型,如果 布尔类型,并且value 为 空字符串 将value矫正为 true
* button.disabled = '' -> 浏览器渲染元素时将 '' 识别为了false,所以这段代码变为 button.diabled = false, 按钮不禁用
*/
if (value === '' && type === 'boolean') {
el[key] = true
} else {
el[key] = value
}
} else {
/**
* el.setAttribute会将属性值字符串化
*/
el.setAttribute(key, vnode.props[key])
}
}
}
}
}
卸载 元素, 获取 parentNode,使用 parentNode.removeChild(el) 来卸载
/**
* 卸载元素
*/
function unmount(vnode) {
const el = container._vnode.el
const parentNode = el.parentNode
if (parentNode) {
parentNode.removeChild(parentNode)
}
}
function renderer(vnode, container) {
if (vnode) {
// ...
} else {
if (container._vnode) {
// const el = container._vnode.el
// const parentNode = el.parentNode
// if (parentNode) {
// parentNode.removeChild(parentNode)
// }
unmount(container._vnode)
}
}
container._vnode = vnode // 保存节点,作为下次渲染的旧节点
}
处理 单个事件 & vue event invoker
const vnode = {
props: {
onClick: () => {},
},
}
function patchProps(el, key, prevValue, newValue) {
if (/^on/.test(key)) {
/**
* 简易版的事件处理函数,每次都需要调用 removeListeners 来移除旧的事件
*/
let name = key.slice(2).toLowerCase()
prevValue && el.removeListeners(name, prevValue) // 移除旧事件处理函数
el.addEventListeners(name, newValue) // 添加事件处理函数
}
}
// 将el._evi 设为普通值,处理单个事件
function patchProps(el, key, prevValue, newValue) {
if (/^on/.test(key)) {
/**
* 使用 vue event invoker 避免每次更新都需要 removeListeners(name,prevValue)
*/
let name = key.slice(2).toLowerCase()
let invoker = el._evi // 重点在这里
if (newValue) {
if (!invoker) {
// 第一次处理
invoker = el._evi = function (e) {
invoker.value(e) // 执行事件处理函数
}
invoker.value = newValue // 事件处理函数
el.addEventListeners(name, invoker)
} else {
// 更新事件,不需要调用 el.removeListeners(name,prevValue) 来移除旧事件,直接更新事件处理函数即可
invoker.value = newValue
}
} else if (invoker) {
/**
* 没有新事件,但是 invoker存在,说明不需要事件处理函数了,直接移除
*/
el.removeListeners(name, invoker)
}
}
}
// 将 el._evi 设为对象,处理多个事件
function patchProps(el, key, prevValue, newValue) {
if (/^on/.test(key)) {
/**
* let props = {
onClick:(){},
onContextmenu(){}
}
*/
const invokers = el._evi || (el._evi = {}) // 将el._evi 变为一个对象,避免元素绑定多个事件时,后面的事件覆盖前面的事件
let invoker = invokers[key]
if (newValue) {
if (!invoker) {
/**
* el._evi = {
* click(){},
* contextmenu(){}
* }
* 第一次: invoker = click
* 第二次: invoker = contextmenu
*/
invoker = el._evi[key] = function (e) {
invoker.value(e)
}
invoker.value = newValue
el.addEventListeners(name, invoker)
} else {
invoker.value = newValue
}
// ...
}
}
}
处理 多个事件
const div = document.getElementById('app')
const fn = () => {
console.log(1)
}
const fn1 = () => {
console.log(2)
}
// 单独绑定两个事件
// div.addEventListener('click', fn)
// div.addEventListener('click', fn1)
// 伪造一个额外的事件, 额外的事件里面调用原来的两个事件
// 调用一次addEventListenr, 回调里面执行多个处理函数 比调用多次 addEventListener, 好
const invoker = function (e) {
fn(e)
fn1(e)
}
div.addEventListener('click', invoker)
const vnode = {
props: {
onClick: [() => {}, () => {}], // onClick 绑定多个事件
},
}
function patchProps(el, key, prevValue, newValue) {
if (/^on/.test(key)) {
// ...
if (!invoker) {
invoker = el._evi[key] = function (e) {
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
invoker.value(e)
}
}
invoker.value = newValue
}
}
}
处理事件冒泡
const bol = ref(false)
const node = {
type: 'div',
props: {
onClick: bol.value
? () => {
alert('div')
}
: false,
},
children: [
{
type: 'p',
props: {
onClick: () => {
bol.value = true
},
},
},
],
}
effect(() => {
renderer.render(node, documente.getElementById('#app'))
})
function patchProps(el, key, prevValue, newValue) {
if (/^on/.test(key)) {
/**
* 点击 p 元素 触发的事件,点击p元素却执行了div元素的没有被绑定的事件
* 1. bol.value = true
* 2. 响应式数据发生变化, 触发 更新,为 div 绑定了事件处理函数
* 3. 事件冒泡触发,div的事件也被执行,导致明明bol为false没有为div绑定事件的情况下,div的事件被执行了
* 发生错误的原因:
* 为div绑定事件在p元素事件冒泡执行之前,导致事件冒泡发生时div元素已经绑定了事件,导致事件执行
*
* 解决办法
* 1. 记录事件绑定的时间,invoker.attached = performance.now()
* 2. 记录事件(冒泡)执行的事件,e.timestamp
* 3. 事件执行的时间 > 事件被绑定的时间,才执行这个事件
* 3. 事件执行的时间 < 事件被绑定的时间,不执行这个事件
*
* 说明:
* p 元素先被执行,记录了 e.timestamp
* 然后更新为div绑定事件,记录了div事件被绑定的事件 invoker.attached ,这个时间 > p元素被执行的时间
* 在 p 元素的事件内发生事件冒泡,记录的是p元素执行的时间 e.timestamp, e.timestamp < invokerd.attached, 所以不需要执行div元素被绑定的事件
*
*
*/
if (!invoker) {
invoker = el._evi[key] = function (e) {
/**
* p 元素 执行的时间 e.timestamp
* 这个时间 e.timestamp < div.attached 直接return
*/
if (e.timestamp < invoker.attached) return
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
invoker.value(e)
}
}
invoker.value = newValue
invoker.attached = performance.now() // 记录事件被绑定的事件
}
}
}
更新子节点 (元素子节点)
-
元素子节点的三种规范:
- 没有子节点 node.children = null
- 一个字符串子节点 node.children = 'string'
- 数组子节点,里面包含各种类型的子节点
// 元素子节点的 type 都是字符串 // 1 const vnode = [ { type: 'p', children: null, }, ] // 2 const vnode = [ { type: 'p', children: '', }, ] // 3 const vnode = [ { type: 'p', children: [ { type: 'span', children: 'text', }, null, 'some text', ], }, ] -
新旧子节点的 9 种情况
-
新子节点 为 null
- 旧子节点 为 null (清空就行,setElementText(el, ''))
- 旧子节点 为 字符串 (清空就行,setElementText(el, ''))
- 旧子节点 为 数组 (逐个卸载就行)
-
新子节点 为 字符串
- 旧子节点 为 null (更新内容,setElementText(el,newValue))
- 旧子节点 为 字符串 (更新内容,setElementText(el,newValue))
- 旧子节点 为 数组 (先卸载所有的旧子节点,然后更新内容,setElementText(el,newValue))
-
新子节点 为 数组
- 旧子节点 为 null (清空容器 setElementText(el, ''),逐个挂载新子节点)
- 旧子节点 为 字符串 (清空容器 setElementText(el, ''),逐个挂载新子节点)
- 旧子节点 为 数组 (核心 diff 算法,比较两组差异)
-
function patchElement(n1, n2) {
const el = (n2.el = n1.el)
const oldProps = n1.props
const newProps = n2.props
/**
* 遍历新的props,newProps[key] !== oldProps[key] 把newProps[key] 替换上去
*/
for (let key in newProps) {
if (oldProps[key] !== newProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
/**
* 再次遍历旧的props,如果新的props没有这个属性,还是以旧props为主
*/
for (let key in oldProps) {
if (!newProps[key]) {
patchProps(el, key, oldProps[key], null)
}
}
patchChildren(n1, n2, el)
}
/**
* 更新节点
* 新旧子节点共有9种情况,渲染器.md 426 有详细注释
* @param {vnode} n1
* @param {vnode} n2
* @param {*} container
*/
function patchChildren(n1, n2, container) {
if (typeof n2.children === 'string') {
/**
* 处理 新子节点为字符串的情况
*/
if (Array.isArray(n1.children)) {
// 旧子节点为数组的情况,需要卸载旧子节点
n1.children.forEach((node) => unmount(node))
}
setElementText(container, n2.childern) // 最后都把内容置为字符串
} else if (Array.isArray(n2.children)) {
/**
* 处理 新子节点 为数组的情况
*/
if (Array.isArray(n1.children)) {
/**
*
* 旧子节点为数组,diff算法
* diff 算法
*/
} else {
/**
*
* 旧子节点为字符串 | 为 null
* 清空容器,逐个挂载新子节点
*/
setElementText(container, '')
n2.children.forEach((node) => patch(null, node, container))
}
} else {
/**
* 新子节点为 null
*
*/
if (Array.isArray(n1.children)) {
// 旧子节点为数组,逐个卸载
n1.children.forEach((node) => unmount(node))
} else {
// 旧子节点为 null | "" 只需要清空内容就行
setElementText(container, '')
}
}
}
文本节点 和 注释节点
// 文本节点和注释节点的type 都是 Symbol
const Text = Symbol()
const textVnode = {
type: Text,
children: '我是文本节点',
}
const Comment = Symbol()
const CommentVnode = {
type: Comment,
children: '我是注释节点',
}
- 更新文本节点 | 更新注释节点
- 旧节点不存在,根据 n2.children 创建新的文本节点(注释节点)插入到容器中
- 旧节点存在,对比两个字符串的值是否一样,不一样更新容器的 nodeValue 即可
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
// ...
const { type } = n2
if (typeof type === 'string') {
// ...
} else if (type === Text) {
// n1, n2 都是文本节点
if (!n1) {
// n1 不存在,根据n2.children的值创建新的文本节点插入到容器中
const el = createTextNode(n2.children)
insert(el, container)
} else {
// n1 存在,比较新旧节点的值,不一样就更新文本节点的内容
const el = (n2.el = n1.el)
if (n1.children !== n2.childern) {
setText(el, n2.childern)
}
}
}
}
Fragment 节点 片段节点,允许 template 下存在多个根节点, Fragment 只会渲染 children
<!-- Item 组件 -->
<template>
<li>1</li>
<li>2</li>
<li>3</li>
</template>
const Fragment = Symbol()
const vnode = {
type: Fragment,
children: [
// children 存储的内容是模板中的所有根节点
{
type: 'li',
childern: 1,
},
{
type: 'li',
childern: 2,
},
{
type: 'li',
childern: 3,
},
],
}
<template>
<List>
<Item />
</List>
</template>
const vnode = {
type: 'ul',
children: [
{
type: Fragment,
children: [
{
type: 'li',
childern: 1,
},
{
type: 'li',
childern: 2,
},
{
type: 'li',
childern: 3,
},
],
},
],
}
- 更新 Fragment 节点
- 旧节点不存在,逐个挂载 n2.children 即可
- 旧节点存在,更新 children 即可 patchChildren
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
// ...
const { type } = n2
if (typeof type === 'string') {
// ...
} else if (type === Fragment) {
// Fragment 节点
if (!n1) {
// 旧 节点不存在,逐个挂载 n2.children 即可
n2.children.forEach((node) => patch(null, node, container))
} else {
// 旧节点存在,更新 children 即可,调用patchChildren
patchChildren(n1.children, n2.children, container)
}
}
}