利用响应系统的能力,自动调用渲染器完成页面的渲染和更新
const { ref, effect } = Vue;
function renderer(domString, container) {
container.innerHTML = domString;
}
const count = ref(0);
effect(() => {
renderer(`<h1>${count.value}</h1>`, document.getElementById('app'));
});
count.value++;
渲染器renderer: 把虚拟DOM渲染成为特定平台的真是元素。
render函数: 以container为挂载点,将vnode渲染为真实的DOM并添加到挂载点下。
render函数基本实现
function createRenderer() {
// 挂载
function mountElement(vnode, container) {
const el = document.createElement(vnode.type);
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
}
// 将元素添加到容器中
conatiner.appendChild(el)
}
// 更新打补丁
function patch(n1, n2, container) {
if (!n1) {
// 挂载
mountElement(n2, container)
}
else {
// 更新,打补丁,暂时忽略
}
}
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container);
} else {
if (container._vnode) {
// 旧vnode存在,新vnode不存在 => 卸载
container.innerHTML = '';
}
}
container._vnode = vnode;
}
return {
render,
};
}
此函数中大量依赖了浏览器的API(document.*)如何修改成通用渲染器呢?
抽象通用渲染器
抽离浏览器特有的API
将操作DOM的API作为配置项,作为createRenderer函数的参数传递。这样就可以在函数内部通过配置项来获取操作DOM的API。
const rener = createRenderer({
// 用于创建元素
createElement(tag) {
return document.createElement(tag);
},
// 用于设置元素的文本节点
setElementText(el, text) {
el.textContent = text;
},
// 用于在给定parent下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor);
},
});
function createRenderer(options) {
const { createElement, setElementText, insert } = options;
function mountElement(vnode, container) {
const el = createElement(vnode.type);
if (typeof el.children === 'string') {
setElementText(el, vnode.children);
}
insert(el, container);
}
// 更新打补丁
function patch(){}
function render(){}
return {
render
}
}
自定义渲染器通过抽象的手段让核心代码不再依赖平台特有的API,再通过个性化配置的能力来实现跨平台。
挂载与更新
vnode.children定义:
// 虚拟节点: 版本vnode-1
const vnode = {
type: 'div',
children: [
{
type: 'p',
children: 'hello'
}
]
}
vnode.children是一个数组,每个元素都是一个独立的虚拟节点对象,这样就形成了树形结构,即: 虚拟DOM树。
// 挂载方法:版本mountElement-1
function mountElement(vnode, container) {
const el = document.createElement(vnode.type);
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
}
else if (Array.isArray(vnode.children)) {
// 遍历每一个节点,调用patch挂载
vnode.children.forEach(child => {
paych(null, child, el)
})
}
// 将元素添加到容器中
insert(el, container);
}
版本mountElement-1
中需要注意两点:
- patch第一个参数为null,因为挂载阶段没有旧vnode。
- path第三个参数为挂载点,因为正在挂载的子元素是div标签的子节点,需要吧刚创建的div元素作为挂载点。
完成挂载后,看一下vnode如何描述一个标签属性。虚拟节点增加vnode.props字段描述元素属性
// 虚拟节点: 版本vnode-2
const vnode = {
type: 'div',
props: {
id: 'foo'
},
children: [
{
type: 'p',
children: 'hello'
}
]
}
props是一个对象,可以通过遍历对象把属性渲染到对应元素上:
// 挂载方法:版本mountElement-2
function mountElement(vnode, container) {
const el = document.createElement(vnode.type);
// ...省略children部分代码
if (vnode.props) {
for(const key in vnode.props){
el.setAtrriubute(key, vnode.peops[key])
// 或者直接设置 el[key] = vnode.props[key]
}
}
// 将元素添加到容器中
insert(el, container);
}
HTML Attributes 与 DOM Properties
HTML Attributes的作用是设置与之对应的 DOM Properties的初始值,一旦值改变,那么 DOM Properties始终存储当前值,而通过getAttribute函数得到的仍然是初始值。
// case: <input value="foo"> // 如果用户修改了输入框的值为bar,那么通过el.value获取的 DOM Properties值为bar,但是el.getAttribute('value')值还是foo el.value // bar el.getAttribute('value') // foo
setAttribute设置的值总会被字符化
el.setAttribute('disabled', false) el.setAttribute('disabled', 'false') // 二者等价
事件的处理
// 虚拟节点: 版本vnode-3
const vnode = {
type: 'div',
props: {
id: 'foo',
onClick: () => {
alert('alert')
}
},
children: [
{
type: 'p',
children: 'hello'
}
]
}
对事件的处理,通过调用addEventListener函数来绑定事件
// 版本patchProps-1
patchProps(el, key, prevValue, nextValue){
// 匹配on开头的属性,视为事件
if (/^on/.test(key)) {
// onClick => click
const name = key.slice(2).toLoweCase();
el.addEventListener(name, nextValue)
}
}
事件更新如何处理呢?
常规方法先调用removeEventListener移除之前的事件,再绑定新的事件,但这种方式性能不好,有中更优的方式处理:
绑定一个伪造的事件处理函数invoker,然后把真正的事件处理函数设置为invoker.value属性的值。这样当更新事件的时候,我们不再需要调用removeEventListener函数移除上一次的绑定事件,只需要更新invoker.value的值即可。
// 版本patchProps-2
patchProps(el, key, prevValue, nextValue){
// 匹配on开头的属性,视为事件
if (/^on/.test(key)) {
// 获取为该元素伪造的事件处理函数
let invoker = el._vei;
const name = key.slice(2).toLoweCase();
if (nextValue) {
if (!invoker) {
invoker = el._vei = e => {
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
invoker.value(e);
}
invoker.value = nextValue;
el.addEventListener(name, invoker);
}
else {
invoker.value = nextValue;
}
}
else if (invoker) {
// 无新的绑定,移除之前的绑定
el.removeEventListener(name, invoker);
}
}
}