Rumah > hujung hadapan web > View.js > Analisis mendalam DOM maya dalam Vue

Analisis mendalam DOM maya dalam Vue

青灯夜游
Lepaskan: 2022-08-08 19:34:55
ke hadapan
1691 orang telah melayarinya

Analisis mendalam DOM maya dalam Vue

Teknologi DOM maya menjadikan pemaparan halaman kami lebih cekap, mengurangkan operasi nod dan meningkatkan prestasi. Artikel ini akan memberi anda analisis mendalam tentang prinsip teknikal dalam vueVirtual DOM dan pelaksanaan khusus rangka kerja Vue. (Belajar perkongsian video: tutorial video vue)

1 RealitiDOMdan proses analisisnya

Dalam bahagian ini kami terutamanya Memperkenalkan proses penghuraian DOM sebenar, dan memperkenalkan proses penghuraian serta masalah sedia ada, sekali gus membawa kepada sebab DOM maya diperlukan. Sekeping gambar bernilai seribu perkataan, seperti yang ditunjukkan di bawah ialah webkit carta aliran kerja enjin pemaparan

Analisis mendalam DOM maya dalam Vue

Semua aliran kerja enjin pemaparan pelayar dibahagikan secara kasar kepada 5 langkah: Cipta DOM pokok —> Cipta Style Rules -> RenderLayoutPaintingLangkah pertama ialah membina pepohon DOM: gunakan penganalisis HTML untuk menganalisis elemen HTML dan membina pepohon DOM

    Langkah kedua ialah menjana helaian gaya: gunakan CSS penganalisis , menganalisis gaya sebaris pada fail dan elemen CSS, dan hasilkan helaian gaya halaman
  • Langkah ketiga ialah membina pepohon Render: kaitkan pepohon DOM dan helaian gaya untuk membina pepohon Render; (Lampiran). Setiap nod DOM mempunyai kaedah lampiran, yang menerima maklumat gaya dan mengembalikan objek pemaparan (juga dikenali sebagai pemapar objek ini akhirnya akan dibina menjadi pepohon Render; koordinat: Menurut struktur pokok Render, tentukan koordinat tepat yang muncul pada paparan untuk setiap nod pada pokok Render; dan kemudian panggil kaedah cat setiap nod untuk melukisnya.
  • Nota:
  • 1.
Pembinaan pokok bermula apabila dokumen dimuatkan?

Membina pokok ialah proses beransur-ansur untuk mencapai pengalaman pengguna yang lebih baik, enjin pemaparan akan memaparkan kandungan pada skrin secepat mungkin. Ia tidak perlu menunggu sehingga keseluruhan penghuraian dokumen selesai sebelum mula membina pokok

dan reka letak.

DOM2. Pokok dibina selepas pokok DOM dan HTML helaian gaya selesai? render Ketiga-tiga proses ini tidak sepenuhnya bebas apabila sebenarnya dilakukan, tetapi akan bertindih, memuatkan, menghurai dan memaparkan pada masa yang sama.

3. Apakah yang perlu anda perhatikan semasa menganalisis Render? DOM CSS dihuraikan secara terbalik dari kanan ke kiri Semakin banyak teg bersarang, semakin perlahan penghuraian.

4. Berapakah kos operasi CSS sebenar? Menggunakan model pembangunan tradisional kami, apabila asli CSS atau

beroperasi

, penyemak imbas akan melaksanakan proses dari awal hingga akhir bermula dari membina pepohon DOM. Dalam satu operasi, saya perlu mengemas kini 10 nod Selepas menerima permintaan JS pertama, penyemak imbas tidak mengetahui bahawa terdapat 9 operasi kemas kini, jadi ia akan melaksanakan proses dengan serta-merta, dan akhirnya melaksanakannya 10 kali. Contohnya, selepas pengiraan pertama, sejurus selepas permintaan kemas kini DOM seterusnya, nilai koordinat nod ini berubah dan pengiraan sebelumnya telah sia-sia. Mengira nilai koordinat nod, dsb. adalah pembaziran prestasi. Walaupun perkakasan komputer telah dikemas kini secara berulang, operasi JS masih mahal, dan operasi yang kerap masih akan menyebabkan halaman membeku, menjejaskan pengalaman pengguna JQDOMDOM 2. DOM Asas DOMDOM DOM

2.1. Faedah maya

Virtual-DOM Maya

direka untuk menyelesaikan masalah prestasi penyemak imbas. Seperti sebelum ini, jika terdapat 10 kemas kini

dalam satu operasi, maya tidak akan beroperasi serta-merta DOM, tetapi akan menyimpan kandungan 10 kemas kini ini ke objek

setempat. dan akhirnya pindahkan objek

ini ke pokok DOM sekaligus, dan kemudian lakukan operasi seterusnya untuk mengelakkan banyak pengiraan yang tidak perlu. Oleh itu, kelebihan menggunakan objek DOM untuk mensimulasikan nod DOM ialah semua kemas kini halaman boleh dicerminkan pada objek DOM (maya diff) dahulu, dan kelajuan mengendalikan objek JS dalam ingatan jelas lebih pantas , selepas kemas kini selesai, petakan objek JS akhir kepada attch sebenar dan biarkan penyemak imbas melukisnya. DOM

2.2. Pelaksanaan algoritma

2.2.1. Gunakan objek JS untuk mensimulasikan DOM pokok

( 1 ) Cara menggunakan JS objek untuk mensimulasikan DOM pokok

Contohnya, nod DOM sebenar adalah seperti berikut:

<div>
<p>Virtual DOM</p>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
<div>Hello World</div>
</div>
Salin selepas log masuk
Salin selepas log masuk

Kami menggunakan JavaScript objek untuk mewakili DOM Nod, gunakan sifat objek untuk merekodkan jenis nod, atribut, sub-nod, dsb. Kod objek nod yang diwakili dalam

element.js adalah seperti berikut:

/**
 * Element virdual-dom 对象定义
 * @param {String} tagName - dom 元素名称
 * @param {Object} props - dom 属性
 * @param {Array<element>} - 子节点
 */
function Element(tagName, props, children) {
    this.tagName = tagName
    this.props = props
    this.children = children
    // dom 元素的 key 值,用作唯一标识符
    if(props.key){
       this.key = props.key
    }
    var count = 0
    children.forEach(function (child, i) {
        if (child instanceof Element) {
            count += child.count
        } else {
            children[i] = '' + child
        }
        count++
    })
    // 子元素个数
    this.count = count
}

function createElement(tagName, props, children){
 return new Element(tagName, props, children);
}

module.exports = createElement;</element>
Salin selepas log masuk

Menurut tetapan objek element, struktur DOM di atas boleh dinyatakan secara ringkas sebagai :

var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [	el('li', { class: 'item' }, ['Item 1']),
	el('li', { class: 'item' }, ['Item 2']),
	el('li', { class: 'item' }, ['Item 3'])
  ]),
  el('div',{},['Hello World'])
])
Salin selepas log masuk

