Vue.js源码分析03——虚拟DOM

前言

之前写过博文Vue的首次渲染过程,在文章中提到,渲染dom的时候会调用vm.render()函数生成VNode,之后会调用vm._update(vnode, ...)来渲染页面。
这篇文章我们从两个方面入手

  1. vm.render()是如何生成虚拟DOM的
  2. 生成了虚拟dom后又进行了什么操作

准备调试虚拟DOM的代码

之前我们准备了首次渲染的调试代码,下面我们new Vue中补充一些参数,去观察Vue是如何进行关于虚拟DOM的操作

<body>
 <div id="app"></div>
</body>
<script>
const vm = new Vue({
  el: '#app',
  render(h){
	const vnode = h(
		'h1',
		{
			attrs: { id: 'title' }
		},
		this.msg
	)
	return vnode
  },
  data: {
	msg: 'Hello Vue'
  }
})
</script>

上面的代码可以看出h()函数主要生成虚拟DOM,他有多种传递参数的方式,在分析源码的过程再去做讨论

h()返回的值一个VNode对象,下面记录下VNode的核心属性:

  • tag
  • data
  • children
  • text
  • elm
  • key

虚拟dom函数执行流程

先去执行通过参数传递的render
拿到返回的VNode
vm.update
render
vm.$createElement最终返回VNode
vm.__patch__
patchVNode
updateChildren

vm.render()是如何生成虚拟DOM的

render

此函数是在core/instance/render.js中定义,这个方法的核心是调用了用户传递过来的render

const { render, _parentVnode } = vm.$options
// vm._renderProxy就是vue实例,观察我们传入的options.render,观察到:vm.$createElement就是我们定义的h函数
vnode = render.call(vm._renderProxy, vm.$createElement)

下面分析vm.$createElement

vm.$createElement

此函数是在core/instance/render.js中定义,这个函数就一句话,核心作用就是对手写的render函数进行渲染。它上面还有个_c方法看起来和$createElement很像,这个方法是对编译生成的render 进行渲染的方法

  // 对编译生成的render 进行渲染的方法
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // 对手写 render 函数进行渲染的方法
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

发现$createElement返回了 createElement,下面看createElement内部实现

createElement

此函数在/core/vdom/create-element.js中定义, 当前这个函数主要是处理我们传递过来的参数, h函数可以接受参数的方式可以参考官方文档,这里就不说了。 处理完参数后,这个函数最终返回_createElement,

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 如果data是数组或者是原始值的时候
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    // data复制给children
    children = data
    data = undefined
  }
  // 如果调用的是用户传入的render函数
  if (isTrue(alwaysNormalize)) {
    // normalizationType = 2
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 1.-------------- 对传入的值做判断 --------------

  /** 如果data不为空并且data中有__ob__的属性,说明data是响应式数据。开发环境报警告⚠️ ,返回空的VNode节点 */ 
  
  // data中又is属性,会把is记录到tag里面
  // <component v-bind:is="currentTabComponent"></component>
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  /* 如果设置:is="fasle" 返回空的VNode节点*/

  /** 如果data中有key,并且key不是原始值, 开发环境警告⚠️ */ 

  /** 省略了一段处理插槽的代码 */
	
  // 2.--------------处理传入children--------------
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 返回一维数组,处理用户手写的render
    // 判断 children 的类型,如果是原始值的话转换成 VNode 的数组 
    // 如果是数组的话,继续处理数组中的元素
	// 如果数组中的子元素又是数组(slot template),递归处理
	// 如果连续两个节点都是字符串会合并文本节点
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 把二维数组,转换成一维数组
    // 如果 children 中有函数组件的话,函数组件会返回数组形式
	// 这时候 children 就是一个二维数组,只需要把二维数组转换为一维数组
    children = simpleNormalizeChildren(children)
  }
  // 3.--------------下面的代码开始创建vnode对象-------------
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // tag是否是html中的保留标签
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
      // 判断是否是 自定义组件
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // 查找自定义组件构造函数的声明
      // 根据 Ctor 创建组件的 VNode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    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()
  }
}

生成了虚拟dom后又进行了什么操作

通过上面的分析,我们已经知道render方法是如何工作的了,执行完createElement后,我们拿到了VNode对象,回到 vm._update(vm._render(), hydrating)。继续向下执行

vm._update

