Vue.js 框架剖析

Vue.js 框架剖析

希望借助此次分享,帮助大家理解在使用 Vue 进行项目开发时,遇到的各种问题,了解 Vue 内部的运行原理,合理进行模块划分设计,从而更加全面的了解 Vue 的开发模式。

前段时间有幸在技术中心筹办的前端训练营中,做过一期 Vue.js 框架剖析的课程,感兴趣的小伙伴可以移步至内网 Elearnig 视频版本 查看完整版本。

本次分享将会从课程中挑选几个比较有意思的部分展开,如: Vue.js 数据劫持方案、依赖收集原理等。

一、Vue.js 是什么?

可能各位看到这个标题会觉得很好笑,为什么要说这么简单的问题?

不着急解释,我们先来看一下 Vue.js 官方给的解释。

​ Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。

​ 另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动

这里的介绍只是很简单的四句话,我们来看一下段落里的三个关键字,分别是:渐进式框架、核心库只关注视图层、为复杂的单页应用提供驱动。

这里的渐进式比较有意思,因为这个特点完全是与其他框架对比得来的。

用过 Angular 的同学应该知道,如果你选择使用它,不管是哪个版本,你必须使用它的模块机制、依赖注入、特殊形式定义组件等。

而 Vue.js 在开发模式上并没有较强的主张,你可以用很低的学习成本开始你的 Vue.js 开发之旅。

核心库只关注视图层这部分也是渐进式的一种体现,Vue.js 核心模块只关注数据到视图部分,较为简单纯粹,不至于臃肿。

可能前两个关键字会特别强调 Vue.js 的弱主张与小巧,最后一句为复杂的单页应用提供驱动则体现了借助于 Vue.js 的生态,小巧的设计也能发挥出的巨大作用。

二、为什么要做框架剖析?

相信很多小伙伴都使用过 Vue.js 进行项目开发,在开发时我们会遇到很多问题,可能最后问题解决了,却不知道它们内在的原因是什么,比如为什么在 created 阶段无法获取到dom,而 mounted 阶段就可以。

因此第一个目的就是帮助大家理解,在使用VUE进行项目开发时,遇到的问题

另外一个目的是:希望通过对几个典型模块的剖析,帮助大家了解 Vue.js 的运行原理

在我们常规的 Vue.js 项目开发时,绝大多数时候都只是在进行业务模块开发。

如何脱离项目运行环境,提升某个 Vue.js 模块为公共模块,如何创建自定义指令,如何合理的进行模块划分设计?这些都需要我们去了解。

第三是目的是:借助此次分享,帮助大家全面了解 Vue.js 的开发模式

以上三点是我们这次分享的主要目的。

三、本次分享主要有哪几点?

TOC大纲

  1. 常见双向绑定实现方案介绍
  2. Vue.js 数据劫持方案原理
  3. 依赖收集原理
  4. 纵览 Vue.js 初始化过程
  5. keep-alive 使用及其原理
  6. 自定义指令开发
  7. 组件复用方式

后面的内容较多,局部篇幅可能较长,您可以根据下面的介绍选择性的阅读。

前三部分会着重介绍几种常见的双向绑定实现的原理,以及展开来看 Vue.js 在双向绑定方面的实现原理。并且会附带介绍为了提升双向绑定的效率,减少不必要重绘而实现的依赖收集。

第四部分会从 Vue 构造类在实例化的过程的角度,来重新梳理各个生命周期。

第五部分介绍一个使用频率比较高,甚至可能被滥用的keep-alive,通过使用方法介绍及实现原理分析帮助大家重新审视 keep-alive 组件。

后两部分介绍两个比较实用的东西,当我们需要开发更通用的 Vue.js 组件时必须了解的知识点。

3.1、常见双向绑定实现方案介绍

双向绑定这个词大家都不陌生,然而在理解上可能会存在稍许的偏差。这里必须明确一个概念:什么是双向绑定

关于双向绑定的理解可能比较多,最简单的解释就是:视图与数据之间的联动,无需手动绑定事件,即可完成数据、视图间的相互联动。

常见的双向绑定实现模式有以下三种:

  • 发布者-订阅者模式
  • 脏值检查
  • 数据劫持

