vue变化相关API实现原理(vm.$watch, vm.$set,vm.$delete)

本文深入解析Vue.js中$set、$delete和$watch三个核心API的实现原理,涉及响应式数据处理、数组变化检测及动态监听数据变化。$set用于安全地向对象新增或修改属性并确保其变为响应式;$delete用于删除响应式属性;$watch则用于创建观察者,监听并响应数据变化。文章详细阐述了这三个方法在处理响应式数据时的内部逻辑,包括依赖收集、通知更新等关键步骤。

1.vm.$set

vm.$set(target, key, value)

处理target是数组的情况

export function set(target, key, val) {
    //处理数组
    if(Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val);
        return val;
    }
    //如果key在target中已经存在,则属于修改数据
    if(key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    //处理新增的属性
    const ob = target.__ob__
    //target不能是vue实例或vue实例的根数据
    if(target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
        'Avoid adding reactive properties to a Vue instance or its root $data' + 
        'at runtime - declare it upfront in the data option.'
    }
    return val;
}
//还不是响应式数据,通过key和val在target上设置就行
if(!ob) {
    target[key] = val;
    return val;
}
//用户在响应式数据上新增了一个属性,用defineReactive将新增属性转化为响应式的
defineReactive(ob.value, key, val)
//最后,向target的依赖发送变化通知,并返回val
ob.dep.notify()
return val

通过splice方法改变数组可以被数组拦截器侦测到,并且把新增的值变为响应式的。

2.vm.$delete

vm.$delete(target, key)

export function del (target, key) {
    //如果是一个数组,使用splice方法,数组拦截器会自动向依赖发送通知
    if(Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key, 1);
        return 
    }
    const ob = (target).__ob__
    //与增加属性一样,target不能是vue实例或vue实例的根数据对象
    if(target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
        'Avoid adding reactive properties to a Vue instance or its root $data' + 
        'at runtime - declare it upfront in the data option.'
        )
        return 
    }
    //如果key不是target自身的属性,则终止程序继续执行
    delete target[key]
    //最后判断target是不是响应式数据,非响应式数据只需要执行删除操作即可
    if(!ob) {
        return 
    }
    ob.dep.notify()
}

3. vm.$watch

用法: vm.$watch(expOrFn, callback, [options])

vm.$watch返回一个取消观察函数,用来停止触发回调

vm.$watch是对Watcher的一种封装。通过watcher完全可以实现vm.$watch的功能

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

当watcher是函数的时候,不止可以动态返回数据,其中读取的所有的所有数据也都会被Watcher观察。也就是说,函数中读取vue实例上的任何数据,watcher都会观察。针对expOrfn,需要进行一次判断

if(typeof expOrFn === 'function') {
    this.getter = expOrFn
} else {
    this.getter = parsePath(expOrFn)
}

执行new Watcher之后,会判断用户是否使用了immediate参数,如果使用了,则立即执行一次cb。而unwatch就是将watcher实例从当前正在观察的状态的依赖列表中删除。

watcher中的teardown方法,现在要在watcher中添加该方法来实现unwatch的功能。首先需要Watcher中记录自己都订阅了谁,也就是watcher实例被收集进了哪些Dep里。然后当Watcher不想继续订阅这些Dep的时候,循环自己记录的订阅列表来通知它们将自己从它们的Dep的依赖列表中移除掉。

在watcher中,我们需要新增一个addDep方法,该方法的作用是在Watcher中记录自己都订阅过哪些Dep。

this.deps = [];
this.depIds = new Set();
//我们用depIds来判断如果当前Watcher是否订阅了Dep,保证不会重复订阅,depIds中存放的就是当前Watcher订阅了哪些Dep,deps是对应的Dep,addSub将Watcher加入到dep的依赖列表中
addDep(dep) {
    const id = dep.id
    if(!this.depIds.has(id)) {
        this.depIds.add(id)
        this.deps.push(dep)
        dep.addSub(this)
    }
}

当Dep数据发生变化的时候,会通知对应的Watcher,Watcher和Dep是多对多的关系。

teardown方法的实现

teardown() {
    let i = this.deps.length;
    while(i--) {
        this.deps[i].removeSub(this)
    }
}

removeSub() {
    const index = this.subs.indexOf(sub)
    if(index > -1) {
        return this.subs.splice(index,1)
    }
}    

deep参数的实现

if(options) {
    this.deep = !!options.deep
} else {
    this.deep = false;
}
//有options参数,则把options.deep转换成布尔值,否则deep置为false

在get方法中收集依赖的时候,deep收集子集依赖的时候必须在window.target = undefined之前,之后收集target就已经被改变了。deep因为要遍历所有的value子值,所以需要递归

//收集依赖的id
const seenObjects = new Set()

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

function _traverse (val, seen) {
    let i, keys
    //标记是对象还是数组
    const isA = Array.isArray(val)
    //既不是对象也不是数组,或者对象被冻结,直接返回
    if((!isA && !isObject(val)) || Object.isFrozen(val)) {
        return 
    }
    //有__ob__属性,已经是一个响应式的对象,将DepId加入seen
    if(val.__ob__) {
        const depId = val.__ob__.dep.id
        if(seen.has(depId)) {
            return
        } 
        seen.add(depId)
    }
    //对于对象和数组进行不同的遍历,对象进行读取的时候会触发getter
    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)    
    }
}    

