Vue源码之关于vm.$delete()/Vue.use()内部原理详解


Posted in Javascript onMay 01, 2019

vm.$delete()

vm.$delete用法见官网。

为什么需要Vue.delete()?

在ES6之前, JS没有提供方法来侦测到一个属性被删除了, 因此如果我们通过delete删除一个属性, Vue是侦测不到的, 因此不会触发数据响应式。

见下面的demo。

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>Vue Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 </head>
 <body>
  <div id="app">
   名字: {{ user.name }} 年纪: {{ user.age }}
   <button @click="addUserAgeField">删除一个年纪字段</button>
  </div>
  <script>
   const app = new Vue({
    el: "#app",
    data: {
     user: {
      name: "test",
      age: 10
     }
    },
    mounted() {},
    methods: {
     addUserAgeField() {
      // delete this.user.age; // 这样是不起作用, 不会触发数据响应式更新
      this.$delete(this.user, 'age') // 应该使用
     }
    }
   });
  </script>
 </body>
</html>

源码分析内部实现

源码位置vue/src/core/instance/state.js的stateMixin方法

export function stateMixin (Vue: Class<Component>) {
  ...
  
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  
  ...

}

然后查看del函数位置, vue/src/core/observer/index.js。

/**
 * Delete a property and trigger change if necessary.
 * target: 将被删除属性的目标对象, 可以是对象/数组
 * key: 删除属性
 */
export function del (target: Array<any> | Object, key: any) {
 // 非生产环境下, 不允许删除一个原始数据类型, 或者undefined, null
 if (process.env.NODE_ENV !== 'production' &&
  (isUndef(target) || isPrimitive(target))
 ) {
  warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
 }
 // 如果target是数组, 并且key是一个合法索引,通过数组的splcie方法删除值, 并且还能触发数据的响应(数组拦截器截取到变化到元素, 通知依赖更新数据)
 if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.splice(key, 1)
  return
 }
 // 获取ob
 const ob = (target: any).__ob__
 // target._isVue: 不允许删除Vue实例对象上的属性
 // (ob && ob.vmCount): 不允许删除根数据对象的属性,触发不了响应
 if (target._isVue || (ob && ob.vmCount)) {
  process.env.NODE_ENV !== 'production' && warn(
   'Avoid deleting properties on a Vue instance or its root $data ' +
   '- just set it to null.'
  )
  return
 }
 // 如果属性压根不在对象上, 什么都不做处理
 if (!hasOwn(target, key)) {
  return
 }
 // 走到这一步说明, target是对象, 并且key在target上, 直接使用delete删除
 delete target[key]
 // 如果ob不存在, 说明target本身不是响应式数据,
 if (!ob) {
  return
 }
 // 存在ob, 通过ob里面存储的Dep实例的notify方法通知依赖更新
 ob.dep.notify()
}

工具函数

// 判断是否v是未定义
export function isUndef (v: any): boolean %checks {
 return v === undefined || v === null
}

// 判断v是否是原始数据类型(基本数据类型)
export function isPrimitive (value: any): boolean %checks {
 return (
  typeof value === 'string' ||
  typeof value === 'number' ||
  // $flow-disable-line
  typeof value === 'symbol' ||
  typeof value === 'boolean'
 )
}

// 判断对象上是否有属性
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
 return hasOwnProperty.call(obj, key)
}

关于__ob__属性, 在很多源码地方我们都会看到类似这样获取ob(Observer实例)

const ob = (target: any).__ob__

牢记只要数据被observe过就会打上这个私有属性, 是在Observer类的构造器里面发生的

export class Observer {
  constructor (value: any) {
  this.value = value
  // 依赖是存在Observe上的dep属性, 再次通知依赖更新时候我们一般使用__ob__.dep.notify()
  this.dep = new Dep()
  this.vmCount = 0
  // 定义__ob__
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
   if (hasProto) {
    protoAugment(value, arrayMethods)
   } else {
    copyAugment(value, arrayMethods, arrayKeys)
   }
   this.observeArray(value)
  } else {
   this.walk(value)
  }
  }
  ...

}

Vue.use()

大家都知道这个方法是用来安装插件的, 是全局api。

具体使用见官网。

通过Vue.use()源码+Vuex部分源码分析插件的安装过程

Vue.use()什么时候被绑在Vue原型上

