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

  • 虚拟DOM轻量、快速:当它们发⽣变化时通过新旧虚拟DOM⽐对可以得到最⼩DOM操作量,配合异步更新策略减少刷新频率,从⽽提升性能
  • 跨平台:将虚拟dom更新转换为不同运⾏时特殊操作实现跨平台
  • 兼容性:还可以加⼊兼容性代码增强操作的兼容性
  1. <div id="app"></div>
  2. <!--安装并引⼊snabbdom-->
  3. <script src="../../node_modules/snabbdom/dist/snabbdom.js"></script>
  4. <script>
  5. // 之前编写的响应式函数
  6. function defineReactive(obj, key, val) {
  7. Object.defineProperty(obj, key, {
  8. get() {
  9. return val
  10. },
  11. set(newVal) {
  12. val = newVal
  13. // 通知更新
  14. update()
  15. }
  16. })
  17. }
  18. // 导⼊patch的⼯⼚init,h是产⽣vnode的⼯⼚
  19. const { init, h } = snabbdom
  20. // 获取patch函数
  21. const patch = init([])
  22. // 上次vnode,由patch()返回
  23. let vnode;
  24. // 更新函数,将数据操作转换为dom操作,返回新vnode
  25. function update() {
  26. if (!vnode) {
  27. // 初始化,没有上次vnode,传⼊宿主元素和vnode
  28. vnode = patch(app, render())
  29. }
  30. else {
  31. // 更新,传⼊新旧vnode对⽐并做更新
  32. vnode = patch(vnode, render())
  33. }
  34. }
  35. // 渲染函数,返回vnode描述dom结构
  36. function render() {
  37. return h('div', obj.foo)
  38. }
  39. // 数据
  40. const obj = {}
  41. // 定义响应式
  42. defineReactive(obj, 'foo', '')
  43. // 赋⼀个⽇期作为初始值
  44. obj.foo = new Date().toLocaleTimeString()
  45. // 定时改变数据,更新函数会重新执⾏
  46. setInterval(() => {
  47. obj.foo = new Date().toLocaleTimeString()
  48. }, 1000);
  49. </script>
  50. </body>

整体流程
core/instance/lifecycle.js

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

core/instance/render.js
_render() ⽣成虚拟dom

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

  1. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  2. const vm: Component = this
  3. const prevEl = vm.$el
  4. const prevVnode = vm._vnode
  5. const restoreActiveInstance = setActiveInstance(vm)
  6. vm._vnode = vnode
  7. // Vue.prototype.__patch__ is injected in entry points
  8. // based on the rendering backend used.
  9. if (!prevVnode) {
  10. // initial render
  11. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  12. } else {
  13. // updates
  14. vm.$el = vm.__patch__(prevVnode, vnode)
  15. }
  16. restoreActiveInstance()
  17. // update __vue__ reference
  18. if (prevEl) {
  19. prevEl.__vue__ = null
  20. }
  21. if (vm.$el) {
  22. vm.$el.__vue__ = vm
  23. }
  24. // if parent is an HOC, update its $el as well
  25. if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  26. vm.$parent.$el = vm.$el
  27. }
  28. // updated hook is called by the scheduler to ensure that children are
  29. // updated in a parent's updated hook.
  30. }

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

  1. Vue.prototype.__patch__ = inBrowser ? patch : noop

patch是createPatchFunction的返回值,传递nodeOps和modules是web平台特别实现

  1. // 传入web平台特有节点操作和属性操作
  2. export const patch: Function = createPatchFunction({ nodeOps, modules })
  • nodeOps 定义各种原⽣dom基础操作⽅法
  • modules 定义了属性更新实现

core\vdom\patch.js
⾸先进⾏树级别⽐较,可能有三种情况:增删改

  • new VNode不存在就删;
  • old VNode不存在就增;
  • 都存在就执⾏diff执⾏更新

image.png
patchVnode
⽐较两个VNode,包括三种类型操作:属性更新、⽂本更新、⼦节点更新
具体规则如下:
1. 新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren
2. 如果新节点有⼦节点⽽⽼节点没有⼦节点,先清空⽼节点的⽂本内容,然后为其新增⼦节点。
3. 当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该节点的所有⼦节点。
4. 当新⽼节点都⽆⼦节点的时候,只是⽂本的替换。

updateChildren
updateChildren主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个VNode的children得出最⼩操作补丁。执
⾏⼀个双循环是传统⽅式,vue中针对web场景特点做了特别的算法优化
image.png
在新⽼两组VNode节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢
oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。

diff算法