Kini ul ialah struktur JavaScript yang diwakili oleh objek DOM Kami mengeluarkan dan melihat struktur data yang sepadan ul seperti berikut:

<.>Analisis mendalam DOM maya dalam Vue

(2) Render objek yang diwakili oleh JS DOM

tetapi tiada struktur sedemikian pada halaman Seterusnya, kami akan memperkenalkan caranya untuk menjadikan

menjadi objek sebenar pada halaman ul struktur, fungsi pemaparan yang berkaitan adalah seperti berikut: DOM

/**
 * render 将virdual-dom 对象渲染为实际 DOM 元素
 */
Element.prototype.render = function () {
    var el = document.createElement(this.tagName)
    var props = this.props
    // 设置节点的DOM属性
    for (var propName in props) {
        var propValue = props[propName]
        el.setAttribute(propName, propValue)
    }

    var children = this.children || []
    children.forEach(function (child) {
        var childEl = (child instanceof Element)
            ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
            : document.createTextNode(child) // 如果字符串,只构建文本节点
        el.appendChild(childEl)
    })
    return el
}
Salin selepas log masuk
Dengan melihat kaedah

di atas, kami akan membina kaedah sebenar. render nod berdasarkan tagName, dan kemudian tetapkan sifat nod ini, dan akhirnya secara Rekursif membina nod anak sendiri. DOM

