virtual DOM

116 阅读6分钟

React vdom 具体实现和 VUE 不同;
Vue 3.0 重写了 vdom;

VNode

JS 模拟 DOM 结构(JS 计算速度更快,使用 JS 计算出最小变更);
每个 VNode 对应一个真实的 DOM 节点(包含渲染一棵树所需的全部信息,如元素标签、属性、文本内容、子节点等),用于记录组件状态的变化,并提供高效的更新机制;
当应用的状态变化时,会生成新的 VTree 与旧的 VTree 进行比较,找出最小的差异,然后仅更新对应的DOM部分。

VDom:

实现 vue 和 React 的基石,所有 VNode 的集合(页面上所有元素以对象形式保存于内存中)

理解

html

<div id="div1" class="container">
 <p>title</p>
 <ul style="font-size: 20px">
   <li>hello</li>
 </ul>
</div>

模拟

const vdom = {
  tag: 'div',
  props: {
    className: 'container',
    id: 'div1'
  },
  children: [
    {
      tag: 'p',
      children: 'title' // 文本内容
    },
    {
      tag: 'ul',
      props: {
        style: 'font-size:20px'
      },
      children: [
        {
          tag: 'li',
          children: 'hello' // 文本内容
        }
      ]
    }
  ]
}

h 函数返回一个 VNode 工厂函数,执行函数返回一个对象(即 JS 模拟的 DOM 结构)

import { h, createApp } from 'vue';

// 定义一个简单的组件
const MyComponent = {
  render() {
    return h('div', { class: 'container' }, [
      h('p', null, '这是一个段落'),
      h('button', { onClick: () => alert('点击了按钮') }, '点击我')
    ]);
  }
};

createApp(MyComponent).mount('#app');

h 函数

function h(sel,b,c) {
  // do something 根据接受的参数做不同的处理
  return vnode(sel, data, children, text, undefined)
}

VNode 函数

function vnode(sel, data, children, text, elm, key) {
  let key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key } // 返回一个模拟的DOM对象
}
`sel` 为 `tag` 标签,如 div p a span;
`data` - 属性 attr;
`children` - 子节点;
`elm` - vnode 对应 DOM 真实元素;
`text` - 文本;
`key` 用于 diff 算法对比新旧 dom 实现更新和重新渲染;

Vue 模板编译

以下 obj.c 输出结果为undefined

const obj = {
  a: 100,
  b: 200,
}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined

使用with能改变{}内自由变量的查找方式(当作obj的属性查找),缺陷会破坏作用域规则

with (obj) {
  console.log(a)
  console.log(b)
  console.log(c) // 报错
}

vue-template-compile将模板编译成render函数即h,执行render返回vnode,执行patch将vDOM渲染成真实DOM;

const compiler = require("vue-template-compiler")
const template = `<p>{{message}}</p>`
const res = compiler.compile(template)
console.log(res.render) // 打印

编译结果

with(this){return _c('p',[_v(_s(message))])}
function installRenderHelpers (target) {
    target._o = markOnce;
    target._n = toNumber;
    target._s = toString;
    target._l = renderList;
    target._t = renderSlot;
    target._q = looseEqual;
    target._i = looseIndexOf;
    target._m = renderStatic;
    target._f = resolveFilter;
    target._k = checkKeyCodes;
    target._b = bindObjectProps;
    target._v = createTextVNode;
    target._e = createEmptyVNode;
    target._u = resolveScopedSlots;
    target._g = bindObjectListeners;
    target._d = bindDynamicKeys;
    target._p = prependModifier;
}

> `this``const vm = new Vue()
> `_c` 为 `createElement` 即`h`函数
> `p` 为标签
> `_v` 对应`createTextVNode`:创建`text`节点子元素
> `_s`对应`toString`:创建`string`类型字符串

完整结果

with (this) {
  return createElement('p', [createTextVNode(toString(message))])
  // 等同于 h('p',{},message)
}

包含子集

const template = `
  <div id="div1" class="container">
      <img :src="imgUrl"/>
  </div>
with (this) {
  return _c(
    "div", // div标签
    { staticClass: "container", attrs: { id: "div1" } }, // 属性
    [
      // 创建IMG标签
      _c("img", { attrs: { src: imgUrl } }),
    ]
  )
}

v-if

const template = `
    <div>
        <p v-if="flag === 'a'">A</p>
        <p v-else>B</p>
    </div>
`
with (this) {
  return _c(
    "div", 
    [flag === "a" ? 
    _c("p", [_v("A")]) : 
    _c("p", [_v("B")])]
  )
}

v-for

const template = `
    <ul>
        <li v-for="item in list" :key="item.id">{{item.title}}</li>
    </ul>
`
with (this) {
  return _c(
    'ul',
    _l(list, function (item) {
      return _c('li', { key: item.id }, [_v(_s(item.title))])
    }),
    0
  )
}

event

const template = `
    <button @click="clickHandler">submit</button>
`
with (this) {
  return _c('button', { on: { click: clickHandler } }, [_v('submit')])
}

