深入理解Vue2.x的虚拟DOM diff原理


Posted in Javascript onSeptember 27, 2017

前言

经常看到讲解Vue2的虚拟Dom diff原理的,但很多都是在原代码的基础上添加些注释等等,这里从0行代码开始实现一个Vue2的虚拟DOM

实现VNode

src/core/vdom/Vnode.js

export class VNode{
 constructor (
  tag, //标签名
  children,//孩子[VNode,VNode],
  text, //文本节点
  elm //对应的真实dom对象
 ){
  this.tag = tag;
  this.children = children
  this.text = text;
  this.elm = elm;
 }
}
export function createTextNode(val){
 //为什么这里默认把elm置为undefined,不直接根据tag 用document.createElement(tagName)把elm赋值?而要等后面createElm时候再赋值呢?
 return new VNode(undefined,undefined,String(val),undefined)
}
export function createCommentNode(tag,children){
 if(children){
  for(var i=0;i<children.length;i++){
   var child = children[i];
   if(typeof child == 'string'){
    children[i] = createTextNode(child)
   }
  }
 }
 return new VNode(tag,children,undefined,null)
}

定义一个Vnode类, 创建节点分为两类,一类为text节点,一类非text节点

src/main.js

import {VNode,createCommentNode} from './core/vdom/vnode'
var newVonde = createCommentNode('ul',[createCommentNode('li',['item 1']),createCommentNode('li',['item 2']),createCommentNode('li',['item 3'])])

在main.js就可以根据Vnode 生成对应的Vnode对象,上述代码对应的dom表示

<ul>

<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>

先实现不用diff把Vnode渲染到页面中来

为什么先来实现不用diff渲染Vnode的部分,这里也是为了统计渲染的时间,来表明一个道理。并不是diff就比非diff要开,虚拟DOM并不是任何时候性能都比非虚拟DOM 要快

先来实现一个工具函数,不熟悉的人可以手工敲下代码 熟悉下

// 真实的dom操作
// src/core/vdom/node-ops.js

export function createElement (tagName) {
 return document.createElement(tagName)
}

export function createTextNode (text) {
 return document.createTextNode(text)
}

export function createComment (text) {
 return document.createComment(text)
}

export function insertBefore (parentNode, newNode, referenceNode) {
 parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node, child) {
 node.removeChild(child)
}

export function appendChild (node, child) {
 node.appendChild(child)
}

export function parentNode (node) {
 return node.parentNode
}

export function nextSibling (node) {
 return node.nextSibling
}

export function tagName (node) {
 return node.tagName
}

export function setTextContent (node, text) {
 node.textContent = text
}

export function setAttribute (node, key, val) {
 node.setAttribute(key, val)
}

src/main.js

import {VNode,createCommentNode} from './core/vdom/vnode'
import patch from './core/vdom/patch'


var container = document.getElementById("app");
var oldVnode = new VNode(container.tagName,[],undefined,container);
var newVonde = createCommentNode('ul',[createCommentNode('li',['item 1']),createCommentNode('li',['item 2']),createCommentNode('li',['item 3'])])


console.time('start');
patch(oldVnode,newVonde); //渲染页面
console.timeEnd('start');

这里我们要实现一个patch方法,把Vnode渲染到页面中

src/core/vdom/patch.js

import * as nodeOps from './node-ops'
import VNode from './vnode'


export default function patch(oldVnode,vnode){
 let isInitialPatch = false;
 if(sameVnode(oldVnode,vnode)){
  //如果两个Vnode节点的根一致 开始diff
  patchVnode(oldVnode,vnode)
 }else{
  //这里就是不借助diff的实现
  const oldElm = oldVnode.elm;
  const parentElm = nodeOps.parentNode(oldElm);
  createElm(
   vnode,
   parentElm,
   nodeOps.nextSibling(oldElm)
  )
  if(parentElm != null){
   removeVnodes(parentElm,[oldVnode],0,0)
  }
 }
 return vnode.elm;
}
function patchVnode(oldVnode,vnode,removeOnly){
 if(oldVnode === vnode){
  return
 }
 const elm = vnode.elm = oldVnode.elm
 const oldCh = oldVnode.children;
 const ch = vnode.children

 if(isUndef(vnode.text)){
  //非文本节点
  if(isDef(oldCh) && isDef(ch)){
   //都有字节点
   if(oldCh !== ch){
    //更新children
    updateChildren(elm,oldCh,ch,removeOnly);
   }
  }else if(isDef(ch)){
   //新的有子节点,老的没有
   if(isDef(oldVnode.text)){
    nodeOps.setTextContent(elm,'');
   }
   //添加子节点
   addVnodes(elm,null,ch,0,ch.length-1)
  }else if(isDef(oldCh)){
   //老的有子节点,新的没有
   removeVnodes(elm,oldCh,0,oldCh.length-1)
  }else if(isDef(oldVnode.text)){
   //否则老的有文本内容 直接置空就行
   nodeOps.setTextContent(elm,'');
  }
 }else if(oldVnode.text !== vnode.text){
  //直接修改文本
  nodeOps.setTextContent(elm,vnode.text);
 }
}

