感觉一章节让我对 Vue 内部的原理更加深入的了解了,如果说前面的响应式系统、虚拟 DOM 的实现是 Vue 的精髓之一的话,这部分实例代码的实现让我对 Vue 组件实例的理解更为深刻,深入 Vue 的代码骨架之中。

# 概述

Vue 实例中有许多方法,本博客记录这些实例方法的实现原理

在 Vue 源码中的 src/core/instance/index.js 中,有这么一段代码:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue)// 初始化相关
stateMixin(Vue)// 数据相关
eventsMixin(Vue)// 事件相关
lifecycleMixin(Vue)// 生命周期相关
renderMixin(Vue)// 渲染相关
export default Vue

这里其实是定义了 Vue 的构造函数,然后分别调用 initMixin、stateMixin、eventsMixin 等函数,实际上就是向 Vue 的原型中挂载方法。

例如:

export function initMixin (Vue) {
  Vue.prototype._init = function (options){
    //...
  }
}

# 与事件相关的实例方法

在上面我们说到,在上面我们调用了 eventsMixin (Vue) 来在 Vue 的原型上挂载方法。使得每个 Vue 实例都能够调用这些方法:

vm 为某 Vue 实例

  • vm.$on
  • vm.$off
  • vm.$once
  • vm.$emit
export function eventsMixin(Vue){
  Vue.prototype.$on = function(event,fn){
    ...
  }
  ...
}

想了下,还是不放实现代码了,只说说我读到的一些想法。

这四个方法的实现实际上有点类似于 Node 中的 EventEmitter,实现方法也有点点类似,但是细节上有些许出入。

思路大概是这样的,在新建 Vue 实例的时候,会初始化一个对象用来存放事件相关的东西:

vm._events = Object.create(null);

# vm.$on

这个方法用于监听当前实例上的自定义事件,实现代码如下:

/**
     * 
     * @param {string | Array<string>} event 
     * @param {Function} fn callback
     * @description 给传入的 event 注册事件回调
     */
    function $on(event,fn){
        if(Array.isArray(event)){
            for(let i = 0,j=event.length;i<j;i++){// 防止中途长度改变了
                this.$on(event[i],fn);
            }
        }else{
            (this._events[event] || (this._events[event] = [])).push(fn);
        }
    }

其实就是,以事件名为_events 的属性名,将函数注册进去。$on 方法也支持多个事件注册回调。

# vm.$off

这个方法用于移除自定义事件监听器。

  • 如果没有提供参数,移除所有事件监听器
  • 只提供了事件参数,移除该事件所有监听器
  • 同时提供两个参数,则移除对应的
/**
     * 
     * @param {string | Array<string>} event 
     * @param {Function} fn 
     * @description 移除自定义事件监听器
     */
    function $off(event,fn){ 
        // 第一种情况
        if(!arguments.length){
            this._events = Object.create(null);
            return this;
        }
        if(Array.isArray(event)){
            for(let i = 0,l = event.length;i<l;i++){
                this.$off(event[i],fn);
            }
            return this;
        }
        // 第二种情况
        const cbs = this._events[event];
        if(!cbs){
            return this;
        }
        if(arguments.length === 1){
            this._events[event] = null;
            return this;
        }
        // 第三种情况
        if(fn){
            const cbs = this._events[event];
            let cb;
            let i = cbs.length;
            while(i--){
                cb = cbs[i];
                if(cb === fn || cb.fn === fn){
                    //fn 属性,用来下面 once 特殊执行 off,因为 once 注册的事件监听器并不是原来的函数
                    cbs.splice(i,1);
                    break;
                }
            }
        }
        return this;
    }

这里为什么我们还要检测注册的回调的 fn 属性呢?

因为下面我们就要讲到,once 的实现方式其实是在外包装了一下原来传入的回调函数,我们通过将 fn 函数设置为包装后的回调函数的 fn 属性,用来对比是否是对应的监听器。

# vm.$once

和 on 方法的效果差不多,但是只触发一次,触发之后移除。

/**
     * 
     * @param {string | Array<string>} event 
     * @param {Function} fn 
     * @description 监听一个自定义事件,但是只触发一次后就移除
     */
    $once(event,fn){
        function on(){
            this.$off(event,on);// 解除后执行
            fn.apply(this,arguments);
        }
        on.fn = fn;
        this.$on(event,on);
        return this;
    }

其实我们就是使用了一个函数来包装原回调监听器,执行后移除,并且设置该包装函数的 fn 属性为原回调监听器,和上面 off 的实现原理相呼应。

# vm.$emit

这个函数触发当前实例上的事件。附加参数都会传给监听器回调。

/**
     * 
     * @param {string} event 
     * @param {...args} 
     * @description 触发当前实例上的某个事件,附加参数都会传给监听器回调
     */
    $emit(event){
        let cbs = this._events[event];
        if(cbs){
            const args = [...arguments].slice(1);
            for(let i = 0,i = cbs.length;i<l;i++){
                try{
                    cbs[i].apply(this,args);
                }catch(e){
                    handleError(e,this,`event handler for ${event}`);
                }
            }
        }
        return this;
    }

