持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 17 天,点击查看活动详情
13. new Vue() 到 修改真实 DOM
start
虚拟 DOM 主要包含:
- 创建 vnode;
- 依据 vnode 修改现有的真实 DOM
修改真实 DOM
上一个章节,介绍了第一条创建不同类型的 vnode 的方式了。那么开始思考第二条,如何 依据 vnode 修改现有的真实 DOM。
修改真实 DOM,主要做了以下三件事:
- 创建新增的节点;
- 删除已经废弃的节点;
- 修改需要更新的节点;
vnode 和 oldVnode
在后续的源码阅读,可能会涉及到这两个变量,先来解释一下
- vnode: 根据最新的状态创建的最新的 vnode(新的);
- oldVnode: 上一次渲染 DOM 创建的 vnode(旧的);
我们修改真实 DOM,依据的是 最新的 vnode。而 oldVnode 主要是用于对比新旧 vnode 的差异。
从 new Vue() 到 修改真实 DOM
在学习 Vue 是如何 修改真实 DOM 之前,很有必要学习一下,从 new Vue()
到 修改真实 DOM
之间的过程。
1. new Vue()
- 之前在学习响应式的时候,我们知道
new Vue()
后,在this._init
中会初始化props methods data computed watch
; - 当然除此之外,还有其他的初始化逻辑,本节暂不拓展这里;
- 本节主要关注
$mount
方法
// \src\core\instance\init.js
// _init方法尾部,会有下列代码
if (vm.$options.el) {
// 如果元素存在,就开始挂载
vm.$mount(vm.$options.el)
}
2. $mount
$mount
英文释义:挂载。(可以理解把我们的组件挂载到页面上
)
$mount
的目的,主要是为了将模板渲染成真实的 DOM。
$mount
本身在很多文件都有定义,例如
src/platform/web/entry-runtime-with-compiler.js
src/platform/web/runtime/index.js
src/platform/weex/runtime/index.js
$mount
之所以有多处定义,是因为$mount
本身和平台,构建方式有关,所以会有很多版本与之对应。本次学习web平台,带编译版本的
$mount
。(因为这个版本常用)
web平台-带编译版本的$mount
// src\platform\web\entry-runtime-with-compiler.js
// 包含编译模板的js文件中的 $mount
// 首先存储一下 原型上的 mount()
const mount = Vue.prototype.$mount
// 定义一个新的 $mount 在新的 $mount 中再去调用旧的 $mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 元素存在然后 query
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
// options其实就是配置,但是配置可以访问到template !!!后续看到这个template在options如何初始化的,研究一下!
const options = this.$options
// resolve template/el and convert to render function
// 解析模板/el并转换为渲染函数
// 没有渲染函数我们再走后续逻辑
if (!options.render) {
// 将模板编译成渲染函数,并且赋值给 options.render
let template = options.template
/* 这里其实就是对 template为字符串且开头是# template为DOM元素的情况进行了处理 */
if (template) {
//1. 如果模板是字符串
if (typeof template === 'string') {
// 首字母是 # ; charAt() 方法从一个字符串中返回指定的字符。
if (template.charAt(0) === '#') {
// 通过这个template名称,获取缓存中的模板
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
// template是字符串而且开头不是 # ,正常的template直接使用即可。
// 2.判断是不是DOM元素
} else if (template.nodeType) {
// 直接拿到DOM元素的innerHTML作为模板
template = template.innerHTML
} else {
// 不是字符串也不是DOM元素, template会报错. 然后退出
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 没有模板,有元素,返回的是 DOM元素的 html 字符串
template = getOuterHTML(el)
}
/* 这里才是正式的渲染逻辑!!! 当然没有template就不渲染 */
if (template) {
/* istanbul ignore if */
// 性能标记
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 核心逻辑 compileToFunctions 编译成函数 `\src\compiler\to-function.js`
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
)
options.render = render
options.staticRenderFns = staticRenderFns
// 此时 options.render 就有渲染函数啦!
/* istanbul ignore if */
// 性能截止
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 执行原本真实的挂载。 (这个地方我们可以发现,包装后的挂载函数$mount,其实就是判断了一下是否有渲染函数,没有才会走 template 。)
// 所以只要我们提供了 render函数 ,template非必须
return mount.call(this, el, hydrating)
}
我梳理一下上述示例代码的逻辑。
首先上述代码版本是, web 平台-带编译版本的$mount
它主要做了这么几个事情:
-
保存了 Vue 原型上原本的
$mount
; -
限制了 Vue实例 不能挂载在 body、html 这样的根节点上;
-
如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法
vue2 中所有的渲染操作,都需要借助 render 方法 ;
compileToFunctions 是我们后面要学习的模板编译,后续继续学习。
-
调用 Vue 原型上原本的
$mount
;
原本的 $mount
// src/platform/web/runtime/index.js
// 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
。
这里为什么会有 原本的
$mount
的说法。其实是因为可以复用的操作,放在了 原本的
$mount
,需要额外拓展的操作,再在这个的基础上编写代码。就导致了会有多个
$mount
。
mountComponent
// \src\core\instance\lifecycle.js
// 挂载组件
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 处理下没有 render 函数的报错;
if (!vm.$options.render) {
// 为了防止出错,默认创建一个注释节点
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 触发 beforeMount 钩子
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
// 性能标记
// 定义了 updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// 开发环境添加了很多性能标记
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 我们将其设置为vm。_watcher在watcher的构造函数中
// 因为观察者的初始补丁可能会调用$forceUpdate(例如inside child
// 组件挂载的钩子),它依赖于vm。_watcher已经定义
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
/* 所以挂载的核心逻辑,其实就是这个 new Watcher 中的 vm._update(vm._render(), hydrating);*/
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
// 调用挂载在self上的实例
// 挂载被调用为渲染创建的子组件在其插入的钩子
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent
做了哪些操作呢?
-
做了一些异常情况的处理;
-
触发了生命周期钩子;
-
new Watcher()
这里的 Watcher 是渲染 Watcher。 之前学习 Watcher 的时候也有了解到这里。
- 首次初始化的时候,会执行
updateComponent
- 当实例状态改变的时候,本质也会执行
updateComponent
- 首次初始化的时候,会执行
-
实际执行的是
vm._update(vm._render(), hydrating)
vm 其实就是 Vue实例
_render
// \src\core\instance\render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// 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 {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
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' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
整个 _render
函数的逻辑看下来,主要作用:利用 vm.$options.render
,初始化最新的 vnode。
// 核心逻辑
vnode = render.call(vm._renderProxy, vm.$createElement);
// 这个地方其实就是依靠 runder 初始化 vnode, 由于涉及到的编译的部分逻辑,后续编译的模块仔细研究。这里暂时略过。
_update
// \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.
}
上述代码整体逻辑:
- 存储旧的vnode
- 开始
vm.__patch__()
通过 _render
生成最新的 vnode。
通过 _update
开始进入有关操作真实 DOM 的逻辑 vm.__patch__
。
end
-
回顾一下虚拟 DOM 的主要内容:
- 创建新的 vnode
- 修改真实 DOM
-
梳理了从
new Vue()
到 修改真实 DOM 的前置逻辑。