本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
hello 咱们好,♀️♀️♀️
我是一个酷爱常识传递,正在学习写作的作者,ClyingDeng
凳凳!
本文主要讲运转时中的runtime-dom
模块烘托时需求用到的API(rendererOptions
)。主要分为两类,一类是针对dom特点操作的API–nodeOps
,另一类便是对于节点类名、款式、特点、事情等变更操作的API–patchProp
。
runtime-dom 运用样例
咱们可能对runtime-dom
不是很了解。它是vue中运转时runtime模块的一部分。
runtime分为三部分:
-
runtime-core 内部主要是与平台无关的运转时核心比方生命周期、watch等API;
-
runtime-dom 内部主要是针对浏览器烘托时所需求的API,包含DOM 相关操作的API、特点、事情处理等;
-
runtime-test 主要用于vue内部测试,保证测试的逻辑与DOM无关并且运转速度比JSDOM快。
咱们先来用用 runtime-dom 这个库。
运用源码中现有的runtime-dom
,经过pnpm run dev runtime-dom
进行打包。在打包的同级目录下引用打包完结的runtime-dom
。
<div id="app"></div>
<script src="./runtime-dom.global.js"></script>
<script>
let { createApp, h, ref } = VueRuntimeDOM
function useCounter() {
const count = ref(0)
const add = () => {
count.value++
}
return { count, add }
}
let App = {
setup() {
let { count, add } = useCounter()
return { count, add }
},
// 每次更新重新调用render办法
render(proxy) {
return h('h1', { onClick: this.add }, 'hello dy' + this.count)
}
}
let app = createApp(App)
app.mount('#app')
</script>
将咱们需求的功用函数提取出来,在setup中获取所需的变量。回来一个新的render函数,这个render函数每次更新的时分都会重新调用。
咱们来看下,当咱们点击该标签时,会不会履行count自增。
能够看出,确实完美完成点击自增!咱们要完成自己的一个runtime-dom
,首要需求知道烘托dom时需求哪些API?!♀️♀️♀️
rendererOptions 中的 nodeOps
首要,dom节点操作的API咱们是必须要准备好的。比方节点的刺进、删去、元素的创立、文本的创立等。节点操作的功用函数经过nodeOps这个目标传递给runtime-core。
- 刺进节点
刺进节点咱们需求知道刺进的节点、父节点、参照物。将当时节点刺进指定父节点内,假如有参照物,刺进到该节点之前。
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
}
- 删去节点
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child) // 删去子节点
}
}
- 其他节点操作
在此,咱们就不打开细说了,相信基本的节点操作,咱们仍是能够理解的
// 创立元素
createElement: (tag, isSVG, is, props) => {
const el = doc.createElement(tag, is ? { is } : undefined)// is 参数目标
return el
},
// 创立文本
createText: text => doc.createTextNode(text),
// 注释
createComment: text => doc.createComment(text),
// 设置文本中的内容 // 哪个节点中的内容 div中的内容
setText: (node, text) => {
// 元素 内容
node.nodeValue = text
},
// ......(其他:设置文本内容、获取父节点、兄弟节点、克隆节点等等)
rendererOptions 中的 patchProp
针对DOM节点操作的功用是有了,但是光有这些必定是不满足的。咱们最熟悉的特点比照,新老节点的比较状况必定是少不了的。这不,patchProp
里面就供给了特点比照的办法。
知道节点烘托的,应该多少都听过diff算法。在此,咱们就需求简略的完成一个节点新旧特点比照的API。
在完成比照办法的时分,咱们需求确定一下入参,简版的比照,咱们至少要知道当时的节点el,当时节点的key值,之前的值和新值这四个参数。
比照新老节点,必定比较它们款式、类、事情、特点、v-html、innerHTML等状况,再依据不同的状况进行不同的比较。
咱们能够这样写patchProp
文件:
import { isOn } from "@vue/shared"
import { patchAttr } from "../modules/attr"
import { patchClass } from "../modules/class"
import { patchEvent } from "../modules/event"
import { patchStyle } from "../modules/style"
// 比对特点 diff算法 特点比对前后值
export const patchProp = (el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren) => {
if (key === 'class') {
patchClass(el, nextValue, isSVG)
} else if (key === 'style') {
// 款式
patchStyle(el, prevValue, nextValue)
} else if (isOn(key)) { // 点击事情
// 比对事情
patchEvent(el, key, nextValue)
} else {
// 其他特点
patchAttr(el, key, nextValue)
}
}
在此,我将isOn
判别是否是事情的正则办法,提取到共用函数shared中,逻辑是这样的:
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)
我将常见的款式、类、事情、特点这四种状况详细阐述一下:
patchClass
patchClass:class 类的节点比照。
简化版别,咱们比照款式能够先判别有无新的类。假如新的类存在,咱们就需求经过className新增类;没有的话,便是类名值为空,直接删去。
export function patchClass(el: Element, value: string | null, isSVG: boolean) {
if (value == null) {
// 新的没有值
el.removeAttribute('class')
} else {
// 新的有值
el.className = value
}
}
patchStyle
patchStyle:style 款式比照。
在进行款式比较的时分,咱们需求先获取当时的节点,在节点的款式进行增改删的操作。需求考虑四种状况:新的有老的没有、新的有老的有、老的有新的没有、老的有新的有。
咱们能够归类一下:
状况一:新的有,就新增! 遍历新的值,给当时节点添加上新的style。
状况二:新的没有,老的有,删去!遍历老的值,假如老的值不存在新的上面就需求将其置空。
export function patchStyle(el: Element, prev: any, next: any) {
const style = (el as HTMLElement).style
// 新的有 要全部加上
if (next) {
for (const key in next) {
style[key] = next[key]
}
}
// 新的没有值,老的有值,移除老的
if (prev) {
// 遍历老的节点
for (const key in prev) {
// 新的没有 去除老的
if (next[key] === null) {
style[key] = null
}
}
}
}
在此,咱们仅仅简略的完成的款式的更新比照,源码中还判别了行内款式display、important等状况。有爱好的能够去vue3中的packages/runtime-dom/src/modules/style.ts
文件debugger玩玩。
patchEvent
patchEvent:event 事情比照。
事情的比照肯能会有点绕,但是不难理解。
初始化
咱们在当时节点上添加一个自定义_vei
特点,标识vue内部用于记载绑定的事情;在咱们需求绑定事情的时分,源码中是经过一个目标来存储事情,事情的绑定替换经过更改目标内的value
值。
function createInvoker(initialValue: EventValue) {
const invoker = (e: Event) => {
// 事情
}
// 创立一个invoker createInvoker将事情存储到变量上 后续更改只需求更改invoker目标内部的value
invoker.value = initialValue
return invoker
}
这样的话,咱们在事情比照的时分,就知道咱们需求的一些必要参数,比方:当时的节点el、键值key、老的事情值prevValue
、新的事情值nextValue
。
export function patchEvent(el: Element & { _vei?: Record<string, Invoker | undefined> }, rawName: string,// 事情名 onXXX
nextValue: EventValue) {
const name = rawName.slice(2).toLowerCase()// 事情称号
// vei = vue event invokers
// 在元素上绑定一个自定义特点 用于记载绑定的事情
const invokers: any = el._vei || (el._vei = {})
// 存在的绑定事情
const existingInvoker = invokers[rawName] // 键名是key 是否已经绑定过事情
//事情比照
}
这边的rawName
便是传递过来的事情称号的key(比方:onclick),咱们需求获取具体事情名,就需求进行一下处理。咱们这仅仅简略截取了on后面的事情名,而源码中运用的是parseName
这个功用函数处理了事情,考虑了事情的修饰符状况。
开端比照:
新老事情需求比照、之前有无绑定过事情两大类,总共四种景象需求咱们去考虑。
-
有新值
nextValue
1.1)绑定过事情 ==> 换绑
1.2)没有绑定过 ==> 新增绑定事情 -
没有新值
nextValue
2.1) 有绑定过 ==> 解绑
2.2) 没有绑定过 ==> 无操作
if (nextValue) {
if (existingInvoker) {
// 1.1 换绑
existingInvoker.value = nextValue
} else {
// 1.2 新增绑定事情 用一个目标存储 每次修正目标内部的value值
// invoker是一个事情 onClick = e => {}
const invoker = invokers[rawName] = createInvoker(nextValue)
el.addEventListener(name, invoker)
}
} else {
// 2.1 remove 解绑
removeEventListener(name, existingInvoker)
invokers[rawName] = undefined
}
第二类没有新值的第二种没有绑定过的状况,没有绑定过咱们就能够不去操作,因而能够疏忽。
源码中的事情比照是先判别有新值且绑定过事情,再判别有新值(新增事情)和没新值(解绑)的状况。
patchAttr
patchAttr:attr 特点比照。
用接收到的新值value去判别,假如有新特点存在,那么就新增节点的特点;不然,就删去本来的老特点。
export function patchAttr(el: Element, key: string, value: any) {
if (value) {
el.setAttribute(key, value)
} else {
el.removeAttribute(key)
}
}
组成 rendererOptions
将节点操作和比照特点的功用函数兼并,就组成了烘托时所需求的rendererOptions
。
import { extend } from '@vue/shared'
const rendererOptions = extend({ patchProp }, nodeOps)
共用功用函数文件 @vue/shared:
export const extend = Object.assign // 特点兼并
最后,将这些API传入到runtime-core中。那么,runtime-core中又做了什么呢?咱们下篇再见分晓!!!
感爱好的朋友能够关注 手写vue3系列 专栏或许点击关注作者ClyingDeng哦(●’◡’●)!。 假如缺乏,请多指导。