Vue source code analysis of virtual DOM and diff algorithm study notes + interview test points and answers + questions and solutions + experience and summary + JS version of snabbdom (castrated)

Summary of premise:

        Most of the source code and pictures in the article come from [Shang Silicon Valley] Vue source code analysis of virtual DOM and diff algorithm. The article focuses on summarizing and understanding virtual DOM and diff algorithm, and focuses on personal notes. I hope it can help friends who are studying to understand, so not every step is There is a display. If necessary, please refer to the top comment under the video of Teacher Shang Silicon Valley at Bilibili. The notes of YK bacteria are very suitable for reading from 0.

0. Interview test points and answers: (for reference only, if there are any mistakes, thank you for pointing them out)

1.What is virtual DOM

         Virtual DOM combines the properties of the real DOM into objects and returns them. The main attributes are sel, data, children, text, elm, and key. sel represents the tag name, and data contains some attribute values, such as the href attribute of the a tag. , the src attribute of the img tag, etc., children is its child node(s), text stores its text attributes, for example, the text of the h1 tag is "This is a title", elm stores the real value of the virtual node DOM, generally when the virtual node is converted into a real DOM, the generated DOM node will be assigned to the elm attribute, and the key is its unique identifier, which is used for minimum updates, etc.

        I have also seen another saying that it is divided into three attributes: tag name (tag), attributes (attrs) and child element objects (children) (I have not understood this in detail)

2. When will diff comparison be performed:

        This is mainly based on the following points:

        ① Only the same node can be compared by diff algorithm. The definition of the same node is that the selector and key values ​​​​are equal. 

        ② Even if it is the same node, but the key value is not specified, if it is in order, it can be updated with a minimum amount, but once the order is reversed or disrupted, the diff will be completely rebuilt (this is the case in the first example of the diff algorithm ②), Therefore, the same node needs to specify a key as a unique identifier to tell diff that they are the same node, otherwise it will be violently demolished.

        ③ Only the same level comparison is performed, and cross-layer comparison is not possible. When a new layer of nested structure is added in the comparison element, for example, there is an additional section structure under the div, even if the innermost child nodes are the same, comparison cannot be performed because the cross-level layered.

3.What are the specific implementation steps of diff? (I didn’t answer the question during the interview 0.0) [Refer to the flow chart of the article for easy understanding]

PS: It is recommended to read the explanation below before reading this, otherwise it will be a bit abstract. My expression is not very popular, and I don’t think it is easy to understand when I read it myself, but this is already the author’s level of expression....

       First, the patch function will be called. When the patch function is called, you need to compare whether oldVnode (old node) and newVnode (new node) are the same node, mainly by comparing their sel and key values. If they are the same, you can proceed. diff refines the comparison, otherwise violently deletes the old one and inserts the new one

        But if the old node is a DOM node, we need to package it into a virtual node first, wrap it into a virtual node through the Vnode function, and then compare it. If the sel and key are the same, perform a diff. If there is no change in the old and new nodes in the diff, Do nothing. Take text as an example. If the text changes, the innerText attribute of the new node will replace the text attribute of the old node. If it is a single attribute change, it is not difficult to judge.

        If the comparison of complex attributes such as children is involved, four hit comparisons using the diff algorithm will be performed. The four hits are: new before and new after (new front node and new after node), new after and old after, and new after. Compared with the old before, new before and old after, it is worth noting that they are executed sequentially here, as can be seen from the updateChildren code. They are executed together in a while() function. The condition of while is oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx means that when the old previous pointer reaches the old back pointer position or the new front pointer reaches the new back pointer position, the judgment needs to be terminated, indicating that the cycle of the new node or the old node has been completed.

        In addition to the four hits, there are still other possibilities under the while condition. At this time, only the new previous node at the new previous pointer position will be compared. It needs to be compared with each item in the old node in a loop, in order to improve the speed of comparison. , the map mapping method is generally used here. The key of the map is the key value corresponding to the attribute name corresponding to the old node (that is, the unique identifier of the node), and the value is the attribute value set to the index of the old node (convenient for confirmation). The position of the node, for subsequent assignment of the virtual node to undefined service). If it does not exist, it proves that it is a newly added item and needs to be inserted in front of the old previous node. If it exists, it proves that the position has changed and the node operation needs to be moved. , the mobile node is still inserted in front of the old node, but the difference from the previous one is that the undefined value needs to be set for the corresponding old node. The purpose of this is to tell the diff algorithm. We have already done the processing here. This node There is a corresponding node in the new node.

        In addition, there are still two situations, but these two are easier to understand. They are when the while loop statement ends, oldStartIdx or <oldEndIdx, which means that although the cycle of the new node is completed, there are still old nodes left, which means there are If you need to delete redundant items, you can removeChild from the old node. The second method is that newStartIdx is still < newOldIdx, which proves that the new node has not ended the cycle and there are still nodes that need to be added. At this time, you only need to add it in front of the old node. Just insert a new node through insertBefore and complete all the above judgments. The diff is considered to have completed a complete round.

        PS:

        Due to the current limited level, the organizational expression may not be easy to understand or may contain errors. As the level and understanding improve, the answer results will be revised regularly.