这个方法在core/instance/lifecycle.js中定义,作用是把VNode渲染成真实DOM,首次渲染会调用,数据更新也会调用。这个方法的核心是调用vm.__patch__方法

 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
   const vm: Component = this
   const prevEl = vm.$el
   const prevVnode = vm._vnode // 这里面存的是我们上一次处理的VNode
   const restoreActiveInstance = setActiveInstance(vm)
   vm._vnode = vnode
   // Vue.prototype.__patch__ is injected in entry points
   // based on the rendering backend used.
   if (!prevVnode) {
     // 如果不存在 说明是首次渲染
     vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
   } else {
     // 数据更新的额时候调用
     vm.$el = vm.__patch__(prevVnode, vnode)
   }
   ...
 }

vm.__patch__

  1. 此函数在/platforms/web/runtime/index.js中定义,就一句话
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
  1. 跳转到patch函数, 发现他是createPatchFunction函数返回的

// nodeOps存储的是封装的一些dom操作
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
// 存储的是处理指令和ref的模块
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 })
  1. 下面我们跳转到createPatchFunction,看一下patch函数式如何初始化的

/core/vdom/patch.js下找到createPatchFunction。这个函数里面的内容很多,是一个高阶函数,返回了patch函数进行函数柯里化,这里我们主要看patch函数的初始化过程

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend){
    let i, j
  const cbs = {}
/* 1.-----------------------------解构参数-----------------------------*/
  // modules 节点的属性/事件/样式的操作
  // nodeOps 节点操作
  const { modules, nodeOps } = backend
/* 2.----------------------------钩子函数存到cbs里---------------------- */
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // 如果模块中定义了对应的钩子函数
        // 最后的形式为:cbs['update'] = [updateAttrs, updateClass, update...]
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
/*3.--------------------------初始化patch-----------------------------*/
  return function patch(oldVnode, vnode, hydrating, removeOnly){
	...
  }
}
  1. 找到了patch,下面看patch里面做了些什么

这个函数的核心功能是:对比两个 VNode 的差异,把差异更新到真实 DOM。如果是首次渲染的话,会把真实 DOM 先转换成 VNode

return function patch(oldVnode,vnode,hydrating,removeOnly){
/* 1.vnode不存在,但是oldVnode存在,执行Destory钩子函数 */
   if (isUndef(vnode)) {
     if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
     return
   }
/* 2.声明两个变量,一个标记当前dom是否挂载到了dom树上,一个存储钩子函数*/
let isInitialPatch = false
const insertedVnodeQueue = []
/* 3. 判断oldVnode是否存在, 不存在就不会渲染到页面,只是创建新的dom存到内存中 */
if(isUndef(oldVnode)){
  isInitialPatch = true
  // 把vnode转成真实dom
  createElm(vnode, insertedVnodeQueue)
}else{
	// ① 判断oldVnode是否是dom元素
	const isRealElement = isDef(oldVnode.nodeType)
	// ② 如果oldVnode不是dom元素,并且和 vnode是 sameVnode
	if(!isRealElement && sameVnode(oldVnode, vnode)){
	    // 开始执行diff, 并把差异更新到dom中去
		patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
	}else{
	  ... // 这地方有关于ssr的操作,不是本文的重点,省略了
	  // 如果第一个参数是真实DOM,把oldVnode 转成vnode节点
	   if (isRealElement) {
	     // 把oldVnode 转成vnode节点,存到oldVnode中
          oldVnode = emptyNodeAt(oldVnode)
	   }
	}
	// ③ 找到oldVnode对应的父节点
	const oldElm = oldVnode.elm
    const parentElm = nodeOps.parentNode(oldElm)
    // ④ vnode转成真实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)
    )
    // ⑤ 判断oldVNode中的获取的parentElm是否存在
    if (isDef(parentElm)) {
        // 把老节点移除,并触发相关的钩子函数
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    // ⑥ 通过isInitialPatch判断当前对应的dom元素是否挂载到dom树上,如果有:触发insertedVnodeQueue里面定义的所有钩子函数
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

patch函数看起来比较复杂,在这个地方加个简单的流程总结

不存在
存在
dom
vnode
老节点不存在
创建vnode对应的dom 存储在内存中
老节点是dom还是vnode
转成vnode,存到oldVnode中去
新的vnode转成真实dom后挂载
跟新的vnode进行对比 把差异更新到dom

path函数调用结束后,我们虚拟dom的更新其实已经完成了。但是我们发现patch 里面调用了两个关键函数,patchVnodecreateElm 。这两个函数一个diff vnode,一个是把vnode转成真实dom,然后挂载到dom树上。

因为文章过长影响观感,对于这两个函数,单独开篇分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值