Vue源码分析 -- 数据驱动流程分析(5)

191 阅读3分钟

_update() 方法

_updata() 的调用时机有两种,1.首次渲染。2.数据更新。
_updata() 的的作用是将VNode渲染生成真实DOM

// src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
  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)
    }
}

因为是首次执行,所以prevVnode不存在,_update()方法会执行vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)进行了初始化。
其中__patch__方法在web下的定义的是patch函数,否者为空

// src/platforms/web/runtime/index.js
// inBrowser用于判断浏览器环境
Vue.prototype.__patch__ = inBrowser ? patch : noop
// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

最终调用的是createPatchFunction()方法,并传入了nodeOps, modules

  • nodeOps 看下方代码,只是一部分例子,但都是针对dom结构的操作方法。
  • modules 是一些dom生成相关的class,event,attrs等
// src/platforms/web/runtime/node-ops.js
import { namespaceMap } from 'web/util/index'

export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}
export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}
// ...

由此看流程Vue.prototype.__patch__ === createPatchFunction({ nodeOps, modules }) === patch
其中createPatchFunction({ nodeOps, modules })方法分别解构传入了nodeOps, modules 参数,那为什么要这么写呢?
因为Vue是跨平台的(web, weex),每个平台对DOM接口的操作方法都是不一样的,这样我们在调用方法之前处理好相对应的辅助函数,这样最终调用方法的时候,就不用再挨个if()else()去判断了。
这里使用的是函数柯里化的思想,也可以理解为一个典型的适配器模式

// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
	let i, j
	const cbs = {}

	const { modules, nodeOps } = backend
	// 这里初始化了每个阶段的生命周期钩子
	for (i = 0; i < hooks.length; ++i) {
  	cbs[hooks[i]] = []
  	for (j = 0; j < modules.length; ++j) {
    		if (isDef(modules[j][hooks[i]])) {
      		cbs[hooks[i]].push(modules[j][hooks[i]])
    		}
  	}
	}

	// 最终将patch函数返回
	return function patch (oldVnode, vnode, hydrating, removeOnly) {
		// ...
  	invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  	return vnode.elm
	}

结合一个例子来分析patch函数

<div id="app"></div>
var app = new Vue({ 
	el: '#app', 
	render: function (createElement) { 
		return createElement('div', { 
			attrs: { 
				id: 'app' 
			}, 
		}, this.message) 
	},
	data: { 
		message: 'Hello Vue!' 
	} 
}) 

然后我们在vm._update 的⽅法⾥是这么调⽤patch ⽅法的: vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) 这里传入的vm.$el就是

的dom对象,vnode对应的是render函数返回的vnode

return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 这里的oldVnode 实际上就是最开始在html页面中写的<div id='app'></div>
if (isUndef(oldVnode)) {
  // ...
} else {
	// 首次渲染,这里的oldVnode是一个真实element节点
  const isRealElement = isDef(oldVnode.nodeType)
  if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // ...
  } else {
    if (isRealElement) {
      // ....
      // either not server-rendered, or hydration failed.
      // create an empty node and replace it
		// 最终触发emptyNodeAt(oldVnode)方法,将真实DOM转化为了VNode
      oldVnode = emptyNodeAt(oldVnode)
		/*
			function emptyNodeAt (elm) {
  			return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
			}
		*/
    }

    // replacing existing element
	  // oldEl是之前的真实DOM,即 div #app
    const oldElm = oldVnode.elm
	  // parentElm 为 oldElm.parentNode 则为body元素
    const parentElm = nodeOps.parentNode(oldElm)

    // create new node
	  // 将虚拟dom转为真实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方法的作用是将VNode转化为真实DOM,并将它挂载到vnode.elm上面,然后调用createChildren(vnode, children, insertedVnodeQueue)方法

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  const data = vnode.data  // 参数 attr
  const children = vnode.children // 子节点
  const tag = vnode.tag  // 标签
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      // ...
      if (isUnknownElement(vnode, creatingElmInVPre)) {
		  // 是指这个标签未定义,例子:使用组件时,如果该组件未注册的话,就会报错
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }
		
    // vnode上不存在ns,则触发 nodeOps.createElement(tag, vnode) 
	  // nodeOps.createElement实际是 通过document.createElement(tagName) 
	  // 也就是最终将render中的app转化为了真实DOM
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, 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)
  }
}

createChildren()方法,首先判断了children是否为数组,数组的话会循环调用createElm(),并将children的每一项传进去,这样就形成了一个递归,如果children是一个text的基础文本节点,就会执行nodeOps.appendChild(),将节点直接插入到创建好的dom结构上,直到children不是一个vnode而是基础text文本节点。然后会执行insert(parentElm, vnode.elm, refElm)方法 因为是递归调用的,所以子元素会优先插入进去,直到循环递归完成后,最后插入父节点。

function createChildren (vnode, children, insertedVnodeQueue) {
	// 首先判断子vnode是否为数组,
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
	  // 数组的话会循环调用createElm()方法,对chilren进行递归
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
	  // 如果是文本text节点的话则直接插入
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

值得一提的是:
(Insert根据是否传入了参考节点,使用了insertBefore或appendChild) 这样保证一次性插入,不做多余的真实dom操作,引发频繁的重绘和回流(message先插入div,div再插入body)

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

最后执行完createElm函数后,新的dom结构已经被加入到了页面中去,但是旧的节点还未删除,所以最终会通过removeVnodes([oldVnode], 0, 0)方法删除旧的节点

// destroy old node
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}