Kami akan menambah struktur

yang dibina pada halaman DOM, seperti berikut: body

ulRoot = ul.render();
document.body.appendChild(ulRoot);
Salin selepas log masuk
Dengan cara ini, halaman

akan mempunyai body sebenar struktur, Kesannya adalah seperti yang ditunjukkan dalam rajah di bawah: DOM

Analisis mendalam DOM maya dalam Vue

2.2.2. Bandingkan perbezaan antara dua pokok maya - DOM Algoritma diff

digunakan untuk membandingkan perbezaan antara dua

pokok Jika perbandingan lengkap dua pokok diperlukan, maka kerumitan masa algoritma diff ialah Virtual DOM. . Tetapi di bahagian hadapan, anda jarang memindahkan diff elemen merentas peringkat, jadi O(n^3) hanya akan membandingkan elemen pada tahap yang sama, seperti yang ditunjukkan dalam rajah di bawah, DOM hanya akan membandingkan dengan Virtual DOM pada tahap yang sama tahap Sebagai perbandingan, tahap kedua hanya akan dibandingkan dengan tahap kedua, supaya kerumitan algoritma boleh mencapai div. divO(n)

Analisis mendalam DOM maya dalam Vue

(1) Depth-first traversal, record the differences

Dalam kod sebenar, pokok lama dan baru akan Traversal depth-first, supaya setiap nod akan mempunyai tanda yang unik:

Analisis mendalam DOM maya dalam VueSemasa traversal depth-first, setiap kali nod dilalui, nod ditambahkan pada yang baharu satu pokok untuk perbandingan. Jika terdapat perbezaan, ia direkodkan dalam objek.

