一、什么是虚构DOM?
虚构DOM是由渲染函数创立的轻量级JavaScript对象,蕴含三个参数:元素,具备数据、prop、attr等的对象,以及一个children数组。数组能够传递子级,每个子级也都具备这些参数,并也能够具备子级,以此类推,直到咱们构建残缺的DOM树为止。

当须要列表更新时,先更改虚构DOM,而后在新虚构DOM和旧虚构DOM之间执diff,算出最大量更新,最初反映到真正的DOM上。
二、snabbdomh
snabbdom是驰名的虚构DOM库,是diff算法的鼻祖,vue源码借鉴了snabbdom;

环境搭建:

  1. 在我的项目文件夹下运行npm init,而后装置snabbdom
npm i -S snabbdom
  1. webpack依赖5版本以上
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
  1. 配置好webpack.config.js文件
const path = require('path')module.exports = {  entry:'./src/index.js',  output:{    //虚构打包门路,不会生成真正的文件夹,而是在8080端口虚构生成    publicPath: 'xuni',    filename: 'bundle.js'  },  devServer:{    port: 8080,    //动态资源文件夹    contentBase: 'www'  }}
  1. 我的项目目录下创立www文件夹,www下创立index.html文件,引入打包好的js
<script src="xuni/bundle.js"></script>
  1. 我的项目目录下创立src文件夹,src下创立js入口文件index.js
  2. 配置我的项目packge.json,实现npm run dev开启一个server
scripts: {  "dev": "webpack-dev-server"}

在终端运行npm run dev能够开启一个8080端口的服务,环境就搭建实现啦。

三、虚构DOM如何被渲染函数生成?--h函数
h函数用来产生虚构节点-vnode
1、h函数的根本用法:

import {  init,  classModule,  propsModule,  styleModule,  eventListenersModule,  h,} from "snabbdom";//创立一个patch函数const patch = init([classModule,propsModule,styleModule,eventListenersModule])//应用h创立一个vnodelet myVnode1 = h('a', {props:{href:'http://www.baidu.com'}}, '百度')console.log(myVnode1) //让虚构节点上树const container = document.getElementById('container')patch(container,myVnode1)

  • h函数创立一个节点
  • 虚构DOM(省略空属性)
  • 实在DOM

应用h函数创立一个带子元素的vnode

虚构DOM(省略空属性)

实在DOM

由例子可见,h函数非常灵便,依据传入参数的不同,利用函数的重载生成不同的虚构DOM

2、手写h函数的外围性能(不思考重载,只思考三个参数的状况)

  • h.js
import vnode from './vnode'//编写一个低配版的h函数,这个函数必须承受三个参数,缺一不可//相当于它的重载性能交弱//调用时的状态必须为上面的三种之一://状态1:h('div',{},'文字')//状态2:h('div',{},[])//状态3: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函数是状态1    return vnode(sel, data, undefined, c, undefined)  } else if (Array.isArray(c)) {    //阐明当初调用h函数是状态2    let children = []    for (let i = 0; i < c.length; i++) {      //查看c[i]必须是一个对象,因为h()返回值为对象      if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {        throw new Error('数组元素必须为h()函数')      } else {        children.push(c[i])      }    }    return vnode(sel, data, children, undefined, undefined)  } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {    //阐明当初调用h函数为状态3    //children数组只有一个元素    return vnode(sel, data, [c], undefined, undefined)  }}

  • vnode.js
export default function (sel, data, children, text, elm) {  const key = data.key  return { sel, data, children, text, elm, key }}
  • index.js(测试)
import h from './mysnabbdom/h'let myVnode = h('ul', {}, [  h('li', {}, h('span', {}, 'peach')),  h('li', {}, h('span', {}, 'banana')),  h('li', {}, [    h('span', {}, 'apple'),    h('ul', {}, [      h('li', {}, 'price:8'),      h('li', {}, 'color:red')    ])  ])])console.log(myVnode)

四、diff算法原理
外围--patch()
1、patch根本应用:

import {  init,  classModule,  propsModule,  styleModule,  eventListenersModule,  h,} from "snabbdom";const patch = init([classModule,propsModule,styleModule, eventListenersModule,]);const vnode = h('ul',[  h('li',{key:'A'},'A')  h('li',{key:'B'},'B')  h('li',{key:'C'},'C')])const newVnode = h('ul',[  h('li',{key:'D'},'D')  h('li',{key:'A'},'A')  h('li',{key:'B'},'B')  h('li',{key:'C'},'C')])//调用patch,两个虚构DOM间进行diff算法,计算最大量更新patch(vnode, newVnode);
  • key是节点的惟一标识,能够通知diff算法,在更改前后它们是否是同一个DOM节点
//判断是否为同一个节点:oleVnode.key === newVnode.key && oldVnode.sel === newVnode.sel
  • 只有是同一个虚构节点,才进行精细化比拟,否则就暴力删除旧的,插入新的。
  • 只进行同层比拟,不会进行跨层比拟,即便是同一片虚构节点,只有是跨层了,则不进行diff

2、模仿实现patch()

首先解决新旧节点不是同一个节点的状况:

  • 新建patch.js文件
import vnode from "./vnode";import createElement from './createElement'export default function (oldVnode, newVnode) {  //判断vnode1是虚构节点还是DOM节点  if (oldVnode.sel == undefined || oldVnode.sel == '') {    //vnode1是DOM节点,需转成虚构节点    oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, undefined, undefined, oldVnode)  }  //判断是否为同一节点  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {    //是同一个,进行精细化比拟    //********diff算法*******  } else {    //不是同一个节点,创立新节点    let newVnodeElm = createElement(newVnode)    if (oldVnode.elm.parentNode && newVnodeElm) {      //将新节点上树,让标杆节点的父元素调用insertBefore办法,将创立的节点,插入到标杆节点之前      oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)    }    //删除旧节点    oldVnode.elm.parentNode.removeChild(oldVnode.elm)  }}
  • 新建createElement.js文件,作用为依据vnode生成对应的DOM
//vnode:需创立DOM的虚构节点export default function createElement(vnode) {  const domNode = document.createElement(vnode.sel)  //判断vnode的inner是否只有文本  if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {    //将文本插入到domeNode    domNode.innerText = vnode.text  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {    //children为数组,具备子元素    //递归创立节点    for (let i = 0; i < vnode.children.length; i++) {      let ch = vnode.children[i]      //一旦调用createElement()办法,就创立了一个DOM,并且将该vnode的elm属性指向了这个创立的DOM      let chDOM = createElement(ch)      domNode.appendChild(chDOM)    }  }  //给vnode补充elm属性  vnode.elm = domNode   //返回elm,elm是一个纯DOM对象  return vnode.elm}
  • index.js测试



点按钮能实现DOM替换

新旧节点为同一个节点时,流程图如下:

解决新旧节点的子节点含text的状况:

  • patch.js新增代码如下
  //判断是否为同一节点  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {    //是同一个,进行精细化比拟    //判断新旧vnode是否是同一个对象    if (oldVnode === newVnode) return    //判断newVnode有没有text属性    if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {      //newVnode有text属性,判断newVnode的text和oldVnode是否雷同      if (newVnode.text != oldVnode.text) {        //text不同,将oldVnode.elm中的innerText扭转为newVnode.text        oldVnode.elm.innerText = newVnode.text      }    } else {      //newVnode没有text属性      //判断oldVnode有没有children      if (oldVnode.children != undefined && oldVnode.children.length > 0) {        //oldVnode和newVnode都有children        //*****diff更新子节点******      } else {        //oldVnode没有,newVnode有,并且oldVnode有text,清空oldVnode中的text,并将newVnode中的children增加给oldVnode的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)        }      }    }  }

减少key属性,标识节点
在vnode.js中配置

五、diff算法外围
在解决子节点时,咱们发现要思考的状况十分多,经典的diff算法优化策略提出四个指针,采纳四种命中查找:

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前
  4. 新前与旧后

查找规定:

  • 四个指针挪动规定:新前/旧前指针只往下移新后/旧后指针只往上移
  • 命中一种就不再往下进行命中判断了,挪动以后命中形式的指针;

    • 若未命中,则程序向下,查找下一种命中;
    • 若四种都未命中,则在所有未解决的旧节点中循环查找,若找到,将旧节点中找到的节点变为undefined,再挪动新前指针指向的节点挪动至旧前指针之前
    • 若循环也未找到,则将该新前指针指向的节点插入到旧前指针之前。再挪动指针,进行下一次命中查找。
  • 完结循环判断条件为:while(新前<=新后&&旧前<=旧后)
  • 旧节点先循环结束,若新节点中还有未遍历的,则未遍历的节点为新增节点,直接插入到旧前指针节点之前即可。
  • 若新节点先循环结束,若旧节点有未遍历的节点,则阐明残余节点是要删除的节点
  • 当新前与旧前命中时,新前与旧前指针都往下移
  • 当新后与旧后命中时,新后与旧后指针都往上移
  • 当新后与旧前命中时,需先将新后指向的节点挪动到过后旧后指针的前面,再将新后指针上移,旧前指针下移
  • 当新前与旧后命中时,需先将新前指向的节点挪动到过后旧前指针的后面,再将新前指针下移,旧后指针上移

六、手写diff算法

  1. 将patch.js中是同一个节点,进行精细化比拟的代码提取成单文件patchVnode.js

  1. 新建updateChildren.js
import patchVnode from './patchVnode'import createElement from './createElement'//封装判断两个节点是否是同一个虚构节点的函数function checkSameVnode(a, b) {  return a.sel == b.sel && a.key == b.key}//参数parentElm:父元素 参数oldCh: 旧子节点对象 参数newCh: 新子节点对象export default function (parentElm, oldCh, newCh) {  let oldStartIdx = 0,             //旧前    newStartIdx = 0,               //新前    oldEndIdx = oldCh.length - 1,  //旧后    newEndIdx = newCh.length - 1,  //新后    oldStartVnode = oldCh[0],      //旧前节点    oldEndVnode = oldCh[oldEndIdx],//旧后节点    newStartVnode = newCh[0],      //新前节点    newEndVnode = newCh[newEndIdx],//新后节点    keyMap = null                  //keyMap缓存未解决的节点  //循环条件  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)) {      //新前与旧前命中      patchVnode(oldStartVnode, newStartVnode)      oldStartVnode = oldCh[++oldStartIdx]      newStartVnode = newCh[++newStartIdx]    } else if (checkSameVnode(oldEndVnode, newEndVnode)) {      //新后与旧后命中      patchVnode(oldEndVnode, newEndVnode)      oldEndVnode = oldCh[--oldEndIdx]      newEndVnode = newCh[--newEndIdx]    } else if (checkSameVnode(newEndVnode, oldStartVnode)) {      //新后与旧前命中      patchVnode(oldStartVnode, newEndVnode)      //在旧后指针之后插入旧前指向的节点      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)      oldStartVnode = oldCh[++oldStartIdx]      newEndVnode = newCh[--newEndIdx]    } else if (checkSameVnode(newStartVnode, oldEndVnode)) {      //新前与旧后命中      patchVnode(oldEndVnode, newStartVnode)      //在旧前指针之前插入旧后指针指向的节点      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)      newStartVnode = newCh[++newStartIdx]      oldEndVnode = oldCh[--oldEndIdx]    } else {      //不满足四种命中,循环查找      //设置keyMap,保留未查找过的节点      if (!keyMap) {        keyMap = {}        for (let i = oldStartIdx; i <= oldEndIdx; i++) {          const key = oldCh[i].key          key != undefined && (keyMap[key] = i)        }      }      //寻找以后项(newStartVnode)在keyMap中的映射地位index      const idxInOld = keyMap[newStartVnode.key]      if (idxInOld == undefined) {        //以后项在oldCh中没有,是一个全新的项        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)      } else {        //以后项在oldCh中存在,需挪动地位        const elmToMove = oldCh[idxInOld]        patchVnode(elmToMove, newStartVnode)        oldCh[idxInOld] = undefined        parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)      }      newStartVnode = newCh[++newStartIdx]    }  }  //查看是否有残余节点  if (newStartIdx <= newEndIdx) {    //新节点有残余,在oldStartVnode之前插入残余新增的节点,若    //insertBefore第二个参数为null时,会将节点增加在结尾    let before = oldStartVnode ? oldStartVnode.elm : null    for (let i = newStartIdx; i <= newEndIdx; i++) {      parentElm.insertBefore(createElement(newCh[i]), before)    }  } else if (oldStartIdx <= oldEndIdx) {    //旧节点有残余,删除节点    for (let i = oldStartIdx; i <= oldEndIdx; i++) {      oldCh[i] && parentElm.removeChild(oldCh[i].elm)    }  }}
  1. patchVnode.js引入updateChildren.js并调用