阅读 228

从源码学习vue生命周期整个流程

u=3447993151,3924355308&fm=26&gp=0.png

初始化实例属性

image.png

  • vue是通过initLifecycle函数来向实例中挂载属性的
export function initLifecycle(vm) {
    const options - vm.$options;
    
    //找出第一个非抽象的父类
    let parent = options.parent;
    if(parent && !options.abstract) { 
        while(parent.$options.abstract && parent.$parent) {// 如果父级是抽象类,直到遇到第一个非抽象类的父级时候,赋值给vm.$parent
            parent = parent.$parent;
        }
        parent.#children.push(vm);
    }
    
    vm.$parent = parent;
    vm.$root = parent? parent.$root: vm; // 表示的当前组件树的根vue实例
    
    vm.children = []; // 表示包含当前实例的直接子组件
    vm.$refs = {};
    
    vm._watcher = null;
    vm._isDestroyed = false;
    vm._isBeingDestroyed = false;
}
复制代码

初始化事件

  • 根据上面的流程图所表示:vue是通过 initEvents 函数来执行初始化事件的
export function initEvents(vm) {
    vm._events = Object.create(null);
    // 初始化父组件附加的事件
    const listrens = vm.$options.parentListeners;
    if(listrens) {
        updateComponentListenners(vm,listeners)
    }
}

// updateComponentListenners 函数源码
 let target;
 function add(event,fn,once) {
     if(once) {
         target.$once(event,fn);

     }else {
         target.$on(event,fn);
     }
 }

function remove(event,fn) {
    target.$off(event,fn);
}
export function updateComponentListenners(vm,listeners,oldListeners) {
    target = vm;
    updateListenners(vm,listeners|| {}, add, remove,fn);// 如果listenners对象中存在某个key,早oldListeners中不存在,那么说明这个事件是需要新增的事件
}
 
// updateListenners  函数

function isUndef(v){
    return v === undefined || v === null
}
export function updateListenners(on,oldOn,add,remove,vm) {
    let name ,cur,old, event;
    for (name in on) {
        cur = on[name];
        old = oldOn[name];
        event = normalizeEvent(name);
        if(isUndef(cur)) { //判断该事件名对应的值是否undefined或者null,如果是,则在控制台触发警告
           process.env.NODE_ENV !== 'production' && warn(`Invalid handler for event '${event.name}':got` + String(cur),vm);
        } else if (isUndef(old)) { // 判断该事件名在oldOn中是否存在,如果不存在,则调用add注册事件
          if (isUndef(cur.fns)) {
            cur = on[name] = createFnInvoker(cur);
          }
          add(event.name, cur, event.once, event.capture, event.passive) 
        } else if (cur !== old) { // 如果都存在,但是他们并不相同,则将事件回调替换成on中的回调  
          old.fns = cur;
          on[name] = old;
        }
      }
      for (name in oldOn) {
        if (isUndef(on[name])) {
          event = normalizeEvent(name)
          remove(event.name, oldOn[name], event.capture)
        }
    }

}
复制代码

初始化状态

  • 初始化状态包括props、methods、data、conputed、watch
graph TD
initState --> initProps
initState --> initMethods
initState --> initData
initState --> initComputed
initState --> initWatch
// initState源码
initState function initState(vm) {
    vm._watchrs = [];
    const opts = vm.$options;
    if(opts.props) initProps(vm.opts.props); // 如果vm.$options中存在props属性,如果存在,则调用props
    if (opts.methods) initMethods(vm,opts.methods);// 如果存在methods属性,则调用初始化methods 
    if (opts.data) {
        initData(vm);
    } else {
        observe(vm._data = {}, true /* asRootData */);// 如果不存在.则直接使用observe函数观察空对象
    }
    if(opts.computed) initComputed(vm,opts.computed);
    if(opts.wathch && opts.watch !== nativeWatch) {
        initWathch(vm,opts.wathch);
    }

}
复制代码

1.初始化props

  • 父组件提供数据,子组件通过props字段选择自己需要那些内容

规格化的props源码:

function normalizeProps(options,vm) {
    const props = options.props;
    if(!props) return; // 如果没有props属性,说明用户没有使用props接收任何数据,那么不需要规格化,直接返回
    const res = {};
    let i,val,name;
    if(Array.isArray(porps)) { // 如果检测为数组,则通过while循环数组的每一项,将Array类型的props规格化成object类型
     i = props.length;
     while(i--) {
         val = props[i];
         if(typeof val === 'string') { 
             name = camelzie(val);
             res[name]= {type: null};
         }else if(process.env.NODE_ENV !== 'production') { // 在非生产环境下打印警告
         
             warn('poprs must be strings when using array syntax');
         }
     }
    
    } else if(isPlanObjcet(props)) { // 规格化的props的类型有可能是基础函数,也有可能是数组,
        for(cosnt key in props) {
            val = props[key];
            name = camelize(key);
            res[name] = isPlanObject(val)?val: {type:val}
        }
    
    } else if(process.env.NODE_ENV !== 'production') {
         
         warn(`Invalid value for option "props":expected an Array or an Object`,`+ but got ${toRawType(props)}.`,vm);
   }
    options.props =res; // 返回规格化的结果
}
复制代码