Daripada perkara di atas, dapat disimpulkan bahawa
// diff 函数,对比两棵树
function diff(oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk(oldNode, newNode, index, patches) {
  var currentPatch = []
  if (typeof (oldNode) === "string" && typeof (newNode) === "string") {
    // 文本内容改变
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 节点相同,比较属性
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // 比较子节点,如果子节点有'ignore'属性,则不需要比较
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else if(newNode !== null){
    // 新节点和旧节点不同,用 replace 替换
    currentPatch.push({ type: patch.REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
}
Salin selepas log masuk
mewakili

, patches[1] mewakili p, dan seterusnya. patches[3]ul

(2) Jenis perbezaan

Jenis perbezaan yang disebabkan oleh operasi termasuk yang berikut:

DOM

Penggantian nod: perubahan nod Untuk contoh, gantikan
    di atas dengan
  • ; tukar tertib divh1: alih, padam dan tambah nod anak baharu Contohnya, untuk nod anak
  • di atas, gantikan
  • dan div Pertukaran jujukan; pulPerubahan atribut: Ubah suai atribut nod, seperti memadamkan kelas gaya
  • di atas;
  • liPerubahan teks: Tukar teks kandungan nod teks, Sebagai contoh, tukar kandungan teks nod class di atas kepada "
  • ";
  • pReal Dom
  • (3) Senaraikan algoritma perbandingan

Algoritma perbandingan untuk nod anak, contohnya, tertib
var REPLACE = 0 // 替换原先的节点
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了节点的属性
var TEXT = 3 // 文本内容改变
Salin selepas log masuk
ditukar kepada

. Bagaimana untuk membandingkan ini? Jika dibandingkan secara berurutan pada tahap yang sama, semuanya akan diganti. Jika daripada dan

berbeza,

akan digantikan dengan p, ul, div. Akhirnya, ketiga-tiga nod akan diganti, yang menjadikan div, p, ul sangat mahal. Sebenarnya, tidak perlu menggantikan nod, tetapi hanya perlu memindahkan nod. Kita hanya perlu tahu bagaimana untuk bergerak. p

        将这个问题抽象出来其实就是字符串的最小编辑距离问题(Edition Distance),最常见的解决方法是 Levenshtein Distance , Levenshtein Distance 是一个度量两个字符序列之间差异的字符串度量标准,两个单词之间的 Levenshtein Distance 是将一个单词转换为另一个单词所需的单字符编辑(插入、删除或替换)的最小数量。Levenshtein Distance 是1965年由苏联数学家 Vladimir Levenshtein 发明的。Levenshtein Distance 也被称为编辑距离(Edit Distance),通过动态规划求解,时间复杂度为 O(M*N)

定义:对于两个字符串 a、b,则他们的 Levenshtein Distance 为:

Analisis mendalam DOM maya dalam Vue

示例:字符串 aba=“abcde” ,b=“cabef”,根据上面给出的计算公式,则他们的 Levenshtein Distance 的计算过程如下:

Analisis mendalam DOM maya dalam Vue

本文的 demo 使用插件 list-diff2 算法进行比较,该算法的时间复杂度伟 O(n*m),虽然该算法并非最优的算法,但是用于对于 dom 元素的常规操作是足够的。

该算法具体的实现过程这里不再详细介绍,该算法的具体介绍可以参照:https://github.com/livoras/list-diff

(4)实例输出

两个虚拟 DOM 对象如下图所示,其中 ul1 表示原有的虚拟 DOM 树,ul2 表示改变后的虚拟 DOM

var ul1 = el('div',{id:'virtual-dom'},[  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [	el('li', { class: 'item' }, ['Item 1']),
	el('li', { class: 'item' }, ['Item 2']),
	el('li', { class: 'item' }, ['Item 3'])
  ]),
  el('div',{},['Hello World'])
]) 
var ul2 = el('div',{id:'virtual-dom'},[  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [	el('li', { class: 'item' }, ['Item 21']),
	el('li', { class: 'item' }, ['Item 23'])
  ]),
  el('p',{},['Hello World'])
]) 
var patches = diff(ul1,ul2);
console.log('patches:',patches);
Salin selepas log masuk

我们查看输出的两个虚拟 DOM 对象之间的差异对象如下图所示,我们能通过差异对象得到,两个虚拟 DOM 对象之间进行了哪些变化,从而根据这个差异对象(patches)更改原先的真实 DOM 结构,从而将页面的 DOM 结构进行更改。

Analisis mendalam DOM maya dalam Vue

2.2.3、将两个虚拟 DOM 对象的差异应用到真正的 DOM

(1)深度优先遍历 DOM

        因为步骤一所构建的         JavaScript 对象树和 render 出来真正的 DOM 树的信息、结构是一样的。所以我们可以对那棵 DOM 树也进行深度优先的遍历,遍历的时候从步骤二生成的 patches 对象中找出当前遍历的节点差异,如下相关代码所示:

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  // 从patches拿出当前节点的差异
  var currentPatches = patches[walker.index]

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  // 深度遍历子节点
  for (var i = 0; i <p><strong>(2)对原有 <code>DOM</code> 树进行 <code>DOM</code> 操作</strong></p><p>我们根据不同类型的差异对当前节点进行不同的 <code>DOM</code> 操作 ,例如如果进行了节点替换,就进行节点替换 <code>DOM</code> 操作;如果节点文本发生了改变,则进行文本替换的 <code>DOM</code> 操作;以及子节点重排、属性改变等 <code>DOM</code> 操作,相关代码如 <code>applyPatches</code> 所示 :</p><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">function applyPatches (node, currentPatches) {
  currentPatches.forEach(currentPatch => {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string')
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}
Salin selepas log masuk
Salin selepas log masuk

(3)DOM结构改变

通过将第 2.2.2 得到的两个 DOM 对象之间的差异,应用到第一个(原先)DOM 结构中,我们可以看到 DOM 结构进行了预期的变化,如下图所示:

Analisis mendalam DOM maya dalam Vue

2.3、结语

相关代码实现已经放到 github 上面,有兴趣的同学可以clone运行实验,github地址为:https://github.com/fengshi123/virtual-dom-example%E3%80%82

Virtual DOM 算法主要实现上面三个步骤来实现:

  • JS 对象模拟 DOM 树 — element.js

    <div>
    <p>Virtual DOM</p>
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
    <div>Hello World</div>
    </div>
    Salin selepas log masuk
    Salin selepas log masuk
  • 比较两棵虚拟 DOM 树的差异 — diff.js

Analisis mendalam DOM maya dalam Vue

  • 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树 — patch.js

    function applyPatches (node, currentPatches) {
      currentPatches.forEach(currentPatch => {
        switch (currentPatch.type) {
          case REPLACE:
            var newNode = (typeof currentPatch.node === 'string')
              ? document.createTextNode(currentPatch.node)
              : currentPatch.node.render()
            node.parentNode.replaceChild(newNode, node)
            break
          case REORDER:
            reorderChildren(node, currentPatch.moves)
            break
          case PROPS:
            setProps(node, currentPatch.props)
            break
          case TEXT:
            node.textContent = currentPatch.content
            break
          default:
            throw new Error('Unknown patch type ' + currentPatch.type)
        }
      })
    }
    Salin selepas log masuk

三、Vue 源码 Virtual-DOM 简析

我们从第二章节(Virtual-DOM 基础)中已经掌握 Virtual DOM 渲染成真实的 DOM 实际上要经历 VNode 的定义、diffpatch 等过程,所以本章节 Vue 源码的解析也按这几个过程来简析。

3.1、VNode 模拟 DOM

3.1.1、VNode 类简析

Vue.js 中,Virtual DOM 是用 VNode 这个 Class 去描述,它定义在 src/core/vdom/vnode.js 中 ,从以下代码块中可以看到 Vue.js 中的 Virtual DOM 的定义较为复杂一些,因为它这里包含了很多 Vue.js 的特性。实际上 Vue.jsVirtual DOM 是借鉴了一个开源库  snabbdom 的实现,然后加入了一些 Vue.js 的一些特性。

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<vnode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<vnode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
}</vnode></vnode>
Salin selepas log masuk

这里千万不要因为 VNode 的这么属性而被吓到,或者咬紧牙去摸清楚每个属性的意义,其实,我们主要了解其几个核心的关键属性就差不多了,例如:

  • tag 属性即这个vnode的标签属性
  • data 属性包含了最后渲染成真实dom节点后,节点上的classattributestyle以及绑定的事件
  • children 属性是vnode的子节点
  • text 属性是文本属性
  • elm 属性为这个vnode对应的真实dom节点
  • key 属性是vnode的标记,在diff过程中可以提高diff的效率

3.1.2、源码创建 VNode 过程

(1)初始化vue

我们在实例化一个 vue 实例,也即 new Vue( ) 时,实际上是执行 src/core/instance/index.js  中定义的 Function 函数。

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
Salin selepas log masuk

通过查看 Vuefunction,我们知道 Vue 只能通过 new 关键字初始化,然后调用 this._init 方法,该方法在 src/core/instance/init.js 中定义。

  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
      
    // 省略一系列其它初始化的代码
      
    if (vm.$options.el) {
      console.log('vm.$options.el:',vm.$options.el);
      vm.$mount(vm.$options.el)
    }
  }
Salin selepas log masuk

(2)Vue 实例挂载

Vue 中是通过 $mount 实例方法去挂载 dom 的,下面我们通过分析 compiler 版本的 mount 实现,相关源码在目录 src/platforms/web/entry-runtime-with-compiler.js 文件中定义:。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  
   // 省略一系列初始化以及逻辑判断代码  
 
  return mount.call(this, el, hydrating)
}
Salin selepas log masuk

我们发现最终还是调用用原先原型上的 $mount 方法挂载 ,原先原型上的 $mount 方法在 src/platforms/web/runtime/index.js 中定义 。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Salin selepas log masuk

我们发现$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 省略一系列其它代码
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 生成虚拟 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}
Salin selepas log masuk
Salin selepas log masuk

从上面的代码可以看到,mountComponent 核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM

(3)创建虚拟 Node

Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件中:

 Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    let vnode
    try {
      // 省略一系列代码  
      currentRenderingInstance = vm
      // 调用 createElement 方法来返回 vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`){}
    }
    // set parent
    vnode.parent = _parentVnode
    console.log("vnode...:",vnode);
    return vnode
  }
