资讯专栏INFORMATION COLUMN

Vue 生命周期详解

snowLu / 3633人阅读

摘要:的钩子函数会在组件停用时被调用。是在构造函数中的声明的变量执行钩子函数执行执行钩子函数执行钩子函数刷新前根据对中的进行排序。

Vue 生命周期详解 Vue 生命周期流程

最开始,用户使用 new Vue() 创建根 Vue 实例,或者 Vue 实例化子组件都会调用_init方法(我们将这两种实例都称为vm):

function Vue(options) {        //Vue 构造函数
    ...
    this._init(options)
}
...
const Sub = function (options) {  // 定义子组件构造函数
    this._init(options)
}

vm实例化时会调用原型方法this._init方法进行初始化:

Vue.prototype._init = function(options) {
    vm.$options = mergeOptions(  // 合并options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
    ...
    initLifecycle(vm) // 开始一系列的初始化
    initEvents(vm)
    initRender(vm)
    callHook(vm, "beforeCreate")        //执行 beforeCreate 钩子
    initInjections(vm)
    initState(vm)
    initProvide(vm)
    callHook(vm, "created")                    //执行 created 钩子
    ...
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}
beforeCreate

首先,将用户提供的options对象,父组件定义在子组件上的eventprops(子组件实例化时),vm原型方法,和Vue构造函数内置的选项合并成一个新的options对象,赋值给vm.$options
接下来,执行 3 个初始化方法:

initLifecycle(vm): 主要作用是确认组件的父子关系和初始化某些实例属性。找到父组件实例赋值给vm.$parent,将自己push给父组件的$children

initEvents(vm): 主要作用是将父组件使用v-on@注册的自定义事件添加到子组件的私有属性vm._events中;

initRender(vm): 主要作用是初始化用来将render函数转为vnode的两个方法vm._cvm.$createElement。用户自定义的render函数的参数h就是vm.$createElement方法,它可以返回vnode。等以上操作全部完成,就会执行beforeCreate钩子函数,此时用户可以在函数中通过this访问到vm.$parentvm.$createElement等有限的属性和方法。

created

接下来会继续执行 3 个初始化方法:

initInjections(vm): 初始化inject,使得vm可以访问到对应的依赖;

initState(vm): 初始化会被使用到的状态,状态包括propsmethodsdatacomputedwatch五个选项。调用相应的init方法,使用vm.$options中提供的选项对这些状态进行初始化,其中initData方法会调用observe(data, true),实现对data中属性的监听,实际上是使用Object.defineProperty方法定义属性的gettersetter方法;

initProvide(vm):初始化provide,使得vm可以为子组件提供依赖。

这 3 个初始化方法先初始化inject,然后初始化props/data状态,最后初始化provide,这样做的目的是可以在props/data中使用inject内所注入的内容。
等以上操作全部完成,就会执行created钩子函数,此时用户可以在函数中通过this访问到vm中的propsmethodsdatacomputedwatchinject等大部分属性和方法。

beforeMount

如果用户在创建根 Vue 实例时提供了el选项,那么在实例化时会直接调用vm.$mount方法开始挂载:

if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

如果未提供el选项,则需要用户手动调用vm.$mount方法开挂载。vm.$mount方法:

运行时版本:
Vue.prototype.$mount = function(el) { // 最初的定义
    return mountComponent(this, query(el));
}
完整版:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el) {  // 拓展编译后的
    var options = this.$options;
    if(!options.render) {
        if(options.template) {
            ...                //一些判断
        } else if (el) {    //传入的 el 选项不为空
            options.template = getOuterHTML(el);
        }
        
        if (options.template) {
                options.render = compileToFunctions(template, ...).render    //将 template 编译成 render 函数
        }
    }
    ...
    return mount.call(this, query(el))    //即 Vue.prototype.$mount.call(this, query(el))
}

在完整版的vm.$mount方法中,如果用户未提供render函数,就会将template或者el.outerHTML编译成render函数。
然后会执行mountComponent函数:

export function mountComponent(vm, el) {
    vm.$el = el
    ...
    callHook(vm, "beforeMount")
    ...
    const updateComponent = function () {
        vm._update(vm._render())    // 调用 render 函数生成 vnode,并挂载到 HTML中
    }
    ...
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, "mounted");
    }
}

