深入理解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 相关文章推荐
JS高级调试技巧:捕获和分析 JavaScript Error详解
Mar 16 Javascript
使用jQuery的attr方法来修改onclick值
Jul 07 Javascript
JS仿Windows开机启动Loading进度条的方法
Feb 26 Javascript
Prototype框架详解
Nov 25 Javascript
使用angularjs创建简单表格
Jan 21 Javascript
解析预加载显示图片艺术
Dec 05 Javascript
原生JS实现图片轮播效果
Dec 26 Javascript
jQuery+PHP+Mysql实现抽奖程序
Apr 12 jQuery
react-native组件中NavigatorIOS和ListView结合使用的方法
Sep 30 Javascript
JavaScript内存泄漏的处理方式
Nov 20 Javascript
100行代码实现一个vue分页组功能
Nov 06 Javascript
javascript实现搜索筛选功能实例代码
Nov 12 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
php短域名转换为实际域名函数
2011/01/17 PHP
PHP中time(),date(),mktime()区别介绍
2013/09/28 PHP
php实现微信公众号企业转账功能
2018/10/01 PHP
获取当前点击按钮的id用this.id实现
2014/03/17 Javascript
jquery+正则实现统一的表单验证
2015/09/20 Javascript
JS表格组件神器bootstrap table详解(基础版)
2015/12/08 Javascript
jQuery validate插件submitHandler提交导致死循环解决方法
2016/01/21 Javascript
浅谈jquery中的each方法$.each、this.each、$.fn.each
2016/06/23 Javascript
JS实现的相册图片左右滚动完整实例
2016/11/23 Javascript
jquery实现下拉框多选方法介绍
2017/01/03 Javascript
从零开始学习Node.js系列教程四:多页面实现数学运算的client端和server端示例
2017/04/13 Javascript
vue地址栏直接输入路由无效问题的解决
2018/11/15 Javascript
详解webpack引用jquery(第三方模块)的三种办法
2019/08/21 jQuery
Vue axios 跨域请求无法带上cookie的解决
2020/09/08 Javascript
[01:59]DOTA2首部纪录片《Free to play》预告片
2014/03/12 DOTA
[48:48]VGJ.T vs Liquid 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
使用python Django做网页
2013/11/04 Python
Python生成随机数的方法
2014/01/14 Python
使用Python的Flask框架来搭建第一个Web应用程序
2016/06/04 Python
离线安装Pyecharts的步骤以及依赖包流程
2020/04/23 Python
Python工程师面试必备25条知识点
2018/01/17 Python
python3库numpy数组属性的查看方法
2018/04/17 Python
对python中xlsx,csv以及json文件的相互转化方法详解
2018/12/25 Python
NumPy 数组使用大全
2019/04/25 Python
Python列表对象实现原理详解
2019/07/01 Python
Python自动化之UnitTest框架实战记录
2020/09/08 Python
Pyqt助手安装PyQt5帮助文档过程图解
2020/11/20 Python
Python实现迪杰斯特拉算法并生成最短路径的示例代码
2020/12/01 Python
如何使用Python进行PDF图片识别OCR
2021/01/22 Python
CSS3媒体查询(Media Queries)介绍
2013/09/12 HTML / CSS
HTML5 创建canvas元素示例代码
2014/06/04 HTML / CSS
学生实习自我鉴定
2013/10/11 职场文书
活动策划求职信模板
2014/04/21 职场文书
酒店管理专业自荐信
2014/05/23 职场文书
《中国梦我的梦》小学生演讲稿
2014/08/20 职场文书
撤诉书怎么写
2015/05/19 职场文书