Vue源码阅读--生命周期(1)

组件化流程(生命周期流程)

首先我们已组建生命周期图示镇图:

第一步: new Vue();

在实际中我们都会通过new Vue()的方式去绑定一个根组件

var vue = new Vue({
    el: "#app",
    components:{
        buttonCounter
    },
    data: function() {
        return {
            name: ""
        }
    }
})
复制代码

那么这时候其调用的是

/**
 * 初始化生成 Vue 全局函数
 * @author guzhanghua
 * @param {*} options
 */
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._init()方法,这时候就到了第二步了

第二步:Init(Events & Lifecycle)

在第二步的时候执行 _init()方法。我们看_init() 方法

Vue.prototype._init = function(options ? : Object) {
    ...
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    // 触发beforeCreate回调
    callHook(vm, 'beforeCreate')
    ...
}
复制代码

这时候执行了 initLifecycle(vm)、 initEvents(vm) 就像生命周期中的(Events & Lifecycle),但是这时候还进行了initRender(vm),为什么没有提及?

因为initRender(vm)主要是初始化 $mount的时候回调render函数的一些属性和方法 如_vnode、_c()。

然后触发了 callHook(vm, 'beforeCreate') beforeCreate生命周期函数。

因为这时候没有进行 initState 所以这时候访问不了组件响应式属性 this.xxx

第三步: Init(injection & reactiivity)

这个过程主要是初始化 inject属性 和响应式数据,即在下一个生命周期的时候可以访问组件的响应式数据属性。

Vue.prototype._init = function(options ? : Object) {
    ...
    // 初始化高阶属性inject
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    // 初始化高阶属性 provide
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    ...
}
复制代码

第四步:Has 'el' option

Vue.prototype._init = function(options ? : Object) {
    ...
    // 对于子组件类型 其都不会通过el去绑定 模板DOM。而对于跟组件其会el:'#app' 所以需要触发 vm.$mount()方法。
    // 子组件在什么时候触发 $mount方法?  在init 钩子函数 最后调用 child.$mount
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}
复制代码

这一步主要是做什么?

我们Vue组件初始化完成了那么就应该 安装组件了。这一步是一个条件语句,我们在new Vue的时候可以

var vue = new Vue({
    el: "#app",
})
复制代码

当然也可以这样

var vue = new Vue({ el: "#app" }).$mount('#app')
复制代码

可见这一步主要是可以实例化的时候就执行mount,也可以之后在vm.mount()手动去挂载。

第五步: Compile template into render function

其实这一步就是执行 $mount函数。

对于带编译的版本其 $mount方法的定义在

src\platforms\web\entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(
    el ? : string | Element,
    hydrating ? : boolean
): Component {
    ...   // 编译 el 元素
    return mount.call(this, el, hydrating)
}
复制代码

然后调用 mount.call()

src\platforms\web\runtime\index.js
// public mount method
Vue.prototype.$mount = function(
    el ? : string | Element,
    hydrating ? : boolean
): Component {
    el = el && inBrowser ? query(el) : undefined
    return mountComponent(this, el, hydrating)
}
复制代码

真正关键的是 mountComponent(this, el, hydrating)

src\core\instance\lifecycle.js
export function mountComponent(
    vm: Component,
    el: ? Element,
    hydrating ? : boolean
): Component {
    vm.$el = el

    // 判断此时是否存在render 函数,
    // Vue中不管是通过el,template,render() 3种方式中的一种去获取模板的 都在最后将其转换成render函数,
    if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        ...
    }
    // 触发钩子函数 看生命周期  之前 Compile template into render function or Compile el's outerHTML as template
    callHook(vm, 'beforeMount')

    return vm
}
复制代码

所以在 callHook(vm, 'beforeMount') 触发 beforeMount生命周期函数的之前主要就是将template el转换成 render函数

第六步: Create vm.$el and replace 'el' with it

这是最重要的一步也是最复杂的一步。

首先我们需要了解一个概念,即Vue不是直接将 我们的template 或者el直接转换成DOM插入DOM树中。而是 先编译成可执行的代码字符串(HTML -> JS),然后执行此代码装换成vnode(JS -> VNode),然后patch将vnode变成真正的DOM插入DOM树(VNode -> DOM)。

而这一步就是执行render 将JS转成vnode、然后patch将vnode变成真正的DOM插入DOM树。

下面我们看代码

src\core\instance\lifecycle.js
export function mountComponent(
    vm: Component,
    el: ? Element,
    hydrating ? : boolean
): Component {
    ...
    let updateComponent
        /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        
    } else {
        // 创建一个更新组件方法
        updateComponent = () => {
            vm._update(vm._render(), hydrating)
        }
    }
    new Watcher(vm, updateComponent, noop, {
        // 在我们的更新队列中 其更新方法 是sort排列 使得 子组件在父组件之后更新
        // 先调用before 然后调用 watcher.run()方法 
        before() {
            if (vm._isMounted) {
                callHook(vm, 'beforeUpdate')
            }
        }
    }, true /* isRenderWatcher */ )
    hydrating = false
    if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
    }
    return vm
}
复制代码

其先创建一个函数updateComponent作为watcher的 expOrFns参数。然后在watcher的get的时候回调执行此方法。 然后触发 vm._render()

/**
 * 作用  就是 执行组件上定义的 render函数  生成 一个vnode
 * @return {vnode} [组件vnode]
 */
Vue.prototype._render = function(): VNode {
    // 第一次 vm = new Vue()
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    ... // 处理slot
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
        // 调用 组件定义的render函数
        vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
        
    }
    // return empty vnode in case the render function errored out
    vnode.parent = _parentVnode
    return vnode
}
复制代码

