之前提到new Vue的过程中,会进行一系列的初始化,并执行挂载函数mount切入,了解一下Dom的的渲染及更新机制。
Vue的$mount挂载函数都做了什么?
$mount函数的定义:
src/platforms/web/runtime/index.js
import { mountComponent } from 'core/instance/lifecycle'
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
$mount的函数其实是返回了mountComponent函数
src/core/instance/lifecycle
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
***
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
***
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent函数主要干了2件事
1.如果没有render函数,那么给一个render函数
2.初始化watch函数,运用观察者模式,当被观察的数据发生变化时,触发_update函数更新dom
另外,在runtime+compiler版本,vue会重载$mount,里面会将tmplate,和el的写法都转为render函数。而runtime-only版本不会。具体看
src/platforms/web/entry-runtime-with-compiler.js
src/platforms/web/entry-runtime.js
那么可以看看Watch这个类都做了什么
Watch做了什么?
src/core/observer/watcher.js
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: ISet;
newDepIds: ISet;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
vm._watcher = new Watcher(vm, updateComponent, noop)
其实就是传递了vm Vue本身和updateComponent函数
当观察到变化时执行updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
最终也就是执行了_update函数,并将_render执行结果传入
_render做了什么?
src/core/instance/render.js
export function initRender (vm: Component) {
***
vm._vnode = null // the root of the child tree
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
***
}
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (vm._isMounted) {
// if the parent didn't update, the slot nodes will be the ones from
// last render. They need to be cloned to ensure "freshness" for this render.
for (const key in vm.$slots) {
const slot = vm.$slots[key]
if (slot._rendered) {
vm.$slots[key] = cloneVNodes(slot, true /* deep */)
}
}
}
vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
***
} else {
vnode = vm._vnode
}
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
***
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
将代码提炼一下
const { render, _parentVnode } = vm.$options
***
vnode = render.call(vm._renderProxy, vm.$createElement)
***
return vnode
可以看到_render函数主要的逻辑就是通过render函数创建vNode,并返回。
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
创建vNode就是通过createElement这个方法
另外
new Vue({
render: (h) => h(App),
}).$mount("#app");
render函数里的h函数也就是createElement
/src/core/vdom/create-element.js createElement暂时不深入看,主要就是创建vnode。
_update做了什么?
src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = 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 */,
vm.$options._parentElm,
vm.$options._refElm
)
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
***
}
其中核心的逻辑就是
vm.$el = vm.__patch__(prevVnode, vnode)
src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
在浏览器环境运行时:
可以看到_update和核心代码就是执行了createPatchFunction方法
patch->=createPatchFunction做了什么?
src/platforms/web/runtime/patch.js
```js
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 })
createPatchFunction接受一个对象里面有nodeOps,modules
nodeOps里面就是封装了一些操作原生dom的函数方法
modules里面包含了各种模块的一些钩子方法
那再看看createPatchFunction的具体实现
/src/core/vdom/patch.js
export const emptyNode = new VNode('', {}, [])
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
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, parentElm, refElm) {
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, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, 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)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
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)) {
// component root element replaced.
// update parent placeholder node element, recursively
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
}
}
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
简化一下patch的代码
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
***
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
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)
)
}
}
return vnode.elm
}
// 首次渲染
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
// 更新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
考虑首次渲染的逻辑,更新逻辑先不管
Patch主要调用了createElm和patchVnode
1.当没有旧的Vnode节点时会直接执行createElm,并且会遍历Vnode的children,去创建create子节点
2.如果之前有渲染过,那么就将新旧dom对比更新,逻辑复杂后面再分析。
3.首次渲染,我们传入了其实是一个Element节点,所以会通过emptyNodeAt将oldVnode转为Vnode对象,然后再执行createElm方法
所以最终核心逻辑执行createElm函数
看看createElm做了什么?
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
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)
}
}
简化代码可以稍微清晰的看见
1.先使用了createComponent去尝试创建组件节点,如果成功就直接返回了
2.会判断vnode.tag是否是合法标签
分三种情况
2.1 如果合法标签,调用createElement方法,创建一个空节点,以便插入子节点,然后调用createChildren方法遍历子节点。
2.2 如果是注释节点,就创建注释节点,并insert插入父节点
2.3 否则就是个文本节点,就创建text节点,并insert插入父节点
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text))
}
}
createChildren就是利用深度优先遍历算法对vnode的children进行遍历,children也是vnode,递归调用createElm并将vnode.elm作为父节点,将children从子到父的逐级挂载到父节点,并渲染到真实dom树上。
总结:
new Vue({
render: (h) => h(App),
}).$mount("#app");
当我们使用new Vue()执行$mount后总的来说vue做了以下工作
1.在new Vue()的时候,执行了_init()函数,初始化生命周期钩子,挂载事件到原型等等一些初始化工作
function Vue (options) {
this._init(options)
}
Vue.prototype._init = function (options?: Object) {
***
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
***
}
2.然后执行$mount()函数进行挂载,主要逻辑为:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
2.1.执行了mountComponent函数,去创建观察者去监听数据变化,并根据变化去改变dom树,其中主要的逻辑就是执行了_update函数,_update函数会在首次渲染执行一次,并在每次数据更新执行。
function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
return vm
}
2.2。 _update函数主要逻辑为:
2.2.1.首次渲染render函数生成的vnode传入_update函数,执行patch函数,创建新的dom树 2.2.2 数据更新的时候,执行patch函数时,会执行diff算法去更新dom树
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
if (!prevVnode) {
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
Vue.prototype.__patch__ = inBrowser ? patch : noop
export const patch: Function = createPatchFunction({ nodeOps, modules })
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}
}
}
也就是主要根据就vnode是否为真实dom,不是的话执行patchVnode比较新旧vnode,否则执行createElm函数
patchVnode 就是通过对比新旧虚拟dom,进行对应的更新
createElm 就是通过遍历整个vnode树,通过递归的方式,由子及到父级逐渐挂载,通过nodeOps封装的操作原生的api去更新到真实dom完成渲染.
到此dom渲染就结束了.
几个核心的逻辑线:
init() -> mount() -> Watch -> update -> render -> patch
render -> createElement
patch -> createElm / patchVnode