无论是前端最早 MVC 框架 knockout,还是后来的 backbone、ember、angular、react、vue,都是这三种方案的具体应用,下面我们来一一分析。

3.1.1、发布者-订阅者模式

这是比较传统的视图更新方式,借助 sub、pub 的方式实现数据和视图的绑定监听,knockout、backbone.js 都是这种方案。

更新数据方式通常做法是vm.set('property', value)。这种方式不太方便,特别在处理复杂的数据结构时尤为痛苦。

我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是就衍生出了下面两种方式。

3.1.2、脏值检查

前面说的发布者-订阅者模式虽然使用起来略微蹩脚,但是他有一个天然的优势。

就是通过set方法来更新数据,使得每一次数据变动都能被捕捉得到。而我们一但使用vm.property = value的方式更新数据,想要监听数据变动就变得比较困难了。

这里以 angular.js 为例,它使用了脏值检测的方式比对数据是否有变更,来决定是否更新视图。只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

3.1.3、数据劫持

数据劫持是一种较为优雅的实现方案,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

这部分先不展开聊,在下一部分会详细介绍。

整体实现较为简洁,从使用者角度来看,它实现了vm.property = value的方式更新数据,从框架实现者的角度,它避免了脏值检查中繁杂的事件监听。

3.2、Vue.js 数据劫持方案原理

前面提到的三种方案,VUE采用的是最后一个数据劫持的方案来实现双向绑定。

那么 VUE 的数据劫持原理是什么呢?

  • 通过Object.defineProperty()来劫持各个属性的setter,getter
  • 引入发布者-订阅者模式
  • 在数据变动时发布消息给订阅者,触发相应的监听回调。

其实 VUE 的双向绑定本质上是数据劫持与发布者-订阅者模式结合的方式来实现的。

接下来我们来看第一个问题:

3.2.1、Object.defineProperty 有什么作用?

Vue.js 的响应式原理依赖于 Object.defineProperty,这一点很重要,这也是Vue.js不支持IE8 以及更低版本浏览器的原因。

defineproperty-caniuse.png

Object.defineProperty方法有以下特性:

  • 允许精确添加或修改对象的属性。
  • 定义属性值是否可被更改
  • 定义属性是否出现在对象的枚举属性中
  • 定义获取、设置时的拦截

最后一点很重要,Object.defineProperty 支持拦截回调的设置,这是 VUE 数据劫持的基础。

当然Object.defineProperty方法只是实现技术基础,并不足以构建数据到视图的双向绑定。

想要实现这些,我们还需要抽象出observer观察模块、compile解析模块、watcher监听模块这三个重要的模块。

3.2.2、数据劫持代码逻辑

前面提到的只是Object.defineProperty方法的原理以及 VUE 相关抽象的描述,可能听起来不太直观。

我们来看一段精简后的示例代码,为了描述方便我给代码分为四段:

// part 1
function defineReactive (obj, key, val, cb) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            /*....依赖收集等....*/
            console.log(`获取${key}的属性,值为:${JSON.stringify(val)}`)
            return val
        },
        set:newVal=> {
            console.log(`设置${key}的属性,旧值为:${JSON.stringify(val)},新值为:${JSON.stringify(newVal)}`)
            val = newVal;
            /*订阅者收到消息的回调*/
            cb();
        }
    })
} 
// part 2
function observe(value, cb) {
    Object.keys(value).forEach((key) => defineReactive(value, key, value[key], cb))
}

// part 3
class Vue {
    constructor(options) {
        this._data = options.data;
        observe(this._data, options.render)
    }
}

// part 4
let app = new Vue({
    el: '#app',
    data: {
        name: 'demo',
        message: 'i am a robot!'
    },
    render(){
            console.log("数据发生变动,开始渲染视图!")
    }
})

这段代码可以直接复制下来执行,你可以先在浏览器的控制台中执行一遍,尝试看下 app 的数据结构。

我们从下往上,分别看一下这四段代码:

part-4:这段代码是我们使用 VUE 最常写的方法之一,通过它来实例化 VUE 组件。因为是示例代码,这里的 el 并没有意义。

part-3:这段代码是精简后的 VUE 构造类,并没有做过多的设计,this._data 接收传递进来的参数,再调用observe 方法进行包装一下就完事了。

