今天学习的时候发现关于_update
有点太复杂了,这部分的源码得慢慢坑,三言两语讲不清楚啊…
首先先记住两点:
_update
在首次渲染和数据更新(即响应式时)会被调用。这次分析只会跟首次渲染有关系;_update
是将VNode
转换为真实的DOM
我们调用_update是在$mount的时候
vm._update(vm._render(), hydrating)
先找到_update
所在的文件
// src/core/instance/lifecycle.js
// 关于首次渲染主要是这行代码
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
// src/platforms/web/runtime/index.js
// inBrowser 表示是否处于浏览器端,其实就是区分浏览器端渲染和服务端渲染。因为在服务端没有真实的 DOM,它不需要把 VNode 转换成真实的 DOM,因此直接返回 noop(空函数)。
Vue.prototype.__patch__ = inBrowser ? patch : noop
// src/platforms/web/runtime/patch.js
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({
nodeOps, modules })
其中,对象的 nodeOps
属性定义在 src/platforms/web/runtime/node-ops.js
中,这个文件里面定义了很多关于原生DOM操作,得去细看一下源码,三言两语讲不完TAT。对象modules
是由 platformModules
和 baseModules
合并而来,里面定义了一些模块的钩子函数
的实现,用于在虚拟 DOM
转换为真实 DOM
之后给真实 DOM
添加 attr
、class
、style
等 DOM
属性。
接下来看看createPatchFunction
:
export const emptyNode = new VNode('', {
}, [])
// 这个是生命周期的钩子
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
function sameVnode (a, b) {
}
function sameInputType (a, b) {
}
function createKeyToOldIdx (children, beginIdx, endIdx) {
}
export function createPatchFunction (backend) {
let i, j
const cbs = {
}
const {
modules, nodeOps } = backend
// 在 patch 的不同时期会调用不同的钩子函数
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]])
}
}
}
// 中间定义了一些辅助函数
function emptyNodeAt (elm) {
}
function createRmCb (childElm, listeners) {
return remove
}
function removeNode (el) {
}
let inPre = 0
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
}
function initComponent (vnode, insertedVnodeQueue) {
}
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
}
function insert (parent, elm, ref) {
}
function createChildren (vnode, children, insertedVnodeQueue) {
}
function isPatchable (vnode) {
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
}
function setScope (vnode) {
}
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
}
function invokeDestroyHook (vnode) {
}
}
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
}
function removeAndInvokeRemoveHook (vnode, rm) {
}
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
}
function findIdxInOld (node, oldCh, start, end) {
}
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
}
function invokeInsertHook (vnode, queue, initial) {
}
let bailed = false
// list of modules that can skip create hook during hydration because they
// are already rendered on the client or has no need for initialization
const isRenderedModule = makeMap('attrs,style,class,staticClass,staticStyle,key')
// Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate (elm, vnode, insertedVnodeQueue) {
}
function assertNodeMatch (node, vnode) {
}
// 初次渲染
// vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
// 传入的 vm.$el 对应的是例子中 id 为 app 的 DOM 对象,这个也就是我们在 index.html 模板中写的 <div id="app">
// vnode 对应的是调用 render 函数的返回值,
// hydrating 在非服务端渲染情况下为 false,removeOnly 为 false。
// 更新渲染
// vm.$el = vm.__patch__(prevVnode, vnode)
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象
// vnode 表示执行 _render 后返回的 VNode 的节点
// hydrating 表示是否是服务端渲染
// removeOnly 是给 transition-group 用的
// 判断 vnode 是否存在,我们的例子是返回了一个vnode的所以跳过
if (isUndef(vnode)) {
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// ...
} else {
// 第一次执行 patch 函数时传递的 oldVnode 参数是 vm.$el ,也就是要被替换的 DOM 节点
// 更新调用 patch 函数时传递的 oldVnode 参数是 prevVnode ,所以是一个虚拟 DOM
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// ...
} else {
if (isRealElement) {
// 服务端渲染, 跳过
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
}
// // 由于传入的 hydrating 为 false ,因此也不执行
if (isTrue(hydrating)) {
}
// 把 oldVnode 转换成 VNode 对象
// 在emptyNodeAt中我们将oldVnode传给了oldVnode.elm
oldVnode = emptyNodeAt(oldVnode)
}
// oldElm拿到原来的 oldVnode
const oldElm = oldVnode.elm
// 拿到父节点body
const parentElm = nodeOps.parentNode(oldElm)
// createElm的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中,接下来分析 createElm
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)
)
// 这是父占位节点,是和组件相关的,这里不会执行
if (isDef(vnode.parent)) {
}
// 判断之前定义的 parentElm 是否存在,有则删除掉对应的节点
// 我们之前在 const parentElm = nodeOps.parentNode(oldElm) 中拿到了id=app 的父元素body
// 之后删除<div id="app"></div>完成新旧节点替换工作。
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// 我们根据之前递归 createElm 生成的 vnode 插入顺序队列,执行相关的 insert 钩子函数
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 最后将vnode.elm(也就是真实DOM)返回
return vnode.elm
}
}
// createElm
// src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ownerArray为空,跳过
if (isDef(vnode.elm) && isDef(ownerArray)) {
// ...
}
vnode.isRootInsert = !nested // for transition enter check
// 调用了 createComponent 函数,这个函数的功能是创建子组件, 我们这里是返回false
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 在 tag 存在的情况下判断 tag 标签是否合法
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
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
)
}
}
// 调用平台 DOM 的操作去创建一个占位符元素
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
if (__WEEX__) {
// weex相关
} else {
// 创建子节点,分析createChildren
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// 然后把 vnode push 到 insertedVnodeQueue 中,分析invokeCreateHooks
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 把 vnode.elm 插入到父节点 parentElm 中,分析insert
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
// 创建注释节点的
} 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)
}
}
// createElm 函数就分析完了,其实基本流程就是先创建当前节点 vnode.elm,然后把 vnode.children 插到 vnode.elm 中,再把 vnode.elm 插到父节点 parentElm 中。
// createChildren
// src/core/vdom/patch.js
// 如果 children 是个数组则遍历数组并递归调用 createElm 把所有虚拟子节点转换为真实 DOM 节点然后插入到父节点 vnode.elm 中;
// 如果是一个文本 VNode 则直接 appendChild 插到 vnode.elm 里面。
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
// invokeCreateHooks
// src/core/vdom/patch.js
// invokeCreateHooks 函数其实就是遍历 cbs.create 中的所有函数,然后把 vnode push 到 insertedVnodeQueue 中
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
// insert
// src/core/vdom/patch.js
function insert (parent, elm, ref) {
if (isDef(parent)) {
// 有参考节点 ref 就调用 insertBefore 插入到参考节点 ref 前,没有就插到父节点 parent 中。
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
总结
_update
的作用是将VNode
转为真实的DOM,即显示在视图上;_update
触发的时机分别是在首次渲染和更新数据的时候,本次讨论的时候是在首次渲染时;_update
的核心是_patch
;_patch
的作用(在首次渲染时)的主要工作是调用createElm
,先创建当前节点vnode.elm
,然后把vnode.children
插到vnode.elm
中,再把vnode.elm
插到父节点parentElm
中;- 最后插入新的节点,删除页面上的旧节点,并执行对应的钩子函数,并返回当前节点
vnode.elm
。 _pacth
函数中使用了柯里化函数的思想,提高了函数的复用性,值得我们去细细推敲。