跳到主要内容

认识

2023年06月10日
柏拉文
越努力,越幸运

一、认识


mountElement 主要用于虚拟 DOM 节点渲染为 真实 DOM 节点, 并挂载到对应容器。基本实现如下所示:

const processElement = (n1, n2, container,anchor) => {
if(n1 == null){
mountElement(n2,container,anchor);
}else{
patchElement(n1,n2,parentComponent);
}
};

const mountElement = (vnode, container) => {
const el = createElement(vnode.type);

if (typeof vnode.children === 'string') {
setElementText(el, vnode.children);
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
/**
* @description: patch 挂载点需要更改为当前 vnode 创建的 DOM 元素 el, 保证 child 子节点挂载到正确的位置
*/
patch(null, child, el);
});
}

if (vnode.props) {
for(let key in vnode.props){
patchProp(el,key,null,props[key]);
}
}

insert(el, container);
};

二、mount 细节


挂载子节点时, 首先要区分其类型:

  1. 如果 vnode.children 是字符串: 则说明元素具有文本子节点

  2. 如果 vnode.children 是数组: 则说明元素具有多个子节点

三、props 细节


3.1 patchProp

export function patchProp(el, key, prevValue, nextValue) {
if (key === 'class') {
patchClass(el, nextValue);
} else if (key === 'style') {
patchStyle(el, prevValue, nextValue);
} else if (shouldSetAsProps(el, key, nextValue)) {
// 通过 shouldSetAsProps 判断 key 属性是否是可读属性 或者 存在于 DOM Properties 中
const type = typeof el[key];
if (type === 'boolean' && nextValue === '') {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
// 如果要设置的属性没有对应的 DOM Properties 或者是可读属性, 则使用 setAttribute 来设置属性
el.setAttribute(key, nextValue);
}
}

3.2 patchProps

patchProps 通过两个循环, 来对属性进行挂载或者更新。

  1. 循环旧属性: 如果旧属性不在新属性中, 去除

    if (oldProps !== EMPTY_OBJ) {
    for (const key in oldProps) {
    if (!(key in newProps)) {
    hostPatchProp(el, key, oldProps[key], null);
    }
    }
    }
  2. 循环新属性: 如果新属性值发生改变, 更新

    for (const key in newProps) {
    const next = newProps[key];
    const prev = oldProps[key];
    if (next !== prev) {
    hostPatchProp(el, key, prev, next);
    }
    }

3.3 shouldSetAsProp

readonly attribute 只能够通过 el.setAttribute() 来设置。比如:

  • input: input.form

3.4 patchClass

class attribute 设置的方式有以下三种:

  • el.setAttribute()

  • el.className = xx

  • el.classList.add()

其中, el.className 的性能最优。因此, 通过 el.className = xxx 处理 class attribute

3.5 patchStyle

3.6 patchEvent

vnode.props 中, 凡是以字符串 on 开头的属性都视作事件,如下所示:

const vnode = {
type: 'p',
props: {
onClick: ()=>{
console.log("onClick");
}
},
children: 'p - children'
}

vnode.props.on事件 将事件绑定到 DOM 元素的过程如下:

  1. 匹配以 on 开头的属性,视为事件

  2. 根据属性名称得到对应的事件名称: onClick --> click

  3. 通过性能高优的方式绑定事件、更好的完成事件更新: 在绑定事件时,绑定一个伪造的事件处理函数 invoker, 然后把真正的处理函数设置为 invoker.value 属性的值。这样更新事件的时候,我们不再需要调用 removeEventListener 函数来移除上一次绑定的事件,只需要更新 invoker.value 的值即可。

  4. 通过为伪造的事件处理函数invoker添加invoker.attached属性, 用来存储事件处理函数被绑定的时间。然后,在 invoker 执行的时候, 通过事件对象的 e.timeStamp 获取事件发生的时间。最后,比较两者, 如果事件处理函数被绑定的时间晚于事件发生的时间,则不执行该事件处理函数。这样一来,屏蔽了所有绑定时间晚于事件触发时间的事件处理函数的执行, 解决了事件冒泡与事件更新之间相互影响的问题。

vnode.props.on事件 将事件绑定到 DOM 元素的实现如下:

function patchEvent(el, key, prevValue, nextValue) {
const invokers = el._vei || (el._vei = {});
let invoker = invokers[key];
const name = key.slice(2).toLowerCase();
if (nextValue) {
/**
* @description: 如果 nextValue 存在 且 没有 invoker ,伪造一个 invoker 并缓存到 el._vei 中
*/
if (!invoker) {
invoker = el._vei[key] = e => {
/**
* @description: 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
*/
if (e.timeStamp < invoker.attached) {
return;
}

/**
* @description: invoker.value 是数组,则遍历 invoker.value 逐个调用事件处理函数
*/
if (Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e));
} else {
invoker.value(e);
}
};
invoker.value = nextValue;
/**
* @description: invoker.attached 存储事件处理函数被绑定的时间
*/
invoker.attached = performance.now();
el.addEventListener(name, invoker);
} else {
/**
* @description: 如果 nextValue 存在 且 之前有 invoker, 只需要更新 invoker.value 的值即可
*/
invoker.value = nextValue;
}
} else if (invoker) {
/**
* @description: 如果 nextValue 不存在, 且 之前有 invoker, 则移除绑定事件
*/
el.removeEventListener(name, invoker);
}
}

3.7 HTML Attributes VS DOM Properties

HTML Attributes 指的是定义在 HTML 标签上的属性。比如说 <div id="class-div"></div>,其中, id 就是 HTML Attribute。当浏览器解析 HTML 之后, 会创建一个与之相符的 DOM 元素对象, 这个 DOM 对象会包含很多属性 Properties。这些属性就是 DOM Properties。很多 HTML AttributesDOM 对象上有很多同名的 DOM Properties,也有一些不同的。HTML AttributesDOM Properties 中相同名称的属性可以看成是 直接映射, 不同的名称可能会有关联。但是在 VueHTML AttributesDOM Properties 有价值的关系在于: HTML Attributes 作用是设置 与之对应的 DOM Properties 的初始值

3.8 el.setAttribute(key,value) VS el[key] = value

el.setAttribute(key,value)HTML 设置属性。其中 value 无论是什么类型, 最后都会被转化为字符串类型, 比如 el.setAttribute(key,false) => el.setAttribute(el,'false')。所以,我们不能总是使用 setAttribute 来将 vnode.props 中的属性设置到 el 上。

el[key] = valueDOM 设置属性。当我们设置 el['disable'] = '' 时, 浏览器解析空字符串时, 会将它置为 true。而我们设置 el['disable'] = '' 却将 el['disable'] = false , 违背了用户的本意, 且我们的实现与浏览器的实现不一致。

因此, 我们需要灵活的在 el.setAttribute(key,value)el[key] = value 中切换, 即优先设置元素的 DOM Properties,但当值为空字符串时, 要手动将值矫正为 true。只有这样, 才能保证代码符合预期。