1. Feel the diff algorithm:

        The diff algorithm is mainly implemented through patches. The purpose is to achieve the minimum amount of updates and reduce overhead.

        Situation ①:

        Increase sequentially, but do not specify the key value

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
  } from "snabbdom";
  
const patch = init([classModule,propsModule,styleModule,eventListenersModule])

// import { h } from "./mysnabbdom/h";

const container = document.getElementById("container")

const vnode1 = h('ul',{},[
  h('li',{},'A'),
  h('li',{},'B'),
  h('li',{},'C'),
  h('li',{},'D')
])

patch(container,vnode1);

const vnode2 = h('ul',{},[
  h('li',{},'A'),
  h('li',{},'B'),
  h('li',{},'C'),
  h('li',{},'D'),
  h('li',{},'E')
])

var btn = document.getElementById("btn")
//点击按钮是,将vnode1改成vnode2
btn.onclick = function(){
  patch(vnode1,vnode2);
}

        In this way, by clicking the button, you will find that there is an additional node E, and modify the original ABCD value through the console as shown below

This is indeed the result, and we can add the

Situation ②: 

Do not specify the key value, but increase in reverse order

The result was not as expected, but all the elements were rebuilt. In fact, the steps he performed were to change A to E, B to A, in order, and finally add D, so it was rebuilt.

 Situation ③:

Reverse order, but specify the key attribute value as the unique identifier in the h function

Results were produced as expected, no reconstruction was performed

 This can be summarized:

①The minimum update is indeed powerful, but for the convenience of comparison, it is best to add a unique identification key to tell the diff algorithm that the element is still the same node before and after the change.

2. When ul in the h function is changed to ol, even if all keywords are added internally, refined comparison cannot be made.

const vnode1 = h('ul',{},[
  h('li',{key:'A'},'A'),
  h('li',{key:'B'},'B'),
  h('li',{key:'C'},'C'),
  h('li',{key:'D'},'D')
])

patch(container,vnode1);

const vnode2 = h('ol',{},[
  h('li',{key:'A'},'A'),
  h('li',{key:'B'},'B'),
  h('li',{key:'C'},'C'),
  h('li',{key:'D'},'D'),
  h('li',{key:'E'},'E')
])

In fact, this is easy to understand. Since the virtual nodes are different, there will be no comparison. How to define whether they are the same node? Only when the selector and key are equal can we say they are the same node.

const vnode1 = h('ul',{key:'0216'},[]}
const vnode2 = h('ul',{key:'0216'},[])
// vnode1和vnode2的选择器都是ul,并且key值都是0216,可以进行diff比较

2.1 When the new and old node types are different, such as code snippet 2.1, container is a DOM node, and vnode1 is a virtual node. At this time, as shown in flow chart 2.1, if it is not a virtual node, comparison cannot be made, and it needs to be packaged into a virtual node and then compared with the key Values ​​and selectors, the judgment method is as shown in code snippet 2.2, the code is ts code: what is after the colon is the variable type, if they are the same, continue the comparison, otherwise violent disassembly

patch(container,vnode1);

                                                        Code snippet 2.1

function sameVnode(vnode1:VNode,vnode2:Vnode):boolean {
    return vnode1.key == vnode2.key && vnode1.sel == vnode2.sel
 }

                                                        Code snippet 2.2

                                                       Flow chart 2.1 (source: Silicon Valley)

Summarize:

① Only the same node can be compared by diff algorithm. The definition of the same node is that the selector and key values ​​​​are equal. 

3. When one more level is nested in the h function, can the internal elements be updated with a minimum amount?

const vnode1 = h('div',{key:'0216'},[
  h('p',{key:'A'},'A'),
  h('p',{key:'B'},'B'),
  h('p',{key:'C'},'C'),
  h('p',{key:'D'},'D')
])

patch(container,vnode1);