如果用户提供了el选项,则会获取用于挂载的真实节点,将此节点赋值给vm.$el属性。
等以上操作全部完成,就会执行beforeMount钩子函数,如果用户提供了el选项,此时在函数中可以通过this访问到vm.$el属性,此时它的值为el提供的真实节点。

mounted

mountComponent方法中,会执行vm._render方法获取vnode

Vue.prototype._render = function() {
    const vm = this
    const { render } = vm.$options

    const vnode = render.call(vm, vm.$createElement)
    
    return vnode
}

vm._render方法中会调用vm.$options.render函数,传入实参vm.$createElement(对应声明render函数时的形参h),得到返回结果vnode
在执行一个如下的render函数的过程中:

render(h) {
    return h(
        "div",    //标签名
        [                //子节点数组
            [
                [h("h1", "title h1")],    //子节点也是通过 h 函数生成 vnode 的
                [h("h2", "title h2")]
            ],
            [
                h(obj, [                //子组件传入 obj 而不是标签名
                    h("p", "paragraph")
                ])
            ]
        ]
    );
}

执行render函数的过程就是递归调用h函数的过程,h函数会根据子组件的options选项对象生成一个vnode,以便之后将它转化为真实节点。

不管是根节点挂载时首次渲染,还是在数据改变后更新页面,都会调用updateComponent方法。_render方法返回的vnode是一个树形结构的JavaScript对象,接下来在updateComponent中会调用_update将这棵虚拟DOM树转化为真实的DOM树:

const updateComponent = function () {
    vm._update(vm._render())    // 调用 render 函数生成 vnode,并挂载到 HTML中
}

vm._update方法会将vm.__patch__方法返回的真实Dom节点赋值给vm.$el

Vue.prototype._update = function(vnode) {
    ...
    if (!prevVnode) {
        // 首次渲染
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
        // 更新
        vm.$el = vm.__patch__(prevVnode, vnode);
    }
    ...
}

vm.__patch__方法传入的参数vm.$el是之前在mountComponent方法中赋值的真实Dom元素,是挂载对象。vm.__patch__会生成并插入真实Dom

Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules }) 

nodeOps是一些操作原生Dom的方法的集合,modulesclass/attrs/style等属性创建、更新、销毁时相应钩子方法的集合,而createPatchFunction函数返回了一个patch函数:

export function createPatchFunction(backend) {
    ...
    const { modules, nodeOps } = backend
    
    return function patch (oldVnode, vnode) {  // 接收新旧 vnode 的 `patch`函数
        ...
        //isDef 函数 : (v) => v !== undefined && v !== null
        const isRealElement = isDef(oldVnode.nodeType) // 是否是真实 Dom
        if(isRealElement) {  // 首次渲染传入的 vm.$el 是真实 Dom
            oldVnode = emptyNodeAt(oldVnode)  // 将 vm.$el 转为 VNode 格式
        }
        ...
    }
}

调用emptyNodeAt函数将传入的vm.$el转化为VNode格式。VNodeVue定义的虚拟节点类,vnodeVNode类的实例对象。

function emptyNodeAt(elm) {
    return new VNode(
        nodeOps.tagName(elm).toLowerCase(), // 对应tag属性
        {},  // 对应data
        [],   // 对应children
        undefined,  //对应text
        elm  // 真实dom赋值给了elm属性
    )
}
包装后的:
{
    tag: "div",
    elm: "
" // 真实dom }

然后继续创建真实Dom

export function createPatchFunction(backend) { 
    ...
    return function patch (oldVnode, vnode) {
        const insertedVnodeQueue = []        //用于缓存 insertedVnode
        ...
        const oldElm = oldVnode.elm  //包装后的真实 Dom 
const parentElm = nodeOps.parentNode(oldElm) // 首次父节点为 createElm( // 创建真实 Dom vnode, // 传入的 vnode insertedVnodeQueue, // 空数组 parentElm, // nodeOps.nextSibling(oldElm) // 下一个兄弟节点 ) return vnode.elm // 返回真实 Dom ,之后在 _update 中覆盖 vm.$el } }

createElm方法根据节点类型生成真实Dom节点,并插入parentElm中。而createElm方法在创建元素节点的过程中,会调用createChildren方法创建子节点,而createChildren方法又会调用createElm方法生成子节点的真实Dom节点,形成了createElm方法的递归调用:

function createElm(vnode, insertedVnodeQueue, parentElm, ...) {
    ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {    //此时可忽略这一步
        return 
    }
    ...
    // 如果要创建的节点是元素节点
    vnode.elm = nodeOps.createElement(tag)  // 先创建一个空元素用于挂载子节点
    createChildren(vnode, children, insertedVnodeQueue)  // 调用 `createChildren` 方法创建子节点
    insert(parentElm, vnode.elm, refElm)  // 将真实元素 vnode.elm 插入父节点中
    ...
}

递归创建子节点,插入父节点,最终生成vm的真实Dom节点vnode.elm
等以上操作全部完成,就会执行mounted钩子函数,此时在函数中可以通过this访问到vm.$el属性,此时它为虚拟vnode转化而来的真实Dom

activated

如果我们研究的实例vm是一个组件实例,而且它被组件包裹,那么它将额外具有两个钩子函数activateddeactivated。我们假设vm是根 Vue 实例root的一个后代组件。
root挂载时,会在它的patch方法中调用createElm方法生成真实Dom节点并插入root的父节点)。
如果有子节点,会先调用createChildren方法,在createChildren中通过createElm方法生成每个子节点的真实Dom节点,再将子Dom节点插入rootDom节点中:

function createChildren(vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
        for (var i = 0; i < children.length; ++i) {
            createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);    // 实参 vnode.elm 传给 parentElm 形参
        }
    }
    ...
}

