Vue源码探秘(_update)
Opened this issue · 0 comments
引言
_update
函数是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染
,一个是数据更新
时。这一节我们主要是看下首次渲染这种情况,数据更新的情况会在后面介绍响应式原理
部分着重分析。
_update
_update
方法的作用是把 VNode
渲染成真实的 DOM
,它的定义在 src/core/instance/lifecycle.js
文件的lifecycleMixin
函数中:
// src/core/instance/lifecycle.js
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.
}
_update
的核心就是调用 vm.__patch__
方法。__patch__
函数在不同平台会有不同的定义,web
端的定义在 src/platforms/web/runtime/index.js
文件中:
// src/platforms/web/runtime/index.js
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
inBrowser
表示是否处于浏览器端,其实就是区分浏览器端渲染
和服务端渲染
。因为在服务端没有真实的 DOM
,它不需要把 VNode
转换成真实的 DOM
,因此直接返回 noop
(空函数)。而在浏览器端则使用 patch
函数,它被定义在 src/platforms/web/runtime/patch.js
文件中:
// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
patch
函数是调用 createPatchFunction
函数的返回值,这里传给 createPatchFunction
函数一个对象,对象的 nodeOps
属性定义在 src/platforms/web/runtime/node-ops.js
中:
// 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)
}
// ...
可以看到,这里面封装了各种各样的 DOM
操作。我们再来看另一个参数 modules
,modules
是这样定义的:
// src/platforms/web/runtime/patch.js
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
modules
是由 platformModules
和 baseModules
合并而来,里面定义了一些模块的钩子函数
的实现,我们先来看一下定义 platformModules
的目录结构(在src/platforms/web/runtime/modules
):
|-- modules
|-- attrs.js
|-- class.js
|-- dom-props.js
|-- events.js
|-- index.js
|-- style.js
|-- transition.js
这里面定义了各种钩子函数
,用于在虚拟 DOM
转换为真实 DOM
之后给真实 DOM
添加 attr
、class
、style
等 DOM
属性,我们这里先不详细介绍,来看一下 createPatchFunction
的实现。函数定义在 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]])
}
}
}
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
}
}
这里定义了 hooks
,也就是生命周期函数,在 patch
的不同时期会调用不同的钩子函数。
createPatchFunction
函数一开始做了遍历,将 hooks
作为 cbs
属性,然后将对应的 modules
的子项 push
到 cbs.hooks
中。
中间定义了一些辅助函数,并在最后 return
一个 patch
函数,这个函数就是vm._update
函数里调用的 vm.__patch__
。
在介绍 patch
的方法实现之前,我们可以思考一下为何 Vue.js
源码绕了这么一大圈,把相关代码分散到各个目录呢?这里其实涉及到了函数柯里化
的概念。
我们知道函数柯里化最大的作用是提高函数的复用性。在 Vue.js
中,不同平台(web
和 weex
)将虚拟 DOM
转换成真实 DOM
的代码实现是不一样的,给真实 DOM
添加各种属性的方法也是不同的。
具体到 createPatchFunction
函数,这里传给它的两个参数 nodeOps
、modules
在不同平台有不同的实现方法(所以定义在 src/platforms
下)。比如 modules
其中一部分是 platformModules
,这个显然就是不同平台有各自的实现。而除了参数的不同,patch
的大部分逻辑是相同的(所以定义在 src/core
下)。
通过 createPatchFunction
把差异化参数提前固化,这样不用每次调用 patch
的时候都传递 nodeOps
和 modules
了,这种编程技巧是非常值得学习的。
回到 patch
方法本身,我们来逐段分析:
return 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 {
// ...
}
// ...
}
这里 patch
函数接收了四个参数:oldVnode
、vnode
、hydrating
、removeOnly
,回顾 _update
中的代码:
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
可以看到第一次执行 patch
函数时传递的 oldVnode
参数是 vm.$el
,也就是要被替换的 DOM
节点;之后更新调用 patch
函数时传递的 oldVnode
参数是 prevVnode
,所以是一个虚拟 DOM
。
回顾Vue 源码探秘(\_render 函数)
的例子:
<script>
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data() {
return {
message: '森林小哥哥'
}
}
</script>
我们围绕这个例子来分析 patch
函数。
第一个 if
语句判断 vnode
是否存在,对于我们这个例子而言,显然是存在的。来到第二个 if else
语句,显然会走 else
语句。我们分段来分析 else
语句的代码:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// ...
} 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 ' +
'<p>, or missing <tbody>. 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)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// ...
}
}
// ...
}
第一个 if
意思是如果 oldVnode
是一个 VNode
(非第一次执行 patch
)并且 oldVnode
和 vnode
是同一个 VNode
, 则给现有根节点打补丁。显然现在这里不会执行,走 else
逻辑。
进到 if (isRealElement)
逻辑,第一个 if
判断的是服务端渲染
,不会执行。下面的 if
由于传入的 hydrating
为 false
,因此也不执行。之后调用了 emptyNodeAt
函数,返回值赋给了 oldVnode
。emptyNodeAt
函数如下:
// src/core/vdom/patch.js
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
其实就是将真实 DOM
转换为 VNode
,另外这里把 oldVnode
作为第六个参数 elm
传进去,因此在下面的语句:
// src/core/vdom/patch.js
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
通过 oldVnode.elm
拿到原来的 oldVnode
赋给 oldElm
, 再通过 parentNode
拿到 oldElm
的父节点,对应到我们的例子就是 body
标签。
接着上面的else
语句往下看:
// src/core/vdom/patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// ...
} else {
// ...
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// ...
} else {
// ...
// 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)
)
// ...
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
这里调用了createElm
函数:
// src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
由于代码比较长,这里我们同样分段来分析。
第一个 if
语句显然不会执行,第二个 if
语句调用了 createComponent
函数,这个函数的功能是创建子组件,这个函数会在下一章详细介绍,现在只需要知道这里会返回 false
。接着往下看:
// src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
const data = vnode.data
const children = vnode.children
const tag = vnode.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
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// ...
} // ...
}
这里 if
语句的主要逻辑是在 tag
存在的情况下判断 tag
标签是否合法,如果是未知标签抛出警告。这个警告,在平时的开发中还是可能会经常遇到的:
之后就是创建真实 DOM
节点了,这里的 createElementNS
和 createElement
前面已经提及了,都是调用原生 js
方法来创建节点。继续往下:
// src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (isDef(tag)) {
// ...
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// weex相关
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} // ...
}
前面的if
语句是 weex
相关的,我们略过,直接看 else
语句。else
语句调用了 createChildren
来创建子节点:
// src/core/vdom/patch.js
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)))
}
}
这里判断如果 children
是个数组则遍历数组并递归调用 createElm
把所有虚拟子节点转换为真实 DOM
节点然后插入到父节点 vnode.elm
中;如果是一个文本 VNode
则直接 appendChild
插到 vnode.elm
里面。
回到 createElm
函数,调用完 createChildren
之后又调用了 invokeCreateHooks
函数:
// src/core/vdom/patch.js
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)
}
}
invokeCreateHooks
函数其实就是遍历 cbs.create
中的所有函数,然后把 vnode
push 到 insertedVnodeQueue
中。
下面接着调用了 insert
函数,把 vnode.elm
插入到父节点 parentElm
中:
// src/core/vdom/patch.js
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)
}
}
}
可以看到 insert
函数也是调用 nodeOps
中的操作 DOM
的方法来实现的。有参考节点 ref
就调用 insertBefore
插入到参考节点 ref
前,没有就插到父节点 parent
中。
回到 createElm
函数,我们看最后一段代码:
// src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (isDef(tag)) {
// ...
} 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)
}
}
if
针对的是 tag
的情况,我们已经分析完了。而 else if
针对的是创建注释节点
的情况,就直接创建注释节点并插入;else
针对的是文本节点
,逻辑也一样。
createElm
函数就分析完了,其实基本流程就是先创建当前节点 vnode.elm
,然后把 vnode.children
插到 vnode.elm
中,再把 vnode.elm
插到父节点 parentElm
中。回到 patch
方法:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// ...
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// ...
} else {
//...
// 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(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
接下来的if
判断的是 vnode.parent
,这是父占位节点
。
父占位节点
,是和组件相关的,这里不会执行,也就不展开细讲。
然后又判断之前定义的 parentElm
是否存在,有则删除掉 vm.$el
对应的节点。在执行这一步前,浏览器的 DOM
结构是这样的:
<body>
<div id="app"></div>
<div id="app">森林小哥哥</div>
</body>
之后删除<div id="app"></div>
完成新旧节点替换工作。