vue源码分析(六) 虚拟dom成真实的dom

一. 前言

上文介绍了什么是vnode,虚拟dom就是用一个原生的js对象来描述dom,那么vnode是如何渲染成真实的dom,本文不涉及组件渲染成真实的dom,主要关注以下几点

  • vnode是如何渲染成真实的dom
  • vnode是何时渲染成真实的dom

二.实例

本文demo 基础代码来自于源码,便于单步调试分析源码执行流程

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <h1>{{foo}}</h1>
        <div>
            <p>{{bar}}</p>
            <h3>{{other}}</h3>
        </div>
    </div>
</body>
<script src="./dist/app.bundle.js"></script>  //打包后的js
// index.js
new Vue({
    render(h) {
        return h('h1', { class: 'p' }, [this.msg, h('span', [h('a', 'game'), h('a', 'over')])])
    },
    data() {
        return {
            msg: 'hello'
        }
    }
}).$mount('#app')

demo中多次调用h方法生成vnode,h方法也就是我们前面文章提到的createElement方法,这个方法中调用了_createElement 方法。因为这里不包含组件,所以最终会调用new VNode(...) 方法生成需要渲染的vnode.

我们在createElm(后面介绍)方法中打一个断点,参考图(1),看看我们需要渲染的vnode究竟是什么?
vue源码分析(六) 虚拟dom成真实的dom_第1张图片

图(1)

在控制台输入 vnode可以看到 vnode的结构,主要关注以下三个属性

  • tag: 指定渲染何种类型的html节点,没有的话可能是注释节点或者是文本节点,上面的例子中有vnode对应三个文本节点
  • text: 文本节点的内容
  • children:虚拟节点的子节点,这个很重要,反应了真实的dom节点间的层级关系。

看图(2)真实dom结构图和图(3)虚拟dom结构图
vue源码分析(六) 虚拟dom成真实的dom_第2张图片

图(2)

vue源码分析(六) 虚拟dom成真实的dom_第3张图片

图(3)

从图(2)和图(3)中可以看到vnode准确的的反映了真实的dom结构,所以接下来我们会重点分析图(3)是如何转换成图(2)的。以及Vue在源码实现的过程中值得学习的地方。

三.vm._update

从上文分析我们得知,无论时用户手写render还是编译生成的render函数最终都会生成vnode,作为_update方法的参数。

// src/core/instance/lifecycle.js  方法中了省略部分代码
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
 
  ...
  let updateComponent
  /* istanbul ignore if */
  
  updateComponent = () => {
     vm._update(vm._render(), hydrating)
  }
 
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
...
  return vm
}

初次渲染时会生成渲染watcher,在创建该实例的过程中会执行updateComponent
方法,从而执行vm._update方法.

// src/core/instance/lifecycle.js
export let activeInstance: any = null

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
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      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
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

	export function setActiveInstance(vm: Component) {
	  const prevActiveInstance = activeInstance
	  activeInstance = vm
	  return () => {
	    activeInstance = prevActiveInstance
	  }
	}

重点关注以下几件事

  • vm._vnode = vnode 指定_vnode为要渲染的vnode

  • setActiveInstance(vm)方法中指定模块内的全局变量activeInstance 为当前实例vm,并返回一个函数restoreActiveInstance ,这里利用了闭包实现。

  • 因为是初次渲染,所以执行

     vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    

下面我看看_patch方法

四. __patch__

src/platform/web/runtime/index.js中定义了__patch__方法

// install platform patch function
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

src/platform/web/runtime/patch.js

/* @flow */

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

nodeOps里面封装了原生的dom操作,modules目录下的文件与attr,props,events相关。

export const patch: Function = createPatchFunction({ nodeOps, modules })

src/core/vdom/patch.js中定义了createPatchFunction。该方法内部定义了很多方法,其中返回了patch方法,这里的实现借助了js闭包。不用每次patch的过程中进行与平台有关的处理。


export function createPatchFunction (backend) {
	...
	createElm() {
	}
	return return function patch (oldVnode, vnode, hydrating, removeOnly) {
		...
	}
	...
}

patch方法中有四个参数,其中hydrating和服务端有关。


function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '

, or missing . Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }

实例中的patch过程主要分为以下几步

 patch (oldVnode, vnode, hydrating, removeOnly)
  • oldVnodeel: div#app,此时调用oldVnode = emptyNodeAt(oldVnode)将真实的dom元素div#app 转化成虚拟dom并且生成的oldVnodeelm属性为div#app

  • 获取div#app的父元素,实例中也就是body

      const parentElm = nodeOps.parentNode(oldElm)
    
  • 创建真实的dom元素并插入到父节点

 createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

五. createElm方法

这个方法中有重要一点,调用createElement(这里不考虑createElementNS)方法,生成真实的dom,并赋值给vnode.elm

 vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

从这里我们可以看到,vnode描述这真实的dom,vnode.elm是根据vnode生成的真实dom.

createElm中会调用createChildren方法,如果有子节点会创建好子节点并插入到父节点。这里使用了递归,也就是所有的子节点全部插入各自的父节点后,生成的整个dom树才会插入到 父节点,示例中是body.

分析图 (3)的vnode插入流程.

首先将生成的整个vnode插入


createElm(
	vnode,
	insertedVnodeQueue, //不用关注
	parentElm, //body
	refElm //不用关注
)

在插入的过程中由于tag===h1children不为空

  • 遍历children,调用createElm方法将文本节点hello插入到父节点h1
  • 在插入tag ==== 'span'vnode节点的过程中,由于该vnodechildren也不为空,所以会将children中的每个child插入到vnode:{tag:'span'}.elm中,然后将span插入到h1中。最后将h1插入到body中。

最关键的是明白以下几点

  • 会根据每个vnode对象生成真实的dom元素,并赋值给vnode对象的.elm属性
  • 在将vnode.elm插入到parentElm(真实的父dom节点)前,会调用createChildren方法,将每个vnode对应的真实dom插入到对应的parentElm 中,最终完成整个虚拟dom对应的真实dom树的渲染,并将整个虚拟dom对应的真实dom树插入到body中。

你可能感兴趣的:(vue源码笔记)