Salin selepas log masuk

Vue.js 利用 _createElement 方法创建 VNode,它定义在 src/core/vdom/create-elemenet.js 中:

export function _createElement (
  context: Component,
  tag?: string | Class<component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<vnode> {
    
  // 省略一系列非主线代码
  
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 场景是 render 函数不是编译生成的
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 场景是 render 函数是编译生成的
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // 创建虚拟 vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}</vnode></component>
Salin selepas log masuk

_createElement 方法有 5 个参数,context 表示 VNode 的上下文环境,它是 Component 类型;tag表示标签,它可以是一个字符串,也可以是一个 Componentdata 表示 VNode 的数据,它是一个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义;children 表示当前 VNode 的子节点,它是任意类型的,需要被规范为标准的 VNode 数组;

3.1.3、实例查看

为了更直观查看我们平时写的 Vue 代码如何用 VNode 类来表示,我们通过一个实例的转换进行更深刻了解。

例如,实例化一个 Vue 实例:

  var app = new Vue({
    el: '#app',
    render: function (createElement) {
      return createElement('div', {
        attrs: {
          id: 'app',
          class: "class_box"
        },
      }, this.message)
    },
    data: {
      message: 'Hello Vue!'
    }
  })
Salin selepas log masuk

我们打印出其对应的 VNode 表示:

1Analisis mendalam DOM maya dalam Vue

3.2、diff 过程

3.2.1、Vue.js 源码的 diff 调用逻辑

Vue.js 源码实例化了一个 watcher,这个 ~ 被添加到了在模板当中所绑定变量的依赖当中,一旦 model 中的响应式的数据发生了变化,这些响应式的数据所维护的 dep 数组便会调用 dep.notify() 方法完成所有依赖遍历执行的工作,这包括视图的更新,即 updateComponent 方法的调用。watcherupdateComponent方法定义在  src/core/instance/lifecycle.js 文件中 。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 省略一系列其它代码
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 生成虚拟 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}
Salin selepas log masuk
Salin selepas log masuk

完成视图的更新工作事实上就是调用了vm._update方法,这个方法接收的第一个参数是刚生成的Vnode,调用的vm._update方法定义在 src/core/instance/lifecycle.js中。

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      // 第一个参数为真实的node节点,则为初始化
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }
Salin selepas log masuk

