深入浅出vue.js 第十三章 实例方法与全局API的实现原理

294 阅读5分钟

上一章介绍了Vue.js内部的整体结构,知道了它会向构造函数添加一些属性和方法。本章中,我们将详细介绍它的实例方法和全局API的实现原理

在Vue.js内部,有这样一段代码

    import {initMixin} from './init'
    import {stateMixin} from './state'
    import {renderMixin} from './render'
    import {eventsMixin} from './events'
    import {lifecycleMixin} from './liftcycle'
    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)
    renderMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    export default(Vue)

其中定义了Vue构造函数,然后分别调用了initMixin,stateMixin,eventMixin,lifecycleMixin和renderMixin这五个函数,并将Vue构造函数当做参数传给了5个函数。

这5个函数的作用是向Vue的原型中挂载方法。

13.1 数据相关的实例方法

与数据相关的实例方法有3个,分别是vm.watch,vm.watch,vm.set,vm.$delete,它们是在stateMixin中挂载到Vue的原型上的,

import {set,del} from '../observer/index'
export function stateMixin(vue){
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.Prototype.$watch = function (expOrFn,cb,options){}
}

当stateMixin被调用时,会向Vue构造函数的prototype属性挂载这个3个实例方法

13.2 事件相关的实例方法

与事件相关的实例方法:vm.on,vm.on,vm.once,vm.offvm.off和vm.emit。当eventMixin被调用时,会添加这个4个实例方法

13.2.1 vm.$on

vm.$on(event,callback)

这个和v-on 好像没关系,别搞混了。

用法 监听当前实例上的自定义事件,事件可以由vm.$emit触发。回调函数会接收所有传入事件所触发的函数的额外参数

vm.$on('test',function(msg){
  console.log(msg)
})
vm.$emit('test','hi')
// => 'hi'

vm._events 是一个对象,用来存储事件。在代码中,我们使用事件名(event)从vm_events中取出事件列表,如果列表不存在,则使用空数组初始化,然后再将回调函数添加到事件列表中

这样事件就注册好了,vm._events哪来的?事实上,在执行new Vue()时,Vue会执行this.init 方法进行一系列初始化操作,其中Vue.js的实例上创建了一个_events属性,用来存储事件

vm.events = Object.create(null)

13.2.2 vm.$off

vm.$off([event,callback])

// 移除所有事件监听器
if(!arguments.length){
    vm._events = Object.create(null)
    return vm
}

this.offvm.off 和vm.off 是同一个方法,vm是this的别名

13.2.3 vm.$once

vm.$once(event,callback) 用法:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器

13.2.4 vm.$emit

vm.$emit(event,[...args]) 触发当前实例上的事件。附加参数都会传递给监听器回调

所有事件监听器回调函数都会存储在vm.events中,所以触发事件的实现思路是使用事件名从vm.events中取出对应的事件监听器回调函数列表。然后依次执行列表中的监听器回调并将参数传递给监听器回调。

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

与生命周期相关的实例方法有4个,分别是vm.mount,vm.mount,vm.forceUpdate,vm.nextTickvm.nextTick和vm.destroy

vm.forceUpdatevm.forceUpdate和vm.destroy是lifecyleMixin挂载的

vm.$nextTick 方法是从renderMixin挂载的

vm.$mount 是跨平台代码中挂载到Vue构造函数的prototype属性上的

vm.$forceUpdate

作用就是迫使Vue.js实例重新渲染

Vue.prototype.$forceUpdate = function(){
    const vm = this
    if(vm.watcher){
       vm._watcher.update()
    }
}

vm.watcher 就是Vue.js实例的watcher,每当组件内依赖的数据发生变化时,都会自动触发Vue.js实例中_watcher 的update方法

vm.$forceUpdate是手动通知Vue.js实例重新渲染

vm.$destroy

Vue.js 实例的$children属性存储了所有子组件

vm.$nextTick

