# Vue实例挂载的实现
入口的路径为:
1、 src/platforms/web/entry-runtime-with-compiler.js
,运行时+编译版本的打包入口,重写了$mount
=>
2、 src/platforms/web/runtime/index.js
,Vue入口,对Vue的扩展=>
3、 src/core/index.js
,对Vue初始化,initGlobalAPI(src/core/global-api/index.js,一些全局API)=>
4、 src/core/instance/index.js (opens new window),Vue的定义文件
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
this instanceof Vue
如果不是用new
创建Vue实例的话,this
会指向上下文,放在全局就是window
,会触发警告。
然后会调用 _init (opens new window) 方法,该方法在src/core/instance/init.js
中,主要做了初始化生命周期、初始化事件、初始化渲染等。
_init
方法主要最后调用了$mount
方法,compiler
版本的 $mount (opens new window) 定义在src/platforms/web/entry-runtime-with-compiler.js
中。
先缓存了原型上的$mount
,然后判断如果没有定义render
方法的话,就把el
或者template
转为render
,这个转化的过程是调用compileToFunctions
方法实现的。
最后调用原先原型上的$mount
方法实现挂载。
原先原型上的 $mount (opens new window) 方法在src/platforms/web/runtime/index.js中。
$mount
方法实际上会去调用 mountComponent (opens new window) 方法,这个方法定义在src/core/instance/lifecycle.js
文件中。
mountComponent
核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent
方法,在此方法中调用 vm._render
方法先生成虚拟 Node
,最终调用 vm._update
更新 DOM。
Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm
实例中的监测的数据发生变化的时候执行回调函数。
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
这里注意 vm.$vnode
表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
# render
Vue 的 _render (opens new window) 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js
文件中。
这段代码最关键的是 render (opens new window) 方法的调用,在 Vue 的官方文档中介绍了 render 函数的第一个参数是 createElement,那么结合之前的例子:
<div id="app">
{{ message }}
</div>
相当于我们编写如下 render 函数:
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
再回到 _render
函数中的 render
方法的调用:
vnode = render.call(vm._renderProxy, vm.$createElement)
可以看到,render
函数中的 createElement
方法就是 vm.$createElement
方法。
vm.$createElement
方法定义是在执行 initRender
方法的时候,可以看到除了 vm.$createElement
方法,还有一个 vm._c 方法,它是被模板编译成的 render
函数使用,而 vm.$createElement
是用户手写 render
方法使用的, 这俩个方法支持的参数相同,并且内部都调用了 createElement
方法。
# createElement
Vue.js
利用 createElement (opens new window) 方法创建 VNode,它定义在 src/core/vdom/create-element.js
中:
createElement
方法实际上是对 _createElement (opens new window) 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode
的函数 _createElement
主要分析下children
的规范化以及VNode
的创建。
# children
的规范化
由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。_createElement 接收的第 4 个参数 children 是任意类型的,因此我们需要把它们规范成 VNode 类型。
这里根据 normalizationType 的不同,调用了 normalizeChildren(children) (opens new window) 和 simpleNormalizeChildren(children) (opens new window) 方法,它们的定义都在 src/core/vdom/helpers/normalzie-children.js 中。
simpleNormalizeChildren 方法调用场景是 render 函数是编译生成的。理论上编译生成的 children 都已经是 VNode 类型的,但这里有一个例外,就是 functional component 函数式组件返回的是一个数组而不是一个根节点,所以会通过 Array.prototype.concat 方法把整个 children 数组打平,让它的深度只有一层。
normalizeChildren 方法的调用场景有 2 种,一个场景是 render 函数是用户手写的,当 children 只有一个节点的时候,Vue.js 从接口层面允许用户把 children 写成基础类型用来创建单个简单的文本节点,这种情况会调用 createTextVNode 创建一个文本节点的 VNode;另一个场景是当编译 slot、v-for 的时候会产生嵌套数组的情况,会调用 normalizeArrayChildren (opens new window) 方法,接下来看一下它的实现。
normalizeArrayChildren 接收 2 个参数,children 表示要规范的子节点,nestedIndex 表示嵌套的索引,因为单个 child 可能是一个数组类型。 normalizeArrayChildren 主要的逻辑就是遍历 children,获得单个节点 c,然后对 c 的类型判断,如果是一个数组类型,则递归调用 normalizeArrayChildren; 如果是基础类型,则通过 createTextVNode 方法转换成 VNode 类型;否则就已经是 VNode 类型了,如果 children 是一个列表并且列表还存在嵌套的情况,则根据 nestedIndex 去更新它的 key。这里需要注意一点,在遍历的过程中,对这 3 种情况都做了如下处理:如果存在两个连续的 text 节点,会把它们合并成一个 text 节点。
经过对 children 的规范化,children 变成了一个类型为 VNode 的 Array。
# VNode
的创建
这里先对 tag 做判断,如果是 string 类型,则接着判断如果是内置的一些节点,则直接创建一个普通 VNode,如果是为已注册的组件名,则通过 createComponent 创建一个组件类型的 VNode,否则创建一个未知的标签的 VNode。 如果是 tag 一个 Component 类型,则直接调用 createComponent 创建一个组件类型的 VNode 节点,本质上它还是返回了一个 VNode
回到 mountComponent 函数的过程,我们已经知道 vm._render 是如何创建了一个 VNode,接下来就是要把这个 VNode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的
# update
Vue 的 _update (opens new window) 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候。_update
方法的作用是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js
中:
_update 的核心就是调用 vm.__patch__
方法,这个方法实际上在不同的平台,比如 web 和 weex 上的定义是不一样的,因此在 web 平台中它的定义在 src/platforms/web/runtime/index.js
中:
Vue.prototype.__patch__ = inBrowser ? patch : noop
可以看到,甚至在 web 平台上,是否是服务端渲染也会对这个方法产生影响。因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数,而在浏览器端渲染中,它指向了 patch (opens new window) 方法,它的定义在 src/platforms/web/runtime/patch.js
中
该方法的定义是调用 createPatchFunction
方法的返回值,这里传入了一个对象,包含 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现,来看一下 createPatchFunction (opens new window) 的实现,它定义在 src/core/vdom/patch.js
中
createPatchFunction
内部定义了一系列的辅助方法,最终返回了一个 patch (opens new window) 方法,这个方法就赋值给了 vm._update
函数里调用的 vm.__patch__
。
patch
方法,它接收 4个参数,oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象;vnode 表示执行 _render 后返回的 VNode 的节点;hydrating 表示是否是服务端渲染;removeOnly 是给 transition-group 用的,之后会介绍。
patch 的逻辑看上去相对复杂,因为它有着非常多的分支逻辑,为了方便理解,我们并不会在这里介绍所有的逻辑,仅会针对我们之前的例子分析它的执行逻辑。之后我们对其它场景做源码分析的时候会再次回顾 patch
方法
先来回顾我们的例子:
var app = new Vue({
el: '#app',
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data: {
message: 'Hello Vue!'
}
})
然后我们在 vm._update 的方法里是这么调用 patch 方法的:
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
结合我们的例子,我们的场景是首次渲染,所以在执行 patch
函数的时候,传入的 vm.$el
对应的是例子中 id 为 app 的 DOM 对象,这个也就是我们在 index.html
模板中写的<div id="app">
, vm.$el
的赋值是在之前 mountComponent
函数做的,vnode 对应的是调用 render
函数的返回值,hydrating
在非服务端渲染情况下为 false
,removeOnly
为 false
。
确定了这些入参后,我们回到 patch 函数的执行过程,看几个关键步骤。
由于我们传入的 oldVnode 实际上是一个 DOM container,所以 isRealElement 为 true,接下来又通过 emptyNodeAt 方法把 oldVnode 转换成 VNode 对象,然后再调用 createElm (opens new window) 方法,这个方法在这里非常重要,来看一下它的实现:
createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。 我们来看一下它的一些关键逻辑,createComponent 方法目的是尝试创建子组件,这个逻辑在之后组件的章节会详细介绍,在当前这个 case 下它的返回值为 false;接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签;然后再去调用平台 DOM 的操作去创建一个占位符元素。
← 异步更新 Webpack打包机制 →