Vue虚拟Dom到真实Dom的转换

再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系。我们先回顾之前的mountComponnet 方法:

export function mountComponent(vm, el) {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')
  ...
  const updateComponent = function () {
    vm._update(vm._render())
  }
  ...
}

我们已经执行完了vm._render 方法拿到了VNode, 现在将它作为参数传给vm._update 方法并执行。 vm._update这个方法的作用就是将VNode 转为真实的Dom, 不过它有两个执行时机:

首次渲染

当执行new Vue 到此时就是首次渲染了, 会将传入的Vnode对象映射为真实的Dom。

更新页面

数据变化会驱动页面发生变化, 这也是vue最独特的特性之一, 数据改变之前和之后生成两份VNode进行比较, 而怎么样在旧的VNode上做最小的改动去渲染页面,这样一个diff算法还是挺复杂的。 如果再没有先说清楚数据响应式是怎么回事之前,直接将diff对理解vue 的整体流程不太好。 所以这章分析首次渲染后, 下一章就是数据响应式, 之后才是diff比较。

先来看看vm._update方法的定义:

Vue.prototype._update = function(vnode) {
  ... 首次渲染
  vm.$el = vm.__patch__(vm.$el, vnode)  // 覆盖原来的vm.$el
  ...
}

这里的 vm. e l 是 之 前 在 = = m o u n t C o m p o n e n t = = 方 法 内 就 挂 载 的 , 一 个 真 实 的 = = D o m = = 元 素 。 首 次 渲 染 会 传 入 v m . el 是之前在 ==mountComponent== 方法内就挂载的, 一个真实的==Dom==元素。 首次渲染会传入 vm. el是之前在==mountComponent==方法内就挂载的,一个真实的==Dom==元素。首次渲染会传入vm.el 以及得到的VNode, 所以看下vm.patch 定义:

Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules }) 

patch 是createPatchFunction 方法内部返回的一个方法, 它接受一个对象:

nodeOps属性:封装了操作原生Dom 的一些方法的集合, 如:创建、插入,移除这些, 我们到使用的地方咋详解。

modules 属性: 创建真实Dom 也需要生成它的如class/attrs/style 等属性。 modules 是一个数组集合,数组的每一项都是这些属性对应的钩子方法, 这些属性的创建,更新,销毁等都有对应钩子方法。 当某一时刻需要做某件事,执行对应的钩子即可。 比如它们都有create 这个钩子方法, 如将这些create 钩子收集到一个数组内, 需要在真实Dom上创建这些属性时,依次执行数组的每一项,也就是依次创建了它们。

PS: 这里modules 属性内的钩子方法是区分平台的, web, weex 以及 SSR 它们调用VNode 方法方式并不相同, 所以vue在这里又使用了函数柯里化这个骚操作, 在createPatchFunction 内将平台的差异化磨平, 从而 patch 方法只用接收新旧node即可。

生成Dom

这里大家记住一句话即可, 无论VNode 是什么类型的节点, 只有三种类型的节点会被创建并插入到Dom中: 元素节点,注释节点, 和文本节点。

我们接着看下createPatchFunction 它返回一个怎样的方法:

export function createPatchFunction(backend) {
  ...
  const { modules, nodeOps } = backend  // 解构出传入的集合

  return function (oldVnode, vnode) {  // 接收新旧vnode
    ...
    const isRealElement = isDef(oldVnode.nodeType) // 是否是真实Dom
    if(isRealElement) {  // $el是真实Dom
      oldVnode = emptyNodeAt(oldVnode)  // 转为VNode格式覆盖自己
    }
    ...
  }
}

首次渲染时没有oldVnode, oldVnode 就是 $el, 一个真实的dom, 经过emptyNodeAt(odVnode) 方法包装:

function emptyNodeAt(elm) {
  return new VNode(
    nodeOps.tagName(elm).toLowerCase(), // 对应tag属性
    {},  // 对应data
    [],   // 对应children
    undefined,  //对应text
    elm  // 真实dom赋值给了elm属性
  )
}

包装后的:
{
  tag: 'div',
  elm: '<div id="app"></div>' // 真实dom
}

-------------------------------------------------------

nodeOps:
export function tagName (node) {  // 返回节点的标签名
  return node.tagName
}

在将传入的==$el== 属性转为了VNode 格式之后,我们继续:

export function createPatchFunction(backend) {
  ...

  return function (oldVnode, vnode) {  // 接收新旧vnode

    const insertedVnodeQueue = []
    ...
    const oldElm = oldVnode.elm  //包装后的真实Dom <div id='app'></div>
    const parentElm = nodeOps.parentNode(oldElm)  // 首次父节点为<body></body>

    createElm(  // 创建真实Dom
      vnode, // 第二个参数
      insertedVnodeQueue,  // 空数组
      parentElm,  // <body></body>
      nodeOps.nextSibling(oldElm)  // 下一个节点
    )

    return vnode.elm // 返回真实Dom覆盖vm.$el
  }
}

------------------------------------------------------

nodeOps:
export function parentNode (node) {  // 获取父节点
  return node.parentNode
}

export function nextSibling(node) {  // 获取下一个节点
  return node.nextSibing
}

createElm 方法开始生成真实的Dom, VNode 生成真实的Dom 的方式还是分为元素节点和组件两种方式, 所以我们使用上一章生成的VNode分别说明。

1. 元素节点生成Dom

{  // 元素节点VNode
  tag: 'div',
  children: [{
      tag: 'h1',
      children: [
        {text: 'title h1'}
      ]
    }, {
      tag: 'h2',
      children: [
        {text: 'title h2'}
      ]
    }, {
      tag: 'h3',
      children: [
        {text: 'title h3'}
      ]
    }
  ]
}

大家可以先看下这个流程图有个印象即可, 再接下来看具体实现时思路会清晰很多(这里先借用网上的一张图):

开始Dom, 来看下它的定义:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  ...
  const children = vnode.children  // [VNode, VNode, VNode]
  const tag = vnode.tag  // div

  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return  // 如果是组件结果返回true,不会继续,之后详解createComponent
  }

  if(isDef(tag)) {  // 元素节点
    vnode.elm = nodeOps.createElement(tag)  // 创建父节点
    createChildren(vnode, children, insertedVnodeQueue)  // 创建子节点
    insert(parentElm, vnode.elm, refElm)  // 插入

  } else if(isTrue(vnode.isComment)) {  // 注释节点
    vnode.elm = nodeOps.createComment(vnode.text)  // 创建注释节点
    insert(parentElm, vnode.elm, refElm); // 插入到父节点

  } else {  // 文本节点
    vnode.elm = nodeOps.createTextNode(vnode.text)  // 创建文本节点
    insert(parentElm, vnode.elm, refElm)  // 插入到父节点
  }

  ...
}

------------------------------------------------------------------

nodeOps:
export function createElement(tagName) {  // 创建节点
  return document.createElement(tagName)
}

export function createComment(text) {  //创建注释节点
  return document.createComment(text)
}

export function createTextNode(text) {  // 创建文本节点
  return document.createTextNode(text)
}

function insert (parent, elm, ref) {  //插入dom操作
  if (isDef(parent)) {  // 有父节点
    if (isDef(ref)) { // 有参考节点
      if (ref.parentNode === parent) {  // 参考节点的父节点等于传入的父节点
        nodeOps.insertBefore(parent, elm, ref)  // 在父节点内的参考节点之前插入elm
      }
    } else {
      nodeOps.appendChild(parent, elm)  //  添加elm到parent内
    }
  }  // 没有父节点什么都不做
}
这算一个比较重要的方法,因为很多地方会用到。

依次判断是否是元素节点, 注释节点,文本节点, 分别创建它们然后插入到父节点里面, 这里主要介绍创建元素节点, 另外两个并没有复杂的逻辑。 我们接下来看下:createChild 方法定义:

function createChild(vnode, children, insertedVnodeQueue) {
  if(Array.isArray(children)) {  // 是数组
    for(let i = 0; i < children.length; ++i) {  // 遍历vnode每一项
      createElm(  // 递归调用
        children[i],
        insertedVnodeQueue,
        vnode.elm,
        null,
        true, // 不是根节点插入
        children,
        i
      )
    }
  } else if(isPrimitive(vnode.text)) {  //typeof为string/number/symbol/boolean之一
    nodeOps.appendChild(  // 创建并插入到父节点
      vnode.elm,
      nodeOps.createTextNode(String(vnode.text))
    )
  }
}

-------------------------------------------------------------------------------

nodeOps:
export default appendChild(node, child) {  // 添加子节点
  node.appendChild(child)
}

开始创建子节点, 遍历VNode 的每一项, 每一项还是使用之前的createElm方法创建Dom。 如果某一项又是数组,继续调用createChild创建某一项的子节点; 如果某一项不是数组, 创建文本节点并将它添加到父节点内。 像这样使用递归的形式将嵌套的VNode全部创建为真实的Dom。

在看一遍流程图, 应该就能减少大家很多疑惑了(这里先借用网上一章图):

