vue@2.x中用到了虚拟DOM技术,基于第三方虚拟DOM库sanbbdom修改。建议阅读本文之前对snabbdom的使用和原理 有一定的了解,可以参考 snabbdom@3.5.1 源码分析。
vue2中组件渲染的核心入口如下:
代码语言:javascript复制// 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
用来生产虚拟节点树的,就像snabbdom中使用h
函数创建虚拟节点树一样。而vm._update
用来将上一步即vm._render生成的虚拟节点树经过patch
操作同步到界面上。
new Wacher(...)
用法在上一节数据驱动详细分析过。updateComponent在首次创建watcher实例时会执行一次,当updateComponent依赖的响应式数据变化时会再次执行。
因此上面new Watcher(vm, updateComponent,..)
方法中的两个操作_render() -> _update(),相当于snabbdom的如下操作
初始化时类比
代码语言:javascript复制const container = document.getElementById("container");
const vnode = h(...); // 创建虚拟节点树
patch(container, vnode); // 同步虚拟DOM树同步到界面
响应式数据更新时类比
代码语言:javascript复制// 如果此时有数据变更引起界面变更
const newVnode = h(...); // 新的虚拟节点树
patch(vnode, newVnode); // 和上一次的虚拟节点树进行diff,将差异同步到界面上
这里的巧妙出是new Watcher两个步骤合并到一起。
下面我们重点看下vue@2.x中关于虚拟DOM的相关逻辑。主要逻辑在src/core/vdom文件夹中。
从入口讲起(对应snabbdom库 init 方法)
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
//...
代码语言:javascript复制// 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
小结
这里主要是创建patch
方法的过程
vnode
vue@2.x中vnode在snabbdom定义的vnode基础上增加了很多其他的属性,
- 后面用到逐个介绍?最终统一在这里汇总一次?
_render:创建虚拟节点树
我们先看下vm._render
方法的定义
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
//... 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函数长什么样子呢?
代码语言:javascript复制<!-- 原始模板 -->
<div id="app"> {{ message }} </div>
代码语言:javascript复制// 编译后的render函数
(function anonymous() {
with (this) {
return _c('div', {attrs: {"id": "app"}}, [_v("n " _s(message) "n")])
}
})
render来自哪里?
- render函数可以由开发者自己提供
- 也提供了
编译 运行时
版本,即可有运行时编译,框架会自动处理将模板处理成render函数 - 更为常见的是
.vue
单文件开发,vue-loader会将其自动将template部分处理成render函数
currentRenderingInstance
的设置
关系链接
- vm.vnode = _parentVnode,当前组件实例的 vnode指向父虚拟节点
- 虚拟节点父子关系建立:
vnode.parent = _parentVnode
- 这里返回的vnode,会在vm.update中被设置给vm:
vm._vnode = vnode
下面重点看下render
函数的执行,还是以上面的render
函数为例,如下
<!-- 原始模板 -->
<div id="app"> {{ message }} </div>
代码语言:javascript复制// 编译后的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的定义
代码语言:javascript复制// 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节点的方法。
代码语言:javascript复制export function installRenderHelpers (target: any) { // target: Vue.prototype
//...
target._s = toString
//...
target._v = createTextVNode
//...
}
我们重点关注_c指向的createElement方法
createElement:创建vnode
代码语言:javascript复制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, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
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技术揭秘
- 记个todo,验证下(虽然不影响整体流程)
下面看下核心逻辑,实际上很清晰了
- 如果
tag
是对象或者是组件构造函数,则调用createComponent
创建组件的虚拟节点(注意,这里并不会创建组件的vue实例,更不会进入组件内部去创建组件的实际内容),createComponent仅仅是创建组件标签
(如<todo-item>
)对应的vnode,本质上和div并无太多区别,主要是会挂载很多信息(props, events等等) - 如果是保留
tag
如div
,直接new vnode - 如果不是保留tag如
todo-item
,并从vm.components
中查找有没有定义该组件- 如果有则
createComponent
- 否则就是创建一个位置节点,同样会new vnode (和
div
的vnode的创建没啥区别)
- 如果有则
- 记个todo, registerDeepBindings 作用?看起来是处理slot场景的bug❓❓❓
下面看下组件vnode创建场景:createComponent
createComponent:创建【组件tag】的vnode(并不会进入组件内部)
代码语言:javascript复制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
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(data/props/methods等等),会通过Vue.extend(Ctor),该方法通过原型继承返回一个构造函数,后面会说到。
如果是异步组件,则走异步组件vnode创建逻辑
resolveConstructorOptions:从注释来看,是担心先创建的组件构造函数而后再注册全局mixin
- 待验证,处理特殊场景,非核心逻辑,不重要
transformModel:(自定义组件v-model),看起来只是语法糖:取值,添加事件回调,
TODO,demo验证下?,不影响主流程
代码语言:javascript复制// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}
extractPropsFromVNodeData:这里只是从传递个组件的数据(vnode.data.attrs || vnode.data.props)中提取属性值(组件定义的props)。这里返回的是一个新对象,该对象后面会变成响应式对象,以被当前组件监听。
注意: 有个细节:从vnode.data.attrs中提取完数据,会对应的属性删除掉,而vnode.data.props则不会。
为什么这么做呢❓❓❓❓
代码语言:javascript复制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) {
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中会用到)
installComponentHooks:给 vnode.data 添加部分钩子(init、prepatch、insert、destroy),后面会碰到每个vnode钩子调用的时机,碰到时在针对每个钩子细说。
获取组件名称,创建组件标签对应的vnode(new vnode),这里重点是保存了组件的数据(事件、属性数据等),因为在后面_update会深入组件内部,进入组件的渲染,而组件的渲染是需要这些数据支撑的。
总结
创建虚拟DOM树,下一步就是调用vm._update将虚拟DOM树同步到界面上。
下一节,重点分析虚拟DOM到界面的过程(包括初始化和更新)。