第七节:vue3渲染原理 Runtime DOM

1,077 阅读4分钟

runtime-dom 针对浏览器运行时,包括DOM API属性事件处理

Vue中为了解耦, 将逻辑分成了两个模块

  • 运行时 核心 (不依赖于平台的 browser test 小程序 app canvas....) 靠的是虚拟dom
  • 针对不同平台的运行时 vue就是针对浏览器平台的
  • 渲染器

自定义渲染器

vue 默认有渲染器的方法

提供自定义入口实现 自定义渲染器,比如:想在创建元素的时候 打印个1,传递的自定义方法最终被 runtime-core 解析

let {createRenderer,h,render,Text} = VueRuntimeDOM
const renderer = createRenderer({
    createElement(element){
        console.log(1)
        return document.createElement(element);
    },
    setElementText(el,text){
        el.innerHTML = text
    },
    insert(el,container){
        container.appendChild(el)
    }
});
renderer.render(h('h1','hello world'),document.getElementById('app'))

创建runtime-dom包

基于vue开发环境搭建 增加新的模块

runtime-dom/package.json

{
    "name": "@vue/runtime-dom",
    "main": "index.js",
    "module": "dist/runtime-dom.esm-bundler.js",
    "unpkg": "dist/runtime-dom.global.js",
    "buildOptions": {
        "name": "VueRuntimeDOM",
        "formats": [
        "esm-bundler",
        "cjs",
        "global"
        ]
    }
}
pnpm i

Runtime DOM

实现节点常用操作

runtime-dom/src/nodeOps 这里存放常见DOM操作API,不同运行时提供的具体实现不一样,最终将操作方法传递到runtime-core中,所以runtime-core不需要关心平台相关代码~

// node节点操作方法
export const nodeOps = {
    // 增删改查
    // child要插入的节点 parent插入谁里边   anchor参照物具体插入到谁后边,没有的话就在最后边插入
    insert(child, parent, anchor = null){ // 添加节点
        parent.insertBefore(child,anchor) //insertBefore 等价于 appendChild
    },
    remove: child => { // 节点删除
        const parent = child.parentNode;
        if (parent) {
            parent.removeChild(child);
        }
    },
    setElementText: (el, text) => {
        // textContent 设置文本元素中的内容
        return el.textContent = text
    }, 
    setText: (node, text) => { 
        //  nodeValue 设置文本节点内容
        // 原生js用的是 document.ceratedTextNode('lyp')
        return node.nodeValue = text
    }, 
    createElement: (tagName) => {// 创建节点
        return document.createElement(tagName)
    },
    createText: text => {// 创建文本
        return document.createTextNode(text)
    },
    querySelector: selector => {
        // 搜索元素
        return document.querySelector(selector)
    },
    parentNode: node => {// 父亲节点
        return node.parentNode
    }, 
    nextSibling: node => {// 下一个节点
        return node.nextSibling
    }
}

dom 属性的操作

入口方法 比对属性

// 比对属性 前后的两个值
// null -> 新值 新增
// 值 -> 新值   更改
// 值 -> null  删除
export const patchProp = (el, key, prevValue, nextValue) => {
    if (key === 'class') {// 类名 el.className
        patchClass(el, nextValue)
    } else if (key === 'style') {// 样式  el.style
        patchStyle(el, prevValue, nextValue);
    } else if (/^on[^a-z]/.test(key)) {  // events  addEventListener on开头紧跟大写字母
        patchEvent(el, key, nextValue)
    } else { // 普通属性 // el.setAttribute
        patchAttr(el, key, nextValue)
    }
}

操作类名

根据最新值设置类名 不需要新旧比对 直接替换 因为 class="a" 增加就是 class="a b"

export function patchClass(el, nextValue) { 
    if (nextValue == null) {
        el.removeAttribute('class');
    } else {
        el.className = nextValue;
    }
}

操作样式

// 更新style prev之前的样式  next新的样式
// {color: red, }   {color:blue,background: red}
export function patchStyle(el, prev, next = {}) {
    const style = el.style;
    for (const key in next) { // 相同样式 用最新的直接覆盖之前的
        style[key] = next[key]
    }
    if (prev) {
        for (const key in prev) {// 去除之前有、现在没有的样式
            if (next[key] === undefined) {
                style[key] = null
            }
        }
    }
}

操作事件

事件是:on开头紧跟大写字母

在绑定事件的时候,绑定一个伪造的事件处理函数invoker,把真正的事件处理函数设置为invoker.value属性的值

类似于:add = add + 自定义事件(里边调用绑定的方法)

元素的自定义属性 _vei('vue-event-invorker' vue事件调用) 值是绑定的事件

  1. 第一次绑定了onCLick事件 “a”:el._vei={}->el._vei['click']=onClick->invoker.value=a->el.addEventListener(click, e => invoker.value(e))->a(e)
  2. 第二次绑定了onCLick事件 “b”:el._vei={click:onClick}->invoker.value=b->el.addEventListener(click, e => invoker.value(e))->b(e)
  3. 第三次绑定了onCLick事件 null:el.removeEventListener(click, e => b(e))
export function createInvoker(callback) {
    // 调用传递事件源e; invoker() 就是 invoker.value()
    const invoker = (e) => invoker.value(e); 
    invoker.value = callback;
    return invoker;
}

export function patchEvent(el, eventName, nextValue) {  // 更新事件
    // 1、查找_vei 属性 第一次没有  初始化{}
    const invokers = el._vei || (el._vei = {});  
    // 2、 看是否存在过事件 有没有缓存过
    const exisitingInvoker = invokers[eventName]; 

    if (nextValue && exisitingInvoker) {  // 3、绑定过事件了 直接更新  upd
        exisitingInvoker.value = nextValue;
    } else { // 4、没有的绑定过 
        // 转化事件名为小写的  onClick 要变成 click
        const name = eventName.slice(2).toLowerCase(); 
        if (nextValue) {// 4.1、如果有新值要 新增事件 并 缓存事件
            const invoker = (invokers[eventName]) = createInvoker(nextValue);
            el.addEventListener(name, invoker);
        } else if (exisitingInvoker) { // 4.2、如果没有新值要删除 并清空缓存
            el.removeEventListener(name, exisitingInvoker);
            invokers[eventName] = undefined
        }
    }
}

操作属性

function patchAttr(el, key, nextValue) {
    if (nextValue == null) {
        el.removeAttribute(key);
    } else {
        el.setAttribute(key, nextValue);
    }
}

创建渲染器

最终我们在 index.js中引入写好的方法,渲染选项就准备好了。 稍后将虚拟DOM转化成真实DOM会调用这些方法

createRenderer接受渲染所需的方法,h方法为创建虚拟节点的方法。这两个方法 在runtime-core中实现。

import { nodeOps } from "./nodeOps"
import { patchProp } from "./patchProp"

// 准备好所有渲染时所需要的的属性
const renderOptions = Object.assign({patchProp},nodeOps);
createRenderer(renderOptions).render(
    h('h1','jw'),
    document.getElementById('app')
);

真实使用时

用户不需要自定义渲染方法,直接走默认的就好

let app = document.getElementById('app')
//render(vnode, container)
render(h('h1',{style: {color: 'red'}},'hello world'),app)