源码位置: vue/src/core/index.js

Vue源码之关于vm.$delete()/Vue.use()内部原理详解

Vue

initGlobalAPI()

源码位置: vue/src/core/global-api/index.js

export function initGlobalAPI (Vue: GlobalAPI) {
  ...
  // 初始化use()
  initUse(Vue)
  ...

}

initUse()

源码位置: vue/src/core/global-api/use.js

export function initUse (Vue: GlobalAPI) {
 // 这里的Vue是构造器函数.
 // 通过以下源码:
 // vue-dev/src/core/global-api/index.js initGlobalAPI()中
 // vue-dev/src/core/index.js 这里执行了initGlobalAPI() => 初始化一些全局api
 // Vue.use(): 安装Vue.js的插件
 // 如果插件是一个对象,必须提供 install 方法
 // 如果插件是一个函数,它会被作为 install 方法
 // install 方法调用时,会将 Vue 作为参数传入
 Vue.use = function (plugin: Function | Object) {
  // installedPlugins存储install后的插件
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  if (installedPlugins.indexOf(plugin) > -1) {
   // 同一个插件只会安装一次
   return this
  }
  // additional parameters
  // 除了插件外的其他参数 Vue.use(MyPlugin, { someOption: true })
  const args = toArray(arguments, 1)
  // 往args存储Vue构造器, 供插件的install方法使用
  args.unshift(this)
  // 分情况执行插件的install方法, 把this(Vue), 参数抛回给install方法
  // 所以我们常说, install这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:
  if (typeof plugin.install === 'function') {
   // plugin是一个对象
   plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
   // plugin是一个函数
   plugin.apply(null, args)
  }
  // install之后会存储该插件避免重复安装
  installedPlugins.push(plugin)
  return this
 }
}

Vuex源码

我们都知道开发一个Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:

那么我们首先就是看Vuex的install方法是怎么实现的

源码位置: vuex-dev/src/store.js

let Vue // bind on install

// install: 装载vuex到vue, Vue.use(Vuex)也是执行install方法
// 关于Vue.use()源码. vue-dev/src/core/global-api/use.js
export function install (_Vue) {
 if (Vue && _Vue === Vue) {
  if (process.env.NODE_ENV !== 'production') {
   console.error(
    '[vuex] already installed. Vue.use(Vuex) should be called only once.'
   )
  }
  return
 }
 // 首次安装插件, 会把局部的Vue缓存到全局的window.Vue. 主要为了避免重复调用Vue.use()
 Vue = _Vue
 applyMixin(Vue)
}

applyMixin()

源码位置: vuex/src/mixin.js

export default function (Vue) {
 const version = Number(Vue.version.split('.')[0])

 if (version >= 2) {
  // 如果是2.x.x以上版本,注入一个全局mixin, 执行vueInit方法
  Vue.mixin({ beforeCreate: vuexInit })
 } else {
  // override init and inject vuex init procedure
  // for 1.x backwards compatibility.
  // 重写Vue原型上的_init方法, 注入vueinit方法 _init方法见 vue-dev/src/core/instance/init.js
  const _init = Vue.prototype._init // 作为缓存变量
  Vue.prototype._init = function (options = {}) {
   options.init = options.init
    ? [vuexInit].concat(options.init)
    : vuexInit
   // 重新执行_init
   _init.call(this, options)
  }
 }

 /**
  * Vuex init hook, injected into each instances init hooks list.
  */
 // 注入store到Vue构造器
 function vuexInit () {
  // 这里的this. 指的是Vue构造器
  /**
   * new Vue({
   *  ...,
   *  store,
   *  route
   * })
   */
  // options: 就是new Vue(options)
  // 源码见 vue-dev/src/core/instance/init.js initMixin方法
  const options = this.$options
  // store injection
  // store是我们使用new Vuex.Store(options)的实例
  // 注入store到Vue构造函数上的$store属性上, 所以我们在Vue组件里面使用this.$store来使用
  if (options.store) {
   // options.store为真说明是根节点root
   this.$store = typeof options.store === 'function'
    ? options.store()
    : options.store
  } else if (options.parent && options.parent.$store) {
   // 子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store
   this.$store = options.parent.$store
  }
 }
}

至于install方法Vuex是如果执行的?