2.初始化methods

  1. 遍历选项中的methods对象,.并将每个属性依次挂载到vm上即可
function initMethods (vm,methods) {
    const props - vm.$options.props;
    for (const key in methods) {
        if(process.env.NODE_ENV !== 'production') {
            if (methods[key] == null) { // 在非生产环境下,校验方法是否合法,并在控制台发出警告
                warn(
                `Method "${key}" has an undefined value in the compontent definition.` + `Did you reference the function correctly?`,vm);
            }
        }
        if (props && hasOwn(props, key)) {
            warn(
            `Method "${key}" has alreay been defined as a props.`,vm);
        }
        if ((key in vm) && isReserved(key)) {
            warn(
            `Method "${key}" conflicts with an existing Vue instance methods .` +
            `Avoid defining component methods that start with_or $.`,vm);
        } 
    }
    vm[key] = methosd[key] == null ? noop :bind(methods[key],vm)// 将方法挂载到vm[key]中
}
复制代码

4.初始化computed

  • 我们知道的计算属的结果会被缓存,只有在所依赖的响应式属性的返回值发生变化时候,才会重新去计算
  • 看如下图关系

未命名文件.png

  • 计算属性computed源码
const computedWatcherOptions = {lazy, true};
function initComputed(vm,computed) {
        const watcher = vm._computedWatcher = Object.create(null); // 注意,创建出来的对象没有原型,他不存在_proto_属性
    // 计算属性在ssr中,只是一个普通的getter方法
    const isSSR = isServerRending();
    for (const key in computed) {
        const userDef = computed[key];
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        if (process.env.NODE_ENV !== 'production' && getter == null) {
            warn(`Getter is missing for computed property "${key}"`);
        }
        // 在非ssr中,为计算属性创建内部观察器
        if(!isSSR) {
            watcher[key] = new Watcher(
                vm,
                getter || noop,
                noop,
                computedWatcherOptions
            );
        }
        if (!(key in vm)) {
            defineComputed(vm, key, userDef);

        } else if (process.env.NODE_ENV !== 'production') {
            if(key in vm.$data) {
                warn(`The ....“${key} in data `, vm);
            } else if(vm.$options.props && key in vm.$options.props) {
                warn(`The ....“${key} in prop `, vm);
            }
        }
    }
    
}
// initComputed函数作用是初始化计算属性, vm是vue实例上下文(this)
复制代码

初始watch

// initWatch接收2个参数,一个vm,一个watch对象,
function initWatch(vm,watch) {
    for(const key in watch) {
        const handler = watch[key];
        if(Array.isArray(handler)) {
            for(let i =0; i<handler.length;i++) {
                createWatcher(vm,key,hnadler);
            }
        } else {
           createWatcher(vm,key,handler);
        }
    
    }
}

//createWatcher函数重要负责处理其他类型的handler并调用vm.$watch划船机watcher观察表达式

function createWatcher(vm,expOrFn,handler,optons) {
   if(isPlainObject(handler)) { // 如果包含了对象,设置一个特殊包含的对象,因此options的值设置为handler;并将handler对象的handler方法
      options = handler; 
      handler = handler.handler;
   }
   if(type handler === 'string') {
       handler = vm[handler];
   }
   return vm.$watch(expOrFn,handler,optons);   
}
// vm:vue执行上下文
// expOrFn: 表示式或计算属性函数
// handler watch对象的值
// options: 用于传递给vm.$watch的选项对象

复制代码

钩子函数

  • 和回调函数一个意思,当系统执行到某处的时候,检测是否有hook(钩子),有的话就执行回调

vue生命周期可以分为8个阶段:

  • beforeCreate 实例创建前:

    new Vue()之后触发的第一个钩子,在当前阶段中data、methods、computed以及watch上的数据和方法均不能被访问。

  • created 实例创建完成:

    在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果你非要想,可以通过vm.$nextTick来访问Dom。

  • beforeMount 挂载前:

    在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated

  • mounted 挂载完成: 在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。也可以向后台发送请求,拿到返回数据

  • beforeUpdate 更新前:

    在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染

  • updated 更新完成:

    当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新

  • beforeDestory 销毁前: 当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

  • destoryed 销毁完成:

    这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁

new Vue()调用的时候发生了什么

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)
}
exprot default Vue

//1. 在上面中可得知,首先进行了安全的检查,在非生产环境下,如果没有使用new 来调用vue,这会在控制台抛出错误警告我们:vue是构造函数,应该使用new关键字来调用
// 2.声明周期的初始化流程在this._init中实现
复制代码

_init 方法的内部原理