所以再次回到上面的createElm方法,此时它被用于创建子节点,如果子节点为组件,在createElm中会调用createComponent方法对子组件进行初始化,生成子组件实例(假设就是vm),初始化子组件调用的是 init 钩子(vnode 有 4 个 management hookinit, prepatch, insertdestroy,在 render 函数生成 vnode 时会加载到 vnode.data.hook 上)。

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */ );    // 暂停执行 createComponent,开始调用 vnode.data.hook.init 钩子进行初始化
        }
        if (isDef(vnode.componentInstance)) {
            // 等 init 钩子执行完再执行,此时 vm 已执行完 $mount 方法,所以在 initComponent 方法中将 vnode push 到 insertedVnodeQueue 中
            initComponent(vnode, insertedVnodeQueue);    
            insert(parentElm, vnode.elm, refElm);    // 将真实元素 vnode.elm 插入父节点中
            if (isTrue(isReactivated)) {
                reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
            }
            return true
        }
    }
}

init 钩子中调用 Sub构造函数实例化子组件:

init: function init(vnode, hydrating) {
    ...
    //调用 `Sub`构造函数实例化子组件,执行 `beforeCreate` 和 `created` 钩子
    var child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance);
    //调用 vm.$mount,执行 `beforeMount` 钩子,然后执行 updateComponent,重复上面的流程
    child.$mount(hydrating ? vnode.elm : undefined, hydrating);    
},

初始化完成后,会调用子组件实例vm$mount方法进行挂载,执行patch方法,在vmpatch方法中又会调用createElm方法生成真实Dom,这时子组件实例会难以避免地再次执行createComponent方法:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm) { 
    ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {    // 如果子节点为组件,调用 createComponent 方法对子组件进行初始化;之后在子组件的 `patch` 方法中又会调用 `createElm` 方法
          return      
    }    
    //继续创建真实节点
    ...    
    vnode.elm = nodeOps.createElement(tag) 
    createChildren(vnode, children, insertedVnodeQueue);    //从这里开始暂停,在 createChildren 中 createElm 子节点
    insert(parentElm, vnode.elm, refElm);        //将真实元素 vnode.elm 插入父节点中
    ...
}

这个时候createComponent不会执行初始化操作,而是直接返回undefined,这样就可以继续创建真实节点,如果后代还有组件,又是一个循环……
所以,父子节点的创建、挂载钩子执行顺序为:
beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount

回到mounted生命周期的createPatchFunction方法,在它返回的patch方法中,私有变量insertedVnodeQueue用于存储这些插入的后代组件的vnode

function patch() {
    var insertedVnodeQueue = [];
    ...
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);    //调用 insert 钩子
    return vnode.elm    //真实 Dom 元素
}
...
//`patch`方法就是 _update 中的 __patch__ 方法,
//它返回真实 Dom 元素给根 Vue 实例的 $el,之后会在 mountComponent 中调用根 Vue 实例的 mounted 钩子(具体看前面 mountComponent 和 _update 方法)
root.$el = root.__patch__(...)    // _update 中
...
callHook(root, "mounted");        // mountComponent 中