<think> 我们正在讨论Vue实例(vm)的方法。根据用户提供的引用和之前的对话,我们需要整理Vue实例的方法。 引用[1]提到了几个实例属性,但用户现在问的是方法。 引用[2]提到了以$开头的属性是公开的,并且提到了vm.$delete,这是一个方法。 引用[3]提供了两个方法:$mount()和$destroy(),并给出了示例。 因此,我们可以总结Vue实例的常用方法,并按照引用[3]的格式进行说明。 注意:用户要求使用中文回答,并且对于公式使用正确的LaTeX格式(但本问题中可能不涉及公式,所以忽略数学公式要求),另外需要生成相关问题。 根据Vue官方文档,Vue实例的方法通常以$开头,包括: - $mount - $destroy - $watch - $set(即vm.$set) - $delete(即vm.$delete) - $on, $once, $off(事件相关方法) - $emit - $nextTick - $forceUpdate 我们可以选择一些常用的方法来详细说明。 由于引用中提到了$mount, $destroy, $set, $delete,我们将重点介绍这些,并补充其他重要方法。 结构: 1. 开头介绍Vue实例方法的一般特点(以$开头,公开API)。 2. 列举并详细说明每个方法(方法名、作用、参数、示例)。 3. 使用表格对比(可选,但引用[3]使用了表格,我们可以用Markdown表格)。 4. 生成相关问题。 注意:引用[1]和引用[2]中提到了实例属性,但用户明确询问方法,所以只讨论方法。 </think> ### Vue 实例(vm)核心方法详解 在 Vue.js 中,实例方法(以 `$` 开头)是框架提供的公开 API,用于操作组件状态和执行关键操作[^2]。以下是常用方法分类说明: --- #### 一、生命周期控制方法 1. **`vm.$mount()`** **作用**:手动挂载未挂载的 Vue 实例 **参数**: ```javascript vm.$mount('#app') // DOM 元素选择器 vm.$mount(document.getElementById('app')) // DOM 元素对象 ``` **示例**: ```javascript const vm = new Vue({ template: '<div>Hello Vue!</div>' }) vm.$mount('#app') // 将实例挂载到 id="app" 的元素 ``` 2. **`vm.$destroy()`** **作用**:完全销毁组件实例,释放资源[^3] **特性**: - 移除所有事件监听器 - 解绑所有指令 - 触发 `beforeDestroy` 和 `destroyed` 钩子 ```javascript vm.$destroy() // 销毁实例 ``` --- #### 二、数据操作方法 1. **`vm.$set()`** **作用**:向响应式对象添加新属性并触发视图更新 **参数**: ```javascript vm.$set(target, propertyName, value) ``` **示例**: ```javascript // 动态添加响应式属性 vm.$set(vm.user, 'age', 25) ``` 2. **`vm.$delete()`** **作用**:删除对象属性并确保触发视图更新 **参数**: ```javascript vm.$delete(target, propertyName) ``` ```javascript vm.$delete(vm.user, 'tempToken') // 安全删除属性 ``` 3. **`vm.$watch()`** **作用**:监听数据变化并执行回调 **参数**: ```javascript const unwatch = vm.$watch('counter', (newVal, oldVal) => { console.log(`计数从 ${oldVal} 变为 ${newVal}`) }) unwatch() // 取消监听 ``` --- #### 三、事件处理方法 | 方法 | 作用 | 示例 | |-------------|-------------------------------|-------------------------------| | `vm.$on()` | 监听自定义事件 | `vm.$on('login', handleLogin)`| | `vm.$once()`| 单次监听事件 | `vm.$once('error', logError)` | | `vm.$emit()`| 触发当前实例事件 | `vm.$emit('update', data)` | | `vm.$off()` | 移除事件监听 | `vm.$off('login')` | ```javascript // 组件间通信示例 vm.$on('data-loaded', (payload) => { this.dataset = payload }) parent.$emit('data-loaded', apiData) ``` --- #### 四、异步与渲染控制 1. **`vm.$nextTick()`** **作用**:DOM 更新后执行回调 **场景**:操作更新后的 DOM 元素 ```javascript vm.message = '更新后文本' vm.$nextTick(() => { console.log(this.$el.textContent) // 输出:更新后文本 }) ``` 2. **`vm.$forceUpdate()`** **作用**:强制重新渲染组件(谨慎使用) **特殊场景**: - 非响应式数据变更后更新视图 - 高级性能优化场景 --- #### 五、方法对比总结 | 方法 | 作用域 | 核心用途 | 响应式触发 | |-----------------|--------------|-----------------------------|-----------| | `$set` | 数据操作 | 添加响应式属性 | ✅ | | `$delete` | 数据操作 | 安全删除属性 | ✅ | | `$mount` | 生命周期 | 手动挂载实例 | - | | `$destroy` | 生命周期 | 销毁实例释放资源 | - | | `$nextTick` | 异步处理 | DOM 更新后回调 | - | | `$watch` | 状态监听 | 数据变化监听 | ✅ | > 注:所有 `$` 开头方法均为 Vue 公开 API,`_` 开头方法为框架内部使用[^2] ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值