1、挂载子节点和元素属性
1.1挂载子节点
一个元素除了文本节点外, 还可以包含其他元素子节点, 非文本节点可以创建为数组
javascript
const vnode = {
type: "div",
children: [
{
type: "p",
children: "hello"
}
]
}
这时候对应修改mountElement函数
javascript
function 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 => {
patch(null, child, el)
})
}
insert(el, container);
}
- patch的第一个参数为null, 挂载阶段, 没有旧的vnode
- patch的第三个参数为el, 为刚刚创建的节点
1.2元素属性
javascript
const vnode = {
type: "div",
props: {
id: "foo"
},
children: [
{
type: "p",
children: "hello"
}
]
}
javascript
function 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 => {
patch(null, child, el)
})
}
if(vnode.props) {
for(let key in vnode.props) {
//HTML Attributes和 DOM Properties都可以设置属性
el.setAttribute(key, vnode.props[key])
}
}
insert(el, container);
}
2、HTML Attributes 和 DOM Properties
-
html attributes和dom properties的名字不总是一模一样的, 例如 class = "foo" 和 el.className
-
不是所有的 html attributes都有之对应的dom properties, 例如 aria-*属性
-
不是所有的 dom properties都有之对应的html attributes, 例如 el.textContent
-
html attributes的作用是设置与之对应的dom properties的初始值, 一旦值改变, 那么DOM properties始终储存着当前值, 而通过getAttribute函数得仍然是初始值
javascript<input value= "foo" /> console.log(el.value) // foo ; console.log(el.getAttribute("value")); // foo 修改文本框的的值变成 bar console.log(el.value) // bar ; console.log(el.getAttribute("value")); // foo // value可以通过另外一个dom properties获取默认值 console.log(el.defaultValue) // foo
-
html attributes提供的默认值不合法, 那浏览器会使用内建的合法值作为对应的dom properties的值
javascript<input type="foo" /> console.log(el.type) // "text"
3、正确设置元素属性
对于html文件来说, 当浏览器解析HTML代码后, 会自动分析HTML Attributes并设置合适的DOM properties,但是用户编辑vue文件中的模板不会被浏览器解析, 所以原本需要浏览器完成的工作, 现在需要框架来完成
3.1、浏览器中
css
<button disabled> Button </button>
浏览器会对应设置 el.disabled = true
3.2、template中
css
const button = {
type: "button",
props: {
disabled: "", // 按钮设置了禁用
}
}
-
通过setAttribute设置
arduinoel.setAttribute("disabled", "") // 按钮禁用
问题: 当设置为false的时候
arduinoel.setAttribute("disabled". false); // 按钮禁用
原因: 使用setAttribue设置的值总是会被字符串化, 上面设置等价于
arduinoel.setAttribute("disabled". "false"); // 按钮禁用, 与预期相反
-
通过DOM Properties设置
iniel.disabled = false // 设置正常
问题: 为设置的值为""的时候, 按钮禁用
原因:因为el.disable为bool类型, 当设置为空字符串时, 浏览器就将值矫正为bool,即是false,所以上面代码的执行结果为
iniel.disabled = false
3.3、处理方法
注意: 存在部分属性dom properties可以读取, 但是实际应该通过setAttribute设置
ini
<form id = "form1"></form>
<input :form="form1" />
javascript
function shouldSetAsProps(el, key, value) {
if(el.tagName === "INPUT" && key === "form") return false;
return key in el;
}
function 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 => {
patch(null, child, el)
})
}
if(vnode.props) {
for(let key in vnode.props) {
let value = vnode.props[key];
if(shouldSetAsProps(el, key, value)){
let type = typeof el[key];
if(type === "boolean" && value === "") {
el[key] = true;
} else {
el[key] = value;
}
} else {
el.setAttribute(key, value)
}
}
}
insert(el, container);
}
3.4、抽取设置属性
javascript
function createRenderer(options) {
const{ createElement, setElementText, insert, patchProps } = options;
function 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 => {
patch(null, child, el)
})
}
if(vnode.props) {
for(let key in vnode.props) {
patchProps(el, key, null, vnodeProps[key])
}
}
insert(el, container);
}
// n1旧vnode n2新vnode, container容器
function patch(n1, n2, container) {
if(!n1) {
mountElement(n2, container);
}
}
function render(vnode, container) {
if(vnode){
// 打补丁(挂载也是一种特殊的打补丁)
patch(container._vnode, vnode, container)
} else {
if(container._vnode) {
container.innerHTML = ";"
}
}
container._vnode = vnode;
}
return {
render,
}
}
const renderer = createRenderer({
createElement(tag){
return document.createElement(tag);
},
setElementText(el, text){
console.log(text)
el.textContent = text;
},
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor) // parent父节点 el需要插入的节点 anchor插入时需要插入在这个节点前面
},
patchProps(el,key, oldValue, newValue) {
// 暂时放在这里
function shouldSetAsProps(el, key, value) {
if(el.tagName === "INPUT" && key === "form") return false;
return key in el;
}
if(shouldSetAsProps(el, key, newValue)){
let type = typeof el[key];
if(type === "boolean" && newValue === "") {
el[key] = true;
} else {
el[key] = newValue;
}
} else {
el.setAttribute(key, newValue)
}
}
});
const vnode = {
type: "button",
props: {
disabled: ""
},
children: "按钮"
}
renderer.render(vnode, document.getElementById("app"));
4、class的处理
class有三种方式绑定
javascript
const vnode = {
type: "p",
props: {
class: "foo bar"
}
}
javascript
const vnode = {
type: "p",
props: {
class: {foo: true, bar: true}
}
}
javascript
const vnode = {
type: "p",
props: {
class:[ "zoo", {foo: true, bar: true}]
}
}
提供一个方法统一
javascript
let className = [ "zoo", {foo: true, bar: true}]
function normalizeClass(className) {
if(typeof className === "string") return className;
if(className && Array.isArray(className)){
let name = [];
className.forEach(item => {
name.push(normalizeClass(item))
})
return name.join(" ");
}
if(className && typeof className === "object") {
let name = [];
for(let key in className) {
if(className[key]) name.push(key);
}
return name.join(" ");
}
return className
}
console.log(normalizeClass(className))
按照原本代码, class in el为false, 所以最后会使用setAttribute,
但是对比 setAttribue、el.className、el.classList的性能, el.className的性能最优
更改代码
javascript
patchProps(el,key, oldValue, newValue) {
// 暂时放在这里
function shouldSetAsProps(el, key, value) {
if(el.tagName === "INPUT" && key === "form") return false;
return key in el;
}
if(key === "class") {
el.className = newValue || ""
} else if(shouldSetAsProps(el, key, newValue)){
let type = typeof el[key];
if(type === "boolean" && newValue === "") {
el[key] = true;
} else {
el[key] = newValue;
}
} else {
el.setAttribute(key, newValue)
}
}
5、卸载操作
之前的卸载处理
javascript
function render(vnode, container) {
if(vnode){
// 打补丁(挂载也是一种特殊的打补丁)
patch(container._vnode, vnode, container)
} else {
if(container._vnode) {
container.innerHTML = ";"
}
}
container._vnode = vnode;
}
问题:
- 没有调用生命周期函数, beforeUnmonted, Unmonted
- 没有调用钩子函数的命令, beforeUnmonted, Unmonted
- 没有移除绑定在DOM元素上的事件处理函数
解决:
在vnode和真实的dom元素之间建立联系
javascript
function mountElement(vnode, container) {
// vnode和真实的el进行绑定
const el = vnode.el createElement(vnode.type);
if(typeof vnode.children === "string") {
setElementText(el, vnode.children)
} else if(Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
patch(null, child, el)
})
}
if(vnode.props) {
for(let key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container);
}
修改render函数
javascript
function render(vnode, container) {
if(vnode){
// 打补丁(挂载也是一种特殊的打补丁)
patch(container._vnode, vnode, container)
} else {
if(container._vnode) {
const el = container._vnode.el; // 获取真实的Dom元素
const parent = el.parent; // 获取el的父元素
if(parent) parent.removeChild(el); // 通过父元素移除el
}
}
container._vnode = vnode;
}
- removeChild移除的元素能够再次使用
- removeChild的性能应该比innerHtml好, 理解上
抽取移除
javascript
unmount(vnode) {
const parent = vnode.el.parent;
if(parent) parent.removeChild(vnode.el);
}
- 在unmount函数, 有机会调用绑定在DOM元素上的指令钩子函数
- 在unmount函数, 可以判断虚拟节点的类型, 组件相关的生命周期函数
6、区分vnode的类型
javascript
function patch(n1, n2, container) {
if(n1 && n1.type !== n2.type) { // 如果类型不同直接重新挂载
unmount(n1);
n1 = null;
}
let { type } = n2;
if(typeof type === "string") { // 节点是普通标签元素
if(!n1) {
mountElement(n2, container); //挂载节点
} else {
patchElement(n1, n2); // 更新节点
}
} else if(typeof type === "object"){ // 节点是组件
} else {
// 省略了其他类型的vnode
}
}
7、事件的处理
7.1、描述事件
在vnode.props对象中, 凡是以on开发的属性都视为事件
7.2、把事件添加到DOM元素上
javascript
const vnode = {
type: "p",
props: {
onClick: () => {
console.log("click");
}
}
}
javascript
patchProps(el,key, oldValue, newValue) {
// 暂时放在这里
function shouldSetAsProps(el, key, value) {
if(el.tagName === "INPUT" && key === "form") return false;
return key in el;
}
if(/^on/.test(key)) {
const name = key.slice(2).toLowerCase();
el.addEventListener(name, newValue)
} else if(key === "class") {
el.className = newValue || ""
} else if(shouldSetAsProps(el, key, newValue)){
let type = typeof el[key];
if(type === "boolean" && newValue === "") {
el[key] = true;
} else {
el[key] = newValue;
}
} else {
el.setAttribute(key, newValue)
}
},
当事件发生变化,
- removeEventListener -> addEventListener (确保事件只会触发一次)
- 把事件封装一层, 不必要进行移除, 性能更优
javascript
if(/^on/.test(key)) {
let invoker = el._vei;
const name = key.slice(2).toLowerCase();
if(newValue) {
if(!invoker) {
invoker = el._vei = (e) => {
invoker.value(e);
}
invoker.value = newValue;
el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
} else {
invoker.value = newValue;
}
// 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
}
}
问题1: 上面的代码存在事件覆盖, 需要改写
javascript
if(/^on/.test(key)) {
const name = key.slice(2).toLowerCase();
let invokers = el._vei || (el._vei = {});
let invoker = invokers[name];
if(newValue) {
if(!invoker) {
invoker = el._vei = (e) => {
invoker.value(e);
}
invoker.value = newValue;
el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
} else {
invoker.value = newValue;
}
// 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
} else if(invoker) {
el.removeEventListener(name, invoker)
}
}
问题2:可以多次调用addEventListener函数为元素绑定同一个类型的事件
javascript
const vnode = {
type: "p",
props: {
onClick: [
() => {console.log("hello")},
() => {console.log("world")}
]
},
children: "按钮"
}
// 在patchProps中
if(newValue) {
if(!invoker) {
invoker = el._vei = (e) => {
if(Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e))
} else {
invoker.value(e);
}
}
invoker.value = newValue;
el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
} else {
invoker.value = newValue;
}
// 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
} else if(invoker) {
el.removeEventListener(name, invoker)
}
}
8、事件冒泡和更新时机问题
javascript
const bol = ref(false);
effect(() => {
const vnode = {
type: "div",
props: bol.value ? {
onClick: () => {
alert("父元素click");
}
} : {} ,
children: [
{
type: "p",
props: {
onClick: () => {
bol.value = true;
alert("子元素click")
}
},
children: "text"
}
]
}
renderer.render(vnode, document.getElementById("app"));
})
渲染完成后, 由于bol.value为false, 不会为div元素绑定事件, 但是当鼠标点击p元素时候, click事件有p元素冒泡到div元素, 理论上div没有绑定事件, 不会触发事件的发生, 但是click元素确实发生了, 分析
- 点击p元素, 触发click事件, bol.value 设置为true
- bol.value为true,触发副作用函数重新执行, div元素绑定click事件
- 事件进行冒泡, 元素div绑定的click事件
解决方法
通过事件的绑定时间和触发事件进行对比, 绑定事件小于触发事件则不执行
javascript
if(/^on/.test(key)) {
const name = key.slice(2).toLowerCase();
let invokers = el._vei || (el._vei = {});
let invoker = invokers[name];
if(newValue) {
if(!invoker) {
invoker = el._vei = function(e) {
if(e.timeStamp < invoker.attached) return;
if(Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e))
} else {
invoker.value(e);
}
}
invoker.value = newValue;
invoker.attached = performance.now();
el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
} else {
invoker.value = newValue;
}
// 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
} else if(invoker) {
el.removeEventListener(name, invoker)
}
}
额外知识点:
- date.now 是js的内置函数, 返回当前的时间戳,不依赖操作系统计时器, 精度较低, 受到系统时间调整或者线程调度影响
- performance.now依赖操作系统计时器, 精度高, 在运行中受到操作系统影响较大
9、更新子节点
子节点的三种情况
less
// 没有子节点
<div></div>
// 文本子节点
<div>Text</div>
// 多个子节点
<div>
<p/>
<p/>
</div>
对应的虚拟节点
javascript
vnode1 = {
type: "div",
children: null,
}
vnode2 = {
type: "div",
children: "Text"
}
vnode3 = {
type: "div",
children: [
{ type: "p", children: null},
{ type: "p", children: null},
]
}
所以对应更新节点的时候就会有9种情况
- 没有 → 没有新增
- 没有 → 文本 新增
- 没有 → 数组 新增
- 文本 → 没有 卸载
- 文本 → 文本 更新
- 文本 → 数组 更新
- 数组 → 没有 卸载
- 数组 → 文本 更新
- 数组 → 数组 更新
javascript
function patchElement(n1, n2) {
const el = n2.el = n1.el;
const oldProps = n1.props;
const newProps = n1.props;
for(const key in newProps) {
if(newProps[key] !== oldProps[key]){
patchProps(el, key, oldProps[key], newProps[key])
}
}
for(const key in oldProps) {
if(!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
patchChildren(n1, n2, el)
}
javascript
function patchChildren(n1, n2, container) {
if(typeof n2.children === "string"){ // 1、新节点是文本节点, 1.1、没有节点 直接设置 1.2、文本节点 直接设置 1.3、数组节点 卸载新增
if(Array.isArray(n1.children)){
n1.children.forEach(child => unmount(child))
}
setElementText(container, n2.children);
} else if(Array.isArray(n2.children)) { // 2、新节点是数组节点
if(Array.isArray(n1.children)) { // 2.1、节点是数组节点 // 这里是diff算法, 暂时全部卸载后再更新
n1.children.forEach(child => unmount(child))
n2.children.forEach(c => patch(null, c, container))
} else { // 2.2、节点为空 不处理后挂载,2.3、文本节点 设置为空后挂载
setElementText(container, "");
n2.children.forEach(c => patch(null, c, container))
}
} else { // 3、新节点为空, 3.1、没有节点 不用处理 3.2、文本节点 设置为空, 3.3、数组节点 直接卸载
if(Array.isArray(n1.children)) {
n1.children.forEach(child => unmount(child))
} else if(typeof n1.children === "string") {
setElementText(container, "");
}
}
}
10、文本节点和注释节点
-
文本节点
因为文本节点不具有标签名称, 需要人为创造唯一的标识
javascript// 创建唯一的标识 const Text = Symbol() const newVnode = { type: "Text", children: "文本内容" } const Comment = Symbol() const newVnode = { type: Comment, children "注释内容" }
javascriptif(type === Text){ // 前面有判断,能进入到这个判断分支的, 要不节点为空, 要不同为Text节点 if(!n1) { const el = n2.el = createText(n2.children); insert(el, container, null); } else { const el = n2.el = n1.el; if(n1.children !== n2.children) { setText(el, n2.children) } } }
javascriptconst renderer = createRenderer({ .... createText(text) { return document.createTextNode(text) }, setText(el, text) { el.nodeValue = text; } });
-
注释节点: 处理方式跟文本节点类似, 不同的是需要使用document.createComment函数创建注释节点
11、Fragment
以下代码在vue2中实现不了, 但是vue3中可以
html
<List>
<Item/>
</List>
html
<!-- list.vue -->
<template>
<ul>
<slot/>
</ul>
</template>
html
<!-- item.vue -->
<template>
<li>1</li>
<li>1</li>
</template>
javascript
const vnode = {
type: Fragment,
children: [
{type: "li", children: "1"},
{type: "li", children: "2"},
{type: "li", children: "3"},
]
}
javascript
if(type === Fragment) {
if(!n1) {
n2.children.forEach(c => patch(null, c, container))
} else {
patchChildren(n1, n2, container)
}
}
javascript
function unmount(vnode) {
if(vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
}
const parent = vnode.el.parent;
if(parent) parent.removeChild(vnode.el);
}
重点, 其实就相当于隔了一层挂载, 隔了一层生成, 这一层是没有没有任何内容, 没有属性, 没有方法, 所以不需要处理属性方法更新,也不需要卸载元素
全部代码
javascript
const Text = Symbol();
const Comment = Symbol();
const Fragment = Symbol();
function createRenderer(options) {
const{ createElement, setElementText, insert, patchProps, unmount, setText, createText } = options;
function mountElement(vnode, container) {
const el = vnode.el = createElement(vnode.type);
if(typeof vnode.children === "string") {
setElementText(el, vnode.children)
} else if(Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
patch(null, child, el)
})
}
if(vnode.props) {
for(let key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container);
}
function patchElement(n1, n2) {
const el = n2.el = n1.el;
const oldProps = n1.props;
const newProps = n1.props;
for(const key in newProps) {
if(newProps[key] !== oldProps[key]){
patchProps(el, key, oldProps[key], newProps[key])
}
}
for(const key in oldProps) {
if(!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
patchChildren(n1, n2, el)
}
function patchChildren(n1, n2, container) {
if(typeof n2.children === "string"){ // 1、新节点是文本节点, 1.1、没有节点 直接设置 1.2、文本节点 直接设置 1.3、数组节点 卸载新增
if(Array.isArray(n1.children)){
n1.children.forEach(child => unmount(child))
}
setElementText(container, n2.children);
} else if(Array.isArray(n2.children)) { // 2、新节点是数组节点
if(Array.isArray(n1.children)) { // 2.1、节点是数组节点 // 这里是diff算法, 暂时全部卸载后再更新
n1.children.forEach(child => unmount(child))
n2.children.forEach(c => patch(null, c, container))
} else { // 2.2、节点为空 不处理后挂载,2.3、文本节点 设置为空后挂载
setElementText(container, "");
n2.children.forEach(c => patch(null, c, container))
}
} else { // 3、新节点为空, 3.1、没有节点 不用处理 3.2、文本节点 设置为空, 3.3、数组节点 直接卸载
if(Array.isArray(n1.children)) {
n1.children.forEach(child => unmount(child))
} else if(typeof n1.children === "string") {
setElementText(container, "");
}
}
}
// n1旧vnode n2新vnode, container容器
function patch(n1, n2, container) {
if(n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
let { type } = n2;
if(typeof type === "string") { // 节点是普通标签元素
if(!n1) {
mountElement(n2, container); //挂载节点
} else {
patchElement(n1, n2); // 更新节点
}
} else if(typeof type === "object"){ // 节点是组件
} else if(type === Text){
// 前面有判断,能进入到这个判断分支的, 要不节点为空, 要不同为Text节点
if(!n1) {
const el = n2.el = createText(n2.children);
insert(el, container, null);
} else {
const el = n2.el = n1.el;
if(n1.children !== n2.children) {
setText(el, n2.children)
}
}
} else if(type === Fragment) {
if(!n1) {
n2.children.forEach(c => patch(null, c, container))
} else {
patchChildren(n1, n2, container)
}
} else {
// 省略了其他类型的vnode
}
}
function render(vnode, container) {
if(vnode){
// 打补丁(挂载也是一种特殊的打补丁)
patch(container._vnode, vnode, container)
} else {
if(container._vnode) {
unmount(container._vnode);
}
}
container._vnode = vnode;
}
return {
render,
}
}
function createElement(tag){
return document.createElement(tag);
}
function setElementText(el, text){
el.textContent = text;
}
function insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor) // parent父节点 el需要插入的节点 anchor插入时需要插入在这个节点前面
}
function patchProps(el,key, oldValue, newValue) {
// 暂时放在这里
function shouldSetAsProps(el, key, value) {
if(el.tagName === "INPUT" && key === "form") return false;
return key in el;
}
if(/^on/.test(key)) {
const name = key.slice(2).toLowerCase();
let invokers = el._vei || (el._vei = {});
let invoker = invokers[name];
if(newValue) {
if(!invoker) {
invoker = el._vei = function(e) {
if(e.timeStamp < invoker.attached) return;
if(Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e))
} else {
invoker.value(e);
}
}
invoker.value = newValue;
invoker.attached = performance.now();
el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
} else {
invoker.value = newValue;
}
// 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
} else if(invoker) {
el.removeEventListener(name, invoker)
}
} else if(key === "class") {
el.className = newValue || ""
} else if(shouldSetAsProps(el, key, newValue)){
let type = typeof el[key];
if(type === "boolean" && newValue === "") {
el[key] = true;
} else {
el[key] = newValue;
}
} else {
el.setAttribute(key, newValue)
}
},
function unmount(vnode) {
if(vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
}
const parent = vnode.el.parent;
if(parent) parent.removeChild(vnode.el);
}
function createText(text) {
return document.createTextNode(text)
}
function setText(el, text) {
el.nodeValue = text;
}
const renderer = createRenderer({
createElement,
setElementText,
insert,
patchProps,
createText,
setText
});