再来看看最早定义组件的方式

Vue.component('my-component', {
  template: `<div id='box'>{{name}}</div> `,
  data() {
    return {
      name: '张三'
    }
  }
})
  <body>
    <div id="root">
      <my-component />
    </div>
    <script>
      Vue.component('my-component', {
        // template: `...`,
        render(h) {
          return h(
            'div',
            {
              attrs: {
                id: 'box'
              }
            },
            '张三'
          )
        }
      })
      new Vue({
        el: '#root'
      })
    </script>
  </body>

Vue 渲染过程

渲染过程: 解析 vue 模板,生成 render 函数 --->触发响应式,监听 data 中的 getter setter, 执行 render 函数( h 函数 )生成 vnode ---> patch(elem,vnode) 渲染出真实Dom

更新过程: data 改变触发 setter ---> 重新执行 render 函数 ---> 生成 newVnode ---> patch(vnode,newVnode)

Vue 是异步渲染的,无论方法中对 data 改变多少次都会汇总成一次; Component render function :将模板编译成 render 函数, 执行 render 函数渲染渲染出真实 dom 树结构,同时会 touchdata,即会触发 data 中的 getterwatcher 则会收集起来进行观察,当进行数据的更改时则会触发 setter并通知进行 re-render

在这里插入图片描述

React 模板编译

JSX 等同于 Vue 模板, Vue 模板不是 html,被编译成 React.createElement()h / render

const elem = <div><p>text</p></div>

tag(标签)、props(属性)、children(子集)

var elem = React.createElement('div', null, React.createElement('p', null, 'text'))

React 组件渲染和更新过程

渲染过程:render() 生成 vnode ---> patch(elem, vnode) setState(newState) ----> dirtyComponents ---> render() 生成 newVnode ----> patch(vnode, newVnode)

React 中的 patch 可拆分为两个阶段:reconciliation 阶段(执行 diff 算法,纯计算),commit 阶段(将 diff 结果渲染)

reconciliation:渲染一个 React 应用程序时,一个描述应用程序的节点树被生成并保存在内存中,然后将该树刷新到渲染环境,当应用程序更新时(通常为 setState),会生成一棵新树,新树与之前的树进行比较,以计算更新渲染应用程序所需的操作

commitreact fiberreact内部运行机制):将reconciliation阶段任务拆分,DOM需要渲染时暂停空闲时恢复即window.requestIdleCallback

Diff

通过新旧 vnode 对比,得出最小的更新范围,最终对需要更新的 dom 进行实际操作;

最开始会遍历 tree1 和 tree 2 继续比较,然后进行排序,导致时间复杂度到达 O^3,后来进行了优化将时间复杂度优化到了O(n),只比较同一层级,如果 tag 不相同直接删除重建,如果 tagkey都相同不再深度比较,而是直接进行一个新旧 vnode 的比较

patch 函数中新旧 vNode 对比的大致原理:

  1. 首选判断参数一是否是容器元素,如果是创建一个空 vnode 并关联
  2. 判断是否前后 vnode 相等,如果相等(tag 和 key 都相等)进行下一步对比,不相等删除重建。

childrentext 的对比,分为4个方向,旧的开始和旧的开始,旧的结束和旧的结束,旧的开始和新的结束,旧的结束和新的开始4个维度进行计算

  • 如果oldVnode含有childrennewVnodetext:移除旧vnode,设置新text
  • 如果oldVnodetextnewVnode含有children:创建新vnode,移除旧text
  • 如果oldVnodetextnewVnode也为text:移除旧的text设置新text
  • 如果oldVnode含有childrennewVnode也含有children:框架不同算法不同,共性都依赖于key 进行计算;

在这里插入图片描述

为什么 key 不建议使用索引

遍历对象的每一个属性深度对比是非常浪费性能的,如果不指定 key 默认为 index 下标

假设现在有这样一段代码:

const users = [{ username: 'bob' }, { username: 'sue' }]
users.map((u, i) => <div key={i}>{u.username}</div>)

它会渲染出 DOM 树

<div key="1">bob</div>
<div key="2">sue</div>

如果对数据进行了 unshift

const users = [
  { username: "new-guy" },
  { username: "bob" },
  { username: "sue" },
];

DOM 树就会变成这样,注意 key 的变化

<div key="1">new-guy</div>
<div key="2">bob</div>
<div key="3">sue</div>

DOM 树的前后对比

<div key="1">bob</div>   |  <div key="1">new-guy</div>
<div key="2">sue</div>   |  <div key="2">bob</div>
                         |  <div key="3">sue</div>

肉眼开头加了一个 new-guy 而已,但是由于 React 使用 key 值来识别变化,所以 React 认为的变化是:

 bob -> new-guy
 sue -> bob
 添加 sue

非常消耗性能,如果我们一开始就给它指定一个合适的 key,React 认为的变化就变成:

                         |  <div key="1">new-guy</div>
<div key="1">bob</div>   |  <div key="2">bob</div>
<div key="2">sue</div>   |  <div key="3">sue</div>