简单来说就是由里向外的挨个创建出真实的Dom, 然后插入到它的父节点内,最后将创建好的Dom插入到body内, 完成创建的过程, 元素节点的创建还是比较简单的, 接下来看下组件式怎么创建的。

组件VNode生成Dom

{  // 组件VNode
  tag: 'vue-component-1-app',
  context: {...},
  componentOptions: {
    Ctor: function(){...},  // 子组件构造函数
    propsData: undefined,
    children: undefined,
    tag: undefined
  },
  data: {
    on: undefined,  // 原生事件
    hook: {  // 组件钩子
      init: function(){...},
      insert: function(){...},
      prepatch: function(){...},
      destroy: function(){...}
    }
  }
}

-------------------------------------------

<template>  // app组件内模板
  <div>app text</div>
</template>

首先看张简易流程图, 留个影响即可,方便理清之后的逻辑顺序(这里借用网上一张图):

使用上一章组件生成VNode , 看下在createElm 内创建组件Dom分支逻辑是怎么样的:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm) {
  ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { // 组件分支
    return
  }
  ...

执行createComponent 方法, 如果是元素节点不会返回任何东西,所以是undefined , 会继续走接下来的创建元节点的逻辑。 现在是组件, 我们看下createComponent 的实现:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if(isDef(i)) {
    if(isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode)  // 执行init方法
    }
    ...
  }
}

首先会将组件的vnode.data赋值给i, 是否有这个属性就能判断是否是组件vnode。 之后的if(isDef(i = i.hook) && isDef(i = i.init)) 集判断和赋值为一体, if 内的i(vnode) 就是执行的组件init(vnode)方法。 这个时候我们来看下组件的init 钩子方法做了什么:

import activeInstance  // 全局变量

const init = vnode => {
  const child = vnode.componentInstance =
    createComponentInstanceForVnode(vnode, activeInstance)
  ...
}

activeInstance 是一个全局的变量, 再update 方法内赋值为当前实例, 再当前实例做 patch 的过程中作为了组件的父实例传入, 在子组件的initLifecycle时构建组件关系。 将createComponentInsanceForVnode 执行的结果赋值给了vnode.componentInstance, 所以看下它的返回的结果是什么:

export  createComponentInstanceForVnode(vnode, parent) {  // parent为全局变量activeInstance
  const options = {  // 组件的options
    _isComponent: true,  // 设置一个标记位,表明是组件
    _parentVnode: vnode,
    parent  // 子组件的父vm实例,让初始化initLifecycle可以建立父子关系
  }

  return new vnode.componentOptions.Ctor(options)  // 子组件的构造函数定义为Ctor
}

再组件的init 方法内首先执行craeeteComponentInstanceForVnode方法, 这个方法的内部就会将子组件的构造函数实例化, 因为子组件的构造函数继承了基类Vue的所有能力, 这个时候相当于执行new Vue({…}) , 接下来又会执行==_init方法进行一系列的子组件的初始化逻辑, 回到_init== 方法内, 因为他们之间还是有些不同的地方:

Vue.prototype._init = function(options) {
  if(options && options._isComponent) {  // 组件的合并options,_isComponent为之前定义的标记位
    initInternalComponent(this, options)  // 区分是因为组件的合并项会简单很多
  }

  initLifecycle(vm)  // 建立父子关系
  ...
  callHook(vm, 'created')

  if (vm.$options.el) { // 组件是没有el属性的,所以到这里咋然而止
    vm.$mount(vm.$options.el)
  }
}

----------------------------------------------------------------------------------------

function initInternalComponent(vm, options) {  // 合并子组件options
  const opts = vm.$options = Object.create(vm.constructor.options)
  opts.parent = options.parent  // 组件init赋值,全局变量activeInstance
  opts._parentVnode = options._parentVnode  // 组件init赋值,组件的vnode
  ...
}

前面都还是执行的好好的, 最后却因为没有el属性, 所以没有挂载,createComponentInstanceForVnode 方法执行完毕。 这个时候我们回到组件的init方法, 补全剩下的逻辑:

const init = vnode => {
  const child = vnode.componentInstance = // 得到组件的实例
    createComponentInstanceForVnode(vnode, activeInstance)

  child.$mount(undefined)  // 那就手动挂载呗
}

我们在init 方法内手动挂载这个组件, 接着又会执行组件的==render()== 方法得到组件内元素节点VNode , 然后执行vm._update(), 执行组件的 patch 方法, 因为 $mount 方法传入的是 undefined, oldVnode 也是undefinned, 会执行__patch_ 内的这段逻辑:

return function patch(oldVnode, vnode) {
  ...
  if (isUndef(oldVnode)) {
    createElm(vnode, insertedVnodeQueue)
  }
  ...
}

这次执行createElm 是没有传入第三个参数父节点的, 那组件创建好的Dom放哪生效了? 没有父节点页要生成Dom不是, 这个时候执行的是组件的 patch , 所以参数vnode 就是组件内元素节点的vnode了:

<template> // app组件内模板
  <div>app text</div>
</template>

-------------------------

{  // app内元素vnode
  tag: 'div',
  children: [
    {text: app text}
  ],
  parent: {  // 子组件_init时执行initLifecycle建立的关系
    tag: 'vue-component-1-app',
    componentOptions: {...}
  }
}

很明显这个时候不是组件了, 即使是组件也没关系, 大不了还是执行一遍createComponent 创建组件的逻辑, 因为总会有组件是由元素节点组成的。 这个时候我们执行一遍创建元素节点的逻辑, 因为没有第三个参数父节点, 所以组件的Dom虽然创建好了, 并不会在这里插入。 请注意这个时候组件的init 已经完成, 但是组件的createComponent 方法并没有完成, 我们补全它的逻辑:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode)  // init已经完成
    }

    if (isDef(vnode.componentInstance)) {  // 执行组件init时被赋值

      initComponent(vnode)  // 赋值真实dom给vnode.elm

      insert(parentElm, vnode.elm, refElm)  // 组件Dom在这里插入
      ...
      return true  // 所以会直接return
    }
  }
}

-----------------------------------------------------------------------

function initComponent(vnode) {
  ...
  vnode.elm = vnode.componentInstance.$el  // __patch__返回的真实dom
  ...
}

无论是嵌套多么深的组件, 遇到组件后就执行 init, 在init 的 patch 过程中又遇到嵌套组件, 那就再执行嵌套组件的init, 嵌套组件完成 __patch__后将真是的Dom插入到它的父节点内, 接着执行完外层组件的 patch 又插入到它的父几点内, 最后插入到body 内, 完成嵌套组件的创建过程, 总之还是一个由里及外的过程。

在回过头看这张图, 相信会很好理解了:

再将本章最初的mountComponent 之后的逻辑补全:

export function mountComponent(vm, el) {
  ...
  const updateComponent = () => {
    vm._update(vm._render())
  }

  new Watcher(vm, updateComponent, noop, {
    before() {
      if(vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true)

  ...
  callHook(vm, 'mounted')

  return vm
}

接下来会将 updateComponent 传入到一个Watcher 的类中, 这个类是干嘛的,我们下一章在介绍。 接下来执行mounted 钩子方法。 至此new vue 的整个流程就全部走完了。 我们回顾下从new Vue 开始执行的顺序:

new Vue ==> vm._init() ==> vm.$mount(el) ==> vm._render()  ==> vm.update(vnode)

最后我们以一个问题来结束本章的内容:

父子两个组件同时定义了 beforeCreate, created, beforeMounte, mounted 四个钩子, 它们的执行顺序是怎样的?

解答:

首先会执行父组件的初始化过程, 所以会依次执行beforeCreate, created, 在执行挂载前又会执行beforeMount钩子, 不过在生成真实dom 的 __patch__过程中遇到嵌套子组件后又会转为去执行子组件的初始化钩子beforeCreate, created, 子组件在挂载前会执行beforeMounte, 再完成子组件的Dom创建后执行 mounted。 这个父组件的 patch 过程才算完成, 最后执行父组件的mounted 钩子, 这就是它们的执行顺序。 如下:

parent beforeCreate
parent created
parent beforeMounte
    child beforeCreate
    child created
    child beforeMounte
    child mounted
parent mounted

到此这篇关于Vue虚拟Dom到真实Dom的转换的文章就介绍到这了,更多相关Vue虚拟Dom到真实Dom内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于Vue虚拟dom问题

    一.什么是虚拟dom? 虚拟dom本质上就是一个普通的JS对象,用于描述视图的界面结构 在vue中,每个组件都有一个render函数, 没有render找template,没有template找el,有el就会把el.outHTML作为template,然后把这串字符串编译成render函数. 有template就不往下找了.有render同理. 每个render 函数都会返回一个虚拟dom树,这也就意味着每个组件都对应一棵虚拟DOM树. 也就是说render目的就是创建虚拟dom,这个组件到底

  • Vue使用虚拟dom进行渲染view的方法

    前提 vue版本:v2.5.17-beta.0 触发render vue在数据更新后会自动触发view的render工作,其依赖于数据驱动:在数据驱动的工作下,每一个vue的data属性都被监听,并且在set触发时,派发事件,通知收集到的依赖,从而触发对应的操作,render工作就是其中的一个依赖,并且被每一个data属性所收集,因此每一个data属性改变后,都会触发render. vue更新监听 看一段代码 // 来自mountComponent函数 updateComponent = fun

  • vue 虚拟DOM快速入门

    虚拟 DOM 什么是虚拟 dom dom 是文档对象模型,以节点树的形式来表现文档. 虚拟 dom 不是真正意义上的 dom.而是一个 javascript 对象. 正常的 dom 节点在 html 中是这样表示: <div class='testId'> <p>你好</p> <p>欢迎光临</p> </div> 而在虚拟 dom 中大概是这样: { tag: 'div', attributes:{ class: ['testId']

  • vue 虚拟DOM的原理

    为什么需要虚拟DOM? 如果对前端工作进行抽象的话,主要就是维护状态和更新视图,而更新视图和维护状态都需要DOM操作.其实近年来,前端的框架主要发展方向就是解放DOM操作的复杂性. 运行js的速度是很快的,大量的操作DOM就会很慢,时常在更新数据后会重新渲染页面,这样造成在没有改变数据的地方也重新渲染了DOM 节点,这样就造成了很大程度上的资源浪费. 在jQuery出现以前,我们直接操作DOM结构,这种方法复杂度高,兼容性也较差.有了jQuery强大的选择器以及高度封装的API,我们可以更方便的

  • vue2.0的虚拟DOM渲染思路分析

    1.为什么需要虚拟DOM 前面我们从零开始写了一个简单的类Vue框架(文章链接),其中的模板解析和渲染是通过Compile函数来完成的,采用了文档碎片代替了直接对页面中DOM元素的操作,在完成数据的更改后通过appendChild函数将真实的DOM插入到页面. 虽然采用的是文档碎片,但是操作的还是真实的DOM. 而我们知道操作DOM的代价是昂贵的,所以vue2.0采用了虚拟DOM来代替对真实DOM的操作,最后通过某种机制来完成对真实DOM的更新,渲染视图. 所谓的虚拟DOM,其实就是 用JS来模

  • Vue虚拟Dom到真实Dom的转换

    再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系.我们先回顾之前的mountComponnet 方法: export function mountComponent(vm, el) { vm.$el = el ... callHook(vm, 'beforeMount') ... const updateComponent = function () { vm._update(vm._render()) } ... } 我们已经执行完了vm._r

  • 一篇文章带你搞懂Vue虚拟Dom与diff算法

    前言 使用过Vue和React的小伙伴肯定对虚拟Dom和diff算法很熟悉,它扮演着很重要的角色.由于小编接触Vue比较多,React只是浅学,所以本篇主要针对Vue来展开介绍,带你一步一步搞懂它. 虚拟DOM 什么是虚拟DOM? 虚拟DOM(Virtual   Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的. 真实的元素节点: <div id="wr

  • vue 虚拟dom的patch源码分析

    本文介绍了vue 虚拟dom的patch源码分析,分享给大家,具体如下: 源码目录:src/core/vdom/patch.js function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0]

  • Vue虚拟DOM详细介绍

    目录 一.什么是虚拟DOM 二.为什么需要虚拟DOM 三.虚拟DOM介绍 一.什么是虚拟DOM 虚拟DOM是对真是DOM的抽象,以JavaScript对象(VNode节点)作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上. Javascript对象中,虚拟DOM表现为一个Object对象,并且最少包含标签名(tag).属性(attrs)和子元素对象(children)三个属性. 创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与

  • Vue虚拟dom被创建的方法

    先来看生成虚拟dom的入口文件: ... import { parse } from './parser/index' import { optimize } from './optimizer' import { generate } from './codegen/index' ... const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } c

  • 详解vue指令与$nextTick 操作DOM的不同之处

    异步更新队列 可能你还没有注意到,Vue 异步执行 DOM 更新.只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变.如果同一个 watcher 被多次触发,只会被推入到队列中一次.这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要.然后,在下一个的事件循环"tick"中,Vue 刷新队列并执行实际 (已去重的) 工作.Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不

  • Vue监听数据渲染DOM完以后执行某个函数详解

    实例如下: vm.$watch('某data数据',function(val){ vm.$nextTick(function() { 某事件(); }); }) 以上这篇Vue监听数据渲染DOM完以后执行某个函数详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

随机推荐