Vue DOM挂载
Vue如何渲染DOM (第二章)
当我们new Vue时发生了什么?
接下来我们就来探究Vue
是如何将该页面进行渲染的。在研究之前,我们必须了解Vue
构造函数。但是在执行Vue
函数前,当我们import Vue from 'vue'
的时候,该框架做了这样的几个初始化:
initMixin(Vue)
//传入Vue,向其原型上添加_init函数
stateMixin(Vue)
//传入Vue,向其原型上添加 $set,$delete,$watch函数 并做其他初始化定义
eventsMixin(Vue)
//传入Vue,向其原型上添加 $on,$once,$off,$emit函数,并做其他初始化定义
lifecycleMixin(Vue)
//传入Vue,向其原型上添加 _update,$forceUpdate,$destroy函数,并做其他初始化定义
renderMixin(Vue)
//renderMixin函数向Vue原型添加 $nextTick,_render,在此之前执行了installRenderHelpers(Vue.prototype) 这个函数的执行也向Vue,.prototype上添加了很多内置的函数
//以上的5个函数的作用是向Vue.prototype上添加属性和方法
//对Vue进行包装
export default Vue
//当我们new Vue({})时就进入了上面定义的Vue函数了
其实上面的函数是在我们导入Vue
时,该框架本身做的一些初始化,这和我们执行Vue
构造函数时内部的初始化不一样。这些函数主要是在我们使用Vue
前,向Vue
构造函数的原型上添加一些函数。而当我们执行Vue
构造函数时,也会在内部进行一些初始化,例如响应式的完成和数据代理。所以我们需要区分这两种初始化。
在我们import Vue from 'vue'的时候,主要做的是向
Vue
构造函数的原型上添加一些函数或属性。
然后我们就可以执行Vue
构造函数了。当我们执行Vue
构造函数的时候,本质上其实就是执行Vue.prototype._init()
方法。
function Vue (options) {
//options是我们传入的配置项
//这个就是大名鼎鼎的vue构造函数,所有的vue项目的开始的地方
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
//这里的this 其实使 vm实例,看是否用了new Vue
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
//这里的this是我们执行new Vue 时构造函数内部生成的实例对象,也可以理解为vm也就是组件的实例对象,这里是根组件的实例对象
this._init(options)
//从这个函数进入我们用initMixin(Vue)初始化添加的_init函数
}
这个this._init()
是在initMixin()
函数中声明的。this._init()
函数中做了很多事情,比如合并options
,执行一些函数:
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')
不过这些暂时不是我们关注的重点,重点是后面的代码,即关于渲染的代码。不过需要注意的是,这里的渲染指的是Vue
已经完成了绝大部分事情,只剩下将浏览器上的视图重新渲染。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
如果我们在vue
构造函数的配置项中传入了el
参数,那么将会执行vm.$mount
函数,而传入的参数是vm,$options.el
。也即是我们的#app
字符串。
vm.$mount
vm.$mount
的挂载其实是在./src/platforms/web/entry-runtime-with-complier.js
当中,如果我们只在./src/core/
的文件中其实是找不到的。vue
的编译一共有两个版本:第一是runtime-only
版本,另一个就是runtime-complier
。在这里我们需要说明的是,什么是运行时+编译,其实当我们写如下代码:
var vm = new Vue({
el:"#app",
data(){
return{
message:123
}
}
})
Vue
在进行模板渲染的时候其实是有一个对#app
进行编译的一个过程,会将模板编译成一个render函数。从某种意义上来讲其实是对性能的一种消耗。假如我们写这样的一段代码:
var vm = new Vue({
el:'#app',
data(){
return{
message:123
}
},
render:h(createElement)=>{
return createElement({
tag:'div'
})
}
})
此时我们添加了一项render
。这个函数的作用是返回一个由createElement
函数生成的一个vdom
。用来代替我们的el
。这样有什么好处呢。好处就是Vue
不需要把模板编译成render函数
。因为我们直接就提供给Vue
其render函数
。这样就省去了模板 ---> render函数
的编译时间。对于runtime + complier
版本的根组件我们不论写的是render/template
,我们必须有一个前提,那就是我们需要写el。原因在于Vue需知道要我们最后的模板渲染到文档的那个位置,所以必须有el,但是对于组件来说,可能就不需要了。
接下来我们来详细的讲解vm.$mount
这个方法。该方法代码如下:
Vue.prototype.$mount = function (
el?: string | Element,//我们传入的el类型有两种,一种是字符串'#app',另一种是一个元素对象,例如document.getElementById('app')
hydrating?: boolean
): Component {
el = el && query(el)//query函数主要是返回一个元素对象,如果我们传入的el存在,那么就返回该元素的对象形式(也就是真正的元素节点),如果不存在,那么就会默认是一个div元素对象
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
//这里告诉我们el不能是body和html。原因是它会发生覆盖,这样就会将原来的模板完全覆盖掉。
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options//这里的this指向的是vm实例对象
// resolve template/el and convert to render function
if (!options.render) {
//如果我们没有render函数。那么会进入该区域代码。
let template = options.template //获取模板
if (template) {
//如果存在template配置项,
if (typeof template === 'string') {//如果配置项的类型为字符串。
if (template.charAt(0) === '#') {//这里我们只处理template为#xxx的格式的模板,也就是类似于template:'#app'这种
template = idToTemplate(template)//该函数返回的是template模板内部的节点的字符串形式。
/* istanbul ignore if */
//这里是template的错误处理
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
//如果我们传入的template是一个节点对象,那么获取该节点对象中的innerHTML,然会的也是字符串形式
template = template.innerHTML
} else {
//不是以上两种格式,那么抛出错误
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
//如果template配置项不存在,那么获取el.outerHTML当作我们的template。返回的也是字符串类型
template = getOuterHTML(el)
}
if (template) {
//这是处理好的template。这种template的来源有两种,第一种是我们自己设置的,另一种就是el.outerHTML来充当template
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
//接下来的代码是进行编译,将我们的模板编译成以js描述的对象,即虚拟DOM,然后将虚拟DOM转化为render(渲染函数)。
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
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
//如果我们没有render函数,其实通过对template进行编译,我们就会获得render函数,
//然后调用mount.call()函数。
//如果我们自己有render函数,那么我们就可以直接调用mount.call函数,不需要去进行编译。
return mount.call(this, el, hydrating)
}
从上面的代码解析中可以看到,我们最终执行的其实就是render
函数,只不过此时的渲染函数的来源有两种:其一是我们自定义的render
函数。其二就是通过模板编译然后生成render
函数。
在我们通过模板编译的时候,Vue
的编译顺序是这样的:
首先如果我们定义了template配置项,那么Vue就不会使用el指定的元素作为模板去编译,而是使用template中的模板去编译。如果我们没有指定template,那么显而易见,Vue只能去使用el指定的模板去编译进而生成render函数。使用template编译有一个前提,那么就是我们没自定义rendere函数。如果我们自义了render函数,那么Vue就用我们定义的,进而不去编译生成render函数了。
总结:Vue首先会看我们有没有自定义render函数,有的话就使用我们自定义的作为最后的render函数。如果没有那么查看我们有没有定义template配置项模板,有的话就使用template进行模板编译然后把生成的render函数作为我们最后使用的render函数。假如我们没有定义template,那么此时Vue只能使用el指定的元素模板去编译,然后把其编译生成的render函数作为我们之后的render函数。懂?在这里需要注意一下,render
函数是一个函数,它的作用是生成虚拟DOM。将生成的render函数挂载到options.render上。
我们会发现,我们最后一定会执行mount.call
函数。那么我们来讲解该函数。
vm.$mount
该函数位于/web/runtime/index.js
当中。其实当我们使用vm.$mount
的时候,并不是直接调用/web/runtime/index.js
中的mount
函数的,而是调用的是上面Vue原型上定义的mount
函数,但是最终函数调用了位于/web/runtime/index.js
中的原始定义的mount
函数。
首先我们来看参数:mount.call(this, el, hydrating)//this -> vm ; el -> 元素对象;hydrating -> 布尔值(和服务端渲染有关,我们可以认为是false)
。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined//这里之所以对再一次的对el进行判断,是因为这里的$mount是runtime-only版本的,所以对你传入的el进行判断。
return mountComponent(this, el, hydrating)
}
我们发现,vm.$mount
函数需要调用mountComponent()
函数。该函数位于./src/core/instance/lifecycle.js
当中。接下来我们讲解mountComponent()
函数。
mountComponent
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el//将el元素对象挂载到vm.$el上,也就是说,vm.$el是在执行$mount的时候挂载上去的。宏观的讲,它是在created钩子函数之后,beforeMount钩子函数之前被挂载的。
if (!vm.$options.render) {//这里的vm.$options.render是处理之后的render函数,也就是说,如果我们如果不传入render函数或者编译后的虚拟DOM无法生成render函数,那么vm.$options.render都为false
vm.$options.render = createEmptyVNode//如果在上述中为真,那么我们就给vm.$options.render赋值一个由空虚拟DOM组成的渲染函数。
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
//这里的警告就是你用了runtime-only,但是你写了template/el那么就会报错,它只能接收render函数。这是版本问题
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
)
}
}
}
//触发berforeMount钩子函数
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
//这和运行性能有关,暂时可以忽视
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
......
} else {
//最终定义updateComponent函数
updateComponent = () => {
//vm._update是在lifecycleMixin(Vue)中定义的
//vm._render是在renderMixin中定义的。
//hydrating:false
//该函数的执行其实是在new Watcher()中执行的,我们暂时只关注它的执行,不去关注在什么地方触发。
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
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
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
函数目前做了什么。
- 将
vm.$el
进行赋值,并触发beforeMount
钩子函数。 - 定义
updateComponent
函数。 - 执行
new Watcher()
构造函数,在该构造函数中执行updateComponent()
函数。
这只是目前我们要探究的mountComponent
函数做的事情,其后面的代码稍后讲解。
当执行到new Watcher
的时候,它的内部首先会为我们的这个组件实例创建一个watcher
实例对象。然后将这个实例对象通过内部的get
函数挂载到全局变量Dep.target
上。这一点很重要,因为它是依赖收集的关键。接着就是执行updateComponent
函数。
vm._render
当我们执行updateComponent()
函数的时候,会先执行vm._render
函数,接下来我们具体讲解该函数。
该函数位于/instance/render.js
中。因为vm._render
函数内部的实现很复杂,所以我们就讲述和渲染有关的代码。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options//从vm.$options中拿到render函数。
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
//vm._renderProxy在生产环境下其实就是vm。通过调用render函数来生产vnode。
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
}
首先const { render, _parentVnode } = vm.$options//从vm.$options中拿到render函数。
。然后
vnode = render.call(vm._renderProxy, vm.$createElement)
。vm._renderProxy
在生产环境下其实就是vm。通过调用render函数来生产vnode。接下来我们来看vm.$createElement
函数。
vm.$createElement
该函数在/instance/render.js
中的initRender
函数当中。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
我们会发现,这里有两个函数vm._c/vm.$createElement
。这两个函数的作用是不同的,vm._c是编译后生成的render函数所执行的函数。而vm.$createElement是提供给我们手写render函数执行的函数。它们两个都会调用createELement
函数。用该函数来生成虚拟DOM。
回到我们的render
函数上来。也就是说,我们最后会通过render
函数来获取到vDOM
。
createElement
我们会发现,无论是我们写的渲染函数,还是内部自己生成的渲染函数,最终底层的代码还是会调用同一个函数,那就是createElement
。该代码如下:
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
代码中的两个if
其实是对参数不一致的一种处理,但最终我们调用的是_createElement
函数。
_createElement
该函数有很多的逻辑结构,我们只讲解目前对我们有用的部分。
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
//............
if (normalizationType === ALWAYS_NORMALIZE) {
//normalizeChildren函数作用如下:
//其实该函数的最终目的也是将我们的多维嵌套数组变成以为数组。也就是说我们返回后的children一定是一个一维的包含子节点对象的一个数组。
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
//simpleNormalizeChildren作用如下:
//将我们的数组拍平,但是它默认你只有两层数组的嵌套,没有其他的嵌套。[[a],[b]] ----> [a,b]
children = simpleNormalizeChildren(children)
}
//从这里开始,创建vNode。
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 假如这里是我们原生的标签,例如div,li,span等等,那么我们就执行后续代码
...........
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
...........
}
VNode
vNode
在Vue
中单独有一个类进行实例化,该类位于/src/core/vdom/vnode.js
。该类结构具体如下:
export default class VNode {
.......
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
}
这里有很多参数,其实我们真正用的上的不多。例如:tag/data/children/text/elm/context
。他返回的是一个对象,该对象的结构类似于:
vNode = {
tag:'div',
children:[],
....
}
用js
对象描述的节点。可能会产生疑惑,为什么我们不直接创建一个节点,而去用js
来描述进而创建一个虚拟节点,原因在于如果我们去创建一个节点,其实浏览器会为该节点添加很多很多的属性。这样开销是很大的。此时我们用虚拟节点来暂时代替,会节约很多资源。
其实_createElement()
函数在目前主要做了两件事。第一:将我们的children
进行拍平。第二就是创建我们的vnode
节点。其实创建之后的节点大致是这样的:
vNode = {
tag:'div',
attrs:{
id:'app'
},
children:undefined
}
这就是节点的javascript
的对象描述形式。
回到正题,我们以runtime+complier
为例,当内部调用vm._c()
函数的时候,此时会返回我们的vDOM
。此时VUE
就会通过该vDOM
来创建真正的节点进而挂载到真实的DOM
上去。
总结:vnode生成历程
vm._render --> render.call() --> vm.$createElement/vm._c --> _createElement --> new VNode() ==> 生成Vnode
。
当我们回头看vnode
的创建过程,我们会发现,除了render
函数我们可以影响外,其他的函数都是Vue
内部定义好的,我们无法调用它们或者修改它们。但正是因为我们可以去影响render
函数,让它变得异常的重要。如果生成不了这个函数,我们就无法去生成vnode
。有的同学可能会说,那没有后面的函数也生成不了啊,的确是这样的,但是我们要清楚,后面的这些函数是尤雨溪写好的,所以它们本身就存在。但是我们是可以影响render
函数的,所以它的存在就依靠我们写的vue
代码。
自定义render
函数:
render(createElement){
return createElement('div',{attrs:{id:'app'}})
}
vm.options.render = render
通过编译生成render
函数:
const code = 'with(this){return _c("div",{attrs:{id:"app"}})}'
const render = new Function(code)
vm.options.render = render
具体编译的时候做了什么进而生成的渲染函数,此时就不是我们讨论的范围了。
通过以上两种方法去生成render
函数后,Vue
就可以通过上面的流程来创建出模板的vnode
。
vm._update
vm._update
的调用有两个地方。第一是我们首次渲染的时候,需要用vm._update
把生成的vDOM
变成真正的DOM
节点。第二个就是当我们的数据改变的时候,我们需要使用vm._update
来进行视图的更新。其实调用vm._update
的根本目的是创建节点渲染视图。我们在什么时候调用的vm._update
?它的代码如下:vm._update(vm._render(), hydrating)
,代码是vm.$mount
调用updateComponent
函数的时候执行的。vm._render()
返回的是我们的vnode
。接下来我们具体分析vm._update
方法:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this//这里是vm实例
const prevEl = vm.$el
const prevVnode = vm._vnode//undefined
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 */)
//首次渲染地时候vm.$el是真实地DOM节点对象 vnode是渲染后生成地虚拟DOM
} 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.
}
因为vm._update
函数在两种情况下去调用它,所以该函数内部其实是对两种情况的一种综合处理。
当第一次调用vm._update
函数的时候,vm.vnode
为undefined
。原因是我们第一次调用时还没有挂载。所以prevVnode
为undefined
。然后执行vm._vnode = vnode
。因为prevVnode
为undefined
。所以执行
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
//vm.$el 是真实的DOM
//vnode 是虚拟DOM
//hydrating: false
//false
vm.patch
该函数位于./platforms/web/runtime/index.js
中。
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch()
export const patch: Function = createPatchFunction({ nodeOps, modules })
//nodeOps是操作DOM的原生API
//modules是class或者说是属性的钩子函数
createPatchFunction
该函数定义了很多辅助函数,不过,该函数最终返回的是它内部最后定义的patch
函数。也就是说,当调用vm.__patch__
函数的时候,最终我们调用的是createPatchFunction
函数中最后定义的patch
函数。
patch
return
function patch (oldVnode, vnode, hydrating, removeOnly){
....
}
//oldVnode:是真实地DOM
//vnode:虚拟DOM(或是我们改变数据后重新生成地虚拟DOM)
//false
patch
函数地调用也分为两种情况。第一种是当我们首次渲染DOM
的时候。第二种情况就是当我们去修改数据的时候Vue
需要更新我们的视图的时候去调用patch
方法。
当我们第一次渲染DOM
的时候,oldVnode
是vm.$el
。是一个真实的DOM
。vnode
是我们自定义或者通过编译生成的虚拟DOM
。当我们首次执行patch
函数的时候,它会执行如下代码:
oldVnode = emptyNodeAt(oldVnode)
emptyNodeAt
函数的作用是将真实的DOM
转化为虚拟DOM
。其实为什么要这样做,理由是可以想明白的。如果我们修改了数据,然后重新进行渲染。在渲染前Vue
会将每一个节点通过diff
算法进行对比,目的是为了减少操作DOM
的次数,但是在对比节点的时候不是通过真实DOM
来进行比较的,而是通过原有的虚拟DOM
和修改后的数据的虚拟DOM
进行比较,此时我们就需要将我们的真实的vm.$el
转化为我们的虚拟DOM。
当我们上述地数据准备好之后,开始执行接下来的代码:
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
createElm
是一个很神奇的函数,就是它将我们渲染后得到的虚拟DOM
变成真实的节点然后挂载到我们文档的大DOM
上的。
createElm
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
)
}
}
//nodeOps是一个原生DOM API的一个封装对象,nodeOps.createElement(tag)其实就是document.createElement(tag)。
//vnode.elm此时就是通过nodeOps.createElement()函数创建的真实节点对象。
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
//WEEX端的渲染
//...........
} else {
//
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
//.......
} else if (isTrue(vnode.isComment)) {
//.......
} else {
//.......
}
}
该函数传入的第一个参数是vnode
。其实该参数准确的来书为父节点,为什么这么说呢。当我们第一次调用createElm
函数的时候,那么此时我们的父节点其实就是根节点。但是后面我们还有子节点的创建。
当我们创建完成的时候,此时我们就应该考虑创建子节点了。于是代码就会执行到此处:
if(__WEEX__){
//.......
}else {
//
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
首先我们来看看createChildren()
函数。
createChildren
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
//判断我们的children是否是一个数组,如果不是,那么其子节点要么为空,要么就是文本节点
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
//这个就是深度遍历并创建节点。此时相当于把我们传入的children[i]当作了我们的根节点来处理。层层递进。它会为我们的每一个节点都调用createElm方法,进而进行一些其他属性的添加或者其他操作。
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
//这种情况就是我们的子节点是一个文本,此时我们调用appendChildren()函数将文本添加到父节点下
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
该函数逻辑也比较容易理解。首先会判断我们的children
是否是一个数组,如果是,说明该节点有子节点,那么我们就创建子节点。它是怎么创建的呢?它是深度遍历优先。通过循环,我们获取到children[i]
这个子节点,然后调用createElm()
函数来创建节点,当我们创建完了,内部又会调用createChildren()
来常见子节点的子节点。然后将子节点的子节点appendChild
到子节点上。就是这样每一层每一层遍历,然后创建出一个完整的基于vnode.elm
的一个小的且真实的DOM树
。
当我们创建完成后。执行insert(parentElm, vnode.elm, refElm)
语句。
insert
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)
}
}
}
传入的三个参数:
-
parentElm
:是我们根节点的父节点,例如:<ul> <div id="app"> {{name}} </div> </ul>
此时
<ul>
就是parentElm
。因为毕竟我们要将创建好的DOM
挂载到我们的文档上去的,所以要找到我们的根节点的文档父节点。 -
vnode.elm
:是我们的根节点。但是要记住,该根节点包括它的子节点及其子节点的所有节点。 -
refel
:参考节点。
其实就是将我们生成后的DOM
挂载到我们的文档上来,实现视图的渲染。
其实有一点很值得我们注意,那就是调用insert
函数的时候的节点插入,我们会发现我们是先调用createChildren()
然后在调用insert()
函数,那就说明我们是先从树的最末端进行插入,倒数第一层插入到倒数第二层,倒数第二层插入到倒数第三层。然后以此类推就生成了一个完整的真实的DOM树
。最后才会将我们的DOM树
添加到文档父节点中。
总结:执行过程
我们上述的执行过程其实可以简化为:
new Vue() --> init --> $mount --> complier --> render --> vnode --> patch --> DOM
。
我们其实可以把Vue
渲染DOM
划分为三个部分:数据处理
,生成虚拟DOM
和 生成节点并挂载
。
转载自:https://juejin.cn/post/7087540171754700836