重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
前面提到 createElement
创建了组件VNode,接着调用 vm._update
,执行 vm._patch
方法把VNode转换为真实节点,这是针对于一个普通的 VNode节点,下面看下在组件中的VNode的区别。
patch
会调用 createEml
创建元素节点,它在 src/core/vdom/patch.js
中:
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // ... }
和普通VNode最不一样的地方就是: createComponent
,因为这里的vnode是一个组件vnode,所以它在创建的时候有些不太一样:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
首先判断 vnode.data
有没有,keepAlive先忽略,接着判断有没有 data.hook
以及这个 hook
中有没有 init
方法,如果有就调用这个方法。
回忆一下,在创建组件 createComponent
方法里面,有一个 installComponentHooks
方法,这个方法会把上面定义的4个hook都初始化一遍(init,prepatch,insert,destroy),然后挂载到 data.hook
上,所以在上面的判断中,init
是有的,然后就执行到了 hook
上的 init
方法中:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }
keepAlive跳过,后面有个 createComponentInstanceForVnode
,这个方法返回了 vnode.componentInstance
,也就是返回了vm的实例,然后调用了 $mount
方法挂载子组件。看下 createComponentInstanceForVnode
, 它传入了两个参数,第一个是组件vnode,第二个是 activeInstance
,后面会提到。来看下 createComponentInstanceForVnode
的定义:
export function createComponentInstanceForVnode ( vnode: any, // we know it's MountedComponentVNode but flow doesn't parent: any, // activeInstance in lifecycle state ): Component { const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent } // check inline-template render functions const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } return new vnode.componentOptions.Ctor(options) }
可以看到传入了两个参数,第一个是组件vnode,第二个参数其实是当前vm的一个实例,定义了一个 options
,有三个键, 中间的 _parentVnode
就是父vnode,它其实是一个占位vnode,一个占位节点。最后返回了一个 new vnode.componentOptions.Ctor(options)
,回忆一下,在创建子组件vnode的时候,用了一个 context.$options.__base
,也就是 Vue.extend
,扩展了一个子组件构造器 Ctor
,接着在创建vnode的时候,有个参数是 { Ctor, propsData, listeners, tag, children }
,这里的 Ctor
就是组件构造器,那么再执行 vnode.componentOptions.Ctor
的时候其实就是执行了 Sub
的构造函数,Sub
在 src/core/global-api/extend.js
中,然后它执行 了 _init
,这个 _init
又回到了 Vue 的初始化,因为子组件的构造器其实是继承了 Vue 的构造器,来再次看下 _init
的细节,和之前不一样的地方,在 src/core/instance/init.js
:
Vue.prototype._init = function (options?: Object) { const vm: Component = this // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // ... // expose real self vm._self = vm 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') // ... if (vm.$options.el) { vm.$mount(vm.$options.el) } }
首先合并了 options
,参数 options._isComponent
现在是true,所以执行 initInternalComponent
,进行合并,看下这个方法:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
创建了一个 vm.constructor.options
对象,然后赋值给 vm.$options
,接下来是重点,它把 _parentVnode
和 parent
传了进来,_parentVnode
就是上面的 createComponentInstanceForVnode
的参数 _parentVnode
,就是占位符的vnode,parent
是当前vm的实例,也就是当前子组件的父vm实例,继续看 initInternalComponent
,把 vnodeComponentOptions
里的一些参数拿出来赋值给 opts
,到此 initInternalComponent
结束。所以这里做的操作就是把通过 createComponentInstanceForVnode
函数传入的参数合并到内部的 $options
里了。
接着看 _init
, initLifecycle
定义在 src/core/instance/lifecycle.js
,看下这个方法:
export let activeInstance: any = null export let isUpdatingChildComponent: boolean = false export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent let parent = options.parent if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent vm.$root = parent ? parent.$root : vm vm.$children = [] vm.$refs = {} vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false }
拿到了一个 options.parent
,这个 parent
实际上就是 activeInstance
,注意 activeInstance
在这个文件中是一个全局变量,它的赋值在 lifecycleMixin
方法中:
export function lifecycleMixin (Vue: Class<Component>) { Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = 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 */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // 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. }
在调用 _update
的时候,赋值了 activeInstance
,也就是说每次调用 _update
,就会把当前的vm实例赋值给 activeInstance
:
activeInstance = vm
同时用 prevActiveInstance
来保留上一次的 activeInstance
,这么做是什么意思?
这里把当前的vm给了 activeInstance
,然后在当前vm实例的vnode在 patch
的过程中,把当前实例作为父vm实例,传给子组件,这样 patch
其实就是一个深度遍历,将当前激活的vm实例给 activeInstance
,然后在初始化子组件的时候,将这个 activeInstance
作为parent参数传入,然后在 initLifecycle
里,就可以拿到当前激活的vm实例,然后把实例作为parent:
const options = vm.$options // locate first non-abstract parent let parent = options.parent if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent vm.$root = parent ? parent.$root : vm vm.$children = [] vm.$refs = {} vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false
此时 parent
是vm实例,parent.$children
塞一个子组件的vm。上面代码中的 vm
是子组件,parent
是它的父组件,然后他们有一层 push
的关系,接着把 vm.$parent
赋值为父组件实例 parent
,至此 initLifecycle
就把这一层父子关系给建立起来了。
继续看 _init
,最后的 vm.$mount
是走不到的,因为现在的 $options
没有 el
:
if (vm.$options.el) { vm.$mount(vm.$options.el) } 复制代码
所以此时 _init
返回的是一个子组件的实例,然后回到 createComponent
里面的 init
钩子,createComponentInstanceForVnode
其实就是返回了一个子组件的实例,接着:
child.$mount(hydrating ? vnode.elm : undefined, hydrating) 复制代码
手动调用了 $mount
来挂载,也就是执行之前的 Vue.prototype.$mount
和 mountComponent
方法,接着执行 _update
的 updateComponent
方法,最后调用 __patch__
渲染VNode:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) function patch (oldVnode, vnode, hydrating, removeOnly) { // ... let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // ... } // ... } 复制代码
然后调用 createElm
,再来看下它的定义:
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // ... if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } 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) /* istanbul ignore if */ if (__WEEX__) { // ... } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } // ... } 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) } } 复制代码
这里只传了2个参数,所以 parentElm
是 undefined
。注意,这里传入的 vnode
是组件渲染的 vnode
,也就是之前说的 vm._vnode
,如果组件的根节点是个普通元素,那么 vm._vnode
也是普通的 vnode
,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值是 false。接下来的过程就和 createComponent
一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm
,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复patch,这样通过一个递归的方式就可以完整地构建了整个组件树。
此时传入的 parentElm
是空,所以对组件的插入,在 createComponent
有这么一段逻辑:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { // .... if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // ... if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } } 复制代码
在完成组件的整个 patch
过程后,最后执行 insert(parentElm, vnode.elm, refElm)
完成组件的 DOM 插入,如果组件 patch
过程中又创建了子组件,那么DOM 的插入顺序是先子后父。
下面附一张自己整理的图,还有一张更大的图在github仓库,有需要的自取~
附述
由于我在这一块花费了好多时间,所以在附上一点自己的理解。
App.vue <template> <div id="app"> <HelloWorld /> </div> </template> 复制代码
id为app作根,是一个渲染vnode,里面有个children,children里有个child就是: ,这个
就是 HelloWorld.vue 文件的 _parentVnode,也就是占位符vnode。
而 真正渲染的是 HelloWorld.vue 文件,调用它的 render 渲染出一个 渲染vnode,这个渲染vnode的_parentVnode 就是
这个占位符vnode。
vm.$vnode = __parentVnode = vnode._parent
vm._vnode = vnode
vm._vnode.parent = vm.$vnode
update方法执行下面两个方法:
- initComponent 是把patch的返回值赋值给 vnode.elm(HelloWorld占位符节点)
- insert是把HelloWorld.vue 插入到父占位符里
此时 HelloWorld插入到App.vue结束
vm._vnode是渲染vnode,vm.$vnode是占位vnode