最终,还是对运行时的render下手了


theme: fancy

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

image.png

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

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

image.png

<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自增。

1.gif

可以看出,确实完美实现点击自增!我们要实现自己的一个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这个功能函数处理了事件,考虑了事件的修饰符情况。

image.png

开始对比:

新老事件需要对比、之前有无绑定过事件两大类,一共四种情形需要我们去考虑。

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

第二类没有新值的第二种没有绑定过的情况,没有绑定过我们就可以不去操作,因此可以忽略。

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

image.png

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哦(●’◡’●)!。 如果不足,请多指教。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容