const vnode2 = h('div',{key:'0216'},h('section',{},[
    h('p',{key:'A'},'A'),
    h('p',{key:'B'},'B'),
    h('p',{key:'C'},'C'),
    h('p',{key:'D'},'D')
]))

 

 It can be seen from the code running results that even if the parent element selector and key value are the same, and multiple layers of structures are nested, diff still reconstructs the element.

Summarize:

① Only the same layer comparison is performed, and cross-layer comparison cannot be performed.

Final summary: (please see above for details)

① Minimum amount of update, it is best to specify a key value for it, so that the minimum amount of update can be achieved even if it is out of order

②Diff algorithm comparison can only be performed on the same node. The definition of the same node is that the selector and key values ​​​​are equal. 

③ Only comparisons at the same level are performed, cross-layer comparisons are not allowed.

2. Problems and solutions:

2.1 Configure a simple development environment:

①npm i -D webpack @5  webpack-cli@3 webpack-dev-server@3 failed to install dependencies:

First

npm i webpack-cli@3

After completion

npm i webpack-dev-server@3

finally passed

npm i -D webpack@5

finish installation. Source of problem solving: Comment area user: えいえんな, thank you very much!

2.2 Function overloading in JavaScript:       

        Function overloading is a function with the same name that performs different functions. It uses different execution methods depending on the parameters passed in. It is related to the parameter type and number of parameters passed in. The more types it can accept, the more powerful the overloading function will be. , and finally distinguish which function was called by returning the return value.

        **There is no real function overloading in JavaScript

       **Reason: Overloading is a very important feature in object-oriented languages. There is no real overloading in JavaScript, it is simulated (because JavaScript is an object-based programming language, not purely object-oriented, and it does not have real overloading. state: such as inheritance, overloading, and rewriting), the essential reason is that the declaration creates a function with the same name and overrides it by default in JS.

        h('div')

        h('div','文字')

        h('div',[])

        h('div',h())

        h('div',{},[])

        h('div',{},'文字')

        h('div',{},h())

2.3 Low-level errors:

Error type:

index.js:15 Uncaught TypeError: (0 , _mysnabbdom_h__WEBPACK_IMPORTED_MODULE_0__.h) is not a function

Wrong writing:

import {h} from "./mysnabbdom/h";

Amendment:

import h from "./mysnabbdom/h";

reason:

        Since the import file export default is exported as in code snippet 6.6, the method is not named, so the exported name is given to the importer. We can import it through import h from "./mysnabbdom/h"; , on the contrary, if {h} is added, it means that the function of the h method exposed by export is found in this module. Since this function is not exposed in the file, an error is reported. From this, we introduce another method to solve the error, such as code Fragment 6.7, we named the exported method h function, which can be introduced by import {h} from "./mysnabbdom/h";

        Summarize:

        If the introduced method uses export default, there is no need to add curly braces. If the imported module has many methods, we can import the required methods on demand (I think this is actually the destructuring assignment of ES6), such as the on-demand import commonly used in Vue projects. Such as code snippet 6.5. Of course, if the import method overlaps with other places or is inconvenient to remember or write, you can also rename it through the as statement, such as code snippet 6.4.

import {studentList as ms } from 'studentList'

                                                                        Code snippet 6.4

import { defineComponent, onMounted, reactive, ref, getCurrentInstance,nextTick  } from 'vue'

                                                        Code snippet 6.5

export default function(sel,data,c){
...
}

                                                        Code snippet 6.6

export function h(sel,data,c){
...
}
//这样的话就能通过{h}方式导入

                                                       Code snippet 6.7

