实现一下Vue全局API

251 阅读3分钟

Vue全局API

Vue.js内部初始化时会依次调用:

  • initMixin
  • stateMixin
  • renderMixin
  • eventsMixin
  • lifycleMixin

initMixin

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

initMixin实现了初始化方法,包括生命周期的流程和响应式的流程启动。

stateMixin

相关的API,有三个$watch$set$delete

$watch

参数:

参数 类型
expOrFn String、Function
callback Function、Object
options Object
deep boolean
immediate boolean

deep 监听对象内部值变化。

immediate以当前值触发回调

用法

用于观察变量或computed函数在Vue实例中的变化。执行回调函数时会得到新数据(newVvalue)和旧数据(oldValue)。

返回值

一个unwatch函数,用于取消watch

var unwatch = vm.&watch('a',(newval,oldval)=>{})
//取消观察
unwatch()

内部原理

Vue.prototype.$watch=function(expOrFn,callback,options){
    const vm = this
    options = options || {}
    const watch = new Watcher(vm,expOrFn,callback,options)
    if(options.immediate){
        callback.call(vm,watch.value)
    }
    return function unwatch(){
        watcher.teardown()
    }
}

第一步好了。先讲一下new Watcher这个,Watcher是Vue收集依赖的功能我之前写过可以翻一下。immediate为true时使用callcallback.call(vm,watch.value)以当前值触发回调,没有旧值就不传了。watcher.teardown(),这个是取消依赖这个功能我没写,通过teardown去除绑定在变量aDep中的Watcher

这里大概实现以下unwatch这个功能

首先Watcher要记录自己订阅了谁,删除时遍历找到,将它从指定变量的Dep中删除即可。

在Watcher中添加一个addDep功能。

Watcher中
export default class Watcher{
    constructor (vm,expOrFn,cd){
        this.vm = vm
        this.deps = []//订阅列表
        this.depIds = new Set()
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    
    ...
    
    addDep(dep){
        const id = dep.id
        if(!this.depIds.has(id)){
            this.depIds.add(id)
            this.deps = push(dep)
            dep.addSub(this)
        }
    }
}

Dep中
let uid =  0 
export default class Dep{
    constructor (){
       this.id = uid ++ //绑定唯一值,(ES6可以直接用Symbol)
       this.subs = []
    }
    
    ...
}

我们使用depIds判断Watcher是否已经订阅了这个dep。如果没有这个判断每次Watcher读取数据时会触发get收集依赖逻辑导致依赖多次收集,每次会多次通知。添加后只有第一次触发会收集这个依赖。

好了收集完依赖我们实现unwatch的功能

 Watcher中
 export default class Watcher{
   ...
   unwatch(){
     let i = this.deps.length
     while(i--){
         this.deps[i].removesSub(this)
     }
   }
 }
 
 Dep中
 export default class Dep{
     ...
     removeSub(sub){
         const index = this.subs.indexOf(sub)
         if(~index){
            return this.subs.splice(index,1)
         }
     }
 }

好了unwatch就完成了,我们继续实行deep

Vue原理无非是依赖的收集与触发,没开启deep我们只触发这个expOrFn变量的依赖,而开启deep后我们只有递归遍历expOrFn变量触发它所有子类的依赖就可以了。

Watcher中
 export default class Watcher{
     constructor (vm,expOrFn,cd){
         this.vm = vm
         
         if(options){
             this.deep = !!options.deep
         }else{
             this.deep = false
         }
         
         this.deps = []//订阅列表
         this.depIds = new Set()
         this.getter = parsePath(expOrFn)
         this.cb = cb
         this.value = this.get()
     }
     
     ...
     