part-2observe 方法的实现也非常简单,首先通过Object.keys枚举this._data的属性,再逐个使用defineReactive方法进行包装。

part-1:这段代码是整个例子的核心,你会发现整个方法里就只有一个Object.defineProperty ,前面 「3.2.1、Object.defineProperty 有什么作用?」部分有对这个方法做过详细的介绍。

现在你可以再次把上面的代码复制下来,在浏览器的控制台中执行一遍,然后再依次执行下面的代码。

// 单独执行下面两行代码
console.log(app._data.name)

app._data.message = 'hello world!'

通过观察控制台输出,与代码中对应位置做对比,应该可以更加方便的理解数据劫持原理。

3.2.3、数据代理实现

不知道你有没有注意到上面代码里的一个问题?

例子里的代码,并不能直接通过app.message获取到i am a robot,而必须借助于app._data.message

这与我们平时使用 VUE 开发时的体验式完全不一样的,为什么呢?

因为通过【part 3】的代码可以看出来,所有的数据都是挂载在this._data上的,所以我们无法直接通过app获取到数据。

要解决这个问题,其实也很简单,我们再来看一段代码。

// 代理
function _proxy (data) {
    const that = this;
    Object.keys(data).forEach(key => {
        Object.defineProperty(that, key, {
            configurable: true,
            enumerable: true,
            get: function proxyGetter () {
                return that._data[key];
      },
      set: function proxySetter (val) {
          that._data[key] = val;
      }
    })
    });
}

// part 3
class Vue {
    constructor(options) {
        this._data = options.data;
        _proxy.call(this, options.data);/*构造函数中*/
        observe(this._data, options.render)
    }
}
  

这段代码有两部分,分别是代理实现方法,【part 3】改进版。

代理实现方法不做过多解释,相信大家一眼就能看到Object.defineProperty 方法。

我们可以这样理解:Vue 实例本身并不存储数据,它仅仅提供了和数据同样结构的获取、设置数据接口,通过枚举、包装,实现数据代理。

3.3、依赖收集原理

现在说第三部分,依赖收集原理,在详细展开之前我们要弄明白,依赖收集指的是什么?

先看下这段代码:

new Vue({
    template: 
        `<div>
            <span>name: {{name}} </span>
            <span>message: {{message}}</span>
        <div>`,
    data: {
        name: 'demo',
        message: 'i am a robot',
        from: 'Earth'
    }
})

这是很常见的一个 VUE 模块,数据定义了namemessagefrom 三个字段,但是模版中仅仅是用了其中两个。

按照前面介绍的数据劫持方案, from 字段在实际模板中虽然没有被用到,但是当 from 的数据被修改(this.from = ‘test')的时候,同样会触发fromsetter导致整个模版重新执行渲染,这显然不正确。

如何避免修改 from 时,模版不必要的渲染,这就是依赖收集要做的事情。

3.3.1、依赖收集类

从前面的描述可以看出来,依赖收集对于提升渲染稳定性有着很重要的作用。

下面这段代码是依赖收集类的实现,可能和我们预期的不太一样,显得过于简单了。

class Dep {
    constructor () {
        this.subs = [];
    }
    addSub (sub: Watcher) {
        this.subs.push(sub)
    }
    removeSub (sub: Watcher) {
        remove(this.subs, sub)
    }
    notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}
function remove (arr, item) {
   …
}

这里可以看出来,Dep类的数据只包含一个依赖引用数组,三个方法分别是添加、移除依赖,以及通知依赖,也就是订阅者进行更新。

依赖收集之所以实现看起来很单薄,因为它最的复杂逻辑散落在 Vue 实例化的过程中。

3.3.2、依赖收集过程

想要知道模版引用了哪些数据其实并不简单,因为复杂的模版定义使得借助静态解析的方式来做依赖收集变得不太现实。

好在 VUE 的数据劫持方案可以精确地捕获到数据的设置、获取动作。

当我们对data上的对象进行取值操作的时候,自然就会触发getter事件,所以我们只要在渲染最开始的时候,对模版进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到。在对data中的数据进行修改的时候,我们只要保证setter会触发被依赖的函数即可。

VUE 构造类改造

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data, options.render);
        let watcher = new Watcher(this);
    }
}
function observe(value, cb) {
    Object.keys(value).forEach((key) => defineReactive(value, key, value[key], cb))
}
function defineReactive (obj, key, val, cb) {
    /*在闭包内存储一个Dep对象*/
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            if (Dep.target) {
                /*Watcher对象存在全局的Dep.target中*/
                dep.addSub(Dep.target);
            }
        },
        set:newVal=> {
            /*只有之前addSub中的函数才会触发*/
            dep.notify();
        }
    })
}