可见这一步最重要的时候 通过render.call(xxx)去回调执行我们的render函数。在render函数中通过 vm._createElement()方法返回一个vnode的VirtualDOM 树。

然后触发 _update()方法

/*
    可见 _update()方法触发的时机有两种。
    1、 当组件初始化渲染的时候 此时组件从AST -> VNode 但是没有生成DOM元素 此时触发_update 进行 VNode -> DOM的过程
    2、 当组件发生更新的时候  此时响应式数据触发 set方法 然后 dep.notify() 去通知渲染Watcher进行重新getter方法
    此时也会触发 _update() 方法
 */
Vue.prototype._update = function(vnode: VNode, hydrating ? : boolean) {
    ...
    if (!prevVnode) {
        // initial render
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */ );
    } else {
        // 当组件更新的时候触发 preVnode 为旧的组件VNode vnode为新render生成的VNode
        // updates
        vm.$el = vm.__patch__(prevVnode, vnode)
    }

    if (vm.$el) {
        vm.$el.__vue__ = vm
    }
}
复制代码

调用 patch() 方法将VNode转成DOM。

VNode转DOM过程中,元素创建的过程、插入的次序、父子组件DOM元素如何插入

首先我们还是看 patch 方法

return function patch(oldVnode, vnode, hydrating, removeOnly) {
    ... // Destory
    let isInitialPatch = false
    const insertedVnodeQueue = []
    // 如果没有真实的 DOM 那么 就可能是 一开始创建的时候  或者 懒加载的组件类型
    // 那么 直接调用createEle 生成DOM
    if (isUndef(oldVnode)) {
        // empty mount (likely as component), create new root element
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue)
    } else {
        // 第一步 oldVode = #app  所以 oldVnode.nodeType = 1;
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // patch existing root node
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
        } else {
            
            // replacing existing element
            const oldElm = oldVnode.elm
            const parentElm = nodeOps.parentNode(oldElm)
            // create new node
            createElm(
                vnode, // 当前的组件vnode
                insertedVnodeQueue,
                // extremely rare edge case: do not insert if old element is in a
                // leaving transition. Only happens when combining transition +
                // keep-alive + HOCs. (#4590)
                oldElm._leaveCb ? null : parentElm, // 父元素
                nodeOps.nextSibling(oldElm)
            )
        }
    }
    // 将vnode转成dom树后,调用 insertedVnodeQueue队列中的 insert的钩子函数
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
}
复制代码

如对于 跟组件 我们oldVnode 就是我们 div#app元素,且 isRealElement === true; 然后执行 createElm(vnode, insertedVnodeQueue,oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) )这时候 parentElm 为 元素

function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
) {
    
    // 是否是嵌套的内部组件
    vnode.isRootInsert = !nested // for transition enter check
    // 如果为true 说明 此当前处理的vnode是一个组件
    // 如果是undefined 说明当前处理的vnode为元素节点
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }
    // 元素节点 保存其data数据
    const data = vnode.data
    // 获取其子vnode
    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)
        }
        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)
    }
}
复制代码

对于 div#app 元素其肯定不是组件 所以createComponent(vnode, insertedVnodeQueue, parentElm, refElm)为false。然后执行到 vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode)。 通过函数柯里化方法定义了各平台的nodeOps(DOM操作方法),去创建当前 div#app元素。 然后 createChildren(vnode, children, insertedVnodeQueue)去深度处理子元素,然后insert(parentElm, vnode.elm, refElm)。

可以组件中 元素的创建是 由上而下。然后通过深度遍历子节点,将各个子节点插入到其父节点的node.elm上

如:

其先创建 div#app 元素,

然后createChildren() 处理子节点。

第一个分支

然后创建 H3节点

然后createChildren() 处理 H3 的子节点;

然后创建文本节点 TEXT(APP);

然后createChildren() 没有获取到子节点,执行insert将文本节点插入到父vnode.elm上,即使得 VNode(H3).elm = H3>APP;

然后执行H3的插入操作 使得 VNode(app).elm = div>h3>app;

第二个分支

一样像第一个分支一样创建父节点,然后处理子节点,然后将子节点插入到父节点的vnode.elm上,这样由上而下创建,处理;自下而上插入生成树。

第三个分支

这时候遇到组件那么 createComponent()将为true;在处理子组件的时候其也是这样。

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
        const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
        // 在create-component.js 中
        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
        }
    }
}
复制代码

通过 i(vnode, false /* hydrating */ ) 生成子组件的组件vnode.elm;然后在 insert(parentElm, vnode.elm, refElm)将子组件vnode.elm 插入到 parentElm下。

通过这个流程我们就知道vnode -> DOM 的过程是通过深度遍历的方式。将各个级别的元素转成DOM元素,然后由上而下插入生成一个DOM树,并保存在vnode.elm上。

在patch的最后 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) 调用组件的 insert钩子函数并在钩子函数中 callHook(componentInstance, 'mounted')

/**
 * 当组件 从vnode -> 真实的DOM 并且插入到 DOM上的时候  
 * 回调 mounted()  钩子函数
 */
insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
        componentInstance._isMounted = true
        callHook(componentInstance, 'mounted')
    }
    ...
},
复制代码
注意:
1. 由此可见我们注意到一个问题:

mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted

即在回调 子组件的 mounted的时候,不能确认子组件已经在浏览器的DOM树上。而需要等到根组件 mounted 的时候才能确认所有的DOM节点都渲染到DOM树上。在 子组件的 mounted的时候 只能确认的是 可以通过 ref 访问的元素节点(注意: 这时候元素可能只是在内存中)。

2. 组件的 mounted 是先子后父,由下而上触发的。

转载于:https://juejin.im/post/5bd96afae51d4520224d94bb

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值