在这个方法当中最为关键的就是 vm.__patch__ 方法,这也是整个 virtual-dom 当中最为核心的方法,主要完成了prevVnodevnodediff 过程并根据需要操作的 vdom 节点打 patch,最后生成新的真实 dom 节点并完成视图的更新工作。

接下来,让我们看下 vm.__patch__的逻辑过程, vm.__patch__ 方法定义在 src/core/vdom/patch.js 中。

function patch (oldVnode, vnode, hydrating, removeOnly) {
    ......
    if (isUndef(oldVnode)) {
      // 当oldVnode不存在时,创建新的节点
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 对oldVnode和vnode进行diff,并对oldVnode打patch  
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } 
	......
  }
}
Salin selepas log masuk

patch 方法中,我们看到会分为两种情况,一种是当 oldVnode 不存在时,会创建新的节点;另一种则是已经存在 oldVnode ,那么会对 oldVnodevnode 进行 diffpatch 的过程。其中 patch 过程中会调用 sameVnode 方法来对对传入的2个 vnode 进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode 只是局部发生了更新,然后才会对这2个 vnode 进行 diff,如果2个 vnode 的基本属性存在不一致的情况,那么就会直接跳过 diff 的过程,进而依据 vnode 新建一个真实的 dom,同时删除老的 dom节点。

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}
Salin selepas log masuk

