渲染器与响系统
在vue中,渲染器是整个框架最重要的功能之一,渲染器的好坏直接决定了vue的性能.下面是一个简单的渲染器结合之前完成的响应系统模拟代码
function renderer(domString, container) {
container.innerHTML = domString;
}
const count = ref(1);
effect(() => {
renderer(`<h1>${count.value}</h1>`, document.getElementById('app'));
});
const button = document.querySelector('button');
button.addEventListener('click', () => {
count.value++;
});
封装一个renderer函数,将h1这段字符串渲染到页面上,结合响应系统,当count.value的值改变,重新触发renderer函数执行,重新渲染,这就是渲染器的基本概念. 接下来,封装一个创建渲染器的函数
function createRenderer() {
function render(domString, container) {
container.innerHTML = domString;
}
return {
render,
};
}
const count = ref(1);
effect(() => {
const renderer = createRenderer();
console.log(renderer);
renderer.render(`<h1>${count.value}</h1>`, document.getElementById('app'));
});
const button = document.querySelector('button');
button.addEventListener('click', () => {
count.value++;
});
为什么还有封装一个createRenderer函数来创建渲染器,我们传的第一个参数其实就是虚拟dom,因为之前把旧的虚拟dom渲染了,后面更新了dom,没有用把整个dom重新渲染,封装创建渲染器函数是为了后面写对比新旧dom的功能,然后进行局部渲染.
自定义渲染器
下面,我们实现一个"通用"的渲染器,将虚拟dom渲染到页面上. 下面是一个vnode(虚拟节点对象)
const vonde = {
type: 'h1',
children: 'hello',
};
const renderer = createRenderer();
renderer.render(vonde, document.querySelector('#app'));
下面是更改的createRenderer函数
function createRenderer() {
function patch(n1, n2, container) {
//如果n1不存在,以为挂载,则调用mountElement
if (!n1) {
mountElement(n2, container);
}else{
//n1不存在,则打补丁,新旧比较,局部替换
}
}
function render(vonde, container) {
if (vonde) {
patch(container._vonde, vonde, container);
} else {
if (container._vonde) {
container.innerHTML = '';
}
}
container._vnode = vonde;
}
return {
render,
};
}
上面代码意思是,当有vnode值,也就是有虚拟节点,则patch更新节点,如果没有,就是虚拟节点没有了,将容器设为空,在把当前节点保存到 container._vnode,即旧节点
patch函数是判断有没有旧节点,没有就是新节点,进行挂载,调用mountElement函数
function mountElement(vonde, container) {
//创建dom元素
const el = document.createElement(vonde.type);
//处理子节点,如果子节点是字符串,代表是文本节点
if (typeof vonde.children === 'string') {
//设置元素的textContent
el.textContent = vonde.children;
}
//将元素添加到容器中
container.appendChild(el);
}
判断虚拟节点类型,创建节点,设置节点内容,在添加到容器中,渲染到页面. 虚拟dom有一个特别的能力,就是跨平台能力,我们上面使用的大多是浏览器的apii,所以,要将这些api抽离,设计通用的渲染器
const renderer = createRenderer({
createElement(tag) {
return document.createElement(tag);
},
//设置文本节点
setElementText(el, text) {
el.textContent = text;
},
//用于给指定的parent添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor);
},
});
我们将这些方法传入创建渲染器函数内,为什么要这么设计呢,因为我们现在是在浏览器,浏览器有这些api,如果换成小程序,uniapp,创建元素,设置元素内容,就不是这些api了
function createRenderer(options) {
//通过options得到操作Dom的api
const { createElement, insert, setElementText } = options;
//挂载元素
function mountElement(vonde, container) {
//创建dom元素
//调用createElement函数创建元素
const el = createElement(vonde.type);
//处理子节点,如果子节点是字符串,代表是文本节点
if (typeof vonde.children === 'string') {
//设置元素的textContent
//调用setElementText设置元素文本节点
setElementText(el, vonde.children);
}
//将元素添加到容器中
//调用insert函数插入容器内
insert(el, container);
}
//打补丁
function patch(n1, n2, container) {
//如果n1不存在,以为挂载,则调用mountElement
if (!n1) {
mountElement(n2, container);
} else {
//n1不存在,则打补丁,新旧比较,局部替换
}
}
//渲染函数
function render(vonde, container) {
if (vonde) {
patch(container._vonde, vonde, container);
} else {
if (container._vonde) {
container.innerHTML = '';
}
}
container._vnode = vonde;
}
return {
render,
};
}
功能上没什么区别,就是将可以将一些操作抽离用我们传入的函数操作
这样我们的元素就成功挂载到页面上了
挂载与更新
上面我们把children值为字符串的元素渲染到页面上,但是一个节点可能具有文本节点,也可能会有其他元素节点,为了描述这些子节点,需要使用数组来描述
const vonde = {
type: 'h1',
children: [
{
type: 'p',
children: 'hello',
},
],
};
我们需要修改一下mountElement函数
//挂载元素
function mountElement(vonde, container) {
//创建dom元素
//调用createElement函数创建元素
const el = createElement(vonde.type);
//处理子节点,如果子节点是字符串,代表是文本节点
if (typeof vonde.children === 'string') {
//设置元素的textContent
//调用setElementText设置元素文本节点
setElementText(el, vonde.children);
} else if (Array.isArray(vonde.children)) {
//如果是数组,则遍历每一个子节点,调用patch函数挂载
vonde.children.forEach((child) => {
patch(null, child, el);
});
}
//将元素添加到容器中
//调用insert函数插入容器内
insert(el, container);
}
这些代码意思是,当我们子节点是一个数组时,循环遍历每一个节点,调用patch函数,patch函数会继续新旧dom对比,我们这里没有新旧dom对比,就是直接挂载,patch会重新调用mountElement函数,这样就会继续递归遍历,直到遍历到children不是数组为止
正确设置元素属性
正常的页面元素都是有属性的,以button为例
<button disabled></button>
什么这段属性,vue会解析成以下虚拟节点
const button = {
type='button',
props:{
disabled:''
}
}
这个虚拟节点,vue会这样把属性设置到节点上
el.setAttribute('disabled','')
这样做逻辑没问题,浏览器会把按钮禁用,但是虚拟节点是下面这样的
const button = {
type='button',
props:{
disabled:false
}
}
由于setAttribute设置的值会被字符串化,所以相当于执行这段代码
el.setAttribute('disabled','false')
所以要使用
el.disabled=false
但是,虚拟节点是这样
const button = {
type='button',
props:{
disabled:''
}
}
js会把空字符串转化为布尔值
el.disabled=false
这里本意是禁用按钮,但是执行的是不禁用的意思,所以这需要特殊处理
//挂载元素
function mountElement(vonde, container) {
//创建dom元素
//调用createElement函数创建元素
const el = createElement(vonde.type);
//处理子节点,如果子节点是字符串,代表是文本节点
if (typeof vonde.children === 'string') {
//设置元素的textContent
//调用setElementText设置元素文本节点
setElementText(el, vonde.children);
} else if (Array.isArray(vonde.children)) {
//如果是数组,则遍历每一个子节点,调用patch函数挂载
vonde.children.forEach((child) => {
patch(null, child, el);
});
}
if (vonde.props) {
for (const key in vonde.props) {
//用in操作符判断key是否存在对应的DOM Properties
if (key in el) {
//获取该 DOM Properties类型
const type = typeof el[key];
const value = vonde.props[key];
//如果是布尔值,且value为空字符串,则矫正为true
if (type === 'boolean' && value === '') el[key] = true;
else el[key] = value;
} else {
//如果没有对应DOM Properties 则使用setAttribute
el.setAttribute(key, vonde.props[key]);
}
}
}
//将元素添加到容器中
//调用insert函数插入容器内
insert(el, container);
}
使用in判断 判断key是否在该元素js对象上,如果有则以为有对应属性直接修改,没有直接调用setAttribute设置属性
const vonde = {
type: 'h1',
children: [
{
type: 'p',
children: 'hello',
},
{
type: 'button',
props: {
disabled: '',
},
children: 'Yes',
},
],
};
当元素属性为只读属性,如input中的from时,无法通过el.form进行设置,所以这一种情况需要特殊处理
//挂载元素
function mountElement(vonde, container) {
//创建dom元素
//调用createElement函数创建元素
const el = createElement(vonde.type);
//处理子节点,如果子节点是字符串,代表是文本节点
if (typeof vonde.children === 'string') {
//设置元素的textContent
//调用setElementText设置元素文本节点
setElementText(el, vonde.children);
} else if (Array.isArray(vonde.children)) {
//如果是数组,则遍历每一个子节点,调用patch函数挂载
vonde.children.forEach((child) => {
patch(null, child, el);
});
}
if (vonde.props) {
for (const key in vonde.props) {
//使用shouldSetAsProps判断是否用setAttribute设置属性
if (shouldSetAsProps(el, key, value)) {
//获取该 DOM Properties类型
const type = typeof el[key];
const value = vonde.props[key];
//如果是布尔值,且value为空字符串,则矫正为true
if (type === 'boolean' && value === '') el[key] = true;
else el[key] = value;
} else {
//如果没有对应DOM Properties 则使用setAttribute
el.setAttribute(key, vonde.props[key]);
}
}
}
//将元素添加到容器中
//调用insert函数插入容器内
insert(el, container);
}
为了代码可读性,将设置元素属性作为选项传递
const renderer = createRenderer({
createElement(tag) {
return document.createElement(tag);
},
//设置文本节点
setElementText(el, text) {
el.textContent = text;
},
//用于给指定的parent添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor);
},
//将属性设置操作封装到patchProps函数中,作为渲染器选项传递
patchProps(el, key, prevValue, nextValue) {
//使用shouldSetAsProps判断是否用setAttribute设置属性
if (shouldSetAsProps(el, key, nextValue)) {
//获取该 DOM Properties类型
const type = typeof el[key];
//如果是布尔值,且value为空字符串,则矫正为true
if (type === 'boolean' && nextValue === '') el[key] = true;
else el[key] = nextValue;
} else {
//如果没有对应DOM Properties 则使用setAttribute
el.setAttribute(key, nextValue);
}
},
});
挂载元素的函数
//挂载元素
function mountElement(vonde, container) {
//创建dom元素
//调用createElement函数创建元素
const el = createElement(vonde.type);
//处理子节点,如果子节点是字符串,代表是文本节点
if (typeof vonde.children === 'string') {
//设置元素的textContent
//调用setElementText设置元素文本节点
setElementText(el, vonde.children);
} else if (Array.isArray(vonde.children)) {
//如果是数组,则遍历每一个子节点,调用patch函数挂载
vonde.children.forEach((child) => {
patch(null, child, el);
});
}
if (vonde.props) {
for (const key in vonde.props) {
//调用patchProps函数
patchProps(el, key, null, vonde.props[key]);
}
}
//将元素添加到容器中
//调用insert函数插入容器内
insert(el, container);
}
class处理
class情况特殊,可以传字符串,对象,还有可能是数组,解析成虚拟节点可能为以下几种情况
//一
const vnode={
type:'p',
props:{
class:'foo bar'
}
}
//二
const vnode={
type:'p',
props:{
class:{foo:true,bar:false}
}
}
//三
const vnode={
type:'p',
props:['foo bar',{baz:true}]
}
因为class有很多种情况,所以要将class转换为字符串,这里封装一个normalizeClass函数
//不同数据类型转换字符串算法,将不同形式类型转换字符串
function normalizeClass(classData) {
const arr = [];
if (typeof classData === 'string') {
classData.split(' ').forEach((item) => arr.push(item));
} else if (Array.isArray(classData)) {
classData.forEach((item) => {
if (typeof item === 'string') arr.push(item);
else if (typeof item === 'object') {
for (const key in item) {
if (item[key]) arr.push(key);
}
}
});
} else if (typeof classData === 'object') {
for (const key in classData) {
if (classData[key]) arr.push(key);
}
}
return arr
}
应该有更好的写法,这里先这样等以后优化
const vonde = {
type: 'h1',
children: [
{
type: 'p',
children: 'hello',
},
{
type: 'button',
props: {
disabled: '',
class: normalizeClass(['foo bar', { baz: true }]),
},
children: 'Yes',
},
],
};
这些class就可以转换为字符串形式,vue发现,当使用el.className时性能是最好的,所以,需要优化以下设置属性的函数
//将属性设置操作封装到patchProps函数中,作为渲染器选项传递
patchProps(el, key, prevValue, nextValue) {
//对class进行特殊处理
if (key === 'class') {
el.className = nextValue || '';
}
//使用shouldSetAsProps判断是否用setAttribute设置属性
else if (shouldSetAsProps(el, key, nextValue)) {
//获取该 DOM Properties类型
const type = typeof el[key];
//如果是布尔值,且value为空字符串,则矫正为true
if (type === 'boolean' && nextValue === '') el[key] = true;
else el[key] = nextValue;
} else {
//如果没有对应DOM Properties 则使用setAttribute
el.setAttribute(key, nextValue);
}
},
这样,不管什么形式的class都可以挂载到节点上了
卸载操作
//第一次挂载
renderer.render(vonde, document.querySelector('#app'));
//第二次挂载
renderer.render(newVonde, document.querySelector('#app'));
看一下上面的代码,这是两次挂载操作,也就是更新dom,如果新的节点设为null,也就意味着什么都不渲染,也就是卸载之前渲染的内容.所以,要进行更新我们挂载节点的函数
function mountElement(vonde, container) {
//创建dom元素
//调用createElement函数创建元素
//让vnode.el引用真实dom
const el = (vonde.el = createElement(vonde.type));
//处理子节点,如果子节点是字符串,代表是文本节点
if (typeof vonde.children === 'string') {
//设置元素的textContent
//调用setElementText设置元素文本节点
setElementText(el, vonde.children);
} else if (Array.isArray(vonde.children)) {
//如果是数组,则遍历每一个子节点,调用patch函数挂载
vonde.children.forEach((child) => {
patch(null, child, el);
});
}
if (vonde.props) {
for (const key in vonde.props) {
//调用patchProps函数
patchProps(el, key, null, vonde.props[key]);
}
}
//将元素添加到容器中
//调用insert函数插入容器内
insert(el, container);
}
封装卸载函数
//卸载操作
function unmount(vonde) {
//根据vnode获取要卸载的真实dom元素
//获取el的父元素
const parent = vonde.el.parentNode;
//调用removeChild移除元素
if (parent) parent.removeChild(vonde.el);
}
优化渲染函数
//渲染函数
function render(vonde, container) {
if (vonde) {
patch(container._vonde, vonde, container);
} else {
if (container._vonde) {
//调用unmount函数卸载vnode
unmount(container._vnode);
}
}
container._vnode = vonde;
}