nextTick 接收一个回调函数作为参数,它的作用是将回调延迟到下次DOM更新周期之后执行。

它与全局方法Vue.nextTick一样,不同的是回调的this自动绑定到调用它的实例上。如果没有提供回调且在支持promise的环境中,则返回一个Promise

new Vue({
  methods:{
    example:function(){
      this.message = 'changed'
      // DOM还没更新
      this.$nextTick(function(){
      //DOM 闲杂更新了
      //this 绑定到当前实例
        this.doSomethingElse()
      })
    }
   }
 })

有个问题: 下次DOM更新周期之后执行,是什么时候呢? 当状态发生变化时,watcher触发渲染的操作不是同步的,而是异步的。

1. 为什么Vue.js使用异步更新队列

react的setData()也是这个原理吧

Vue.js中有个队列,每当需要渲染时,会将watcher推送到这个队列中,在下一次事件循环中再让watcher触发渲染流程

如果在同一轮事件循环中有两个数据发生了变化,那么组件的watcher会收到两份通知,从而进行两次渲染。事实上,并不需要渲染两次,虚拟DOM会对整个组件进行渲染,所以只需要等所有状态修改完毕后,一次性将整个组件的DOM渲染到最新即可

要解决这个问题,收到通知watcher实例添加到队列中缓存起来,并判断队列是否已经存在相同的watcher。然后在下一次事件循环中,会让队列watcher触发渲染。这样保证了即便在同一事件循环中有两个状态发生改变,watcher最后也执行一次渲染流程

2.什么是事件循环

我们都知道JavaScript是一门单线程且非阻塞的脚本语言。

微任务:

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick 宏任务
  • setTimeout
  • setInterval
  • setImmediate
  • MessageChannel
  • requesAnimationFrame
  • I/O
  • UI 交互事件

3. 什么是执行栈

当我们执行了一个方法时,JavaScript会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中有这个方法的私有作用域,上层作用域的指向,方法的参数,私有作用域中定义的变量以及this对象。这个执行环境会被添加到一个栈中,这个栈就是执行栈

new Vue({
  methods: {
    example:function(){
      // 先使用nextTick 注册回调
      this.$nextTick(function(){
        //DOM 没有更新
      })
      // 然后修改数据
      this.message = 'changed'
    }
  },
})

new Vue({
  methods: {
    example:function(){
      // 先使用setTimeout向宏任务中注册回调
    setTimeout(_ => {
      // DOM 现在更新了
    },0)
      // 然后修改数据向微任务中注册回调 ---- watcher的更新是微任务,异步更新的
      this.message = 'changed'
    }
  },
})

微任务优先级太高,也可能出现问题。也可强制使用宏任务的方法

vm.$mount

vm.$mount([elementOrSelector])

如果Vue.js实例在实例化时没有收到el选项,则它处于“未挂载”状态,没有关联的DOM元素。我们可以使用vm.$mount手动挂载一个未挂载的实例。

13.4 全局API的实现原理

现在我们已经了解了Vue.js实例方法的内部原理,接下来将介绍全局API的内部原理

13.4.1 Vue.extend

Vue.extend(options) 用法: 使用基础Vue构造器创建一个"子类",其参数是一个包含”组件选项“的对象。data选项是特例,在Vue.extend()中,它必须是函数:

var Profile = Vue.extend({
  template:'<p>{{firstName}}{{lastName}} aka {{alias}}',
  data:function(){
    return {
      firstName:'Walter',
      lastName:'Whilte',
      alias:'Heisenberg'
    }
  }
})
// 创建Profile实例,并挂载到一个元素上
new Profile().$mount('#mount-point')

全局API和实例方法不同,后者是在Vue的原型上挂载方法,也就是Vue.prototype上挂载方法,而前者是直接在Vue上挂载方法。

Vue.extend = function(extendOptions){
// 做点什么
}

总体来说,其实就是创建了一个Sub函数并继承了父级。如果直接使用Vue.extend,则Sub继承于Vue构造函数