diff 过程中主要是通过调用 patchVnode 方法进行的:

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    ...... 
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 如果vnode没有文本节点
    if (isUndef(vnode.text)) {
      // 如果oldVnode的children属性存在且vnode的children属性也存在  
      if (isDef(oldCh) && isDef(ch)) {
        // updateChildren,对子节点进行diff  
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        // 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去  
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 删除elm下的oldchildren
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // oldVnode有子节点,而vnode没有,那么就清空这个节点  
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
      nodeOps.setTextContent(elm, vnode.text)
    }
    ......
  }
Salin selepas log masuk

从以上代码得知,

diff 过程中又分了好几种情况,oldCholdVnode的子节点,chVnode的子节点:

  • 首先进行文本节点的判断,若 oldVnode.text !== vnode.text,那么就会直接进行文本节点的替换;
  • vnode  没有文本节点的情况下,进入子节点的 diff
  • oldChch 都存在且不相同的情况下,调用 updateChildren 对子节点进行 diff
  • oldCh不存在,ch 存在,首先清空 oldVnode 的文本节点,同时调用 addVnodes 方法将 ch 添加到elm真实 dom 节点当中;
  • oldCh存在,ch不存在,则删除 elm 真实节点下的 oldCh 子节点;
  • oldVnode 有文本节点,而 vnode 没有,那么就清空这个文本节点。

3.2.2、子节点 diff 流程分析