vmroot的后代,vm.$vnode也在root实例的patch方法的insertedVnodeQueue中。在invokeInsertHook函数中,会调用这些vnodeinsert钩子:

function invokeInsertHook(vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    if (isTrue(initial) && isDef(vnode.parent)) {    
        vnode.parent.data.pendingInsert = queue;    //缓存 insertedVnode
    } else {
        //只有最初的实例的 initial 为 false,所以会延迟到根 Vue 实例 patch 方法的末尾调用所有后代组件的 insert 钩子
        for (var i = 0; i < queue.length; ++i) {
            queue[i].data.hook.insert(queue[i]);    //调用缓存的 insertedVnode 的 insert 钩子
        }
    }
}

假如当前调用的是vm.$vnode.data.hook.insert方法:

insert: function insert(vnode) {    //传入 vm.$vnode
    var context = vnode.context;        //父组件实例
    var componentInstance = vnode.componentInstance;    //vnode 对应的组件实例 vm
    if (!componentInstance._isMounted) {
        componentInstance._isMounted = true;
        callHook(componentInstance, "mounted");    //调用 vm 的 mounted 钩子函数(所以子组件的 mounted 钩子先于父组件被调用)
    }
    if (vnode.data.keepAlive) {        //true
        if (context._isMounted) {
            // 父组件更新中
            queueActivatedComponent(componentInstance);    // 父组件更新时,将 `vm` push 到 Vue 全局变量 activatedChildren 中,等待执行 `activated` 钩子函数
        } else {
            // 父组件挂载中
            activateChildComponent(componentInstance, true /* direct */ );    //调用 `vm` 的 `activated` 钩子函数
        }
    }
}

由此可知,Vue会按照root实例的patch方法的insertedVnodeQueuevnode的顺序执行mounted钩子。而在节点树中,越底端的组件越先创建好完好的真实Dom节点并插入父Dom节点中,其vnode也越先被pushinsertedVnodeQueue中,所以越先执行它的mounted钩子。
所以,完整的父子节点的创建、挂载钩子执行顺序为:
beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted

vm.$vnode.data.hook.insert方法中调用的activateChildComponent函数会调用vm及其后代组件的activated钩子函数:

function activateChildComponent(vm, direct) {
    ...
    if (vm._inactive || vm._inactive === null) {
        vm._inactive = false;
        for (var i = 0; i < vm.$children.length; i++) {
            activateChildComponent(vm.$children[i]);    //递归调用子组件的 activated 钩子
        }
        callHook(vm, "activated");        //调用 vm 的 activated 钩子
    }
}

vm首次挂载,调用mounted钩子函数后,会马上调用activated钩子函数。
之后vmactivated钩子函数会在 keep-alive 组件激活时调用激活时被调用,具体调用时机是在flushSchedulerQueue函数执行完queue中所有的watchers后。

deactivated

vmdeactivated钩子函数会在 keep-alive 组件停用时被调用。
patch方法的最后,会删除旧节点:

function patch() {
    ...
    removeVnodes(parentElm, [oldVnode], 0, 0);        // 在 removeVnodes 中调用 invokeDestroyHook(oldVnode) 删除旧节点
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
}

如果要删除的vnodedestroy钩子,则调用vnode.data.hook.destroy

function invokeDestroyHook(vnode) {
    var i, j;
    var data = vnode.data;
    if (isDef(data)) {
        if (isDef(i = data.hook) && isDef(i = i.destroy)) {
            i(vnode);        //调用 vnode.data.hook.destroy 钩子
        }
        ...
    }
}
destroy: function destroy(vnode) {
    var componentInstance = vnode.componentInstance;
    if (!componentInstance._isDestroyed) {
        if (!vnode.data.keepAlive) {                                
            componentInstance.$destroy();        //    调用 vm.$destroy()
        } else {
            deactivateChildComponent(componentInstance, true /* direct */ );    //调用子组件的 "deactivated" 钩子
        }
    }
}