13.4.2 Vue.nextTick

Vue.nextTick([callback,context])

vm.msg = 'Hello'
// DOM还没有更新
Vue.nextTick(function(){
 // DOM 更新了
})

// 作为一个Promise 使用
Vue.nextTick().then(function(){ // DOM 更新了})

13.4.3 Vue.set

Vue.set(target,key,value) Vue.set和vm.$set 的实现原理相同

是Vue和组件的区别吗 为啥要设置两个呢?

13.4.4 Vue.delete

Vue.delete(target,key) 上同

13.4.5 Vue.directive

注册或获取全局指令 Vue.directive(id,[definition])

Vue.directive('my-directive',{
  bind:function(){},
  inserted:function(){},
  update:function(){},
  componentUpdated:function(){},
  unbind:function(){}
})
// 注册(指令函数)
Vue.directive('my-directive',function(){
  // 这里将会被bind和update调用
})
// getter 方法,返回已注册的指令
var myDirective = Vue.directive('my-directive')

虽然代码复用和抽象的主要形式是组件,但是有些情况下,仍需对普通DOM元素进行底层操作,这时就会用到自定义指令

13.4.6 Vue.filter

注册或获取全局过滤器 Vue.filter(id,[definition])

Vue.filter('my-filter',function(value){
 // 返回处理后的值
})

// getter 方法,返回已注册的过滤器
var myFilter = Vue.filter('my-filter')

过滤器可以用在两个地方:双花括号插值和v-bind表达式

13.4.7 Vue.component

Vue.component(id,[definition]) 注册或获取全局组件。注册组件时,还会自动使用给定的id设置组件的名称。

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component',Vue.extend({ }))
// 注册组件,传入一个选项对象(自动调用Vue.extend)
Vue.component('my-component',{})
// 获取注册的组件(始终返回构造器)
var MyComponent = Vue.component('my-component')

13.4.8 Vue.use

Vue.use(pulgin) 安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作用参数出啊如。install方法被同一插件多次调用时,插件也只会被安装一次

13.4.9 Vue.mixin

Vue.mixin(mixin) 全局注册一个混入(mixin),影响注册之后的每个Vue.js实例.插件作者可以使用混入向组件注入自定义行为(例如:监听生命周期钩子)。

// 自定义的选项myOption注入一个处理器
Vue.mixin({
  created:function(){
    var myOption = this.$options.myOption
    if(myOption){
      console.log(myOption)
    }
  }
})

new Vue({
  myOption:'hello!'
})
// =>'hello!'

Vue.mixin方法注册后,会影响之后创建的没个Vue.js实例,因为该方法会更改Vue.options属性

其实原理并不复杂,只是将用户传入的对象与Vue.js自身的options属性合并在一起,


import { mergeOptions} from '../util/index'
export function ininMixin(Vue){
  Vue.mixin = function(mixin){
    this.options = mergeOptions(this.options,mixin)
    return this
  })
}

因为mixin方法修改了Vue.options属性,而之后创建的没个实例都会用到该属性,所以会影响创建的没个实例。但也正是因为有影响,所以mixin在某些场景下才堪称神器

13.4.10 Vue.compile Vue.compile(template) 编译模板字符串返回包含渲染函数的对象。只在完整版中才有效

var res = Vue.compile('<div><span>{{msg}}</span></div>')

new Vue({
  data:{
    msg:'hello'
  },
  render:res.render
})

总结

本章中,我们详细介绍了Vue.js的实例方法和全局API的实现原理。它们的区别在于:实例方法是Vue.prototype上的方法,而全局API是Vue.js上的方法。

实例方法又分为数据,事件和生命周期这三个类型。

在介绍实例方法以及全局API的实现原理的同时,我们还介绍了扩展知识,例如在介绍vm.$nextTick时我们介绍了JavaScript事件循环机制,以及微任务和宏任务之间的区别等