     get(){
       window.target = this
       let value = this.getter.call(vm , vm)
       
       if(this.deep){
           traverse(value)
       }
       
       window.target = undefined
       return value
     }
 }

重点来了traverse函数。注意一定要在window.target = undefined之前调用。

const seenObject = new Set()

export function traverse(val){
    _traverse(val,seenObject)
    seenObject.clear()
}

function _traverse(val,seen){
    let i,keys
    const isA = Array.isArray(val)
    if((!isA && !isObject(val)) || Object.isFeozen(val)){
        return
    }
    if(val._ob_){
        const depId = val._ob_.dep.id
        if(seen.has(depId)){
            return
        }
        seen.add(depId)
    }
    if(isA){
        i= val.length
        while (i--) _traverse(val[i] , seen)
    }else{
        keys = Object.keys(val)
        i = keys.length
        while(i--)_traverse(val[key[i],seen)
    }
}

好了我们一步一步理解

const isA = Array.isArray(val)
if((!isA && !isObject(val)) || Object.isFeozen(val)){
    return
}

判断val值是否是数组或对象,或者是被冻结的对象(不可扩展,所有属性都是不可配置的)。如果是直接跳出
if(val._ob_){
    const depId = val._ob_.dep.id
    if(seen.has(depId)){
        return
    }
    seen.add(depId)
}

获取每个Dep的唯一值id,确保不会重复收集依赖
if(isA){
      i= val.length
      while (i--) _traverse(val[i] , seen)
    }else{
      keys = Object.keys(val)
      i = keys.length
      while(i--)_traverse(val[keys[i]],seen)
}

如果是数组循环数组其中每一项递归调用_traverse。如果是对象,使用Object.keys获取键值,递归调用所有子类。
他们在val[i]或val[keys[i]]时会触发getter收集依赖流程,会将window.target中保存的依赖收集。
好了,deep的原理就是遍历循环对象中所有包括自己的子类,添加一个watcher的依赖到各自的Dep中。(所以尽量不要深度监听渲染列表用的数据或循环引用的变量,会爆栈的)

$set

参数:

参数 类型
target Object、Array
key string、number
value any

用法

因为ES6前无法元编程,对象添加变量或删除变量是无法监听到的,所以我们使用$set来对象添加新属性,并转换为响应式的。

返回值

一个unwatch函数,用于取消$set,往上面看功能一样。

情况1: target是个数组

export function set(target,key,value){
    if(Array.isArray(target) && isValidArrayIndex(val)){
        target.length = Math.max(target.length,key)
        target.splice(key,1,val)
        return val
    }
}
//是否是个有效的有序数组
function isValidArrayIndex (val) {
 var n = parseFloat(String(val));
 //Math.floor(n) === n验证是否是整数 
 return n >= 0 && Math.floor(n) === n && isFinite(val)
}

如果是个有效的数组,先改变数组长度,在使用splice插入值(数组使用了代理的方法在原型链中插入并修改了原生方法push、slice、pop、shift、unshift等,用于收集删除依赖于元素)

情况2: 已存在target

export function set(target,key,value){
    if(Array.isArray(target) && isValidArrayIndex(val)){
        target.length = Math.max(target.length,key)
        target.splice(key,1,val)
        return val
    }
    
    if(key in target && !(key in Object.prototype)){
        target[key] = val
        return val
    }
}
在已经有的情况下,直接修改参数。会直接触发`setter`从而触发自身所有依赖。

情况3: 新增

export function set(target,key,value){
    if(Array.isArray(target) && isValidArrayIndex(val)){
        target.length = Math.max(target.length,key)
        target.splice(key,1,val)
        return val
    }
    
    if(key in target && !(key in Object.prototype)){
        target[key] = val
        return val
    }
    
    const ob = target._ob_
    if(target._isVue || (ob && ob.vmCount)){
        process.env.NODE_ENV !== 'prodection' && warn(...)
        return val
    }
    if(!ob){
        target[key] = val
        return val
    }
    defineReactive(ob.value,key,val)
    ob.dep.notify()
    return val
}

好了,我们在一步一步理解一下

const ob = target._ob_
if(target._isVue || (ob && ob.vmCount)){
   process.env.NODE_ENV !== 'prodection' && warn(...)
   return val
}

如果是响应式数据那么会有_ob_属性,target不能是Vue实例或根元素(Vue.$data)
不能是正式环境,防止了线上版本被恶意修改导致不可预计事件发生。

if(!ob){
    target[key] = val
    return val
}
如果不是响应式数据就直接修改数据就好了

defineReactive(ob.value,key,val)
ob.dep.notify()
return val
将新增变量修改为存取描述符(set和get属性),触发target中所有依赖,通知他们数据修改了。

$delete

参数:

参数 类型
target Object、Array
key string、number

用法

因为ES6前无法元编程,对象添加变量或删除变量是无法监听到的,所以我们使用$delete来对象删除属性。

不能是Vue实例或根数据对象(Vue.$data)

let vm=new Vue({
   data(){
    return{
        obj:{
            name:"WSQ"
        }
    }
  }
})
delete vm.obj.name
vm.obj._ob_.dep.notify()
你不使用$delete,可以手动触发依赖。

这里我们直接写和set中类似

export function set(target,key){
    if(Array.isArray(target) && isValidArrayIndex(key)){
        target.splice(key,1)
        return
    }
    
    const ob = target._ob_
    if(target._isVue || (ob && ob.vmCount)){
        process.env.NODE_ENV !== 'prodection' && warn(...)
        return
    }
    
    //如果不是自有属性,直接跳出(hasOwnProperty判断是否是自有属性,不包括原型链)
    if(!hasOwnProperty(target,key)){
        return
    }
    
    delete target[key]
    
    //如果不是响应式数据就没必要触发依赖了
    if(!ob){
        return
    }
    
    ob.dep.notify()
}

好了stateMixin中的方法就实现完了

eventsMixin

相关的API,有四个$on$once$off$emit

$on

参数:

参数 类型
event String、Array(2.2之后支持)
callback Function

用法

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

Vue.prototype.$on = function(event,fn){
    const vm = this
    if(Array.isArray(event)){
        for(let i = 0,l = event.length;i < l;i++){
            this.$on(event[i),fn)
        }
    }else{
        (vm._event[event] || (vm._event[event] = [])).push(fn)
    }
    return vm
}

实现方法不难。当event是数组时遍历每个子元素递归调用$on。否则就添加到_events事件列表中(在init中创建,是个空对象。vm._events = Object.create(null))。

$off

参数:

参数 类型
event String、Array(2.2之后支持)
callback Function

用法

移除自定义事件监听器。

  • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.$off = function(event,fn){
    const vm = this
    //1,如果没有提供参数,则移除所有的事件监听器
    if(!arguments.length){
        vm._events = Object.create(null)
        return vm
    }
    //将vm._events初始化,就相当于移除所有的事件监听器
    
    
    //2,如果只提供了事件,则移除该事件所有的监听器;
    //event支持数组,遍历所有子元素移除对应的事件监听器
    if(Array.isArray(event)){
        for(let i = 0,l = event.length;i < l;i++){
            this.$off(event[i),fn)
        }
        return vm
    }
    
    //不存在这个事件监听器
    if(!vm._events[event]){
        return vm
    }else{
        vm._events[event] = null
        return vm
    }
    
    //3,如果同时提供了事件与回调,则只移除这个回调的监听器。
    if(fn){
        const cbs = vm._events[event]
        let cb
        let i = cbs.length
        while(i--){
            cb = cbs[i]
            if(cb === fn || cb.fn === fn){
                cbs.splice(i,1)
                break
            }
        }
    }
    //while(i--)这里是从数组后向前遍历的,因为会splice删除数据,如果从前遍历删除数据后后面的数据向前移动会过滤一个元素。
    return vm
}

$once

参数:

参数 类型
event String、Array(2.2之后支持)
callback Function

用法

监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

Vue.prototype.$once = function(event,fn){
    const vm = this
   function on (){
       vm.$off(event,on)
       fn.apply(vm,argumnets)
   }
   on.fn = fn
   vm.$on(event,on)
   return vm
}

好了,简简单单。在注册时使用$on来监听事件,在执行$once注册的事件时删除其在_events中的监听器并执行。这就是$off中有if(cb === fn || cb.fn === fn)的原因。

$emit

参数:

参数 类型
event String、Array(2.2之后支持)
arguments any

用法

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

这个大家都不陌生,子组件向父组件传递参数就是使用$emit。实现思路就是在_events中找到对应的执行。

Vue.prototype.$emit = function(event){
   const vm = this
   const cbs = vm._events[event]
   if(cbs){
       cosnt args = Array.from(arguments ,1)
       for(let i =0;l= cbs.length;i<l;i++){
           try{
               cbs[i].apply(vm,args)
           }catch(e){
               handleErroe(e,vm,`报错原因...`)
           }
       }
   }
   return vm
}

lifecycleMixin

有两个$forceUpdate$destory

$forceUpdate

用法

迫使Vue实例重新渲染。只影响本身实例和包含插槽内容的组件,不包括所有子组件。

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

原理很简单,Watcher实例有一个update函数会通知本身所有的依赖并执行渲染流程。手动调用这个方法即可。

$destory

用法

完全销毁一个实例。清除与其他组件的链接并且解绑其全部指令和监听器。

Vue.prototype.$destory = function(event){
  const vm = this
  
  //防止重复执行,当_isBeingDestroyed是true时就代表进入beforeDestroy生命周期,实例在摧毁中。
  if(vm._isBeingDestroyed){
      return
  }
  
  //进入beforeDestroy生命周期
  callHook(vm,'beforeDestroy')
  vm._isBeingDestroyed = true
  
  //删除自己与父级之间的连接
  cosnt parent = vm.$parent
  if(parent && !parent._isBeingDestroyed && !vm.$options.abstract){
      remove(parent.$children,vm)
  }
  //从watcher监听的所有依赖中删除watcher
  if(vm._wathcer){
      vm._watcher.teardown()
  }
  let i = vm._wathcers.length
  while(i--){
      vm._watchers[i].teardown()
  }
  vm._isDestroed = true
  //在vdone树中触发destroy钩子函数解绑
  vm._patch_(vm._vnode,null)
  //触发destroy钩子函数
  callHook(vm,'destroyed')
  //移除所有事件监听器
  vm.$off()
}

function remove(arr,item){
    if(arr.length){
        cosnt index = arr.indexOf(item)
        if(index > -1){
            return arr.splice(index,1)
        }
    }
}