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去除绑定在变量a的Dep中的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)
}
}
}