Vue.prototype._init = function (options) {
    vm.$options = mergeOptons(
        resloveConstructorOptions(vm.construtor),
            options||{},
            vm
        );
}
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm,'beforeCreate');
initInjections(vm);// 在data/props前初始化inject
initState(vm);
initProvide(vm);// 在data/props后初始化provide
callHook(vm,'created');

//如果用户在实例化vue时候传递了el选项,则自动开启模板编译阶段与挂载阶段
// 如果用户在传递el选项.这不进入下一个生命周期流程
// 用户在需要执行vm.$mount方法,手动开启模板编译阶段与挂载阶段
if(vm.$options.el){
    vm.$mount(vm,$options.el)
}

复制代码

钩子函数源码

1.beforeCreate与created

// src/core/instance/init
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    // 合并选项部分已省略
    
    initLifecycle(vm)  
    // 主要就是给vm对象添加了 $parent、$root、$children 属性,以及一些其它的生命周期相关的标识
    initEvents(vm) // 初始化事件相关的属性
    initRender(vm)  // vm 添加了一些虚拟 dom、slot 等相关的属性和方法
    callHook(vm, 'beforeCreate')  // 调用 beforeCreate 钩子
    //下面 initInjections(vm) 和 initProvide(vm) 两个配套使用,用于将父组件 _provided 中定义的值,通过 inject 注入到子组件,且这些属性不会被观察
    initInjections(vm) 
    initState(vm)   // props、methods、data、watch、computed等数据初始化
    initProvide(vm) 
    callHook(vm, 'created')  // 调用 created 钩子
  }
}

// src/core/instance/state
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

// 1.可以看到beforeCreate钩子调用是在initState之前的,而从上面的第二段代码我们可以看出initState的作用是对props、methods、data、computed、watch等属性做初始化处理。
//2. 在beforeCreate钩子的时候没有对props、methods、data、computed、watch上的数据的访问权限。在created中才可以
复制代码

2.beforeMounted与mounted


// mountComponent 核心就是先实例化一个渲染Watcher
// 在它的回调函数中会调用 updateComponent 方法
// 两个核心方法 vm._render(生成虚拟Dom) 和 vm._update(映射到真实Dom)
// src/core/instance/lifecycle
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    ...
  }
  callHook(vm, 'beforeMount')  // 调用 beforeMount 钩子

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
    // 将虚拟 Dom 映射到真实 Dom 的函数。
    // vm._update 之前会先调用 vm._render() 函数渲染 VNode
      ...
      const vnode = vm._render()
      ...
      vm._update(vnode, hydrating)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判断是否 mouted 完成 并且没有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')  //调用 mounted 钩子
  }
  return vm
}
//在执行vm._render()函数渲染VNode之前,执行了 beforeMount钩子函数,在执行完 vm._update() 把VNode patch到真实Dom后,执行 mouted钩子。也就明白了为什么直到mounted阶段才名正言顺的拿到了Dom

复制代码

beforeUpdate和updated

  // src/core/instance/lifecycle
 new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判断是否 mouted 完成 并且没有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')  // 调用 beforeUpdate 钩子
      }
    }
  }, true /* isRenderWatcher */)
 
 // src/core/observer/scheduler 
 function callUpdatedHooks (queue) {
   let i = queue.length
   while (i--) {
     const watcher = queue[i]
     const vm = watcher.vm
     if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
       // 只有满足当前 watcher 为 vm._watcher(也就是当前的渲染watcher)
       // 以及组件已经 mounted 并且没有被 destroyed 才会执行 updated 钩子函数。
       callHook(vm, 'updated')  // 调用 updated 钩子
       }
     }
   }
   
   
   // src/instance/observer/watcher.js
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    // 在它的构造函数里会判断 isRenderWatcher,
    // 接着把当前 watcher 的实例赋值给 vm._watcher
    isRenderWatcher?: boolean
  ) {
    // 还把当前 wathcer 实例 push 到 vm._watchers 中,
    // vm._watcher 是专门用来监听 vm 上数据变化然后重新渲染的,
    // 所以它是一个渲染相关的 watcher,因此在 callUpdatedHooks 函数中,
    // 只有 vm._watcher 的回调执行完毕后,才会执行 updated 钩子函数
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    ...
}
复制代码

#beforeDestroy和destroyed

 // src/core/instance/lifecycle.js
  // 在 $destroy 的执行过程中,它会执行 vm.__patch__(vm._vnode, null)
  // 触发它子组件的销毁钩子函数,这样一层层的递归调用,
  // 所以 destroy 钩子函数执行顺序是先子后父,和 mounted 过程一样。
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')  // 调用 beforeDestroy 钩子
    vm._isBeingDestroyed = true
    // 一些销毁工作
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // 拆卸 watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    ...
    vm._isDestroyed = true
    // 调用当前 rendered tree 上的 destroy 钩子
    // 发现子组件,会先去销毁子组件
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')  // 调用 destroyed 钩子
    // 关闭所有实例侦听器。
    vm.$off()
    // 删除 __vue__ 引用
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 释放循环引用
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
复制代码

总结:

文章分类
前端
文章标签