本文为稀土技术社区首发签约文章,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快。

终究,仍是对运转时的render下手了

咱们先来用用 runtime-dom 这个库。

运用源码中现有的runtime-dom,经过pnpm run dev runtime-dom进行打包。在打包的同级目录下引用打包完结的runtime-dom

终究,仍是对运转时的render下手了

<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这个功用函数处理了事情,考虑了事情的修饰符状况。

终究,仍是对运转时的render下手了

开端比照:

新老事情需求比照、之前有无绑定过事情两大类,总共四种景象需求咱们去考虑。

  • 有新值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
}

第二类没有新值的第二种没有绑定过的状况,没有绑定过咱们就能够不去操作,因而能够疏忽。

源码中的事情比照是先判别有新值且绑定过事情,再判别有新值(新增事情)和没新值(解绑)的状况。

终究,仍是对运转时的render下手了

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哦(●’◡’●)!。 假如缺乏,请多指导。