# 生命周期相关的实例方法

主要是四个方法:vm.mountvm.mount、vm.forceUpdate、vm.nextTickvm.nextTick、vm.destroy。

其中 vm.forceUpdatevm.forceUpdate和vm.destroy 是在 lifecycleMixin 中挂载到 Vue 构造函数的原型上的。

# vm.$forceUpdate

这个方法的作用是迫使 Vue 实例重新渲染。前面我们也说过 Vue2 的响应式系统,在 Vue2 之中,更新粒度为组件级别,一个组件实例对应一个组件级别 Watcher 实例,实际上,只要调用组件对应的 watcher 的 update 方法即可。

# vm.$destroy

这个方法的作用很明显,就是完全销毁一个实例。

它做的事情大概有以下几个:

  1. 先判断是否已经销毁,已经销毁不需重复销毁
  2. 触发 Vue 实例的生命周期 beforeDestroy 钩子。
  3. 清理当前组件实例和父组件之间的联系(Vue 实例的 $children 属性存储了所有子组件)
  4. 销毁实例上的所有 watcher(包括组件级别的 watcher 和用户通过 $watch 方法创建的 watcher)
  5. 给 Vue 实例添加_isDestroyed 属性来表示已经被销毁
  6. 触发 Vue 实例的生命周期 destroyed 钩子。
  7. 移除实例上的所有事件监听器。

具体就是这七步,然后我们结合源代码来看:

Vue.prototype.$destroy = function () {
    const vm: Component = this
    //1. 判断
    if (vm._isBeingDestroyed) {
      return
    }
  	//2. 触发
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    //3. 清理自己和父组件的联系
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    //4. 清理组件级别的 watcher(存在_watcher)
    if (vm._watcher) {
      vm._watcher.teardown()
    }
  	// 清理用户创建的 watcher
    let 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--
    }
    // 表示已经被销毁
    vm._isDestroyed = true
    // 触发 destroy 钩子函数解绑指令
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // 移除所有事件监听器
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }

# vm.$nextTick

它接收一个回调函数作为参数,然后在下次 DOM 更新周期之后执行。

面向场景:更新了状态后有时需要对新 DOM 做一些操作时。

# 前置知识

在说这个 API 的原理之前,需要先说说 Vue 的一些特性。

在 Vue 之中,当状态发生变化,会通知依赖这个状态的所有 watcher,然后触发虚拟 DOM 渲染流程。在 watcher 触发渲染这个操作并不是同步的,它是异步的。Vue 在内部有一个队列 —— 异步更新队列,每当需要渲染时,就将要渲染的 watcher 推送到这个队列,下一次事件循环再统一清空队列。

好处就是能够减少重复,组件的 watcher 要是再一轮事件循环中多次收到通知需要渲染,实际上只需一次渲染。

事件循环的话这里不再仔细说了,前面其他博客也讲过很多次,可以参考我之前的博客。

# API 本身

这个 API 有几个特性:

  • 回调执行前反复调用,也只会添加一个任务
  • 当任务触发,依次执行

vm.$nextTick 和 Vue.nextTick 是一样的,所以我们直接说 Vue.nextTick 的原理

# Vue2.4 之前

const callbacks = [];
let pending = false;
function flushCallbacks(){
	pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for(let i = 0;i<copies.length;i++){
		copies[i]();
  }
}
let microTimerFunc;
const p = Promise.resolve();
microTimerFunc = () => {
  p.then(flushCallbacks);
}
export function nextTick(cb,ctx){// 回调函数和执行环境
	callbacks.push(() => {
		if(cb)cb.call(ctx);
  });
  if(!pending){
		pending = true;
    microTimerFunc();
  }
}

这段代码有几个要点:

  • pending 变量用于防止反复添加任务到微任务队列中,一轮事件循环只会添加一次。
  • Vue2.4 版本之前,nextTick 方法使用微任务,因为微任务优先级较高,可能会出现一些问题

# Vue2.4 之后

正所谓在 2.4 之前都使用微任务,后来 Vue 提供了强制使用宏任务的方法。

具体代码就不贴了,和之前有几个区别:

  • 利用了一个变量来判断是否使用宏任务
  • 新增了一个函数 withMacroTask,给回调函数做了一层包装,让更新 DOM 操作推到宏任务队列中。
  • 优先使用 setImmediate,然后 MessageChannel、setTimeout
  • 如果浏览器不支持 Promise,则降级成宏任务添加

# 全局 API

全局 API 和实例方法不太一样,后者是在 Vue 的原型上挂载方法,而前者是直接在 Vue 上挂载方法。

如:

Vue.extend = function(options){
	...
}
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

orange 微信支付

微信支付

orange 支付宝

支付宝

orange 贝宝

贝宝