vue3源码浅析:createRenderer
createApp执行过程
createApp是Vue3的入口,执行该函数可以得到一个应用实例,createRenderer就是在这个过程中定义的,要想彻底了解createRenderer对于createApp执行过程必须先要了解其执行过程
根据源码createApp核心过程简化如下
//4.定义createAppApi
const createAppApi = (render) => {
return function createApp() {
const app = {
use() {},
mixin() {},
component() {},
directive() {},
mount() {
...
render(...)
...
}
};
return app;
}
};
//3.定义createRenderer,RendererOptions是一个对象,里面包含节点和属性的处理方法,通过重新配置RendererOptions里面的方法可以实现自定义渲染器
const createRenderer = (RendererOptions) => {
return {
render,
createApp: createAppApi(render)
}
};
//2.定义renderer
const renderer = createRenderer(RendererOptions);
//1.声明Vue和定义createApp方法
const Vue = {
createApp(options) {
const app = renderer.createApp(options);
const { mount } = app;
app.mount = (containerOrSelector) => {
const container = document.querySelector(containerOrSelector);
const proxy = mount(container);
return proxy;
}
return app;
}
};
createRenderer
createRenderer 源码中实际返回的是一个baseCreateRenderer函数,baseCreateRenderer返回的才是渲染器,而渲染器核心方法则是render
RendererOptions是一个对象,定义了对元素/属性的增删改查功能,其作为参数传递给createRenderer(准确的说是baseCreateRenderer),根据传递的方法不同,来满足用户对于自定义元素和属性处理方式的要求
下图中可以了解到RendererOptions和createRenderer各有哪些方法
RendererOptions
接下来我们针对rendererOptions部分方法看看vue3到底是怎么对元素和属性操作,源码位置如下:
其中nodeOps表示元素节点的操作,patchProp表示对节点属性的操作
从源码中,可以看出来处理方式都是对于DOM元素的增删改查操作
// packages\runtime-dom\src\nodeOps.ts
const doc = (typeof document !== 'undefined' ? document : null) as Document
/**
* @description:
* @param {*} child 来自createElement创建的元素
* @param {*} parent 来自于mount传递的元素
* @return {*}
*/
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
/**
* @description: 创建元素
* @param {tag} string 必须,值为遍历组件得到的所有元素的标签名
* @return {*} DOM元素
*/
createElement: (tag, isSVG, is): Element =>
isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined),
createText: text => doc.createTextNode(text),
createComment: text => doc.createComment(text),
setText: (node, text) => {
node.nodeValue = text
},
setElementText: (el, text) => {
el.textContent = text
},
parentNode: node => node.parentNode as Element | null,
nextSibling: node => node.nextSibling,
querySelector: selector => doc.querySelector(selector),
setScopeId(el, id) {
el.setAttribute(id, '')
},
cloneNode(el) {
return el.cloneNode(true)
},
属性部分包括class/style/事件的处理
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
switch (key) {
// special
case 'class':
patchClass(el, nextValue, isSVG)
break
case 'style':
patchStyle(el, prevValue, nextValue)
break
default:
if (isOn(key)) {
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
)
} else {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value
// store value as dom properties since non-string values will be
// stringified.
if (key === 'true-value') {
;(el as any)._trueValue = nextValue
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
patchAttr(el, key, nextValue, isSVG)
}
break
}
}
实现一个自定义createRenderer
目的:自定义一个组件,该组件功能为给定数据画出一个柱状图
template: '<bar-chart :data="dataRef" @click = "add"></bar-chart>',
setup() {
const dataRef = ref(chartData)
const add = () => {
dataRef.value.push({
title: '青铜',
count: 200,
color: 'brown'
})
}
return {
add,
dataRef
}
}
效果图
方式:通过修改createRenderer的RendererOptions利用canvas绘制柱状图,而非写一个组件文件
定义RendererOptions
从源码可知RendererOptions定义了对template得到的元素进行增删改查,同时对元素属性进行操作
本例中RendererOptions需要定义的方法有以下三个:
-
元素创建,通过定义createElement,返回的DOM元素作为后续操作的el
本例子因为是要通过canvas绘制柱状图,el将作为一个对象,通过判断el内tag的类型来创建canvas
-
元素插入,通过定义insert,将组件转变成DOM元素插入到div#app内
-
元素属性操作,将属性加入到el上
因为只要求实现元素的增和查的作用,和对元素属性的操作
const {createRenderer} = Vue
const renderer = createRenderer({
/**
* @description: 创建DOM元素
* @param {*} tag 来自template模板内所有元素的标签名
* @return {*}
*/
createElement(tag) {
//tag的值为'bar-chart',直接作为对象返回,后续会作为一个el元素操作
return { tag }
},
/**
* @description: 元素属性操作
* @param {*} el 来自createElement创建的元素
* @param {*} key 创建元素的所有属性名
* @param {*} prevValue 首次执行为空
* @param {*} nextValue 创建元素的当前的属性名
* @return {*}
*/
patchProp(el, key, prevValue, nextValue) {
//将元素属性加入到el中
el[key] = nextValue;
},
/**
* @description: 插入元素
* @param {*} child 来自createElement返回的元素,本例中为el对象
* @param {*} parent 来自于mount传递的元素
* @return {*}
*/
insert(child, parent) {
//draw完成canvas的绘制并返回canvas元素
const childNode = draw(child);
//将canvas元素插入到div#app中
parent.appendChild(childNode);
//如果组件有绑定事件,则执行
if (child.onClick) {
childNode.addEventListener('click', () => {
child.onClick();
})
}
},
})
重写createApp方法
首先要知道为什么要重写createApp方法,通过改变RendererOptions我们已经改变了renderer中的render方法,希望程序按照我们的方法执行,那就需要将renderer方法进行替换,而renderer的替换需要动到createApp方法,所以需要重写createApp方法
那么要怎么重写呢,要点如下
- 替换renderer
- 保留其他的功能,重点有以下两个
- 得到app并返回实现链式调用
- 重写mount方法将container元素传给mount
function createCanvasApp(...args) {
const app = renderer.createApp(...args);
const {
mount
} = app;
app.config.isCustomElement = tag => tag === 'bar-chart'
app.mount = (containerOrSelector) => {
const container = document.querySelector(containerOrSelector);
container.innerHTML = ''
const proxy = mount(container)
return proxy
}
return app
}
canvas绘制柱状图
canvas绘制分两步:
- canvas元素创建
- 通过传递的data创建柱状图
/**
* @description: 柱状图绘制
* @param {*} canvas canvas元素
* @param {*} props 即前文的child对象,表示tag标签名和数据或者事件
* @return {*}
*/
const drawBarChart = (canvas, props) => {
const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;
const OriginPos = {
x: 50,
y: 100,
};
const step = 20;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, width, height);
const {
data
} = props;
data.forEach((chartDatas, index) => {
ctx.fillStyle = chartDatas.color;
ctx.fillRect(OriginPos.x + step * index, OriginPos.y, 10, chartDatas.count);
});
}
/**
* @description: 通过传递过来的创建canvas需要的tag标签名和数据或者事件
* @param {*} child 一个对象,表示tag标签名和数据或者事件
* @return {*} canvas元素
*/
const draw = (child) => {
let canvas;
if (child.tag == 'bar-chart') {
canvas = document.createElement('canvas');
}
drawBarChart(canvas, child);
return canvas;
}
通过自定义渲染器绘制柱状图
const chartData = [{
title: '青铜',
count: 200,
color: 'brown'
},
{
title: '砖石',
count: 300,
color: 'skyblue'
},
{
title: '王者',
count: 50,
color: 'gold'
},
]
createCanvasApp({
template: '<bar-chart :data="dataRef" @click = "add"></bar-chart>',
setup() {
const dataRef = ref(chartData)
const add = () => {
dataRef.value.push({
title: '青铜',
count: 200,
color: 'brown'
})
}
return {
add,
dataRef
}
}
})
.mount("#app")
未完成功能:事件处理更新组件
此部分会触发parentNode和nextSibling,需要对RendererOptions的两个函数parentNode和nextSibling重定义,这部分需要了解下diff过程有机会补充上