深入理解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代码
Nov 10 Javascript
setinterval()与clearInterval()JS函数的调用方法
Jan 21 Javascript
JavaScript实现鼠标点击后层展开效果的方法
May 13 Javascript
JavaScript中的setMilliseconds()方法使用详解
Jun 11 Javascript
jQuery的end()方法使用详解
Jul 15 Javascript
js 获取当前web应用的上下文路径实现方法
Aug 19 Javascript
BootStrap glyphicon图标无法显示的解决方法
Sep 06 Javascript
微信小程序 引用其他js文件实现代码
Feb 22 Javascript
详解如何用webpack打包一个网站应用项目
Jul 12 Javascript
jQuery获取随机颜色的实例代码
May 21 jQuery
简单了解小程序+node梳理登陆流程
Jun 24 Javascript
JavaScript实现拖拽功能
Feb 11 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
javascript 有趣而诡异的数组
2009/04/06 Javascript
AJAX的跨域与JSONP(为文章自动添加短址的功能)
2010/01/17 Javascript
jquery实现网站超链接和图片提示效果
2013/03/21 Javascript
离开当前页面前使用js判断条件提示是否要离开页面
2014/05/02 Javascript
Javascript实现简单二级下拉菜单实例
2014/06/15 Javascript
基于jQuery实现文本框只能输入数字(小数、整数)
2016/01/14 Javascript
3种vue组件的书写形式
2017/11/29 Javascript
利用node实现一个批量重命名文件的函数
2017/12/21 Javascript
Vue高版本中一些新特性的使用详解
2018/09/25 Javascript
解决angular双向绑定无效果,ng-model不能正常显示的问题
2018/10/02 Javascript
详解使用element-ui table组件的筛选功能的一个小坑
2018/11/02 Javascript
layui自定义ajax左侧三级菜单
2019/07/26 Javascript
vue父子组件通信的高级用法示例
2019/08/29 Javascript
微信小程序实现打开并下载服务器上面的pdf文件到手机
2019/09/20 Javascript
JQuery插件tablesorter表格排序实现过程解析
2020/05/28 jQuery
解决vue路由name同名,路由重复的问题
2020/08/05 Javascript
[01:15]《辉夜杯》北京网鱼队巡礼
2015/10/26 DOTA
在漏洞利用Python代码真的很爽
2007/08/26 Python
Python 类与元类的深度挖掘 I【经验】
2016/05/06 Python
Request的中断和ErrorHandler实例解析
2018/02/12 Python
Python使用matplotlib模块绘制图像并设置标题与坐标轴等信息示例
2018/05/04 Python
Scrapy框架爬取西刺代理网免费高匿代理的实现代码
2019/02/22 Python
Python变量及数据类型用法原理汇总
2020/08/06 Python
selenium自动化测试入门实战
2020/12/21 Python
HTML5印章绘制电子签章图片(中文英文椭圆章、中文英文椭圆印章)
2019/06/03 HTML / CSS
美国女性运动零售品牌:Lady Foot Locker
2017/05/12 全球购物
美国正版电视节目和电影在线观看:Hulu
2018/05/24 全球购物
世界汽车零件:World Car Parts
2019/09/04 全球购物
大专毕业生自我鉴定
2013/11/21 职场文书
菜篮子工程实施方案
2014/03/08 职场文书
竞选体育委员演讲稿
2014/04/26 职场文书
银行竞聘演讲稿
2014/05/16 职场文书
2014年度安全生产目标管理责任书
2014/07/25 职场文书
拾金不昧感谢信
2015/01/21 职场文书
小学五年级语文上册教学计划
2015/01/22 职场文书
小学大队委竞选口号
2015/12/25 职场文书