调用`vmdeactivated钩子,递归调用子组件的deactivated 钩子:

function deactivateChildComponent() {
    ...
    for (var i = 0; i < vm.$children.length; i++) {
        deactivateChildComponent(vm.$children[i]);        //递归调用子组件的 "deactivated" 钩子
    }
    callHook(vm, "deactivated");        //调用 "deactivated" 钩子
    ...
}

这些操作在父组件的patch方法中执行,父组件patch后,会调用mounted或者updated钩子。

beforeUpdate

每个组件实例都对应一个watcher实例,它是在mountComponent方法中,在调用mounted钩子之前实例化的:

export function mountComponent(vm, el) {
    ...
    callHook(vm, "beforeMount")
    ...
    const updateComponent = function () {
        vm._update(vm._render(), hydrating);
    };
    new Watcher(vm, updateComponent, noop, {
        before: function before () {                    //在 run 之前执行
         if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, "beforeUpdate");            // beforeUpdate 钩子等待执行
            }
        }
    }, true /* isRenderWatcher */);
    ...
    callHook(vm, "mounted");
}

如果是RenderWatchervm._watcher会用它赋值:

var Watcher = function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;                    //关联组件
    if (isRenderWatcher) {
        vm._watcher = this;
    }
    vm._watchers.push(this);
    ...
    this.before = options.before;
    ...
    if (typeof expOrFn === "function") {
        this.getter = expOrFn;        //即 vm._watcher.getter = updateComponent
    }
    this.value = this.lazy ? undefined : this.get();    //this.get 中会调用 this.getter,所以 new Watcher 就立即调用 updateComponent
}

watcher会在组件渲染的过程中把接触过的数据属性记录为依赖。之后当依赖的值发生改变,触发依赖的setter方法时,会通知watcher,从而使它关联的组件(vm)重新渲染。
一旦侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。
等当前事件循环结束,下一次事件循环开始,Vue会刷新队列并执行已去重的工作。Vue会尝试使用Promise.thenMutationObserversetImmediate发布的微任务来执行queue中的watcher

function flushSchedulerQueue () {
    queue.sort(function (a, b) { return a.id - b.id; });    //queue 是在 Vue 构造函数中的声明的变量
    ...
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        if (watcher.before) {
            watcher.before();        //执行 beforeUpdate 钩子函数
        }
        id = watcher.id;
        has[id] = null;
        watcher.run();    //执行 watcher
        ...
    }
    ...
    // call component updated and activated hooks
    callActivatedHooks(activatedChildren.slice());    //执行 activated 钩子函数
    callUpdatedHooks(queue.slice());        //执行 updated 钩子函数
}

刷新前根据 idqueue 中的 watcher 进行排序。这样可以确保:

watcher排在子watcher前,组件从父级更新到子级。(因为父母总是在子级之前创建,所以id更小);

在一个组件中,用户声明的watchers总是在render watcher之前执行,因为user watchers更先创建;

如果在父组件的watcher运行期间,销毁了某个子组件,可以跳过该子组件的watcher

在执行watcher.run方法之前,会执行watcher.before方法,从而执行beforeUpdate钩子函数。

updated

在执行watcher.run方法时,会调用watcher.getter方法,而其中某个watcher(vm._watcher)关联的就是我们的vm,它的getter是可以更新vmupdateComponent方法:

Watcher.prototype.run = function run () {
        if (this.active) {
            var value = this.get();        //调用 watcher.get 方法
            ...
        }
        ...
}
Watcher.prototype.get = function get () {
        ...
        try {
            value = this.getter.call(vm, vm);    //调用 watcher.getter 方法
        }
        ...
}

调用updateComponent方法

updateComponent = function () {
    vm._update(vm._render(), hydrating);
};

vm._render方法会重新执行render函数生成vnode,然后vm._update方法会将vnode转化为真实Dom,挂载到HTML中,并覆盖vm.$el
等以上操作全部完成,在flushSchedulerQueue函数的最后会执行子组件的activated钩子函数和vmupdated钩子函数:

function flushSchedulerQueue () {
    ...
    callActivatedHooks(activatedChildren.slice());    //执行 activated 钩子函数
    callUpdatedHooks(queue.slice());        //执行 updated 钩子函数
}

function callUpdatedHooks (queue) {
    var i = queue.length;
    while (i--) {
        var watcher = queue[i];
        var vm = watcher.vm;
        if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
            callHook(vm, "updated");    //执行 updated 钩子函数
        }
    }
}

updated钩子函数中通过this.$el访问到的vm.$el属性的值为更新后的真实Dom
beforeUpdateupdated钩子函数的执行顺序真好相反,因为在flushSchedulerQueue函数中是索引递增处理queue中的watcher的,所以执行beforeUpdate钩子函数的顺序和queuewatcher的顺序相同;而在callUpdatedHooks函数中是按索引递减的顺序执行_watcher关联实例的updated钩子的,和queue_watcher顺序相反。
再加上父watcher排在子watcher前,所以如果父、子组件在同一个事件循环中更新,那么生命周期钩子的执行顺序为:
beforeUpdate => 子beforeUpdate => 子updated => 父updated

beforeDestroy

调用vm.$destroy销毁vm实例:

Vue.prototype.$destroy = function() {
    var vm = this;
    if (vm._isBeingDestroyed) {
        return
    }
    callHook(vm, "beforeDestroy");
    vm._isBeingDestroyed = true;
    
    // remove self from parent
    var parent = vm.$parent;
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
        remove(parent.$children, vm);
    }
    
    // teardown watchers
    if (vm._watcher) {
        vm._watcher.teardown();
    }
    var i = vm._watchers.length;
    while (i--) {
        vm._watchers[i].teardown();
    }
    
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
        vm._data.__ob__.vmCount--;
    }
    
    // call the last hook...
    vm._isDestroyed = true;
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null);
    // fire destroyed hook
    callHook(vm, "destroyed");
    // turn off all instance listeners.
    vm.$off();
    // remove __vue__ reference
    if (vm.$el) {
        vm.$el.__vue__ = null;
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
        vm.$vnode.parent = null;
    }
};

在调用beforeDestroy钩子前未进行销毁操作,所以在这一步,实例仍然完全可用。

destroyed

vm.$destroy执行的操作有

删除vm.$parent.$children中的vm

销毁vm._watcher(渲染 watcher),销毁vm._watchers[i]中的所有watcher

删除数据 observer 中的引用;

调用destroyed钩子函数;

...

其中vm.__patch__(vm._vnode, null)可以销毁所有子实例。

Vue 生命周期流程图

Vue 父子组件生命周期钩子执行顺序

父子组件挂载过程:父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted

子组件被keep-alive组件包裹(忽视keep-alive组件),父子组件挂载过程:父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 子activated => 父mounted

只修改父组件或子组件的数据:beforeUpdate => updated

在同一事件循环中修改父子组件的数据(无论先后):父beforeUpdate => 子beforeUpdate => 子updated => 父updated

父组件将数据传给子组件的一个 prop,且它们分别是父、子组件的依赖,在修改父组件的数据时:父beforeUpdate => 子beforeUpdate => 子updated => 父updated

子组件的v-show指令绑定父组件的数据,在修改父组件的数据时:父beforeUpdate => 父updated,子组件保持mounted状态不变;

子组件的v-show指令绑定父组件的数据,子组件被keep-alive组件包裹,在修改父组件的数据时:父beforeUpdate => 父updated,子组件保持activated状态不变;

子组件的v-if指令绑定父组件的数据,在修改父组件的数据时:

true => false: 父beforeUpdate => 子beforeDestroy => 子destroyed => 父updated

false => true: 父beforeUpdate => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父updated

子组件的v-if指令绑定父组件的数据,子组件被keep-alive组件包裹,在修改父组件的数据时:

true => false: 父beforeUpdate => 子deactivated => 父updated

首次 false => true: 父beforeUpdate => 子beforeCreate => 子created => 子beforeMount => 子mounted => 子activated => 父updated

再次 false => true: 父beforeUpdate => 子activated => 父updated

子组件的is属性绑定父组件的数据,父组件将子组件一切换为子组件二:
beforeUpdate => 子二beforeCreate => 子二created => 子二beforeMount => 子二mounted => 父beforeUpdate => 子一beforeDestroy => 子一destroyed => 父updated => 父updated

子组件的is属性绑定父组件的数据,子组件被keep-alive组件包裹,父组件将子组件一切换为子组件二:

首次:父beforeUpdate => 父beforeUpdate => 子二beforeCreate => 子二created => 子二beforeMount => 子一deactivated => 子二mounted => 子二activated => 父updated => 父updated

再次:父beforeUpdate => 子一deactivated => 子二activated => 父updated

动态组件触发两次父beforeUpdateupdated的原因:
在第一次事件循环只触发了一次父组件的_watcher,在调用 render函数重新生成父组件vnode的过程中:

var render = function() {    //Vue 编译 template 而来的 render 函数
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c("img", {
        attrs: { alt: "Vue logo", src: require("./assets/logo.png") }
      }),
      _c("p", [_vm._v(_vm._s(_vm.message))]),
            //被 keep-alive 组件包裹的情况,在生成 keep-alive 组件的 vnode 时,第二次触发了父组件的`_watcher`
      _c("keep-alive", [_c(_vm.now, { tag: "component" })], 1)
            //不被 keep-alive 组件包裹的情况,在生成子二组件的`vnode`时,第二次触发了父组件的`_watcher`
            _c(_vm.now, { tag: "component" })
        ],
    1
  )
}

其实 keep-alive 组件情况更具体一点,也是在生成 keep-alive 组件的孩子,子二组件的vnode时触发的_watcher
然后这个watcher会被插到queue中当前wacther的后面(根据 wacther.id的大小插入正确的位置):

function queueWatcher(watcher) {
    var id = watcher.id;
    if (has[id] == null) {    //在 flushSchedulerQueue 中,执行 watcher.run 之前,已经令 has[id] = null;
        has[id] = true;        //所以同 id 的 wacther 可以被插入 queue 中
        if (!flushing) {
            queue.push(watcher);
        } else {
            // if already flushing, splice the watcher based on its id
            // if already past its id, it will be run next immediately.
            var i = queue.length - 1;
            while (i > index && queue[i].id > watcher.id) {
                i--;
            }
            queue.splice(i + 1, 0, watcher);
        }
    }
    ...
}

等当前watcher.run执行完,再执行它。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106940.html

相关文章

  • 详解vue生命周期

    摘要:注意看下此时还是没有选项钩子函数和间的生命周期在这一阶段发生的事情还是比较多的。钩子函数和钩子函数间的生命周期当发现中的数据发生了改变,会触发对应组件的重新渲染,先后调用和钩子函数。 首先,每个Vue实例在被创建之前都要经过一系列的初始化过程,这个过程就是vue的生命周期。首先看一张图吧~这是官方文档上的图片相信大家一定都会很熟悉: showImg(https://segmentfau...

    svtter 评论0 收藏0
  • Vue 实例中的生命周期钩子详解

    摘要:实例在文档中经常会使用这个变量名表示实例,在实例化时,需要传入一个选项对象,它可以包含数据模板挂载元素方法生命周期钩子等选项。通俗说就是实例从创建到销毁的过程,就是生命周期。 Vue 实例中的生命周期钩子 Vue 框架的入口就是 Vue 实例,其实就是框架中的 view model ,它包含页面中的业务处理逻辑、数据模型等,它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程...

    gityuan 评论0 收藏0
  • 实例化vue发生了什么?(详解vue生命周期)

    摘要:实例化发生了什么详解生命周期本文将对的生命周期进行详细的讲解让你了解一个实例的诞生都经历了什么我在上建立了一个存放笔记的仓库以后会陆续更新一些知识和项目中遇到的坑有兴趣的同学可以去看看哈欢迎传送门实例化一个这是一个方法触发钩子函数组件实例刚 实例化vue发生了什么?(详解vue生命周期) 本文将对vue的生命周期进行详细的讲解,让你了解一个vue实例的诞生都经历了什么~ 我在Githu...

    pcChao 评论0 收藏0
  • 详解 mpvue 小程序框架 及和原生的差异

    摘要:在这一步,实例已完成以下的配置数据观测,属性和方法的运算,事件回调。可以直接写等标签的写法之前会的工程师上手框架的成本较低 简介 1.美团工程师推出的基于Vue.js封装的用于开发小程序的框架2.融合了原生小程序和Vue.js的特点3.可完全组件化开发 特点 1.组件化开发2.完成的Vue.js开发体验(前提是熟悉Vue)3.可使用Vuex管理状态4.Webpack构建项目5.最终H5...

    IamDLY 评论0 收藏0
  • Vue生命周期

    摘要:和下面手动调用在控制台中输入在这个阶段会销毁实例,生命周期结束。外部实例中的函数显示的效果参考链接组件的生命周期详解生命周期 为什么要认识Vue的生命周期 Vue的生命周期是一个非常重要的点,如果不懂Vue的生命周期,那么很多时候,就不知道Vue的实际渲染时机,程序中会出现各种bug。 因此,学习Vue的生命周期是非常用必要的。 showImg(https://segmentfault...

    y1chuan 评论0 收藏0

发表评论

0条评论

snowLu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<