搭建开发环境
要模拟 React 的核心原理我们只需要将babel转换jsx代码是调用的createElement方法转换成自定义的React即可。所以搭建开发环境时只需要能够转换jsx和es语法以及能够启动开发服务器即可。
创建文件结构
在空文件夹下创建如下的文件结构
其中TinyReact文件夹存放要实现的React核心代码
开发依赖
依赖插件 | 描述 |
---|---|
webpack | 打包工具 |
webpack-cli | webpack命令行工具 |
webpack-dev-server | 开发服务器插件,用于启动一个开发服务器 |
html-webpack-plugin | 根据模板生成html文件,并自定引入打包好的js、css等文件 |
clean-webpack-plugin | 打包之前清除文件 |
babel-loader | js文件加载器,用于转换文件中的jsx以及es6语法 |
@babel/core | babel核心库 |
@babel/preset-env | 将es6语法转换成es5 |
@babel/preset-react | 将jsx语法转化成普通的es5代码 |
webpack配置
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: './src/index.js',
mode: 'development',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
devtool: 'source-map',
devServer: {
hot: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './index.html'
})
]
}
babel配置
要想让babel使用自定义的React提供的方法编译jsx有两种方式:
- 注释:在要使用自定义React转换的文件开头使用注释
/** @jsx TinyReact.createElement */
来告诉babel要使用指定的方法来转换当前文件的jsx语法。
- 修改babel配置:给转换jsx语法的插件@babel/preset-react插件的选项参数gragma传入自定义的createElement方法
.babelrc文件{ "presets": [ "@babel/preset-env", [ "@babel/preset-react", { "pragma": "TinyReact.createElement" } ] ] }
配置命令行启动
"scripts": {
"start": "webpack serve"
},
模拟实现React15核心功能
首先在TinyReact文件夹中创建index.js用于集中导出,要保持index.js文件的纯净,只做导出。
转换jsx代码
在使用babel转换jsx代码时,会调用React提供的createElement方法。因此首先要创建一个createElement.js文件并导出一个createElement方法。然后在TinyReact/index.js中导出。这里这样导出的原因是createElement的使用是通过TinyReact.createElement这种方式来使用的。
// TinyReact/index.js
import createElement from "./createElement"
export default {
createElement
}
createElement(type, config, …children)接收3个以上的参数,返回一个virtualDom对象。
type: 节点类型
config: 用户传入的参数对象
children:从第三个参数开始都是节点的子节点
/**
* 创建virtualDom对象
* 1. 排除为false/true/null的子节点,这些节点不会渲染
* 2. 处理子节点中的文本节点,将文本节点转化为对象
* 3. 将处理后的子节点数组添加到返回对象的props属性,并合并选项参数config
* 4. 返回virtualDom对象
*/
export default function createElement(type, config, ...children) {
const childElements = [].concat(...children).reduce((r, child) => {
if (child !== false || child !== true || child !== null) {
if (child instanceof Object) {
r.push(child)
} else { // 文本节点
r.push(createElement('text', {textContent: child}))
}
}
return r
}, [])
return {
type,
props: Object.assign({children: childElements}, config)
}
}
文本节点传入时是字符串形式,为了方便后续处理,需要将字符串转化为统一的virtualDom对象。
在src/index.js编写代码测试,输出jsx转换之后的结果。
// src/index.js
import TinyReact from "./TinyReact"
const jsx = (
<div>
<p>哈哈112</p>
<button onClick={() => { alert(123) }}>按钮</button>
</div>
)
console.log(jsx)
yarn start
启动项目之后观察浏览器后台输出确实是经过自定义createElement转换之后的结果。
render方法的实现
render方法主要功能是将virtualDom转化为真实Dom,并渲染到容器中。这里我们先不考虑组件,只考虑html标签的渲染。
普通节点初次渲染
在TinyReact文件夹中创建一个render文件夹,并导出一个render方法,render方法暂时只接受两个参数:virtualDom是要渲染的虚拟Dom,container是一个真实的dom,是virtualDom渲染的容器。在render方法中调用diff方法。在diff方法中统一处理dom的更新和渲染。
// TinyReact/render.js
import diff from "./diff";
export default function render(virtualDom, container) {
diff(virtualDom, container)
}
// TinyReact/index.js
import createElement from "./createElement"
export { default as render } from "./render"
export default {
createElement
}
TinyReact下创建diff.js文件,导出diff方法。diff方法要判断是否是初次渲染,如果是初次渲染,需要调用mountElement方法来渲染virtualDom。更新的情况我们稍后处理。
// TinyReact/diff.js
import mountElement from "./mountElement"
export default function diff(virtualDom, container, oldDom) {
const oldVirtualDom = oldDom && oldDom.__virtualDom
if (!oldVirtualDom) { // 是否已经渲染,通过判断老节点是否存在
// 初次渲染
mountElement(virtualDom, container)
}
}
再次在TinyReact下创建mountElement.js文件导出mountElement方法。该方法主要功能是将根据virtualDom来创建真实Dom,然后将真实Dom渲染到容器中。
这里我们要根据virtualDom的类型来分别。virtualDom可能是一个普通节点,也可能是一个组件。作为一个普通节点时virtualDom的type属性一定是一个字符串,这里我们将一些判断性的功能函数都放到utils.js文件中管理。组件时type可能是一个类或函数。要为这两种情况分别定义两个方法来处理。
处理普通节点时我们使用mountNativeElement方法来处理
import mountNativeElement from "./mountNativeElement";
import { isComponent } from "./utils";
/**
* 渲染虚拟dom
* @param {*} virtualDom
* @param {*} container
*/
export default function mountElement(virtualDom, container, oldDom) {
// 判断虚拟dom类型是组件还是普通元素
if (isComponent(virtualDom)) { // 组件
} else { // 普通元素
mountNativeElement(virtualDom, container)
}
}
// TinyReact/utils.js
/**
* 判断节点是否是组件节点
* @param {*} virtualDom
* @returns
*/
export const isComponent = virtualDom => {
return typeof virtualDom.type !== 'string'
}
TinyReact下创建mountNativeElement.js文件导出mountNativeElement方法。主要功能是根据virtualDom对象创建真实Dom,然后渲染到container容器中。这里创建dom对象的过程可以封装到一个单独的方法createDomElement中进行管理。
// TinyReact/mountNativeElement.js
import createDomElement from "./createDomElement"
/**
* 渲染普通元素节点
* @param {*} virtualDom
* @param {*} container
*/
export default function mountNativeElement(virtualDom, container) {
// 判断是文本节点还是元素节点
if (virtualDom.type === 'text') { // 文本节点,container文本内容设置为文本节点内容
container.textContent = virtualDom.props.textContent
} else { // 元素节点,创建节点,并设置节点属性,将节点添加到container
// 创建节点
const element = createDomElement(virtualDom)
// 将节点添加到container中
container.appendChild(element)
}
}
createDomElement创建dom时,需要区分是文本节点还是元素节点,这两种节点的创建方法有所不同。
如果是文本节点,只需要创建并返回节点dom即可。
如果是元素节点,需要将props中的一些属性设置为元素的属性,并为元素添加事件处理。此外还要对节点的子节点进行递归处理。节点属性的处理功能能被复用所以创建updateElementNode.js文件来定义一个updateElementNode方法来处理。
// TinyReact/createDomElement.js
import mountElement from "./mountElement"
import updateElementNode from "./updateElementNode"
/**
* 根据虚拟Dom创建真实Dom
* @param {*} virtualDom
* @returns
*/
export default function createDomElement(virtualDom) {
let element = null
// 判断是文本节点还是元素节点
if (virtualDom.type === 'text') { // 文本节点,container文本内容设置为文本节点内容
element = document.createTextNode(virtualDom.props.textContent)
} else { // 元素节点,创建节点,并设置节点属性,将节点添加到container
// 创建节点
element = document.createElement(virtualDom.type)
// 为节点添加/更新属性
updateElementNode(virtualDom, element)
// 将virtualDom 对象记录在真实dom上
element.__virtualDom = virtualDom
// 遍历处理子节点元素
virtualDom.props.children.forEach(child => {
mountElement(child, element)
})
}
return element
}
// TinyRreact/updateElementNode.js
/**
* 创建/更新节点属性
* @param {*} element
* @param {*} virtualDom
*/
export default function updateElementNode(element, virtualDom) {
// 获取节点属性
const props = virtualDom.props
// 遍历节点属性,将节点属性赋给dom元素
Object.keys(props).forEach(propName => {
// 获取节点属性对应的值
const propValue = props[propName]
if (propName.startsWith('on')) { // 以on开头的属性是事件属性,需要为dom添加事件监听
const eventName = propName.slice(2).toLowerCase()
element.addEventListener(eventName, propValue)
} else if (propName === 'value' || propName === 'selected') { // value和selected需要设置到dom上
element[propName] = propValue
} else if (propName === 'className') { // 对className进行处理,转化为class
element.setAttribute('class', propValue)
} else if (propName !== 'children') { // 为 dom 添加 children 以外的属性
element.setAttribute(propName, propValue)
}
})
}
到此为止,非组件节点的渲染就实现完了。启动项目,我们定义的展示内容就呈现在浏览器上了。
组件的初次渲染
在mountElement方法判断了节点是组件还是普通节点,如果是组件则调用mountComponent方法来渲染。
// TinyReact/mountElement.js
import mountComponent from "./mountComponent";
import mountNativeElement from "./mountNativeElement";
import { isComponent } from "./utils";
/**
* 渲染虚拟dom
* @param {*} virtualDom
* @param {*} container
*/
export default function mountElement(virtualDom, container, oldDom) {
// 判断虚拟dom类型是组件还是普通元素
if (isComponent(virtualDom)) { // 组件
mountComponent(virtualDom, container)
} else { // 普通元素
mountNativeElement(virtualDom, container)
}
}
创建mountComponent.js文件,导出mountComponent方法来渲染组件。组件的渲染时,首先要现将组件转化为virtualDom对象,然后再调用mountElement来渲染组件。类组件需要通过执行实例的render方法来获取类组件的virtualDom对象,函数组件则通过直接调用函数本身来获取组件对应的virtualDom对象。
// TinyReact/mountComponent.js
import mountElement from "./mountElement";
import { isFunctionComponent } from "./utils";
/**
* 渲染组件元素
* @param {*} virtualDom
* @param {*} container
*/
export default function mountComponent(virtualDom, container) {
let newVirtualDom = null
if (isFunctionComponent(virtualDom)) { // 函数组件
newVirtualDom = buildFunctionComponent(virtualDom)
} else { // 类组件
newVirtualDom = buildClassComponent(virtualDom)
}
// 挂载组件到容器中
mountElement(newVirtualDom, container)
}
/**
* 创建function组件的virtualDom对象
* @param {*} virtualDom
* @returns
*/
function buildFunctionComponent(virtualDom) {
return virtualDom.type(virtualDom.props || {})
}
/**
* 创建class组件的virtualDom对象
* @param {*} virtualDom
* @returns
*/
function buildClassComponent(virtualDom) {
const instance = new virtualDom.type(virtualDom.props || {})
return instance.render()
}
在utils.js中添加判断是否是函数组件的方法isFunctionComponent。
/**
* 判断节点是否是函数组件
* @param {*} virtualDom
* @returns
*/
export const isFunctionComponent = virtualDom => {
return typeof virtualDom.type === 'function' && virtualDom.type.prototype && !virtualDom.type.prototype.render
}
此外要定义类组件,还需要定义一个Component类,类组件需要继承该类。方法需要在TinyReact/index.js中导出。
// TinyReact/index.js
import createElement from "./createElement"
export { default as render } from "./render"
import Component from "./Component"
export default {
Component,
createElement
}
// TinyReact/Component.js
export default class Component {
constructor(props) {
this.props = props
}
render() {}
}
在src/index.js中定义一个组件,然后通过render方法渲染组件。启动项目,组件内容呈现到浏览器中。自此组件初次功能实现完毕。
// src/index.js
import TinyReact, { render } from "./TinyReact"
class Hello extends TinyReact.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
<p>♥</p>
<p>{ this.props.title }</p>
<p>♥</p>
</div>
)
}
}
render(<Hello title="hello" />, root)
渲染更新
页面初次渲染之后,间隔几秒,再次调用render方法渲染不同的内容,将进入到更新流程。
首先在render方法中引入第三个参数oldDom,表示更新之前的真实Dom,默认值为container容器中的第一个子节点。并将oldDom传入diff
// TinyReact/render.js
import diff from "./diff";
export default function render(virtualDom, container, oldDom = container.firstChild) {
diff(virtualDom, container, oldDom)
}
接下来修改diff方法,实现更新的主要逻辑。渲染更新时,需要判断是组件更新还是普通节点更新。
// TinyReact/diff.js
import diffComponent from "./diffComponent"
import mountElement from "./mountElement"
import { isComponent } from "./utils"
export default function diff(virtualDom, container, oldDom) {
const oldVirtualDom = oldDom && oldDom.__virtualDom
if (!oldVirtualDom) { // 是否已经渲染,通过判断老节点是否存在
// 初次渲染
mountElement(virtualDom, container, oldDom)
} else if(isComponent(virtualDom)) { // 组件更新
const oldComponent = oldVirtualDom.component
diffComponent(virtualDom, container, oldDom, oldComponent)
} else { // 普通节点更新
diffNativeElement(virtualDom, container, oldVirtualDom, oldDom)
}
}
调用diffComponent更新组件差异时需要用到更新前组件的实例,在创建组件实例时要将组件实例保存到virtualDom上。修改mountComponent.js中的buildClassComponent,创建类组件实例时将实例保存到virtualDom上,同时修改mountComponent方法,挂载元素之后需要调用类组件的componentDidMount生命周期钩子。
// TinyReact/mountComponent.js
import mountElement from "./mountElement";
import { isComponent, isFunctionComponent } from "./utils";
/**
* 渲染组件元素
* @param {*} virtualDom
* @param {*} container
*/
export default function mountComponent(virtualDom, container, oldDom) {
let newVirtualDom = null
let component = null
if (isFunctionComponent(virtualDom)) { // 函数组件
newVirtualDom = buildFunctionComponent(virtualDom)
} else { // 类组件
newVirtualDom = buildClassComponent(virtualDom)
component = newVirtualDom.component
}
// 挂载组件到容器中
mountElement(newVirtualDom, container, oldDom)
// 如果是类组件需要调用组件的生命周期钩子
if (component) {
component.componentDidMount()
}
}
/**
* 创建function组件的virtualDom对象
* @param {*} virtualDom
* @returns
*/
function buildFunctionComponent(virtualDom) {
return virtualDom.type(virtualDom.props || {})
}
/**
* 创建class组件的virtualDom对象
* @param {*} virtualDom
* @returns
*/
function buildClassComponent(virtualDom) {
const instance = new virtualDom.type(virtualDom.props || {})
const newVirtualDom = instance.render()
// 记录组件实例,更新时需要通过virtualDom来获取组件实例
newVirtualDom.component = instance
return newVirtualDom
}
组件更新
需要判断是否是同一个组件。如果是同一个组件调用updateComponent方法更新组件。否则直接调用mountElement方法将组件渲染到容器中。
// TinyReact/diffComponent.js
import mountElement from "./mountElement";
import updateComponent from "./updateComponent";
export default function diffComponent(virtualDom, container, oldDom, oldComponent) {
// 1. 判断组件是否是同一个组件
if(isSameComponent(virtualDom, oldComponent)) { // 是同一个组件,对比组件差异部分
updateComponent(virtualDom, container, oldDom, oldComponent)
} else { // 不是同一个组件,直接调用渲染新的组件
mountElement(virtualDom, container, oldDom)
}
}
export const isSameComponent = (virtualDom, oldComponent) => {
return oldComponent && virtualDom.type === oldComponent.constructor
}
更新组件首先要更新组件实例的props属性,然后调用组件实例的render方法重新生成虚拟Dom对象,再通过diff渲染差异部分。在此过程中调用组件的生命周期钩子。
// TinyReact/updateComponent.js
import diff from "./diff"
/**
* 更新组件的差异部分,并触发组件的生命周期函数
* @param {*} virtualDom
* @param {*} container
* @param {*} oldDom
* @param {*} oldComponent
*/
export default function updateComponent(virtualDom, container, oldDom, oldComponent) {
oldComponent.componentWillReceiveProps(virtualDom.props)
if (oldComponent.shouldComponentUpdate(virtualDom.props)) {
// 更新之前的props
const prevProps = oldComponent.props
// 更新组件的props
oldComponent.updateProps(virtualDom.props)
// 重新生成组件的虚拟Dom
const newVirtualDom = oldComponent.render()
newVirtualDom.component = oldComponent
// 通过diff方法更新差异
diff(newVirtualDom, container, oldDom)
oldComponent.componentDidUpdate(prevProps)
}
}
这里需要更新组件实例的props属性,因此需要在TinyReact.Component中定义一个updateProps方法来实现。此外还需要初始化组件的生命周期函数。
// TinyReact/Component.js
export default class Component {
constructor(props) {
this.props = props
}
render() {}
updateProps(props) {
this.props = props
}
// 生命周期函数
componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps(nextProps) {}
shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state
}
componentWillUpdate(nextProps, nextState) {}
componentDidUpdate(prevProps, preState) {}
componentWillUnmount() {}
}
普通节点更新
首先判断节点类型是否一致,如果节点类型一致还需要判断是文本节点还是元素节点。如果是文本节点,调用updateTextNode更新文本节点内容。如果是元素节点,则首先调用updateElementNode更新节点属性,然后循环递归对比子节点。最后再调用unmountNodez逐个删除多余节点。
节点类型不一致,直接创建新的dom,然后去替换container中的oldDom。
// TinyReact/diffNativeElement.js
import diff from "./diff"
import unmountNode from "./unmountNode"
import updateElementNode from "./updateElementNode"
import updateTextNode from "./updateTextNode"
export default function diffNativeElement(virtualDom, container, oldVirtualDom, oldDom) {
if (virtualDom.type === oldVirtualDom.type) { // 类型一致,判断是文本节点还是元素节点
if (virtualDom.type === 'text') {
updateTextNode(virtualDom, oldVirtualDom, oldDom)
} else {
updateElementNode(virtualDom, oldDom, oldVirtualDom)
const childNodes = oldDom.childNodes
// 循环递归对比子节点
virtualDom.props.children.forEach((child, i) => {
diff(child, oldDom, childNodes[i])
})
// 删除多余节点
if (childNodes.length > virtualDom.props.children.length) {
let i = childNodes.length
while(i > virtualDom.props.children.length) {
unmountNode(childNodes[i - 1])
i--
}
}
}
} else { // 类型不一致,根据virtualDom重新生成dom,然后替换oldDom
const newDom = createDomElement(virtualDom)
container.replaceChild(newDom, oldDom)
}
}
文本节点的更新比较简单,只需要当文本内容不一致时,更新oldDom的textContent。
// TinyReact/updateTextNode.js
/**
* 更新文本节点文本内容
* @param {*} virtualDom
* @param {*} oldVirtualDom
* @param {*} oldDom
*/
export default function updateTextNode(virtualDom, oldVirtualDom, oldDom) {
if (virtualDom.props.textContent !== oldVirtualDom.props.textContent) {
oldDom.textContent = virtualDom.props.textContent
// 更新虚拟Dom
oldDom.__virtualDom = virtualDom
}
}
元素节点的属性更新,在原来的基础上对事件属性进行特殊处理,如果是属性更新, 需要注销上一次注册的事件处理函数。同时删除多余属性。
// TinyReact/updateElementNode.js
/**
* 创建/更新节点属性
* @param {*} element
* @param {*} virtualDom
*/
export default function updateElementNode(virtualDom, element, oldVirtualDom) {
// 获取节点属性
const props = virtualDom.props || {}
const oldProps = (oldVirtualDom && oldVirtualDom.props) || {}
// 遍历节点属性,将节点属性赋给dom元素
Object.keys(props).forEach(propName => {
// 获取节点属性对应的值
const propValue = props[propName]
const oldPropValue = oldProps[propName]
if (propName.startsWith('on')) { // 以on开头的属性是事件属性,需要为dom添加事件监听
const eventName = propName.slice(2).toLowerCase()
element.addEventListener(eventName, propValue)
if (oldVirtualDom) { // 事件属性更新,需要取消上一个事件监听函数
element.removeEventListener(eventName, oldPropValue)
}
} else if (propName === 'value' || propName === 'selected') { // value和selected需要设置到dom上
element[propName] = propValue
} else if (propName === 'className') { // 对className进行处理,转化为class
element.setAttribute('class', propValue)
} else if (propName !== 'children') { // 为 dom 添加 children 以外的属性
element.setAttribute(propName, propValue)
}
})
// 遍历oldProps,删除 props中没有的属性
Object.keys(oldProps).forEach(propName => {
if (!props[propName]) {
const oldPropValue = oldProps[propName]
if (propName.startsWith('on')) { // 以on开头的属性是事件属性,需要为dom添加事件监听
const eventName = propName.slice(2).toLowerCase()
// 删除事件属性,需要取消事件监听函数
element.removeEventListener(eventName, oldPropValue)
} else if (propName === 'value' || propName === 'selected') { // value和selected需要设置到dom上
element[propName] = undefined
} else if (propName === 'className') { // 对className进行处理,转化为class
element.removeAttribute('class', propValue)
} else if (propName !== 'children') { // 为 dom 添加 children 以外的属性
element.removeAttribute(propName, propValue)
}
}
})
}
节点的删除则是通过节点自身的remove方法来实现。
// TinyReact/unmountNode.js
export default function unmountNode(node) {
node.remove()
}
以上就是渲染更新功能的实现。
component state更新
通过组件实例调用setState更新state,需要在Component.js中添加setState方法。组件state更新之后需要经过以下过程:
-
合并state状态。
-
调用render方法生成新的virtualDom。
-
获取组件state更新之前的真实Dom。
要获取组件state更新之前的真实Dom,需要在生成组件对应真实Dom时,将生成的真实Dom记录到组件的实例上。// TinyReact/createDomElement.js import mountElement from "./mountElement" import updateElementNode from "./updateElementNode" /** * 根据虚拟Dom创建真实Dom * @param {*} virtualDom * @returns */ export default function createDomElement(virtualDom, oldDom) { let element = null // 判断是文本节点还是元素节点 if (virtualDom.type === 'text') { // 文本节点,container文本内容设置为文本节点内容 element = document.createTextNode(virtualDom.props.textContent) } else { // 元素节点,创建节点,并设置节点属性,将节点添加到container // 创建节点 element = document.createElement(virtualDom.type) // 为节点添加/更新属性 updateElementNode(virtualDom, element, oldDom) // 将virtualDom 对象记录在真实dom上 element.__virtualDom = virtualDom // 记录组件实例对应的真实Dom const component = virtualDom.component if (component) { component.setDom(element) } // 遍历处理子节点元素 virtualDom.props.children.forEach((child, i) => { mountElement(child, element) }) } return element }
-
调用diff方法渲染state更新后的差异。
// TinyReact/Component.js
import diff from "./diff"
export default class Component {
constructor(props) {
this.props = props
}
render() {}
updateProps(props) {
this.props = props
}
setDom(dom) {
this._dom = dom
}
getDom() {
return this._dom
}
setState(partialState) {
// 合并状态,
this.state = Object.assign({}, this.state, partialState)
// 调用render重新获取virtualDom
const virtualDom = this.render()
// 获取组件实例对应的dom
const oldDom = this.getDom()
// 调用diff更新差异
diff(virtualDom, oldDom.parentNode, oldDom)
}
// 生命周期函数
componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps(nextProps) {}
shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state
}
componentWillUpdate(nextProps, nextState) {}
componentDidUpdate(prevProps, preState) {}
componentWillUnmount() {}
}
组件更新
组件更新需要区分更新前后是否是同一个组件
- 更新前后是同一个组件,需要先将老的组件实例的 props 更新,然后重新调用 render 生成 virtualDom,再通过 diff 方法更新。
- 更新前后不是同一个组件,则直接调用 mountElement 渲染新的组件。
通过key进行节点对比
通过key对比节点的实现分为以下几个步骤:
- 将真实Dom中具有key属性的子节点存入到一个对象。
- 遍历新的节点的子节点时,
- 子节点如果存在key属性,则先从对象中查找是否有对应key属性的dom,如果有则调整位置,然后通过diff渲染节点差异。如果没有则将子节点插入当前索引的元素之前。
- 没有key属性,也需要将子节点插入当前索引的元素之前。
- 遍历对象中的所有属性,将没有在新的子节点对应的key属性的节点删除。