diff算法

虚拟 dom

虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。

优点

  • 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能
patch(vnode, h('div#app', obj.foo))
  • 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台
const patch = init([snabbdom_style.default])
patch(vnode, h('div#app', {style:{color:'red'}}, obj.foo))
  • 兼容性:还可以加入兼容性代码增强操作的兼容性

必要性

vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大
型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,
这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。

整体流程

mountComponent() core/instance/lifecycle.js
渲染、更新组件

// 定义更新函数
const updateComponent = () => {
// 实际调用是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}

_render core/instance/render.js
生成虚拟dom

_update core\instance\lifecycle.js
update负责更新dom,转换vnode为dom

patch() platforms/web/runtime/index.js
__patch__是在平台特有代码中指定的

Vue.prototype.__patch__ = inBrowser ? patch : noop

代码实现

环境搭建

yarn init
yarn add webpack webpack-cli webpack-dev-server html-webpack-plugin
"dev": "webpack-dev-server --open --mode=development --hot",
"build": "webpack --mode=production"

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin'),
    { resolve } = require('path');

module.exports = {
  entry: './src/js/index.js',
  output: {
    path: resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  devtool: 'source-map',
  plugins: [
    new HtmlWebpackPlugin({
      template: resolve(__dirname, 'src/index.html')
    }) 
  ],
  devServer: {
    static: './'
  }
}

实现虚拟 dom

虚拟 dom js 形式

import { createElement, render, renderDOM } from './virtualDom'


const vDom = createElement(
  'ul', 
  {class:'list',style:'color:#efef;width:500px;height:300px;background-color:brown;'}, 
  [
    createElement('li', { class:'item', 'data-index': 0 }, [
      createElement('p', { class: 'text' }, ['第一个列表项'])
    ]),
    createElement('li', { class:'item', 'data-index': 1 }, [
      createElement('p', { class: 'text' }, [
        createElement('span', { class: 'title' }, ['第2个列表项'])
      ])
    ]),
    createElement('li', { class:'item', 'data-index': 2 }, [
      createElement('p', { class: 'text' }, ['第3个列表项'])
    ])
  ]
)

const rDom = render(vDom)
renderDOM(rDom)

虚拟dom

virtualDom.js

import Element from './Element'

export function createElement(type, props, childrens) {
  return new Element(type, props, childrens)
}

Element.js

class Element {
  constructor(type, props, childrens) {
    this.type = type
    this.props = props
    this.childrens = childrens
  }
}

export default Element 

到这里实现了 createElement 方法, 生成了虚拟 dom

下面实现 render:createElement => createElement(), 虚拟 dom 转真实 dom 的方法

virtualDom.js

export function render(vDom) {
  const { type, props, childrens } = vDom

  const dom = document.createElement(type)
  Object.keys(props).forEach(key => {
    switch(key) {
      case 'value':
        if(dom.tagName == 'INPUT' || dom.tagName == 'TEXTAREA') { // input、textarea 的value 设置方式和其他标签不同
          dom.value = props[key]
        } else {
          dom.setAttribute(key, props[key])
        }
      default: dom.setAttribute(key, props[key])
    }
  })
 
  childrens.map(item => {
    item = item instanceof Element
           ?
           render(item)
           :
           document.createTextNode(item)

    dom.appendChild(item)
  })
  
  return dom
}

执行render 方法后得到

最后执行渲染真实 dom 操作

export function renderDOM (vDom) {
  document.querySelector('#app').appendChild(vDom)
}

简单实现一个 diff 算法

首先我们先要获取一个补丁规则, 也就是 patches, 他是经过 oldVN 和 newVN 对比出来的。
例子:

// 补丁规则
const patches = {
  0: [
    { // 修改属性
      type: 'ATTR',
      attr: { css: 'list-wrap' }
    }
  ],
  2: [
    { // 修改属性
      type: 'ATTR',
      attr: { css: 'title' }
    }
  ],
  3: [
    { // 修改文本
      type: 'TEXT',
      text: '今晚不喝酒'
    }
  ],
  6: [
    { // 删除操作
      type: 'REMOVE',
      index: 6
    }
  ],
  7: [
    { // 替换节点
      type: 'REPLACE',
      newNode: newNode
    }
  ]
}

两个新旧 vnode 如下:

const vDom1 = createElement(
  'ul', 
  {class:'list',test:6,style:'color:#efef;width:500px;height:300px;background-color:brown;'}, 
  [
    createElement('li', { class:'item', 'data-index': 0 }, [
      createElement('p', { class: 'text' }, ['第一个列表项'])
    ]),
    createElement('li', { class:'item', 'data-index': 1 }, [
      createElement('p', { class: 'text' }, [
        createElement('span', { class: 'title' }, ['第2个列表项'])
      ])
    ]),
    createElement('li', { class:'item', 'data-index': 2 }, [
      createElement('p', { class: 'text' }, ['第3个列表项'])
    ])
  ]
)

const vDom2 = createElement(
  'ul', 
  {class:'list-wrap test','data-test':66,style:'color:#efef;width:501px;height:300px;background-color:brown;'}, 
  [
    createElement('li', { class:'item', 'data-index': 0 }, [
      createElement('p', { class: 'title' }, ['今晚不喝酒'])
    ]),
    createElement('li', { class:'item', 'data-index': 1 }, [
      createElement('p', { class: 'text' }, [])
    ]),
    createElement('div', { class:'item', 'data-index': 2 }, [
      createElement('p', { class: 'text' }, ['第3个列表项'])
    ])
  ]
)

const patches = diffDom(vDom1, vDom2) // 进行 diff 对比

vDom1, vDom2 虚拟 dom 形式

diffDom.js

let patches = {}
  ,depth = 0;

function diffDom(oldDom, newDom) {
  vNodeWalk(oldDom, newDom, depth)
  
  return patches // 返回补丁
}

function vNodeWalk(oldVn, newVn, index) {
  let vnPatch = []

  if(!newVn) { // 如果 new virtual dom  不存在
    vnPatch.push({ // 加入规则
      type: 'remove',
      index
    })
  }else if(typeof oldVn === 'string' && typeof newVn === 'string') { // 如果是文本
    vnPatch.push({ // 加入规则
      type: 'text',
      text: newVn
    })
  }else if(oldVn.type === newVn.type){ // 如果是标签 
    const attrPatch = diffAttr(oldVn.props, newVn.props) // 处理得标签规则 ex: {class: 'list-wrap test', data-test: 66}

    if(Object.keys(attrPatch).length > 0) {
      vnPatch.push({ // 加入规则
        type: 'attr',
        attrs: attrPatch
      })
    }
    // 如果type 存在,说明是标签,还要对比子元素
    diffChild(oldVn.childrens, newVn.childrens) // 通层级对比,深度优先遍历
  }else {
    vnPatch.push({ // 加入规则
      type: 'replace',
      newNode: newVn
    })
  }
  
  
  if(vnPatch.length > 0) {
    patches[index] = vnPatch // 写入规则表
  }
}

// 对比属性
function diffAttr(oldProps, newProps) {
  let attrPath = {}

  for(let key in oldProps) {
    // 修改属性
    if((newProps[key] || newProps[key] == 0) && oldProps[key] !== newProps[key]) {
      attrPath[key] = newProps[key]
    }
  }

  for(let key in newProps) {
    // 增加属性
    if(!oldProps[key] && oldProps[key] != 0) {
      attrPath[key] = newProps[key]
    }
  }

  return attrPath
}

// 对比 child
function diffChild(oc, nc) {
  oc.map((c, i) => {
    vNodeWalk(c, nc[i], ++depth) // 递归, 通层级对比,深度优先遍历
  })
}

export default diffDom

最后处理得 补丁

更改 - 根据 patches

index.js

doPatch(rDom, patches)

** doPatch.js ** 这里是根据 真实dom和patches规则比对, 模拟更新视图

import { render } from './virtualDom'
import Element from './Element'

let _patches = {},
  index = 0;

function doPatch(rDom, patches) {
  _patches = patches
  console.log(_patches)
  rDomWalk(rDom)
}

function rDomWalk(rDom) {
  const rnPatch = _patches[index ++],
    childNodes = rDom.childNodes;
    
  [...childNodes].map(child => { // 先往下遍历,然后执行handleDom, 所以这里是从下向上执行
    rDomWalk(child)
  });

  // 节点和对应的 patched 补丁匹配,和路由嵌套原理差不多处理到一个规则表matched再去匹配
  rnPatch && handleDom(rDom, rnPatch);
}

function handleDom(node, patch) {
  patch.map(item => {
    console.log(node, item)
    
    switch(item.type){
      case 'text': 
        node.textContent = item.text
        break;
      case 'attr': 
        Object.keys(item.attrs).forEach(key => {
          item.attrs[key]
          ?
          node.setAttribute(key, item.attrs[key])
          :
          node.removeAtrribute(key)
        })
        break;
      case 'remove':
        console.log(node, item)
        node.parentNode.removeChild(node);
        break;
      case 'replace':
        const newNode = item.newNode instanceof Element
                        ? render(item.newNode)
                        : document.createTextNode(item.newNode)
          
          node.parentNode.replaceChild(newNode, node) 
        break;
      default: 
       break;
    }
  })
}

export default doPatch
posted on 2022-11-11 02:35  京鸿一瞥  阅读(47)  评论(0编辑  收藏  举报