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事件调用) 值是绑定的事件
- 第一次绑定了onCLick事件 “a”:
el._vei={}
->el._vei['click']=onClick
->invoker.value=a
->el.addEventListener(click, e => invoker.value(e))
->a(e)
- 第二次绑定了onCLick事件 “b”:
el._vei={click:onClick}
->invoker.value=b
->el.addEventListener(click, e => invoker.value(e))
->b(e)
- 第三次绑定了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)