最近一直在研究react 虚拟dom、diff 算法。发现其实虚拟dom也没有想象中那么难,只要抓住几个要点,掌握它的实现流程、原理,那么用代码来实现就简单多了。
以下全为个人见解,有遗漏或不正确的希望友友们及时指出
- 创建虚拟dom树
- 将新产生的虚拟dom树与上一次的虚拟dom树进行比较,产生patch补丁包
- 将补丁包更新到真实dom树
- 将dom挂载到页面某元素下面
- 首先再来回顾一下,react虚拟dom的意义:
为了解决浏览器的性能问题之一(渲染dom),当进行dom操作时,只要把dom相关操作交给vertual dom,它会进行一些处理,比如diff、patch,减少浏览器的重排与重绘。以最少的代价来渲染dom,就是最小化性能对资源池(老树)的操作
- 开始看源码之前再回顾了解一下,dom节点由哪些组成:
先来看下我们的HTML标签结构
<div class="demo" >
<p class="childDemo"> Click Me</p>
</div >
DOM 元素包含的信息其实只有三个:标签名,属性,子元素。
我们用 javascript对象来表示
{
tag: 'div',
attrs: { className: 'demo'},
children: [
{
tag: 'p',
arrts: { className: 'childDemo' },
children: ['Click Me']
}
]
}
所有在创建DOM元素、进行深度优先遍历、diff、patch等等操作,都是围绕这几个来写源码
- 构建虚拟dom
//根据传进来的值 产生虚拟dom 对象
/*使用js对象来表示一个DOM节点很简单,只需要记录tagName, props, children*/
class Element{
constructor(tagName, props, children){
this.tagName = tagName;
this.props = props;
this.children = children;
}
}
//生成虚拟dom
function createElement(tagName,props,children){
return new Element(tagName,props,children)
}
let vertualDom = createElement('ul',{class:"list"},[
createElement('li',{class:"item"},['a']),
createElement('li',{class:"item"},['a']),
createElement('li',{class:"item"},['a'])
])
//负责将虚拟dom处理成真实的dom
function render(eleObj){
let el = document.createElement(eleObj.tagName);
for(let key in eleObj.props){
//设置属性的方法
setAttr(el,key,eleObj.props[key]);
}
eleObj.children.forEach((child)=>{
child = (child instanceof Element)?render(child):document.createTextNode(child);
el.appendChild(child)
})
return el;
}
//设置属性
function setAttr(node,key,value){
switch(key){
case"value":
if(node.tagName.toUpperCase()==="INPUT"||
node.tagName.toUpperCase()==="TEXTAREA"){
node.value = value
}else{
node.setAttribute(key,value);
}
break;
case"style":
node.style.cssText = value;
break;
default:
node.setAttribute(key,value);
break;
}
}
//把dom挂在到页面中
function renderDOm(el,target){
target.appendChild(el);
}
简陋版虚拟dom构建完成,下面来看下dom diff操作
其实当我们在react 中setState操作时,就会重新创建一个大的对象,这个大的对象就是虚拟dom对象 ,创建完成后会进行新老vertual dom树的比较(diff),将两棵树中不同的部分抽取出来,这就是patch,将patch更新到真实dom上,然后渲染到页面。这就是以最小的代价来操作更新dom
// 对比前后两棵树 之前的差别 返回一个补丁对象
function diff(oldTree,newTree){
let patches = {}; //补丁包
let index = 0; //当前的节点的标志。因为在深度优先遍历的过程中,每个节点都有一个index。
walk(oldTree,newTree,index,patches); // 开始进行深度优先遍历
return patches; //最终返回两棵树的差异
}
//对比属性的不同 props
function diffAttr(oldAttrs,newAttrs){
let patch = {} //差异化props
//更新
for(let attr in oldAttrs){
if(oldAttrs[attr]!== newAttrs[attr]){
patch[attr] = newAttrs[attr]
}
}
//新增
for(let attr in newAttrs){
if(!oldAttrs.hasOwnProperty(attr)){
patch[attr] = newAttrs[attr]
}
}
return patch;
}
const ATTRS = "ATTRS";
const TEXT = "TEXT";
const REMOVE = "REMOVE";
const REPLACE = 'REPLACE';
let Index = 0; //针对children 深度优化遍历 当前的节点的标志
//递归遍历子节点的不同
function diffChildren(oldNode,newNode,index,patches){
oldNode.forEach((child,idx)=>{
walk(child ,newNode[idx],++Index,patches)
})
}
function isString(node){
return Object.prototype.toString.call(node)=="[object String]";
}
// 具体的对比方法 对两棵树进行深度优先遍历。
function walk(oldNode,newNode,index,patches){
let currentPatches = [];
if(!newNode){ //没有新的节点。删除
currentPatches.push({type:REMOVE,index});
}
else if(isString(oldNode)&&isString(newNode)){ //判断文本是否变换
if(oldNode!==newNode){
currentPatches.push({type:TEXT,text:newNode});
}
}
else if(oldNode.tagName === newNode.tagName){ //tagName 相同 判断属性
let attrs = diffAttr(oldNode.props,newNode.props); //patch's props
//将patch添加到此轮比较的补丁包里
if(Object.keys(attrs).length>0){
currentPatches.push({type:ATTRS,attrs})
}
//比较children
diffChildren(oldNode.children,newNode.children,index,patches)
}else{
//节点被替换了
currentPatches.push({type:REPLACE,newNode})
}
if(currentPatches.length>0){ //产生补丁包
patches[index] = currentPatches;
}
- 虚拟dom创建是先序深度优化
- diff只会对同层进行比较,参考diff三大策略
- patch 时是倒序的,从下到上
具体的diff算法详解请查看https://blog.csdn.net/lulu_678/article/details/88249461