export class Store {
 constructor (options = {}) {
  // 浏览器环境下安装vuex
  if (!Vue && typeof window !== 'undefined' && window.Vue) {
   install(window.Vue)
  }
  ...
 }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript窗口宽高,鼠标位置,滚动高度(详细解析)
Nov 18 Javascript
使用jquery提交form表单并自定义action的实现代码
May 25 Javascript
详解vue.js的事件处理器v-on:click
Jun 27 Javascript
Javascript中的getter和setter初识
Aug 17 Javascript
微信小程序 获取javascript 里的数据
Aug 17 Javascript
js实现上传并压缩图片效果
Jan 10 Javascript
vuex进阶知识点巩固
May 20 Javascript
JavaScript引用类型之基本包装类型实例分析【Boolean、Number和String】
Aug 09 Javascript
Vux+Axios拦截器增加loading的问题及实现方法
Nov 08 Javascript
JS数据类型STRING使用实例解析
Dec 18 Javascript
详解Node.JS模块 process
Aug 31 Javascript
JavaScript原始值与包装对象的详细介绍
May 11 Javascript
Vue.extend实现挂载到实例上的方法
May 01 #Javascript
JS html事件冒泡和事件捕获操作示例
May 01 #Javascript
JS实现的贪吃蛇游戏案例详解
May 01 #Javascript
javascript原型链学习记录之继承实现方式分析
May 01 #Javascript
微信小程序实现卡片左右滑动效果的示例代码
May 01 #Javascript
微信小程序常见页面跳转操作简单示例
May 01 #Javascript
浅谈对于react-thunk中间件的简单理解
May 01 #Javascript
You might like
深入php函数file_get_contents超时处理的方法详解
2013/06/03 PHP
php获取一个变量的名字的方法
2014/09/05 PHP
php中filter_input函数用法分析
2014/11/15 PHP
jquery下为Event handler传递动态参数的代码
2011/01/06 Javascript
js Dialog 实践分享
2012/10/22 Javascript
阻止子元素继承父元素事件具体思路及实现
2013/05/02 Javascript
Javascript与jQuery方法的隐藏与显示
2015/01/19 Javascript
JS制作简单的三级联动
2015/03/18 Javascript
在Ubuntu系统上安装Ghost博客平台的教程
2015/06/17 Javascript
值得分享的bootstrap table实例
2016/09/22 Javascript
[原创]jQuery实现合并/追加数组并去除重复项的方法
2018/04/11 jQuery
React组件重构之嵌套+继承及高阶组件详解
2018/07/19 Javascript
webpack实现一个行内样式px转vw的loader示例
2018/09/13 Javascript
jQuery事件blur()方法的使用实例讲解
2019/03/30 jQuery
微信小程序中weui用法解析
2019/10/21 Javascript
Ajax获取node服务器数据的完整步骤
2020/09/20 Javascript
[02:17]2016国际邀请赛中国区预选赛VG战队领队采访
2016/06/26 DOTA
使用python将mdb数据库文件导入postgresql数据库示例
2014/02/17 Python
Python Paramiko模块的安装与使用详解
2016/11/18 Python
Python实现利用最大公约数求三个正整数的最小公倍数示例
2017/09/30 Python
Python选择网卡发包及接收数据包
2019/04/04 Python
python numpy实现文件存取的示例代码
2019/05/26 Python
django mysql数据库及图片上传接口详解
2019/07/18 Python
使用python客户端访问impala的操作方式
2020/03/28 Python
python归并排序算法过程实例讲解
2020/11/04 Python
CSS Grid布局教程之网格单元格布局
2014/12/30 HTML / CSS
美国和加拿大房车出售在线分类广告:RVT.com
2018/04/23 全球购物
澳大利亚优惠网站:Deals.com.au
2019/07/02 全球购物
Ever New美国:澳大利亚领先的女装时尚品牌
2019/11/28 全球购物
C++程序员求职信范文
2014/04/14 职场文书
公司副总经理岗位职责
2014/10/01 职场文书
夫妻分居协议书范本(有子女版)
2014/11/01 职场文书
2014年酒店服务员工作总结
2014/12/08 职场文书
2015年新学期寄语
2015/02/26 职场文书
如何写一份成功的商业计划书
2019/06/25 职场文书
quickjs 封装 JavaScript 沙箱详情
2021/11/02 Javascript