Dep.target = null;

Watcher类

class Watcher {
    constructor (vm, expOrFn, cb, options) {
        this.cb = cb;
        this.vm = vm;
                // 全局标记    
        Dep.target = this;
        this.cb.call(this.vm);
    }

    update () {
        this.cb.call(this.vm);
    }
}

上面是 Vue、Watcher两个类,再加最前面的 Dep 一共是三个类。

先来看一下 Watcher 这个类,Watcher是一个订阅者对象,可以简单理解是为更新模版的一个订阅对象。

经过Dep 在渲染时的串联,在第一次模版渲染结束后,依赖收集就已经完成了,如果有依赖的话,闭包内实例化dep就已经收集到了watcher 对象

如果在第一次模版渲染结束后,未收集到任何依赖的话,第六步通知dep去更新就不会有任何反应,也就起到了开头所说的,避免修改未被引用的数据时,模版不必要的渲染,

3.4、纵览 Vue.js初始化过程

我们在使用 VUE 进行开发的时候会比容易好奇,在 new 一个 Vue 对象的时候,内部究竟发生了什么?

带着这个疑问,我们从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)
}

VUE 的构造类非常简单,复杂的逻辑全部在init方法内部。

VUE init 函数

Vue.prototype._init = function (options?: Object) {
    …
    // expose real self
    vm._self = vm
    /*初始化生命周期*/
    initLifecycle(vm)
    /*初始化事件*/
    initEvents(vm)
    /*初始化render*/
    initRender(vm)
    /*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    /*初始化props、methods、data、computed与watch*/
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    /*调用created钩子函数并且触发created钩子事件*/
    callHook(vm, 'created')
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      /*格式化组件名*/
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }
    if (vm.$options.el) {
      /*挂载组件*/
      vm.$mount(vm.$options.el)
    }
  }
}

VUE 在 init 阶段主要做了这了以下两件事:

  • 初始化生命周期、事件、render函数、state等
  • 挂载组件,实现组件和 dom 的连接

VUE 在生命周期早些阶段,也就是 beforeCreate 与 created 之间,会初预先通过 _self 标记自己。

在此过程中,会依次初始化props、methods、data、computed 与 watch,这也是 Vue.js 对 options 中的数据进行“响应式化”(即双向绑定)的过程。

生命周期.png

这张图是从官网引入过来的,它完整地描述了 VUE 生命周期的各个阶段。

这里有两个特别有意思的生命周期,就是destroybeforeDestroy。这是两个非常有用的生命周期钩子,很多项目的内存泄漏、逻辑紊乱都是由于这个生命周期钩子没有被很好的使用引起的。

2015年的时候我曾写过一篇文章,叫《对象的自我销毁》,那时候 Vue 还没火起来,文章主要针对的是原生组件开发时需要注意的事项,但是放在 Vue 组件开发中,逻辑依旧是通用的。

这里列举几个需要使用到destroybeforeDestroy场景,供大家参考。

一: DOM事件监听越界

常规情况下,一个组件需要监听的仅仅是自身的DOM。偶尔也会有另一种情况,对象不得不操作自身之外的DOM。

拿常见的瀑布流组件为例,除了自身事件,还要监听页面的滚动、浏览器尺寸重置等事件。因此当瀑布流组件被移除后,遗留的对window的事件监听还在,事件监听回调内对组件的引用会导致整个组件常驻内存无法被回收,直至页面关闭。

二:JS 生命周期过长

一部分场景下,某段 JS 会在整个生命周期中反复被调用。比如轮播图自动播放,倒计时时钟的重绘,或者是支付状态轮询。无论是使用setInterval不断调取,或者是 setTimeout 递归延时。这两者在组件被移除时同样不会随之被清除,因此也需要组件在被销毁时手动解除定时器。

