VirtualDOM是什么
Virtual DOM 对象就是DOM对象的JavaScript表现形式,就是使用JavaScript对象来描述DOM对象信息
<div className="app">
<h1>Hello</h1>
<p>Hello,world</p>
</div>
复制代码
上面代码转化为jsx代码
{
type: 'div',
props: {
className: 'app'
},
children: [{
type: 'h1',
props: null,
children: [{
type: 'text',
props: {
textContent: 'Hello'
}
},
{
type: 'p',
props: null,
children: [{
type: 'text',
props: {
textContent: 'Hello,world'
}
}]
}]
}
复制代码
环境搭建
npm init -y
# 安装依赖 webpack相关 和 babel
npm i -D webpack webpack-dev-server webpack-cli @babel/core @babel/preset-env @babel/preset-react babel-loader html-webpack-plugin
复制代码
配置webpack
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/**@type {import ('webpack').Configuration} */
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'main.bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [{
test: /\.js(x?)$/,
loader: 'babel-loader'
}]
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 8080,
hot: true,
},
devtool: 'source-map',
plugins: [
new HtmlWebpackPlugin({
title: 'Simple react',
template: './public/index.html'
}),
new webpack.HotModuleReplacementPlugin({})
]
}
复制代码
创建webpack入口文件src/main.js
、public/index.html
配置babel,其中pragma
是编译 JSX 表达式时替用于换所使用的函数,默认是React.createElement
,这个我们可以查看 www.babeljs.cn/docs/babel-…
{
"presets": [
"@babel/preset-env",
["@babel/preset-react", {
"pragma": "SpReact.createElement"
}]
]
}
复制代码
这里我们只需要把它配置成我们想要的函数名即可
这样开发环境就搭建完毕
创建VirtualDOM对象
创建SpReact/index.js
、SpReact/createElement.js
SpReact/index.js
用来导出所有SpReact
上的所有方法
import createElement from './createElement'
export default {
createElement
}
复制代码
SpReact.createElement
用来创建VirtualDOM对象,之前我已经将编译 JSX 表达式换成了这个
export default function createElement(type, props, ...children) {
return {
type,
props,
children
}
}
复制代码
就这个,一个简单的VirtualDOM对象就创建完成了,后续会逐渐完善这个方法
在main.js
中打印下转换过后的VirtualDOM对象
import SpReact from "./SpReact"
const jsx = (<div><h1>VirtualDOM</h1><div>创建VirtualDOM</div></div>)
console.log(jsx)
复制代码
转换过后的jsx
{
"type": "div",
"props": null,
"children": [{
"type": "h1",
"props": null,
"children": ["VirtualDOM"]
}, {
"type": "div",
"props": null,
"children": ["创建VirtualDOM"]
}]
}
复制代码
我们需要在完善一下
- 文本节点为对应的virtualDOM转换成对象
- 布尔值和null在页面上不显示
- props中可以访问到children
export default function createElement(type, props, ...children) {
const newChildren = children.reduce((res, child) => {
if (typeof child === 'string') {
res.push(createElement('text', {
textContent: child
}))
} else if (typeof child !== 'boolean' && child !== null) {
res.push(child)
}
return res
}, [])
return {
type,
props:{
...props,
children: newChildren
},
children: newChildren
}
}
复制代码
VirtualDOM对象转化为真实DOM对象
创建render
方法,我们会调用SpReact.render
方法将VirtualDOM
渲染到页面上
import diff from "./diff";
export default function render(VirtualDOM, container, oldDOM = container.lastChild) {
diff(VirtualDOM, container, oldDOM)
}
复制代码
export default function diff(virtualDOM, container, oldDOM) {
if (!oldDOM) {
mountElement(virtualDOM, container)
}
}
复制代码
// 创建的DOM节点,将创建的DOM节点添加到页面上
function mountElement(virtualDOM, container) {
// 创建当前virtualDOM的DOM节点,遍历创建子元素,调用mountElement
const dom = createDOM(virtualDOM)
// 将创建的DOM节点添加到页面上
container.appendChild(dom)
}
// 创建当前virtualDOM的DOM节点, 遍历创建子元素的DOM节点
function createDOM(virtualDOM) {
const type = virtualDOM.type
let el
if (type === 'text') {
el = document.createTextNode(virtualDOM.props.textContent)
} else {
el = document.createElement(type)
}
virtualDOM.children.forEach(child => {
mountElement(child, el)
})
return el
}
复制代码
给DOM元素添加属性
const jsx = (<div className="aa"><h1 id="title" onClick={fn}>VirtualDOM</h1><div>创建VirtualDOM</div></div>)
复制代码
我们需要为元素节点添加事件
function createDOM(virtualDOM) {
const type = virtualDOM.type
let el
if (type === 'text') {
el = document.createTextNode(virtualDOM.props.textContent)
} else {
el = document.createElement(type)
// 绑定事件
updateNodeElement(el,virtualDOM)
}
virtualDOM.children.forEach(child => {
mountElement(child, el)
})
return el
}
复制代码
把props上的属性(除了children)添加到DOM上
export default function updateNodeElement(el, virtualDOM) {
const props = virtualDOM.props
Object.keys(virtualDOM.props).forEach(propName => {
const val = props[propName]
if (propName.startsWith('on')) {
const eventName = propName.slice(2).toLowerCase()
el.addEventListener(eventName, val)
} else if (propName === 'className') {
el.setAttribute('class', val)
} else if (propName !== 'children') {
el.setAttribute(propName, val)
}
})
}
复制代码
组件
区分函数组件和类组件
// 区分是否是组件
export function isComponent(virtualDOM){
return virtualDOM&& typeof virtualDOM.type === 'function'
}
// 判断是否是函数组件
export function isFunctionComponent(virtualDOM){
// (virtualDOM.prototype && virtualDOM.prototype.render)表示是类组件,
// 因为类组件下有render方法,即使没有定义,也会从父类中(Component)继承
return isComponent(virtualDOM)&& !(virtualDOM.type.prototype && virtualDOM.type.prototype.render)
}
复制代码
首先对之前的代码进行改造
- 判断是否为组件,不是组件就调用
mounNativeElement
- 是组件就调用
mountComponent
- 判断是否是函数组件
- 函数组件调用
buildFunctionComponent
- 类组件调用
buildClassComponent
- 最后,将返回的虚拟DOM调用
mountElement
- 这里没有直接调用
mounNativeElement
是因为组件可能会返回一个组件
- 这里没有直接调用
function mountElement(virtualDOM, container) {
if (isComponent(virtualDOM)) {
mountComponent(virtualDOM, container)
} else {
mounNativeElement(virtualDOM, container)
}
}
function mounNativeElement(virtualDOM, container) {
const dom = createDOM(virtualDOM)
container.appendChild(dom)
}
function mountComponent(virtualDOM, container) {
let newVirtualDOM
if (isFunctionComponent) {
newVirtualDOM = buildFunctionComponent(virtualDOM)
} else {
newVirtualDOM = buildClassComponent(virtualDOM)
}
mountElement(newVirtualDOM, container)
}
复制代码
处理函数组件
函数组件处理非常简单,只需要调用函数组件,将函数组件返回的virtualDOM返回即可
function buildFunctionComponent(virtualDOM) {
return virtualDOM.type(virtualDOM.props || {})
}
复制代码
function App(){
return (<div>123</div>)
}
SpReact.render(<App/>, document.getElementById('container'))
复制代码
处理类组件
首先需要定义类组件的父类
export default class Component {
constructor(props){
this.props = props
}
render() {
}
}
复制代码
返回组件render函数的VirtualDOM
function buildClassComponent(virtualDOM) {
const component = new virtualDOM.type(virtualDOM.props)
const newVirtualDOM = component.render()
return newVirtualDOM
}
复制代码
import SpReact from "./SpReact"
export default class App extends SpReact.Component{
render() {
return (<h1>{this.props.title}</h1>)
}
}
复制代码
更新
const jsx = (<div className="aa"><h1 id="title">VirtualDOM</h1><div>创建VirtualDOM</div></div>)
const jsx1 = (<div className="aa"><h1 id="title">VirtualDOM123</h1><div>创建VirtualDOM123</div></div>)
SpReact.render(jsx, container)
setTimeout(() => {
SpReact.render(jsx1, container)
},1000)
复制代码
VitrualDOM对象比对
export default function diff(virtualDOM, container, oldDOM) {
if (!oldDOM) {
mountElement(virtualDOM, container)
} else {
// VirtualDOM对象比对
}
}
复制代码
比对时,我们需要拿到新旧的VirtualDOM对象进行比较,所以在创建DOM对象时,需要保存下VirtualDOM对象保存下来。
这里把VirtualDOM对象保存在DOM上
function createDOM(virtualDOM) {
const type = virtualDOM.type
let el
if (type === 'text') {
el = document.createTextNode(virtualDOM.props.textContent)
} else {
el = document.createElement(type)
updateNodeElement(el, virtualDOM)
}
virtualDOM.children.forEach(child => {
mountElement(child, el)
})
el.__virtualDOM = virtualDOM
return el
}
复制代码
节点类型相同
接下来,处理节点类型(type)相同时
- 当前节点不需要更新,需要更新节点的属性事件
- 节点相同时,需要对比子节点
- 节点多余时需要删除多余的节点
export default function diff(virtualDOM, container, oldDOM) {
if (!oldDOM) {
mountElement(virtualDOM, container)
} else {
const oldVirtualDOM = oldDOM.__virtualDOM
// type相同时,说明当前节点不需要更新
if (oldVirtualDOM.type === virtualDOM.type) {
// 更新文本
if (virtualDOM.type === 'text') {
updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
} else {
// 更新节点上的事件和属性
updateNodeElement(oldDOM, virtualDOM)
}
// 节点进行比对
const oldChildNodes = oldDOM.childNodes
virtualDOM.children.forEach((child, index) => {
diff(child, oldDOM, oldChildNodes[index])
})
// 当节点多余时,需要删除多余的节点
if (oldChildNodes.length > virtualDOM.children.length) {
for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
unmountElement(oldChildNodes[i])
}
}
}
}
}
复制代码
// 更新文本节点
export function updateNodeText(virtualDOM, oldVirtualDOM, oldDOM) {
if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
oldDOM.textContent = virtualDOM.props.textContent
oldDOM.__virtualDOM = virtualDOM
}
}
// 删除节点
export function unmountElement(el) {
el.remove()
}
复制代码
更新元素上的事件,对原来的updateNodeElement
方法进行完善
export function updateNodeElement(el, virtualDOM, oldVirtualDOM) {
const props = virtualDOM.props || {}
const oldProps = oldVirtualDOM && oldVirtualDOM.props || {}
Object.keys(virtualDOM.props).forEach(propName => {
const val = props[propName]
const oldVal = oldProps[propName]
if (val === oldVal) {
return
}
if (propName.startsWith('on')) {
const eventName = propName.slice(2).toLowerCase()
el.addEventListener(eventName, val)
if (oldVal) {
el.removeEventListener(eventName, oldVal)
}
} else if (propName === 'className') {
el.setAttribute('class', val)
} else if (propName !== 'children') {
el.setAttribute(propName, val)
}
})
// 删除旧VirtualDOM对象中存在,而新VirtualDOM对象中不存在的属性
// 即删除DOM中多余的事件和属性
Object.keys(oldProps).forEach(oldPropName => {
const oldVal = oldProps[oldPropName]
const val = props[oldPropName]
if (!val) {
if (oldPropName.startsWith('on')) {
const oldEventName = oldPropName.slice(2).toLowerCase()
el.removeEventListener(oldEventName, oldVal)
} else if (oldPropName === 'className') {
el.removeAttribute('class', oldVal)
} else if(propName !== 'children') {
el.removeAttribute(oldPropName, oldVal)
}
}
})
}
复制代码
节点类型不相同
节点类型不相同并且不是组件,直接创建新的VirtualDOM
所对应的DOM并替换掉原来的DOM元素
export default function diff(virtualDOM, container, oldDOM) {
if (!oldDOM) {
mountElement(virtualDOM, container)
} else {
const oldVirtualDOM = oldDOM.__virtualDOM
if (oldVirtualDOM.type === virtualDOM.type) {
if (virtualDOM.type === 'text') {
updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
} else {
updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
}
} else if (!isComponent(virtualDOM)) {
const el = createDOM(virtualDOM)
container.replaceChild(el, oldDOM)
}
const oldChildNodes = oldDOM.childNodes
virtualDOM.children.forEach((child, index) => {
diff(child, oldDOM, oldChildNodes[index])
})
if (oldChildNodes.length > virtualDOM.children.length) {
for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
unmountElement(oldChildNodes[i])
}
}
}
}
复制代码
组件更新
setState更新
需要在子组件的父组件上声明setState
方法,修改组件的state值,调用组件的render方法生产新的VirtualDOM
对象,然后比对新旧VirtualDOM
对象,更新页面DOM元素
export default class Component {
constructor(props) {
this.props = props
}
setState(state) {
this.state = {
...this.state,
...state
}
// 生成新的VirtualDOM对象
const virtualDOM = this.render()
// 获取旧的DOM对象
const oldDOM = this.getDOM()
// 将component属性添加的virtualDOM上
virtualDOM.component = this
// 比对
diff(virtualDOM, oldDOM.parentNode, oldDOM)
}
setDOM(dom) {
this.__DOM = dom
}
getDOM() {
return this.__DOM
}
render() {
}
复制代码
将组件保存到新的VirtualDOM
对象上
function buildClassComponent(virtualDOM) {
const component = new virtualDOM.type(virtualDOM.props)
const newVirtualDOM = component.render()
newVirtualDOM.component = component
return newVirtualDOM
}
复制代码
创建VirtualDOM
对象的DOM元素之后,如果是组件对应的VirtualDOM
对象,需要将组件的DOM元素保存到组件上。
function mounNativeElement(virtualDOM, container) {
const dom = createDOM(virtualDOM)
container.appendChild(dom)
const component = virtualDOM.component
if (component) {
component.setDOM(dom)
}
}
复制代码
判断组件是否是同一节点
SpReact.render(<App title="App"/>, container)
setTimeout(() => {
SpReact.render(<App title="App1"/>, container)
},1000)
复制代码
如果新的VirtualDOM
对象是个组件
export default function diff(virtualDOM, container, oldDOM) {
if (!oldDOM) {
mountElement(virtualDOM, container)
} else {
const oldVirtualDOM = oldDOM.__virtualDOM
if (oldVirtualDOM.type === virtualDOM.type) {
if (virtualDOM.type === 'text') {
updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
} else {
updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
}
} else if (!isComponent(virtualDOM)) {
const el = createDOM(virtualDOM)
container.replaceChild(el, oldDOM)
} else {
diffComponent(virtualDOM, oldVirtualDOM, oldDOM, container)
}
const oldChildNodes = oldDOM.childNodes
virtualDOM.children.forEach((child, index) => {
diff(child, oldDOM, oldChildNodes[index])
})
if (oldChildNodes.length > virtualDOM.children.length) {
for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
unmountElement(oldChildNodes[i])
}
}
}
}
复制代码
判断是不是同一个组件
export function isSameComponent(virtualDOM,oldVirtualDOM){
const oldComponent = oldVirtualDOM.component
return oldComponent && virtualDOM.type === oldComponent.constructor
}
复制代码
新旧VirtualDOM
进行比对时,如果不是同一个组件,则需要创建新的VirtualDOM
对象的节点对应的DOM,并删除掉oldDOM
function diffComponent(virtualDOM, oldVirtualDOM, oldDOM, container) {
if (isSameComponent(virtualDOM, oldVirtualDOM)) {
updateComponent(virtualDOM, oldVirtualDOM, oldDOM, container)
} else {
// 这里没有直接创建对应的DOM元素,
// 是因为创建组件对应的DOM元素逻辑比较多,如将component保存到VirtualDOM对象上等
// 可以对原来的逻辑进行完善,复用之前的逻辑
mountElement(virtualDOM, container, oldDOM)
}
}
复制代码
mountElement后,最终都会调用mounNativeElement
,这里在mounNativeElement
里将oldDOM对象删除
export function mountElement(virtualDOM, container, oldDOM) {
if (isComponent(virtualDOM)) {
mountComponent(virtualDOM, container, oldDOM)
} else {
mounNativeElement(virtualDOM, container, oldDOM)
}
}
export function mountComponent(virtualDOM, container, oldDOM) {
let newVirtualDOM
if (isFunctionComponent(virtualDOM)) {
newVirtualDOM = buildFunctionComponent(virtualDOM)
} else {
newVirtualDOM = buildClassComponent(virtualDOM)
}
mountElement(newVirtualDOM, container, oldDOM)
}
export function mounNativeElement(virtualDOM, container, oldDOM) {
const dom = createDOM(virtualDOM)
container.appendChild(dom)
if (oldDOM) {
unmountElement(oldDOM)
}
const component = virtualDOM.component
if (component) {
component.setDOM(dom)
}
}
复制代码
如果是同一个组件,就更新组件的props,重新生成virtualDOM对象,进行比对
export function updateComponent(virtualDOM, oldVirtualDOM, oldDOM, container) {
const oldComponent = oldVirtualDOM.component
// 更新组件的props
oldComponent.updateProps(virtualDOM.props)
const nextVirtualDOM = oldComponent.render()
nextVirtualDOM.component = oldComponent
diff(nextVirtualDOM, container, oldDOM)
}
复制代码
调用生命周期函数,首先需要在Component类上加上生命周期函数
export function updateComponent(virtualDOM, oldVirtualDOM, oldDOM, container) {
const oldComponent = oldVirtualDOM.component
oldComponent.componentWillReceiveProps()
let props = virtualDOM.props
let oldProps = oldVirtualDOM.props
if (oldComponent.shouldComponentUpdate(props, oldProps)) {
oldComponent.componentWillUpdate(props)
oldComponent.updateProps(virtualDOM.props)
const nextVirtualDOM = oldComponent.render()
nextVirtualDOM.component = oldComponent
diff(nextVirtualDOM, container, oldDOM)
oldComponent.componentDidUpdate(oldProps)
}
}
复制代码
ref属性
<h1 ref={title => this.title = title}>{this.props.title}</h1>
复制代码
创建DOM元素时,执行ref属性对应的函数
export function createDOM(virtualDOM) {
const type = virtualDOM.type
let el
if (type === 'text') {
el = document.createTextNode(virtualDOM.props.textContent)
} else {
el = document.createElement(type)
updateNodeElement(el, virtualDOM)
}
virtualDOM.children.forEach(child => {
mountElement(child, el)
})
if (virtualDOM && virtualDOM.props && virtualDOM.props.ref) {
virtualDOM.props.ref(el)
}
el.__virtualDOM = virtualDOM
return el
}
复制代码
给组件添加ref属性,ref对应的属性值就是该组件
<Alert ref={alert => this.alert = alert}/>
复制代码
export function mountComponent(virtualDOM, container, oldDOM) {
let newVirtualDOM
if (isFunctionComponent(virtualDOM)) {
newVirtualDOM = buildFunctionComponent(virtualDOM)
} else {
newVirtualDOM = buildClassComponent(virtualDOM)
}
mountElement(newVirtualDOM, container, oldDOM)
const component = newVirtualDOM.component
// 如果是组件,就调用组件的生命周期函数
// props中存在ref属性,就调用ref属性所对应的函数
if(component){
component.componentDidMount()
if (component.props && component.props.ref) {
component.props.ref(component)
}
}
}
复制代码
key属性比对
同一父节点下的子节点,可以通过key属性比对VirtualDOM对象
export default function diff(virtualDOM, container, oldDOM) {
if (!oldDOM) {
mountElement(virtualDOM, container)
} else {
const oldVirtualDOM = oldDOM.__virtualDOM
if (oldVirtualDOM.type === virtualDOM.type) {
if (virtualDOM.type === 'text') {
updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
} else {
updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
}
- const oldChildNodes = oldDOM.childNodes
- virtualDOM.children.forEach((child, index) => {
- diff(child, oldDOM, oldChildNodes[index])
- })
// 比对子节点
+ diffChildren(virtualDOM, oldDOM)
- if (oldChildNodes.length > virtualDOM.children.length) {
- for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
- unmountElement(oldChildNodes[i])
- }
- }
} else if (!isComponent(virtualDOM)) {
- const el = createDOM(virtualDOM)
- container.replaceChild(el, oldDOM)
// 这里修改下不是组件的逻辑,因为virtualDOM对象有可能是组件创建的
// 最终会调用mounNativeElement处理组件的逻辑
+ mountElement(virtualDOM, container, oldDOM)
} else {
diffComponent(virtualDOM, oldVirtualDOM, oldDOM, container)
}
}
}
复制代码
- 首先判断旧的节点上存不存在key属性,不存在,就还用之前的逻辑(循环调用diff进行比对)
- 存在key属性
- 用新virtualDOM中的key属性去去旧节点中查找,如果存在,判断位置是否正确,不正确,就将旧节点移动的正确的位置
- 如果不存在就创建virtualDOM对应的DOM
function diffChildren(virtualDOM, oldDOM) {
const oldChildNodes = oldDOM.childNodes
let keyElements = {}
for (let i = 0, len = oldChildNodes.length; i < len; i++) {
const el = oldChildNodes[i]
if (el.nodeType === 1) {
const key = el.getAttribute('key')
if (key) {
keyElements[key] = el
}
}
}
const hasNoKey = Object.keys(keyElements).length === 0
if (hasNoKey) {
virtualDOM.children.forEach((child, index) => {
diff(child, oldDOM, oldChildNodes[index])
})
} else {
// 存在key属性
virtualDOM.children.forEach((child, index) => {
const key = child.props.key
if (key) {
const el = keyElements[key]
if (el) {
// 移动oldChilNodes位置
if(el !== oldChildNodes[index]) oldDOM.insertBefore(el, oldChildNodes[index])
// 比对
else diff(child, oldDOM, el)
} else {
// 重新创建
mountElement(child, oldDOM, oldChildNodes[index], true)
}
}
})
}
if (oldChildNodes.length > virtualDOM.children.length) {
for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
unmountElement(oldChildNodes[i])
}
}
}
复制代码
mountElement
加了个参数,是否是新增,如果是新增,在调用mounNativeElement
时,就不会删除oldDOM。在插入元素时,需要将新元素插入到oldDOM之前
export function mountElement(virtualDOM, container, oldDOM, isNew) {
if (isComponent(virtualDOM)) {
mountComponent(virtualDOM, container, oldDOM, isNew)
} else {
mounNativeElement(virtualDOM, container, oldDOM, isNew)
}
}
export function mounNativeElement(virtualDOM, container, oldDOM, isNew) {
const dom = createDOM(virtualDOM)
if (oldDOM) {
// 将元素插入的oldDOM元素之前
container.insertBefore(dom, oldDOM)
if (!isNew) {
unmountElement(oldDOM)
}
} else {
container.appendChild(dom)
}
const component = virtualDOM.component
if (component) {
component.setDOM(dom)
}
}
export function mountComponent(virtualDOM, container, oldDOM, isNew) {
let newVirtualDOM
if (isFunctionComponent(virtualDOM)) {
newVirtualDOM = buildFunctionComponent(virtualDOM)
} else {
newVirtualDOM = buildClassComponent(virtualDOM)
}
mountElement(newVirtualDOM, container, oldDOM, isNew)
const component = newVirtualDOM.component
if (component) {
component.componentDidMount()
if (component.props && component.props.ref) {
component.props.ref(component)
}
}
}
复制代码
key属性删除节点
如果不存在key属性,则还是使用之前的根据索引去删除多余元素
如果存在key属性,则需要遍历旧的节点,并使用旧节点的key到新节点中查找对应key属性的元素,如果新节点中不存在,则说明当前节点已经被删除
function diffChildren(virtualDOM, oldDOM) {
const oldChildNodes = oldDOM.childNodes
let keyElements = {}
for (let i = 0, len = oldChildNodes.length; i < len; i++) {
const el = oldChildNodes[i]
if (el.nodeType === 1) {
const key = el.getAttribute('key')
if (key) {
keyElements[key] = el
}
}
}
const hasNoKey = Object.keys(keyElements).length === 0
if (hasNoKey) {
// 比对节点
virtualDOM.children.forEach((child, index) => {
diff(child, oldDOM, oldChildNodes[index])
})
// 删除节点
if (oldChildNodes.length > virtualDOM.children.length) {
for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
unmountElement(oldChildNodes[i])
}
}
} else {
const newKeyElements = {}
virtualDOM.children.forEach((child, index) => {
const key = child.props.key
if (key) {
newKeyElements[key] = child
const el = keyElements[key]
if (el) {
if (el !== oldChildNodes[index]) oldDOM.insertBefore(el, oldChildNodes[index])
else diff(child, oldDOM, el)
} else {
mountElement(child, oldDOM, oldChildNodes[index], true)
}
}
})
for (let i = 0; i < oldChildNodes.length; i++) {
let oldChild = oldChildNodes[i]
let oldKey = oldChild.__virtualDOM.props.key
if (!newKeyElements[oldKey]) {
unmountElement(oldChild)
}
}
}
}
复制代码
删除节点
删除节点还需要考虑节点是节点还是文本
- 文本,直接删除节点
- 组件,调用卸载组件的生命周期函数
- 如果删除的节点有ref属性,则需要删除ref属性
- 如果有事件,则需要删除对应节点的事件
export function unmountElement(el) {
const virtualDOM = el.__virtualDOM
if (virtualDOM.type === 'text') {
el.remove()
return
}
// 不是文本
const component = virtualDOM.component
// 组件生命周期
if (virtualDOM.component) {
component.componentWillMount()
}
const props = virtualDOM.props
// 清空ref属性
if (props && props.ref) {
props.ref(null)
}
// 删除事件
Object.keys(props).forEach(propName => {
if (propName.startsWith('on')) {
const event = propName.slice(2).toLowerCase()
const val = virtualDOM.props[propName]
el.removeEventListener(event, val)
}
})
// 删除子节点事件
if (el.childNodes.length) {
el.childNodes.forEach(child => {
unmountElement(child)
})
}
// 删除当前元素
el.remove()
}
复制代码