前言
vue3的渲染原理将围绕 runtime-dom 和 runtime-core去实现, runtime-dom注重对dom的操作, runtime-core提供核心的方法,用来处理渲染, 渲染篇共分为4篇去讲,当前为第一篇: (介绍runtime-dom中属性操作和节点操作篇)
节点操作
下面代码讲述的主要是对应dom的增删改查等等进行一系列的封装
// 节点相关操作
// runtime-dom/src/nodeOps.ts
const doc = (typeof document !== 'undefined' ? document : null)
export const nodeOps = {
// 创建元素
createElement: (tag) => doc.createElement(tag),
// 删除元素
remove: child => {
const parent = child.parentNode
if(parent) {
parent.removeChild(child)
}
},
// 插入元素
insert: (child, parent, anchor = null) => {
parent.insertBefore(child, anchor)
},
// 创建文本
createText: text => doc.createTextNode(text),
// 创建注释
createComment: text => doc.createComment(text),
// 设置文本
setText: (node, text) => {
node.nodeValue = text
},
// 设置元素文本
setElementText: (el, text) => {
el.textContent = text
},
// 父节点
parentNode: node => node.parentNode,
// 兄弟节点
nextSibling: node => node.nextSibling,
// 获取dom元素
querySelector: selector => doc.querySelector(selector),
// 设置id属性
setScopeId(el, id) {
el.setAttribute(id, '')
},
// 克隆节点
cloneNode(el) {
const cloned = el.cloneNode(true)
// if (`_value` in el) {
// ;(cloned as any)._value = (el as any)._value
// }
return cloned
},
}
属性操作
我们将所有关于对dom属性操作的api放入到patchProp.ts这个文件中
// runtime-dom/src/patchProp.ts
import { isOn } from "@vue/shared"
import { patchAttr } from "./modules/attrs"
import { patchClass } from "./modules/class"
import { patchEvent } from "./modules/events"
import { patchStyle } from "./modules/style"
/**
* 属性相关
* @param el 元素
* @param key 键
* @param prevValue 旧值
* @param nextValue 新值
*/
export const patchProp = (el, key, prevValue, nextValue) => {
// class操作
if(key === 'class') {
patchClass(el, nextValue)
}else if(key === 'style') { // style操作
patchStyle(el, prevValue, nextValue)
}else if(isOn(key)) { // 判断事件
patchEvent(el, key, nextValue)
}else { // 其他属性操作
patchAttr(el, key, nextValue)
}
}
在上面这段代码中, 我们判断了传过来的key的什么类型的, 然后调用对应的处理函数
class操作
// modules/class.ts
export function patchClass(el, value) {
if(value == null) {
value = ''
}
el.className = value
}
在浏览器中, 为一个元素设置calss的三种方式, 即使用setAttribute、el.className或者el.classList 从vue设计与实现中可以知道使用el.className的性能最优,因此我们这里跟着vue3使用el.calssName俩添加类名
上面的代码通过传过来的类名value, 然后为对应dom(el)设置类名
style操作
// modules/style.ts
export function patchStyle(el, prevValue, nextValue) {
const style = el.style // 获取样式
if(nextValue == null) {
el.removeAttribute('style') // 删除所有样式
}else {
// 1.老的里新的有没有
if(prevValue) {
for(let key in prevValue) {
if(nextValue[key] == null) {
style[key] = ''
}
}
}
// 2.新的里面需要复制到style里
for(let key in nextValue) {
style[key] = nextValue[key]
}
}
}
可以看到设置style跟class并不一样, 因为一个dom可以设置多个class, 而style只能设置一个
上面的代码先是获取到了对应传过来的style样式, 然后去对比之前的style, 如果出现重复,就用最新的去代替旧的, 如果没有出现过, 就直接设置到style里面
event操作
// modules/event.ts
// 举例
// h('div', {onClick:function() {console.log('on')}})
export function patchEvent(el, key, nextValue) {
// 对函数的缓存 => _vei存储列表
const invokers = el._vei ?? (el._vei = {})
const exists = invokers[key] // 判断缓存中是否有这个事件相对应的函数
if (nextValue && exists) { // 绑定事件
// 事件存在, 更新invoker.value的值即可
invoker.value = nextValue
} else {
const eventName = key.slice(2).toLowerCase() // 举例中click
if (nextValue) {
let invoker = invokers[eventName] = createInvoker(nextValue) // {click: fn} 一一对应
el.addEventListener(eventName, invoker)
} else {
el.removeEventListener(eventName, exists)
invokers[eventName] = undefined // 移除了之后
}
}
}
function createInvoker(value) {
const invoker = (e) => {
invoker.value(e)
}
invoker.value = value
return invoker
}
这里, 我们采用了一种性能更优的方式来完成事件的处理,在绑定事件时,我们可以绑定一个伪造的时间处理函数invoker, 然后将真正的事件处理函数设置为invoker.value, 这样子,当事件更新的时候, 我们将不再需要调用removeEventListener函数来移除上一次绑定的事件, 只需要更新invoker.value的值即可
看一下上面代码, 我们先从el._vei中读取对应的invoker, 如果invoker不存在,则将伪造的的invoker作为时间处理函数, 并将它缓存到el._vei当中;然后将真正的事件处理函数赋给invoker.value属性,然后把伪造的invoker函数作为时间处理函数绑定到元素上, 可以看到, 当事件触发时,执行的实际是伪造的事件函数, 在其内部间接的执行了真正的事件处理函数invoker.value(e)
attrs操作
// modules/attrs.ts
export function patchAttr(el, key, value) {
if(value == null) {
el.removeAttribute(key)
}else {
el.setAttribute(key, value)
}
}
上面的代码先是判断了传过来的值即(value),如果是个空, 则删除对应的属性, 如果不为空, 则新增或者更新当前key的值