function updateChildren(parentElm,oldCh,newCh,removeOnly){
  //这里认真读下,没什么难度的,不行的话 也可以搜索下图文描述这段过程的

 let oldStartIdx = 0;
 let newStartIdx =0;
 let oldEndIdx = oldCh.length -1;
 let oldStartVnode = oldCh[0];
 let oldEndVnode = oldCh[oldEndIdx];
 let newEndIdx = newCh.length-1;
 let newStartVnode = newCh[0]
 let newEndVnode = newCh[newEndIdx]
 let refElm;
 const canMove = !removeOnly
 while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
  if(isUndef(oldStartVnode)){
   oldStartVnode = oldCh[++oldStartIdx]
  }else if(isUndef(oldEndVnode)){
   oldEndVnode = oldCh[--oldEndIdx]
  }else if(sameVnode(oldStartVnode,newStartVnode)){
   patchVnode(oldStartVnode,newStartVnode)
   oldStartVnode = oldCh[++oldStartIdx]
   newStartVnode = newCh[++newStartIdx]
  }else if(sameVnode(oldEndVnode,newEndVnode)){
   patchVnode(oldEndVnode,newEndVnode)
   oldEndVnode = oldCh[--oldEndIdx];
   newEndVnode = newCh[--newEndIdx];
  }else if(sameVnode(oldStartVnode,newEndVnode)){
   patchVnode(oldStartVnode,newEndVnode);
   //更换顺序
   canMove && nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))
   oldStartVnode = oldCh[++oldStartIdx]
   newEndVnode = newCh[--newEndIdx]
  }else if(sameVnode(oldEndVnode,newStartVnode)){
   patchVnode(oldEndVnode,newStartVnode)
   canMove && nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)
   oldEndVnode = oldCh[--oldEndIdx]
   newStartVnode = newCh[++newStartIdx]
  }else{
   createElm(newStartVnode,parentElm,oldStartVnode.elm)
   newStartVnode = newCh[++newStartIdx];
  }
 }

 if(oldStartIdx > oldEndIdx){
  //老的提前相遇,添加新节点中没有比较的节点
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx+1].elm
  addVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx)
 }else{
  //新的提前相遇 删除多余的节点
  removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)
 }
}
function removeVnodes(parentElm,vnodes,startIdx,endIdx){
 for(;startIdx<=endIdx;++startIdx){
  const ch = vnodes[startIdx];
  if(isDef(ch)){
   removeNode(ch.elm)
  }
 }
}

function addVnodes(parentElm,refElm,vnodes,startIdx,endIdx){
 for(;startIdx <=endIdx;++startIdx ){
  createElm(vnodes[startIdx],parentElm,refElm)
 }
}

function sameVnode(vnode1,vnode2){
 return vnode1.tag === vnode2.tag
}
function removeNode(el){
 const parent = nodeOps.parentNode(el)
 if(parent){
  nodeOps.removeChild(parent,el)
 }
}
function removeVnodes(parentElm,vnodes,startIdx,endIdx){
 for(;startIdx<=endIdx;++startIdx){
  const ch = vnodes[startIdx]
  if(isDef(ch)){
   removeNode(ch.elm)
  }
 }
}
function isDef (s){
 return s != null
}
function isUndef(s){
 return s == null
}
function createChildren(vnode,children){
 if(Array.isArray(children)){
  for(let i=0;i<children.length;i++){
   createElm(children[i],vnode.elm,null)
  }
 }
}
function createElm(vnode,parentElm,refElm){
 const children = vnode.children
 const tag = vnode.tag
 if(isDef(tag)){
  // 非文本节点
  vnode.elm = nodeOps.createElement(tag); // 其实可以初始化的时候就赋予
  createChildren(vnode,children);
  insert(parentElm,vnode.elm,refElm)
 }else{
  vnode.elm = nodeOps.createTextNode(vnode.text)
  insert(parentElm,vnode.elm,refElm)
 }
}
function insert(parent,elm,ref){
 if(parent){
  if(ref){
   nodeOps.insertBefore(parent,elm,ref)
  }else{
   nodeOps.appendChild(parent,elm)
  }
 }
}