3. Experience and summary:

        After learning virtual DOM and diff algorithm, I learned what virtual DOM is. Virtual DOM actually converts the properties in the real DOM into object form storage. The purpose of this is also very clear. In fact, it is to compare with the diff algorithm and convert it into Objects are convenient for us to operate data, and because we operate virtual DOM instead of real DOM, the overhead will be very small. The conversion of virtual DOM mainly uses the h function. Its function is to create virtual nodes. The data of virtual nodes will be based on objects. The formal storage is returned, and after a series of diffs, the createElement function is used, which is to create a real DOM based on the properties of the virtual node. The patch function is then used to update the node according to different situations and whether it meets the minimum update conditions. The tree operation is to convert it into a real page DOM element; the main function of the diff algorithm is to compare between nodes and compare whether the same item exists in the new and old virtual nodes. If it exists, update the patchVnode with the minimum amount. If it does not exist, then update the patchVnode with the minimum amount. Create a new node. It is worth noting here that the diff comparison is a virtual node, not a real DOM element. When doing a diff comparison, you need to pass in the key keyword as the only representation. What will happen if you do not pass it in?

        According to the example in code 3.2 combined with the code analysis in point 3.1, it can be seen that the condition for judging whether it is the same node is to see whether the sel and key attributes of the old and new nodes are the same. Since they are both li tags, sel is both li, and lacks the unique identifier of key, so every time The first hit can be achieved, as shown in Figures 3.3 and 3.4. It seems to be the result of a minimum amount of update. In fact, every value has been modified, which violates the principle of minimum amount of update. If it is a minimum amount of update, the text will not disappear. Because the result of the minimum update is actually to move the node, not to dismantle and rebuild it, this reflects the importance of the key value, and is also why v-for needs to pass in a unique identifier during development, which is to serve the minimum update diff.

         function checkSameVnode(a,b){
            return a.sel == b.sel && a.key == b.key
            }

        // 开始四次命中判断
        // 新前和旧前
        if (checkSameVnode(oldStartVnode,newStartVnode)){
            console.log("1新前和旧前命中")
            patchVnode(oldStartVnode,newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else
        // 新后和旧后
        if (checkSameVnode(oldEndVnode,newEndVnode)){
            console.log("2新后和旧后命中")
            patchVnode(oldEndVnode,newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else
        // 新后与旧前
        if (checkSameVnode(oldEndVnode,newEndVnode)){
            console.log("3新后和旧前命中")
            patchVnode(oldStartVnode,newEndVnode)
            //当3新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
            parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSilbling)
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else
        // 新前与旧后
        if (checkSameVnode(oldEndVnode,newStartVnode)){
            console.log("4新前和旧后命中")
            patchVnode(oldEndVnode,newStartVnode)
            //第四种情况需要移动节点,移动新前到旧前的前面
            parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } 

                                                                3.1

var myVnode1 = h('ul',{},[
    h('li',{},'A'),
    h('li',{},'B'),
    h('li',{},'C'),
    h('li',{},'D'),
    h('li',{},'E'),
])


var myVnode2 = h('ul',{},[
    h('li',{},'E'),
    h('li',{},'C'),
    h('li',{},'D'),
    h('li',{},'A'),
    h('li',{},'B'),
    
])

                                                                3.2

                                                                3.3

                                                                3.4

4. JS version of snabbdom (castrated version)

Summary of premise:

        Only the key function code implementation is provided here. The project structure and file configuration are not within the scope of the description. The h function adds some restrictions for easy understanding: if the virtual node has a text attribute, it will not have a children attribute, and if it has a children attribute, there will be no text . The purpose of adding this condition is to simplify the discussion and prevent confusion due to complex logic. In fact, it mainly explains the implementation principle of diff. In the case of if else, you only need to take out typical examples. If the discussion is detailed enough, it will be easy to ignore the basics, because the focus is not to look at others. How many situations are there, but how to perform diff.

h function:

        The main function is to create a virtual node , which will accept part of the value, and then convert it into an object and return it by calling the Vnode function. This creates a virtual node as shown in Figure 4.1.

        It is worth noting here that when the third parameter of a virtual node is an array, there is no need to perform loop processing, because each item in the array is created through the h function, and it will go through the h function again. Judgment, then the children returned to the outer h function at this time are already virtual nodes and do not need to be processed. This is a subtle point.

var myVnode1 = h('ul',{},[
    h('li',{key:'A'},'A'),
    h('li',{key:'B'},'B'),
    h('li',{key:'C'},'C'),
    h('li',{key:'D'},'D'),
    h('li',{key:'E'},'E'),
])

                                                                4.1

import vnode from "./vnode";

//编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可
//相当于它的重载功能较弱
// 也就是说,调用的时候形态必须是下满三种之一
// h("div",{},'文字')
// h("div",{},[])
// h("div",{},h())
export default function(sel,data,c){
        //检查参数的个数
        if(arguments.length != 3 ){
            throw new Error('对不起,h函数是低配版,最少三个参数!')
        }
        // 检查参数c的类型
        if (typeof c == 'string' || typeof c == 'number'){
            // 说明现在调用h函数是第一个类型
            return vnode(sel,data,undefined,c,undefined)
        }else if (Array.isArray(c)){
            //说明现在调用h函数是第二种
            let children = []
            //遍历c,收集children中的h函数生成的数据
            for (let i =0;i<c.length;i++){
                //检查c[i]必须是一个对象,如果不满足
                if(!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
                throw new Error("传入的数组参数中有项不是h函数")
                // 这里不用执行c[i],因为你的测试语句中已经有了执行,数组[]中h函数就是执行了
                //h函数作用会帮我们判断传入children函数有没有不合法的,如果没有的话,此时只需要收集好就可以了
                children.push(c[i])
            }
            //循环结束,就说明children数据收集完毕,收集完成就可以传给vnode函数,此时可以返回虚拟节点了
            return vnode(sel,data,children,undefined,undefined)
        }else if (typeof c == 'object' && c.hasOwnProperty('sel')){
            //说明现在调用h函数是第三种
            //传入的c是唯一的children,不用执行c,因为测试语句中已经执行了(调用了h函数进行判断)
            let children = [c]
            return vnode(sel,data,children,undefined,undefined)
        }else {
            throw new Error('传入的第三个参数类型不对!')
        }
}

// console.log(vnode("div",2,3,4,5))

Vnode function:

        The function of the Vnode function is very simple. It just combines the accepted parameters into an object and returns it. What is returned is the attributes in the virtual node.

//函数的功能非常简单,就把传入的5个参数组合合成对象返回
export default function(sel,data,children,text,elm) {
    const key = data.key;
    return {
        // sel:sel,
        // data:data,
        // children:children,
        // text:text,
        // elm:elm,
        // ES6新语法特性,属性值和属性名相同可以缩写
        sel,data,children,text,elm,key
    }
}

createElement function:

        The function of the createElement function is to convert the virtual node into a real DOM structure . Because in the end, the tree that is presented to the page needs to be the real DOM instead of the virtual node, so this step is essential. It is worth noting that the h function is said before. By calling the h function multiple times to avoid the loop problem, you still need to face the loop problem again when it is converted into a real DOM structure. That is, how to deal with the DOM structure generated by the internal children attribute?

        For the convenience of explanation in the 4.1 code, before the myVnode1 virtual node becomes a real DOM node, it will create a real DOM node based on its sel attribute, but at this time it is an orphan node and is received using the variable domNode. The clever thing is Come on, create and appendChild the child node in the children attribute to the domNode node, forming a "node group". Since this step is performed before vnode.elm = domNode, return vnode.elm assignment and return, return This is the complete structure of myVnode1. Since then, the operation of converting the virtual node with the children attribute into the real DOM has been completed.

PS:

        Orphan node:

        In fact, it's because it hasn't climbed the tree yet, or more generally speaking, it has not yet been connected to the page text, but the node has been created. When we learned the basics of js, we all know that to create a node, we need to createElement first, and then pass appendChild or The insertBefore method adds the node to the corresponding position

        Node group:

        I defined it myself, which means that although the tree has not yet been added, since the real DOM node has been generated through domNode, the next child nodes will have a benchmark based on which the appendChild operation can be performed, thus forming a group of related node groups.

// // 真正创建节点。将vnode创建为DOM,插入到pivot(标杆节点)这个元素之前
// export default function (vnode,pivot) {
//     console.log('目的是把虚拟节点',vnode,'插入到标杆',pivot)
//     //创建以一个DOM节点,这个节点现在是孤儿节点(未上树或者说没插入)
//     //依据节点选择器名称创建节点
//     let domNode = document.createElement(vnode.sel)
//     //有子节点还是有文本??
//     if(vnode.text != "" && (vnode.children == undefined || vnode.children.length ==0)){
//         //它内部是文字
//         domNode.innerText = vnode.text
//         //将孤儿节点上书,让标杆节点的父元素调用insertBefore方法
//         //将新的孤儿节点插入到标签节点之前就能在页面中显示真实DOM元素了
//         pivot.parentNode.insertBefore(domNode,pivot)
//     } else if (Array.isArray(vnode.children) && vnode.children.length > 0){
//         //它内部是子节点,就要递归创建节点

//     }
// }

// 真正创建节点。将vnode创建为DOM
export default function createElement(vnode) {
    //将虚拟节点变成真正的DOM节点,但是未插入
    //创建以一个DOM节点,这个节点现在是孤儿节点(未上树或者说没插入)
    //依据节点选择器名称创建节点
    let domNode = document.createElement(vnode.sel)
    //有子节点还是有文本??
    if(vnode.text != "" && (vnode.children == undefined || vnode.children.length ==0)){
        //它内部是文字
        domNode.innerText = vnode.text
        //补充elm属性
        vnode.elm = domNode
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0){
        //它内部是子节点,就要递归创建节点
        for (let i=0;i<vnode.children.length;i++){
            let ch = vnode.children[i]
            //将内部的虚拟子节点创建成真实的DOM节点
            let chDOM = createElement(ch)
            //将转换成真实DOM的内部子节点挨个追加到父元素下,为什么可以直接追加?
            //因为已经确定新旧节点不同并且在patch函数处通过createElement函数为新节点创建了真实DOM节点
            //因为这步在return vnode.elm语句之前,所以其实新节点还没有被return回去,但是由于已经有了DOM结构
            //所以内部虚拟节点生成的真实DOM节点就能以他为标杆追加到DOM中去
            //可以这样理解,先创建一个父节点DOM,在return回去之前,将内部虚拟节点转换真实DOM串联在一起
            //然后再插入到页面中进行显示,就是将节点关系先连接成一个节点团,然后return回去通过patch上树
            console.log(ch)
            domNode.appendChild(chDOM)
        }
    }
    // 补充elm属性,这步很关键,子节点们结团后还需要赋值给vnode.elm才有值返回
    vnode.elm = domNode
    //返回elm,elm属性是一个纯DOM对象
    return vnode.elm
}

patch function:

        The main function of the patch function is to add virtual nodes to the tree. In human terms, it converts virtual nodes into real DOM nodes . Virtual nodes are used for judgment and other data processing. In fact, users only care about the changes in the real DOM and only see the changes. To obtain the changes in the real DOM, the role of patch is to convert it into DOM, and the final result of the complex logical judgment is to judge whether this node needs to be patched, that is, whether it needs to be displayed, whether it needs to be climbed up the tree, and if it needs to be called The patch function can cause the virtual node to be displayed on the page and become a real DOM.

        Since the tree on the node is divided into two situations, one is the tree on the same node, the other is the tree on the same node, the second is a more violent method, the amount of code is less, the situation is also very simple, delete the old node Just add a new node, but the first case is more complicated, so it is separated into a method called patchVnode, which is a tree operation on the same node.

import vnode from "./vnode";
import createElement from "./createElement";
import patchVnode from "./patchVnode";

export default function (oldVnode,newVnode) {
    
    //判断传入的第一个参数,是DOM节点还是虚拟节点?
    if(oldVnode.sel == '' || oldVnode.sel == undefined) {
        //传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode)
    }
    // console.log(oldVnode,newVnode,"查看一个节点样子")
    //判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel){
        console.log("是同一个节点")
        patchVnode(oldVnode,newVnode)
    }else{
        console.log("不是同一个节点,暴力插入新的删除旧的")
        // createElement(newVnode,oldVnode.elm)
        let newVnodeElm = createElement(newVnode)
        //插入到老节点之前
        if(oldVnode.elm.parentNode && newVnodeElm){
            oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

patchVnode function:

        The main function of patchVnode is that the tree on the same node can still be divided into two situations. One is that the text is different, and the old and new nodes have text attributes. The text attribute changes. This situation is also very simple. The second is that the old and new nodes have text attributes. Nodes have the children attribute. This situation is the most complicated because there are many children and they can be divided into many situations, such as modification, addition, deletion, reordering, etc., so a separate method is encapsulated called the updateChildren function.

import createElement from "./createElement";
import updateChildren from './updateChildren'

export default function patchVnode(oldVnode,newVnode){
            // 判断新旧vnode是否是同一个对象
        if (oldVnode === newVnode) return;
        // 判断新vnode有没有text属性
        if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)){
            console.log("新Vnode有text属性")
            if (newVnode.text != oldVnode.text){
                // 如果新虚拟节点的text和老虚拟节点的text不同,那么直接让新的text写入老的elm中即可。
                // 如果老的elm中的是children,那么也会立即消失
                oldVnode.elm.innerText = newVnode.text
            }
        }else{
            console.log("新Vnode没有text属性")
            // 判断老的有没有children
            if (oldVnode.children != undefined && oldVnode.children.length > 0){
                // 老的有children,此时就是最复杂的情况。就是新老都有children。
                // 
                // 
                // 
                updateChildren(oldVnode.elm,oldVnode.children,newVnode.children)
            }else {
                // 老的没有children,新的有children
                // 清空老的节点内容
                oldVnode.elm.innerHTML = ''
                // 遍历新的vnode的子节点,创建DOM,上树
                for (let i=0;i<newVnode.children.length;i++){
                    let dom = createElement(newVnode.children[i])
                    oldVnode.elm.appendChild(dom)
                }
            }
        }
}

updateChilren function:

        The main function of the updateChilren function is to process the processing operations of different situations where the same node has the children attribute. This is one of the core places. The diff algorithm of snabbdom is used. This diff algorithm is very good and basically covers the four common operations performed by users. Operation, there is a lot of content covered here. For details, you can read the introduction by teacher Shang Silicon Valley. I will not introduce it here.

        It should be noted that in addition to the four core hits, as shown in Figure 4.12, even if the four hits are staggered, the judgment can still be made. That is a circular judgment. The key in oldVnode is saved through map mapping, and then the key value is searched. If If there is a correspondence, perform the patchVnode operation. If not, it proves that the item in newVnode is new. Then you need to add a real node to oldstartVnode, so that even if it is not the four common hits, it can still be processed.

        In addition to the above situations, there may also be two situations 4.13 and 4.14. One is that the new nodes have been traversed, and the old nodes still need to be traversed. At this time, deletion operations need to be performed. The other is that the old nodes have been traversed, and the new nodes still need to be traversed. , new operations need to be performed at this time, because for example, the while() large loop in 4.15 handles four types of hits and their supplementary processing results, while 4.13 and 4.14 are the remaining two situations, and the supplements are completed In these two cases, a simple virtual DOM and diff algorithm model is completed.

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

                                                                       4.15

var myVnode1 = h('ul',{},[
    h('li',{key:'A'},'A'),
    h('li',{key:'B'},'B'),
    h('li',{key:'C'},'C'),
    h('li',{key:'D'},'D'),
    h('li',{key:'E'},'E'),
])


var myVnode2 = h('ul',{},[
    h('li',{key:'E'},'E'),
    h('li',{key:'B'},'B'),
    h('li',{key:'A'},'A'),
    h('li',{key:'D'},'D'),
    h('li',{key:'C'},'C'),
    h('li',{key:'F'},'F'),
    h('li',{key:'G'},'G'),
])

                                                                        4.14

var myVnode1 = h('ul',{},[
    h('li',{key:'A'},'A'),
    h('li',{key:'B'},'B'),
    h('li',{key:'C'},'C'),
    h('li',{key:'D'},'D'),
    h('li',{key:'E'},'E'),
])


var myVnode2 = h('ul',{},[
    h('li',{key:'E'},'E'),
    h('li',{key:'B'},'B'),
    
])

                                                                        4.13

var myVnode1 = h('ul',{},[
    h('li',{key:'A'},'A'),
    h('li',{key:'B'},'B'),
    h('li',{key:'C'},'C'),
    h('li',{key:'D'},'D'),
    h('li',{key:'E'},'E'),
])


var myVnode2 = h('ul',{},[
    h('li',{key:'E'},'E'),
    h('li',{key:'B'},'B'),
    h('li',{key:'A'},'A'),
    h('li',{key:'D'},'D'),
    h('li',{key:'C'},'C'),
    
])

                                                                4.12

import patchVnode from "./patchVnode"
import createElement from "./createElement"

// 判断是否是同一个虚拟节点
function checkSameVnode(a,b){
    return a.sel == b.sel && a.key == b.key
}

export default function updateChildren(parentElm,oldCh,newCh){

    // 四个指针
    // 旧前
    let oldStartIdx = 0
    // 新前
    let newStartIdx = 0
    // 旧后
    let oldEndIdx = oldCh.length - 1
    // 新后
    let newEndIdx = newCh.length - 1

    // 旧前节点
    let oldStartVnode = oldCh[0]
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx]
    // 新前节点
    let newStartVnode = newCh[0]
    // 新后节点
    let newEndVnode = newCh[newEndIdx]

    let keyMap = null

    // 开始大while了
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
        //首先进行的不是四种命中的判断,而是略过已经是undefined标记过的值,表示已经被处理过了
        if(oldStartVnode == null || oldCh[oldStartIdx] == undefined){
            oldStartVnode = oldCh[++oldStartIdx]
        }else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined){
            oldEndVnode = oldCh[--oldEndIdx]
        }else if (newStartVnode == null || newCh[newStartIdx] == undefined){
            newStartVnode = newCh[++newStartIdx]
        }else if (newEndVnode == null || newCh[newEndIdx] == undefined){
            newEndVnode = newCh[--newEndIdx]
        }else
        // 开始四次命中判断
        // 新前和旧前
        if (checkSameVnode(oldStartVnode,newStartVnode)){
            console.log("1新前和旧前命中")
            patchVnode(oldStartVnode,newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else
        // 新后和旧后
        if (checkSameVnode(oldEndVnode,newEndVnode)){
            console.log("2新后和旧后命中")
            patchVnode(oldEndVnode,newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else
        // 新后与旧前
        if (checkSameVnode(oldEndVnode,newEndVnode)){
            console.log("3新后和旧前命中")
            patchVnode(oldStartVnode,newEndVnode)
            //当3新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
            parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSilbling)
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else
        // 新前与旧后
        if (checkSameVnode(oldEndVnode,newStartVnode)){
            console.log("4新前和旧后命中")
            patchVnode(oldEndVnode,newStartVnode)
            //第四种情况需要移动节点,移动新前到旧前的前面
            parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else{
            //四种命中都没有匹配到
            // 寻找key的map
            if (!keyMap){
                keyMap = {}
                // 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象,查找速度更快
                for (let i =oldStartIdx;i <= oldEndIdx;i++){
                    const key = oldCh[i].key
                    if(key!=undefined) {
                        keyMap[key] = i
                    }
                }
            }
            console.log(keyMap)
            // 寻找当前这项(newStartIdx)这项在keyMap种的映射的位置序号
            const idxInOld = keyMap[newStartVnode.key]
            console.log(idxInOld)
            if(idxInOld == undefined) {
                // 判断,如果idxInOld是undefined表示它是全新的项
                // 被加入的项(就是newStartVode这项)现不是真正DOM节点
                parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)
            }else {
                //如果不是undefined,不是全新的项,而是要移动的项
                const elmToMove = oldCh[idxInOld]

                patchVnode(elmToMove,newStartVnode)
                //把这项设置成undefined,表示这个节点已经被处理过了
                oldCh[idxInOld] = undefined
                //移动阶段位置,将从新节点中找到的对应就节点定义成undefined后,需要将真正DOM节点移动到旧的开始节点之前
                parentElm.insertBefore(elmToMove.elm,oldStartVnode.elm)
            }
            //只移动新的头
            newStartVnode = [++newStartIdx]
        }
    }
    
    // 继续看看有没有落下的情况,循环结束了start依旧比old小
    // 需要新增的情况,也就是说遍历完新节点之后还有剩余,那么需要进行新增操作
    if(newStartIdx <= newEndIdx){
        //为什么是插到之前呢?很好理解,因为oldStartVnode其实是移动的跟随指针,oldStartVnode是
        // 已经处理完了很多节点了,处理完的在他上面,所以新的处理结果也已经加在他之前
        console.log("new还有剩余节点没有处理,要加项,将剩余项全部加到oldStartVnode之前")
        // //找到插入的标杆,这里使用了三元表达式,有两种情况,一是如果是第一次插入那么newCh[newEndIdx+1].elm表示的是
        // // 新节点的下一个,那么这个时候肯定是null,所以就在后面加一个相当于appendChild,二是如果已经有了第二个开始,那么就会
        // // 有一个新节点存在,那么最新的就插入他的前面即可
        // // 这里需要三元表达式还有一个原因是因为null不能打点会报错
        // const before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm

        // for(let i = newStartIdx;i<=newEndIdx;i++){
        //     //insertBefore方法可以自动识别null,如果是是null就会自动排到队尾去,就是和appendChild是一致的
        //     // 这里newCh[i]还没变成真正DOM元素,所以需要调用createElement函数将其变成DOM
        //     parentElm.insertBefore(createElement(newCh[i]),before)
        // }
        // 遍历新的newCh,添加到老的没有处理之前
        for(let i = newStartIdx;i<=newEndIdx;i++){
                 //insertBefore方法可以自动识别null,如果是是null就会自动排到队尾去,就是和appendChild是一致的
                 // 这里newCh[i]还没变成真正DOM元素,所以需要调用createElement函数将其变成DOM
                  parentElm.insertBefore(createElement(newCh[i]),oldCh[oldStartIdx].elm)
        }
    }else
    // 还有一种情况是Old节点没有循环完毕,这种情况就是老节点多了,就是需要删除老节点
    if(oldStartIdx <= oldEndIdx){
        console.log("old还有剩余节点没有处理,要删")
        // 批量删除oldStart和oldEnd指针之间的项
        for(let i= oldStartIdx;i<=oldEndIdx;i++){
            if(oldCh[i]){
                parentElm.removeChild(oldCh[i].elm)
            }
        }
    }
}

Overall process diagram:

Guess you like

Origin blog.csdn.net/weixin_54515240/article/details/129989649
Recommended