三:DOM 之外的异步事件

比较常见的情形就是 API 请求。当一个请求结束之前组件被销毁, API 数据返回后的操作无需继续进行。也有一定风险因为数据已被移除导致操作报错。

3.5、keep-alive 使用及其原理

相信各位在使用 VUE 进行开发的时候,在一些特定场合肯定有使用过 keep-alive,比如配合 router 实现本地化的视图模块。

然而直接配合 router 却不加以合理规划,很容易导致 keep-alive 被滥用。

<keep-alive>
    <component></component>
</keep-alive>

在分析之前先看一下,keep-alive 是什么?

keep-alive是Vue.js的一个内置组件。它能够将未激活的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。

这段介绍同样是摘录自 VUE 官网,这里有两点很重要:

第一:keep-alive 是一个组件

第二:他不会被渲染到真实 DOM 中

既然 keep-alive 是一个组件,那么它的使用以及特性,都需要按照组件的特点来分析。

3.5.1、keep-alive的使用

props属性
keep-alive组件提供了includeexclude两个属性来允许组件有条件地进行缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。

<!-- 缓存name为a的组件 -->
<keep-alive include="a">
  <component></component>
</keep-alive>

<!-- name为a的组件将不会被缓存。 -->
<keep-alive exclude="a">
  <component></component>
</keep-alive>

生命钩子
keep-alive为下级组件提供了两个生命钩子,分别是activated与deactivated。
因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,需要用activated与deactivated这两个生命钩子来得知当前组件是否处于活动状态。

3.5.2、keep-alive组件的实现

{
  created () {
      /* 缓存对象 */
      this.cache = Object.create(null)
  },
  destroyed () {
          /* destroyed钩子中销毁所有cache中的组件实例 */
      for (const key in this.cache) {
          pruneCacheEntry(this.cache[key])
      }
  }
}

前面提到 keep-alive 是一个组件,这一点很重要,因为他的实现完全基于组件的特性。

created阶段会创建一个cache对象,用来作为缓存容器,保存vnode节点。

destroyed阶段则在组件被销毁的时候清除cache缓存中的所有组件实例。

render () {
    /* 得到slot插槽中的第一个组件 */
    const vnode: VNode = getFirstComponentChild(this.$slots.default)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
        /* 获取组件名称,优先获取组件的name字段,否则是组件的tag */
        const name: ?string = getComponentName(componentOptions)
        /* name不在inlcude中或者在exlude中则直接返回vnode(没有取缓存) */
        if (name && (
          (this.include && !matches(this.include, name)) ||
          (this.exclude && matches(this.exclude, name))
        )) {
            return vnode
        }
        const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
        /* 如果已经做过缓存了则直接从缓存中获取组件实例给vnode,还未缓存过则进行缓存 */
        if (this.cache[key]) {
            vnode.componentInstance = this.cache[key].componentInstance
        } else {
            this.cache[key] = vnode
        }
        /* keepAlive标记位 */
        vnode.data.keepAlive = true
    }
    return vnode
}

前面还提到另一点比较重要,就是 keep-alive 虽然是一个组件,但它不会被渲染到 dom 中。

keep-alive 组件在渲染是采用嫁接的方式,借助子组件进行渲染。

首先通过 getFirstComponentChild 获取第一个子组件。这一点很直观地说明了,为什么 keep-alive 只支持单个子节点的缓存。

接下来获取子组件的name,存在组件名则直接使用组件名,否则会使用tag。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功说明不需要进行缓存,则不进行任何操作直接返回vnode。

匹配成功,则根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。

最后再返回vnode。

整个 keep-alive 的过程就算完成了。

3.6、自定义指令开发

指令是 VUE 模块开发最关键,最重要的一环,官方 api 自带的指令提供了非常丰富、非常方便的方式,将常见的编码场景进行提炼,使用这些指令能在编程中令人感到愉悦。

这里就不一一说明了,大家可以参考API来学习使用。

  • v-text
  • v-html
  • v-show
  • v-if
  • v-else
  • v-else-if
  • v-for
  • v-on
  • v-bind
  • v-model
  • v-pre
  • v-cloak
  • v-once

Vue 默认提供的指令已经可以应付绝大多数场景了,然而有时也需要我们自己去创造一些指令

