深入理解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 相关文章推荐
script标签的 charset 属性使用说明
Dec 04 Javascript
jquery中html、val与text三者属性取值的联系与区别介绍
Dec 29 Javascript
CSS3,HTML5和jQuery搜索框集锦
Dec 02 Javascript
jquery结合CSS使用validate实现漂亮的验证
Jan 29 Javascript
在JavaScript的jQuery库中操作AJAX的方法讲解
Aug 15 Javascript
JavaScript DOM 对象深入了解
Jul 20 Javascript
jQuery Pagination分页插件使用方法详解
Feb 28 Javascript
深入理解Angular4中的依赖注入
Jun 07 Javascript
JS实现中文汉字按拼音排序的方法
Oct 09 Javascript
js实现延迟加载的几种方法详解
Jan 19 Javascript
JS原生瀑布流效果实现
Apr 26 Javascript
浅析vue-router实现原理及两种模式
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
substr()函数中文版
2006/10/09 PHP
百度站点地图(百度sitemap)生成方法分享
2014/01/09 PHP
php获取域名的google收录示例
2014/03/24 PHP
ModelDialog JavaScript模态对话框类代码
2011/04/17 Javascript
iframe子父页面调用js函数示例
2013/11/07 Javascript
DOM基础教程之使用DOM设置文本框
2015/01/20 Javascript
JavaScript和JQuery的鼠标mouse事件冒泡处理
2015/06/19 Javascript
AngularJS使用angular-formly进行表单验证
2015/12/27 Javascript
javascript实现下拉提示选择框
2015/12/29 Javascript
angular.js分页代码的实例
2016/07/27 Javascript
JS代码实现百度地图 画圆 删除标注
2016/10/12 Javascript
js在ie下打开对话窗口的方法小结
2016/10/24 Javascript
JavaScript仿微博发布信息案例
2016/11/16 Javascript
Vue非父子组件通信详解
2017/06/12 Javascript
AngularJS实现图片上传和预览功能的方法分析
2017/11/08 Javascript
详解layui中的树形关于取值传值问题
2018/01/16 Javascript
vue进行图片的预加载watch用法实例讲解
2018/02/07 Javascript
详解Vue中数组和对象更改后视图不刷新的问题
2018/09/21 Javascript
Webpack中SplitChunksPlugin 配置参数详解
2020/03/24 Javascript
[06:04]DOTA2英雄梦之声Vol19卓尔游侠
2014/06/20 DOTA
[00:55]深扒TI7聊天轮盘语音出处3
2017/05/11 DOTA
Python实现二叉树结构与进行二叉树遍历的方法详解
2016/05/24 Python
Python3.5 win10环境下导入kera/tensorflow报错的解决方法
2019/12/19 Python
python如何实现不可变字典inmutabledict
2020/01/08 Python
在python中实现求输出1-3+5-7+9-......101的和
2020/04/02 Python
学python爬虫能做什么
2020/07/29 Python
百思买加拿大:Best Buy Canada
2018/03/20 全球购物
护士节策划方案
2014/05/19 职场文书
财会专业毕业生自荐信
2014/07/09 职场文书
读群众路线的心得体会
2014/09/03 职场文书
2015年教师教学工作总结
2015/04/28 职场文书
罗马假日观后感
2015/06/08 职场文书
三国演义读书笔记
2015/06/25 职场文书
初一年级组工作总结
2015/08/12 职场文书
Spring Security中用JWT退出登录时遇到的坑
2021/10/16 Java/Android
聊聊redis-dump工具安装问题
2022/01/18 Redis