这次来看看vue的虚拟dom是咋肥色儿~
回顾
之前分析如何数据响应到视图最后发现是调用了__patch()__
方法来生成/diffdom
的. 最后留下了2个问题~ 1. template或者el是如何被编译成render的. 2. patch的实现.
template或者el被编译成render差不多就是正则匹配~ 然后统一成render函数的格式, 所以我们直接用render函数套进patch可以知道patch的参数的样子, 可以先看patch的实现.
本文叙事方式为树藤摸瓜, 顺着看源码的逻辑走一遍, 查看的vue的版本为2.5.2. 我fork了一份源码用来记录注释.
开始了
先来承接上局的源码分析~
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if (!prevVnode) { vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false , vm.$options._parentElm, vm.$options._refElm ) vm.$options._parentElm = vm.$options._refElm = null } else { vm.$el = vm.__patch__(prevVnode, vnode) }
|
从这里看出~ 创建dom调用的时候传了6个参数, diff的时候传了2个参数. 那么就想一个例子来看创建和diff的过程.
1 2 3
| render: function (createElement) { return createElement('h1', this.blogTitle) }
|
这个例子是从vue文档的render function这里拿来的~ 现在我们就来看看这个例子的调用发生了什么~ (和之前一样是以web为例).
参数中的_parentElm
和_refElm
暂时没找到, 缓缓, 其中的vnode
, preVnode
, vnode
都是_render()
方法的返回值(上篇讲过了), 那么我们来看看_render()
方法吧.
_render()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options ... let vnode try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) if (process.env.NODE_ENV !== 'production') { if (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 } } else { vnode = vm._vnode } } 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() } vnode.parent = _parentVnode return vnode } }
|
节选了一段代码, 这里做的事情就是: render.call(vm._renderProxy, vm.$createElement)
, 或者在发生错误的时候尝试使用renderError()
方法(好像之前也说过了), 如果再错误就避免系统崩溃创建一个空vnodevnode = createEmptyVNode()
. 那么一切正常的话就是调用render方法, 我们把之前的例子套进去~
vm.renderProxy
之前说过就是vm
. $createElement
找到了在src/core/vdom/create-element.js
. 代入例子结果为:
1
| vm.$createElement('h1', vm.blogTitle)
|
所以看一下_createElement()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<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 (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() }
|
createElement里先判断了tag: 是否是字符串/是否html标签/是否自定义组件来调用new VNode()
或是createComponent()
.
VNode的构造函数啥都没做, 就保存下数据然后返回vnode. createComponent()
又引入了好多, 子组件作为之后讨论的话题吧.
对于我们的例子~ 返回值就是new VNode('h1', vm.blogTitle, undefined, undefined, undefined, vm)
. 也就是一个包含了这些信息的vnode对象.
那么来看__patch__()
方法~
src/platforms/web/runtime/patch.js
:
1 2 3 4 5 6 7 8 9 10
| 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'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
|
这里根据平台的node操作库和平台专有module来生成patch函数.
nodeOps在web中就是操作dom的动作了, document.createElement这种.
modules的值是各个声明周期调用的方法. 在createPatchFunction()
里只有一小段代码关于调用的, 下面马上会贴. 下一节进入createPatchFunction()
来看看__patch__()
方法的面目.
__patch__()
到核心了~ 先贴一下createPatchFunction()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| export function createPatchFunction (backend) { let i, j const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } ... return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }
let isInitialPatch = false const insertedVnodeQueue = []
if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { ... }
|
我们来看一下我们例子是怎么调用的:
1 2 3 4 5 6
| vm.__patch__( vm.$el, vnode, false, false , vm.$options._parentElm, vm.$options._refElm ) vm.__patch__(prevVnode, vnode)
|
先来看创建, 创建dom的内容很简单: createElm(vnode, insertedVnodeQueue, parentElm, refElm)
, 贴一下createElm的核心部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { ... const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode)
if (__WEEX__) { ... } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) }
if (process.env.NODE_ENV !== 'production' && data && data.pre) { creatingElmInVPre-- } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }
|
这里的主要流程是: 判断是否有tag, 如果是的话, 创建tag的dom, 如果不是, 判断是否是注释来添加注释或文字节点.
创建dom:
1 2 3
| vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode)
|
贴上:
这里的parentElm和refElm我竟然找了2小时没找到, 估摸着应该是父组件或是el. 之后再研究了.
createElemenNS的话就是针对svg和math~ 最后调用的就是document.createElement('h1')
了(针对本文的例子), 然后调用createChildren(vnode, children, insertedVnodeQueue)
来把我们的h1
创建文字子节点.
1 2 3 4 5 6 7 8 9 10 11 12
| function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }
|
// diff dom的代码看不动了, vue的源码告一段落. 小总结: vnode是保存node信息的对象, 调用patch的时候调用平台专属的node操作来贴到真实dom上.