举个例子,你在开发一个组件,类似于百度首页,组件默认界面上只有一个搜索框,进入界面光标必须聚焦在搜索框内,这个需求你会怎么做?

这个需求有很多种方法都能满足,比如在生命周期钩子 mounted 之后获取搜索框元素,执行获取焦点,再或者使用 autofocus 属性。

第一种略显繁琐,第二种又存在很多 BUG,这时指令就是一个比较好的方法。

指令的定义有两种,一种是全局定义,整个项目中都能用,另一种是局部定义,只能在特定 vievModel 中用。

我们来看下代码,这两种定义方式代码本质上是一样的,都是在 dom 被插入到父级节点时,执行 focus 方法。

使用起来非常简单,只需要在元素上增加 v-focus 即可。

// 全局定义
Vue.directive('focus', {
  inserted: function (el) {
    el.focus()
  }
})
// 局部定义
directives: {
  focus: {
    inserted: function (el) {
      el.focus()
    }
  }
}
<!-- 使用指令 -->
<input v-focus>

指令定义对象可使用钩子

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

借助于这些指令钩子,可以实现很多好用的指令,如loading、clickOutside、fixLayout等效果。

3.7、组件复用方式

3.7.1、局部注册

常规的组件复用可以采用传统的局部注册的方式,实现起来简单又好用。

var ComponentA = { /* ... */ }
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA
  }
})
<div id="app">
  <component-a></component-a>
</div>

3.7.2、全局注册

如果某个模块足够通用,比如弹框、按钮,再使用局部注册的方式会显得格外繁琐,因此就会有全局注册的方案。

Vue.component('component-a', {
  // ... 选项 ...
})
<div id="app">
  <component-a></component-a>
</div>

3.7.3、组件打包方案

可能我们经常在使用 VUE 第三方类库的时候,常用Vue.use(xxx)的模式,这种又是如何实现的呢?

还是不猜了,直接看看 Vue 时如何实现的。

Vue.use = function (plugin: Function | Object) {
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  if (installedPlugins.indexOf(plugin) > -1) {
   return this
  }
  // additional parameters
  const args = toArray(arguments, 1)
  args.unshift(this)
  if (typeof plugin.install === 'function') {
   plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
   plugin.apply(null, args)
  }
  installedPlugins.push(plugin)
  return this
}

从源码中我们可以看出来,Vue 会预先判断这个插件是否被注册过,不允许重复注册。

并且接收的plugin参数的限制为 Function、Object 两种类型,对于这两种类型分别作了两种处理。

这里暂且不做细致的分析,只看if (typeof plugin.install === 'function') {这一行。这里处理的就是 Vue 推荐的插件打包格式,下面看个例子。

// 定义 confirm 组件
const confirm = {
    // ....
}
// 定义 dialog 组件
const dialog = {
    // ....
}
// 定义 message 组件
const message = {
  // ...
}
// 定义 focus 指令
const focusDirective = {
  inserted: function (el) {
    el.focus()
  }
}
// 导出插件
export {
    confirm,
  dialog,
  message,
  focusDirective,
  // 提供给 Vue.use 的 install 方法
  install () {
        Vue.component('confirm', confirm)
        Vue.component('dialog', dialog)
        Vue.component('message', message)
    Vue.directive('focus', focusDirective)
    }
}

上面的例子几乎是 Vue 插件开发的一种范式,即对外提供局部引用的支持,也提供了Vue.use一键安装。

当你需要开发较为通用的模块时,可以考虑使用插件定义的方式来实现。

四、结语

随着前端工程的复杂度越来越高,Vue 也越来越多的被项目开发所采用,希望借助此次分享,能够帮助大家更好的理解 Vue 开发。

回顾下此次分享介绍了哪些内容:

  1. 常见双向绑定实现方案介绍
  2. Vue.js 数据劫持方案原理
  3. 依赖收集原理
  4. 纵览 Vue.js 初始化过程
  5. keep-alive 使用及其原理
  6. 自定义指令开发
  7. 组件复用方式

相关链接:内网《Vue 框架剖析》Elearnig 视频版本

添加新评论

我们会加密处理您的邮箱保证您的隐私. 标有星号的为必填信息 *