(1)Vue.js 源码

        这里着重分析下updateChildren方法,它也是整个 diff 过程中最重要的环节,以下为 Vue.js 的源码过程,为了更形象理解 diff 过程,我们给出相关的示意图来讲解。

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 为oldCh和newCh分别建立索引,为之后遍历的依据
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // 直到oldCh或者newCh被遍历完后跳出循环
    while (oldStartIdx  oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
Salin selepas log masuk

在开始遍历 diff 前,首先给 oldChnewCh 分别分配一个 startIndexendIndex 来作为遍历的索引,当oldCh 或者 newCh 遍历完后(遍历完的条件就是 oldCh 或者 newChstartIndex >= endIndex ),就停止oldChnewChdiff 过程。接下来通过实例来看下整个 diff 的过程(节点属性中不带 key 的情况)。

(2)无 keydiff 过程

我们通过以下示意图对以上代码过程进行讲解:

(2.1)首先从第一个节点开始比较,不管是 oldCh 还是 newCh 的起始或者终止节点都不存在 sameVnode ,同时节点属性中是不带 key标记的,因此第一轮的 diff 完后,newChstartVnode 被添加到 oldStartVnode的前面,同时 newStartIndex前移一位;

1Analisis mendalam DOM maya dalam Vue

(2.2)第二轮的 diff中,满足 sameVnode(oldStartVnode, newStartVnode),因此对这2个 vnode 进行diff,最后将 patch 打到 oldStartVnode 上,同时 oldStartVnodenewStartIndex 都向前移动一位 ;

1Analisis mendalam DOM maya dalam Vue

(2.3)第三轮的 diff 中,满足 sameVnode(oldEndVnode, newStartVnode),那么首先对  oldEndVnodenewStartVnode 进行 diff,并对 oldEndVnode进行 patch,并完成  oldEndVnode 移位的操作,最后newStartIndex前移一位,oldStartVnode 后移一位;

1Analisis mendalam DOM maya dalam Vue

(2.4)第四轮的 diff中,过程同步骤3;

1Analisis mendalam DOM maya dalam Vue

(2.5)第五轮的 diff 中,同过程1;

1Analisis mendalam DOM maya dalam Vue

(2.6)遍历的过程结束后,newStartIdx > newEndIdx,说明此时 oldCh 存在多余的节点,那么最后就需要将这些多余的节点删除。

1Analisis mendalam DOM maya dalam Vue

(3)有 keydiff 流程

vnode 不带 key 的情况下,每一轮的 diff 过程当中都是起始结束节点进行比较,直到 oldCh 或者newCh 被遍历完。而当为 vnode 引入 key 属性后,在每一轮的 diff 过程中,当起始结束节点都没有找到sameVnode 时,然后再判断在 newStartVnode 的属性中是否有 key,且是否在 oldKeyToIndx 中找到对应的节点 :

  • 如果不存在这个 key,那么就将这个 newStartVnode作为新的节点创建且插入到原有的 root 的子节点中;
  • 如果存在这个 key,那么就取出 oldCh 中的存在这个 keyvnode,然后再进行 diff 的过;

通过以上分析,给vdom上添加 key属性后,遍历 diff 的过程中,当起始点结束点搜寻diff 出现还是无法匹配的情况下时,就会用 key 来作为唯一标识,来进行 diff,这样就可以提高 diff 效率。

带有 Key属性的 vnodediff 过程可见下图:

(3.1)首先从第一个节点开始比较,不管是 oldCh 还是 newCh 的起始或者终止节点都不存在 sameVnode,但节点属性中是带 key 标记的, 然后在 oldKeyToIndx 中找到对应的节点,这样第一轮 diff 过后 oldCh 上的B节点被删除了,但是 newCh 上的B节点elm 属性保持对 oldChB节点elm引用。

1Analisis mendalam DOM maya dalam Vue

(3.2)第二轮的 diff 中,满足 sameVnode(oldStartVnode, newStartVnode),因此对这2个 vnode 进行diff,最后将 patch 打到 oldStartVnode上,同时 oldStartVnodenewStartIndex 都向前移动一位 ;

1Analisis mendalam DOM maya dalam Vue

(3.3)第三轮的 diff中,满足 sameVnode(oldEndVnode, newStartVnode),那么首先对 oldEndVnodenewStartVnode 进行 diff,并对 oldEndVnode 进行 patch,并完成 oldEndVnode 移位的操作,最后newStartIndex 前移一位,oldStartVnode后移一位;

Analisis mendalam DOM maya dalam Vue

(3.4)第四轮的diff中,过程同步骤2;

2Analisis mendalam DOM maya dalam Vue

(3.5)第五轮的diff中,因为此时 oldStartIndex 已经大于 oldEndIndex,所以将剩余的 Vnode 队列插入队列最后。

2Analisis mendalam DOM maya dalam Vue

3.3、patch 过程

通过3.2章节介绍的 diff 过程中,我们会看到 nodeOps 相关的方法对真实 DOM 结构进行操作,nodeOps 定义在 src/platforms/web/runtime/node-ops.js 中,其为基本 DOM 操作,这里就不在详细介绍。

export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

export function createComment (text: string): Comment {
  return document.createComment(text)
}

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}
Salin selepas log masuk

3.4、总结

通过前三小节简析,我们从主线上把模板和数据如何渲染成最终的 DOM 的过程分析完毕了,我们可以通过下图更直观地看到从初始化 Vue 到最终渲染的整个过程。

2Analisis mendalam DOM maya dalam Vue

四、总结

本文从通过介绍真实 DOM 结构其解析过程以及存在的问题,从而引出为什么需要虚拟 DOM;然后分析虚拟DOM 的好处,以及其一些理论基础和基础算法的实现;最后根据我们已经掌握的基础知识,再一步步去查看Vue.js 的源码如何实现的。从存在问题 —> 理论基础 —> 具体实践,一步步深入,帮助大家更好的了解什么是Virtual DOM、为什么需要 Virtual DOM、以及 Virtual DOM的具体实现,希望本文对您有帮助。

(学习视频分享:web前端开发编程基础视频

Atas ialah kandungan terperinci Analisis mendalam DOM maya dalam Vue. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:juejin.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Cadangan popular
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan