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

网友投稿 249 2023-03-29


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

前言

经常看到讲解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

var child = children[i];

if(typeof chRNdXmsnLild == '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表示

    var child = children[i];

    if(typeof chRNdXmsnLild == '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表示

    先实现不用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)){

    patRNdXmsnLchVnode(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(isDefhttp://(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

    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)

    }

    }

    }

    这就是完整实现了

    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)

    }

    }

    }

    这就是完整实现了


    版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

    上一篇:接口功能如何设计测试用例(接口测试举例说明)
    下一篇:详解Struts2标签遍历
    相关文章

     发表评论

    暂时没有评论,来抢沙发吧~