这是一个系列文章,请关注 vue@2.6.11 源码分析 专栏
vue@2.x中用到了虚拟DOM技术,基于第三方虚拟DOM库sanbbdom
修改。建议阅读本文之前对snabbdom
的使用和原理有一定的了解,可以参考 snabbdom@3.5.1 源码分析 专栏。
vue2中组件渲染的核心入口如下:
// src/core/instance/lifecycle.js
export function mountComponent (vm: Component, el: ?Element, hydrating?: boolean): Component {
vm.$el = el
//...
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
//...
return vm
}
其中vm._render
用来生成虚拟DOM树的。而vm._update
用来将上一步即vm._render
生成的虚拟DOM树经过patch
(打补丁)更新到界面上。
new Wacher(...)
用法在上一节数据驱动详细分析过。updateComponent
在首次创建Watcher
实例时会执行一次,当updateComponent
依赖的响应式数据变化时会再次执行。
因此上面new Watcher(vm, updateComponent,..)
方法中的两个操作_render() -> _update()
,相当于snabbdom的如下操作
- 初始化时类比
const container = document.getElementById("container");
const vnode = h(...); // 创建虚拟节点树
patch(container, vnode); // 同步虚拟DOM树同步到界面
- 响应式数据更新时类比
// 如果此时有数据变更引起界面变更
const newVnode = h(...); // 新的虚拟节点树
patch(vnode, newVnode); // 和上一次的虚拟节点树进行diff,将差异同步到界面上
这里的巧妙是new Watcher(...)
将两个步骤合并到一起。
下面我们重点看下vue@2.x中关于虚拟DOM的相关逻辑。主要逻辑在src/core/vdom文件夹中。
从入口讲起
patch方法是跨平台的,因此在编译入口处便做了区分,web平台下
运行时的编译入口在:src/platforms/web/runtime/index.js,此时就定义了__patch__
方法,然后在vm._update
会调用vm.__patch__
实现diff
能力
// src/platforms/web/runtime/index.js
import { patch } from './patch'
//...
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
//...
// src/platforms/web/runtime/patch.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
就相当于snabbdom
中init
方法,nodeOps
是因为跨平台的原因放在这里(私有化),这里重点关注modules
,在snabbdom
中说到module
会借助patch
过程中触发的各种钩子参与DOM的修改。这里都有哪些module呢,分为两类:基础module和跨平台module,如下:
可能会单独出一个小节分析这些module
vnode
vue@2.x中vnode在snabbdom定义的vnode基础上增加了很多其他的属性
核心定义如下,但是在其构造函数中还定义了很多其他属性,是为了支持SSR、函数组件、异步组件等场景的(不必特别关注)。
// interface for vnodes in update modules
declare type VNodeWithData = {
tag: string;
data: VNodeData;
children: ?Array<VNode>;
text: void;
elm: any;
ns: string | void;
context: Component;
key: string | number | void;
parent?: VNodeWithData;
componentOptions?: VNodeComponentOptions;
componentInstance?: Component;
isRootInsert: boolean;
};
上面定义的大多数属性和snabbdom保持一致,多出的和组件有关,如下
vnode属性 | 含义 |
---|---|
context | 父组件实例 |
parent | 父vnode(placeholder vnode) |
componentInstance | 当前组件实例 |
componentOptions | 父组件传递给当前组件实例的选项(属性,事件,孩子等等) |
_render:创建虚拟DOM树
我们先看下vm._render
方法的定义
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
//... slot相关,暂忽略
// 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) {
//... 异常处理
} finally {
currentRenderingInstance = null
}
//...
// set parent
vnode.parent = _parentVnode
return vnode
}
这里关注三个地方
render
函数的执行,render
函数长什么样子呢?<!-- 原始模板 --> <div id="app"> {{ message }} </div>
render来自哪里?// 编译后的render函数 (function anonymous() { with (this) { return _c('div', {attrs: {"id": "app"}}, [_v("\n " + _s(message) + "\n")]) } })
render
函数可以由开发者自己提供- 也提供了
编译 + 运行时
版本,即可有运行时编译,框架会自动处理将模板处理成render
函数 - 更为常见的是
.vue
单文件开发,vue-loader会将其自动将template
部分处理成render
函数
currentRenderingInstance
的设置- 关系链接
vm.$vnode = _parentVnode
,当前组件实例的$vnode
指向父vnode(即plcaeholder vnode)vnode.parent = _parentVnode
,这里的vnode是组件实际内容的根vnode- 这里返回的vnode,会在vm.update中被设置给vm:
vm._vnode = vnode
所以,组件实例和vnode两个类型的父子关系都建立了:
vm._vnode = vnode // Vue.prototype._update 中设置的
vm.$vnode = _parentVnode
vnode.parent = _parentVnode
// initLifecycle 中设置的
vm.$parent = parent
parent.$children.push(vm)
下面重点看下render
函数的执行,还是以上面的render
函数为例,如下
<!-- 原始模板 -->
<div id="app"> {{ message }} </div>
// 编译后的render函数
(function anonymous() {
with (this) {
return _c('div', {attrs: {"id": "app"}}, [_v("\n " + _s(message) + "\n")])
}
})
显然里面用到了的_c
、_v
都是函数,主要是_c
,该函数等价于snabbdom的h
函数,用来创建虚拟DOM。
需要注意到with
的用法,with中的this就是组件实例,该实例上挂载_c
这些方法,以及render
函数中用到数据如上面demo中的message
。with特性
下面看下_c
,_v
的定义
// src/core/instance/render.js
import { createElement } from '../vdom/create-element'
export function initRender (vm: Component) {
//...
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运行时代码执行时就会执行 renderMixin
-> installRenderHelpers(Vue.prototype)
,该方法挂载了一些工具方法和创建DOM节点的方法。
export function installRenderHelpers (target: any) { // target: Vue.prototype
//...
target._s = toString
//...
target._v = createTextVNode
//...
}
我们重点关注_c
指向的createElement
方法
createElement:创建vnode
import VNode, { createEmptyVNode } from './vnode'
import { createComponent } from './create-component'
//...
// alwaysNormalize: 调用 vm.$createElement 方法时,传递ture,看到 _render() -> render.call(vm, vm.$createElement),也就是执行用户自己提供的render函数时会走这里
// createFunctionComponent 又有可能
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean): VNode | Array<VNode> {
//... 参数纠正
//... 特殊场景,属性规范化设置,不重要
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number): VNode | Array<VNode> {
//... vnode data 不能是响应式数据,如果是返回空vnode
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) { // 动态组件
tag = data.is
}
//... 如果没有tag,返回空vnode
//... 规范化孩子,不重要
let vnode
if (typeof tag === 'string') {
let Ctor
if (config.isReservedTag(tag)) {
vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 组件
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else { // new Vue({render: h => h(App)}) // 用户手动提供 render函数
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
// 如果vnode是数组,取第一个
} else if (isDef(vnode)) {
//...
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
// 如果没有返回空vnode
}
}
// ref #5318
// necessary to ensure parent re-render when deep bindings like :style and
// :class are used on slot nodes
function registerDeepBindings (data) {
if (isObject(data.style)) {
traverse(data.style)
}
if (isObject(data.class)) {
traverse(data.class)
}
}
上面注释提到了children的规范化,解释参考黄轶-vue技术揭秘
下面看下核心逻辑,实际上很清晰了
如果tag
是对象或者是组件构造函数(else分支),则调用createComponent
创建组件虚拟节点即placeholder vnode
。注意,这里并不会创建组件的vue实例,更不会进入组件内部去创建组件的实际内容,createComponent
仅仅是创建组件标签
(如<todo-item>
)对应的vnode,本质上和div
并无太多区别,主要是会挂载很多信息(props、events等等)
如果是保留tag
如div
,直接new VNode
如果不是保留tag
如todo-item
,调用resolveAsset
从vm.$options.components
中查找有没有定义该组件。注意,前面说到过组件实例选项的合并,会去合并祖先构造函数的选项如Vue.options,全局组件和指令等都保存在这里,因此这里自然也会查找到全局组件指令。
如果查找到有对应的组件定义,则调用createComponent
创建placeholder vnode
;否则就是创建一个未知vnode
,同样会new VNode
- ❎ registerDeepBindings 作用?看起来是处理slot场景有关,暂遗留。
下面看下组件placeholder vnode
的创建过程,见createComponent
分析,如下:
createComponent:创建组件tag的placeholder vnode
export function createComponent (Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void {
if (isUndef(Ctor)) return
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
if (typeof Ctor !== 'function') return
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
// return ... 异步组件,单独的逻辑,后面会单独小节说
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
// 从注释来看是担心先创建的组件构造函数而后再注册全局mixin
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
if (isTrue(Ctor.options.functional)) {
// return ... 函数式组件的创建 是单独的逻辑,后面有可能单独小节说下
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
//... 抽象组件的slot需要特殊处理? 如果时间允许单独看看
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }, asyncFactory)
return vnode
}
- 创建组件肯定需要一个构造函数的,如果
Ctor
是组件对象(各种组件选项),会通过Vue.extend(Ctor)
,该方法通过原型继承返回一个构造函数,后面会说到。 - 如果是异步组件,则走异步组件vnode创建逻辑
- transformModel,有时间的话会单独分析一下
v-model
的实现,暂时忽略 ❎ extractPropsFromVNodeData
:创建一个对象res
,来存储当前组件从父组件那里接受的属性值,该对象会经过initProps
变成响应式对象,以被当前组件监听。注意这里的vnode.data
是从render
函数调用_c
传过来的。export function extractPropsFromVNodeData (data: VNodeData, Ctor: Class<Component>, tag?: string): ?Object { // we are only extracting raw values here. // validation and default values are handled in the child component itself. const propOptions = Ctor.options.props if (isUndef(propOptions)) { return } const res = {} const { attrs, props } = data if (isDef(attrs) || isDef(props)) { for (const key in propOptions) { // hyphenate 将属性处理成连字符 "-" 连接 const altKey = hyphenate(key) checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false) } } return res }
- 如果是函数组件,则单独走函数组件vnode创建逻辑
- 获取监听的事件名称,自定义事件在
data.on
上,native事件在data.nativeOn
,处理后自定义事件保存到vnode.componentOptions.listeners
上,native事件保存到vnode.data.on
上。- 自定义事件是在
_init
->initEvent
中会用到,通过vue框架自己的事件机制(发布-订阅)实现; - 而native事件是在events模块(src/platforms/web/runtime/modules/events.js)上处理的,当然是通过浏览器提供的api如
addEventListener
来处理的
- 自定义事件是在
installComponentHooks
:给 vnode.data 添加部分钩子(init
、prepatch
、insert
、destroy
),后面会碰到每个vnode钩子调用的时机,碰到时再针对每个钩子细说。注意这些钩子是安装给组件placeholder vnode
上的,都非常重要。
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
//...
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
//...
},
insert (vnode: MountedComponentVNode) {
//...
},
destroy (vnode: MountedComponentVNode) {
//...
}
}
- 获取组件名称,创建组件标签对应的vnode(
new VNode
),这里重点是保存了组件的数据(事件、属性数据等),因为在后面vm._update
会进真正开始创建子组件实例而后渲染子组件,而子组件的渲染是需要这些数据支撑的。
Vue.extend
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
return Sub
}
function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
这里最重要的是通过原型继承返回一个子构造函数
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
//...
return Sub
剩下的构造函数选项处理,不赘述。
另外注意到这里也调用了initProps
和initComputed
的逻辑(这里处理的是静态属性),这是出于性能考虑,将公共执行逻辑提到构造函数中执行。有注释佐证,如下:
// initProps -> initProps方法中的部分注释
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
// initProps -> initComputed方法中的部分注释
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
总结
vnode的创建,更重要的是组件placeholder vnode
的创建。最终vm._render
方法会返回一个根虚拟DOM(实际上是一个虚拟DOM树,对吧)。当然,下一步就是要把虚拟DOM数更新到界面上。
下一节,重点分析虚拟DOM -> 界面的过程