这就是完整实现了

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript 学习笔记(七)字符串的连接
Dec 31 Javascript
jQuery extend 的简单实例
Sep 18 Javascript
JS对select控件option选项的增删改查示例代码
Oct 21 Javascript
js获取会话框prompt的返回值的方法
Jan 10 Javascript
jquery+css3实现会动的小圆圈效果
Jan 27 Javascript
用jQuery.ajaxSetup实现对请求和响应数据的过滤
Dec 20 Javascript
Bootstrap select多选下拉框实现代码
Dec 23 Javascript
js+html制作简单日历的方法
Jun 27 Javascript
js删除对象/数组中null、undefined、空对象及空数组方法示例
Nov 14 Javascript
利用jquery和BootStrap实现动态滚动条效果
Dec 03 jQuery
js 压缩图片的示例(只缩小体积,不更改图片尺寸)
Oct 21 Javascript
关于javascript中的promise的用法和注意事项(推荐)
Jan 15 Javascript
bootstrap table实现双击可编辑、添加、删除行功能
Sep 27 #Javascript
Three.js中网格对象MESH的属性与方法详解
Sep 27 #Javascript
JS实现的简单四则运算计算器功能示例
Sep 27 #Javascript
Three.js利用顶点绘制立方体的方法详解
Sep 27 #Javascript
js实现扫雷小程序的示例代码
Sep 27 #Javascript
Three.js如何实现雾化效果示例代码
Sep 27 #Javascript
浅谈Angular4中常用管道
Sep 27 #Javascript
You might like
评分9.0以上的动画电影,剧情除了经典还很燃
2020/03/04 日漫
(PHP实现)只使用++运算实现加法,减法,乘法,除法
2013/06/27 PHP
测试php函数的方法
2013/11/13 PHP
PHP操作文件的一些基本函数使用示例
2014/11/18 PHP
PHP浮点比较大小的方法
2016/02/14 PHP
Nginx下ThinkPHP5的配置方法详解
2017/08/01 PHP
php+redis实现商城秒杀功能
2020/11/19 PHP
JavaScript 应用技巧集合[推荐]
2009/08/30 Javascript
JS 动态获取节点代码innerHTML分析 [IE,FF]
2009/11/30 Javascript
JavaScript Date对象 日期获取函数
2010/12/19 Javascript
jquery 模板的应用示例
2013/11/12 Javascript
JS使用for循环遍历Table的所有单元格内容
2014/08/21 Javascript
js 通过cookie实现刷新不变化树形菜单
2014/10/30 Javascript
jQuery+CSS3实现树叶飘落特效
2015/02/01 Javascript
javascript实现checkbox全选的代码
2015/04/30 Javascript
javascript实现动态改变层大小的方法
2015/05/14 Javascript
利用JQuery阻止事件冒泡
2016/12/01 Javascript
javascript获取指定区间范围随机数的方法
2017/09/08 Javascript
浅谈vue.js导入css库(elementUi)的方法
2018/03/09 Javascript
vue.js提交按钮时进行简单的if判断表达式详解
2018/08/08 Javascript
js计算两个时间差 天 时 分 秒 毫秒的代码
2019/05/21 Javascript
Vue响应式原理Observer、Dep、Watcher理解
2019/06/06 Javascript
vant中的toast轻提示实现代码
2020/11/04 Javascript
python bmp转换为jpg 并删除原图的方法
2018/10/25 Python
用python3教你任意Html主内容提取功能
2018/11/05 Python
Python数据类型之Dict字典实例详解
2019/05/07 Python
django settings.py 配置文件及介绍
2019/07/15 Python
Python+OpenCv制作证件图片生成器的操作方法
2019/08/21 Python
Python搭建代理IP池实现存储IP的方法
2019/10/27 Python
python打印n位数“水仙花数”(实例代码)
2019/12/25 Python
CSS3 3D制作实战案例分析
2016/09/18 HTML / CSS
应聘教师求职信
2014/07/19 职场文书
关于感恩的演讲稿400字
2014/08/26 职场文书
mysql如何配置白名单访问
2021/06/30 MySQL
Grafana可视化监控系统结合SpringBoot使用
2022/04/19 Redis
使用 DataAnt 监控 Apache APISIX的原理解析
2022/07/07 Servers