vm.$set
vm.$set 是全局 Vue.set 的别名,它们是同一个方法。
用法:向响应式对象中添加一个属性,并且确保这个新属性同样是响应式的。当数据发生变化的时候,它会触发视图的更新。
注意:
- 它必须用于向响应式对象上添加新属性
- 因为 Vue 无法探测到普通对象上新增的属性。
- 如果这个属性已存在并且不是响应式的,set方法会更新它的值,但不会把它转化成响应式的。
- 对象不能是 Vue实例,或者 Vue 实例的根数据对象($data)。
<div id="app">
{{ obj.title }}
<hr>
{{ obj.name }}
<hr>
{{ arr }}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
obj: {
title: 'Hello Vue'
},
arr: [1, 2, 3],
},
});
// 在控制台执行下面的代码
// vm.obj.name = 'foo' // 无效:数据变化,视图没变化
// vm.arr[0] = 100 // 无效:数据变化,视图没变化
// 刷新页面:测试正常操作
// vm.$set(vm.obj, 'name', 'foo') // 生效
// vm.$set(vm.arr, 0, 100) // 生效
// 刷新页面:测试给非响应式对象添加属性
// vm.staticObj = {}
// vm.$set(vm.staticObj, 'name', 'bar') // 打印查看没有 getter/setter
// 刷新页面:测试修改非响应式的属性
// vm.obj.count = 0
// vm.$set(vm.obj, 'count', 100) //打印查看没有 getter/setter
// 刷新页面
// vm.$set(vm, 'a', 1) // 报错
// vm.$set(vm.$data, 'b', 1) // 报错
</script>
定义位置
-
Vue.set() 构造函数中的方法(静态方法)
src\core\global-api\index.js
// 静态方法 set delete nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick
-
vm.$set() 实例方法
src\core\instance\state.js
// src\core\instance\index.js // 继续混入(注册) vm 的成员:$data/$props/$set/$delete/$watch stateMixin(Vue) // src\core\instance\state.js Vue.prototype.$set = set Vue.prototype.$delete = del
初始化 GlobalApi 时将 Vue.set 指向 set 函数。
set 函数在 src\core\observer\index.js
中定义。
所以 set 函数是和响应式相关的。
实例方法 vm.$set 也指向了同一个 set 函数。
所以 Vue.set 和 vm.$set 是相同的。
set 源码
// src\core\observer\index.js
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
// 判断目标对象是否是undefined 或 原始值
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果 target 是数组,判断 key 是否是合法的数组索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 更新数组的长度
target.length = Math.max(target.length, key)
// 通过 splice 对 key 位置的元素进行替换
// array.js 重定义的 splice 修改数组元素可以触发响应式机制
target.splice(key, 1, val)
return val
}
// 判断 key 是否是对象本身的属性,而不是Object原型上的成员
if (key in target && !(key in Object.prototype)) {
// 如果已经存在,那它应该已经进行了响应式处理
// 更新属性的值,触发响应式机制
target[key] = val
return val
}
// 获取 target 中的 observer 对象
const ob = (target: any).__ob__
// 判断 target 是 Vue 实例 或 $data 根数据(ob.vmCount=1)
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
}
// 如果 ob 不存在,说明 target 不是响应式对象
// 也就没有对这个属性作响应式处理,直接赋值
if (!ob) {
target[key] = val
return val
}
// 调用 defineReactive 设置对象的响应式属性
defineReactive(ob.value, key, val)
// 发送通知
ob.dep.notify()
return val
}
vm.$delete
vm.$delete(vm.obj, 'msg')
用法:删除对象的属性(数组的元素也属于对象的属性)。如果对象是响应式的,确保删除能触发更新视图。
这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是一般很少会使用它。
它与 $set 非常相似:
- 目标对象不能是一个 Vue 实例 或 Vue 实例的根数据对象
- 和 Vue.delete 相同,都是定义为 del 函数。
- 在
src\core\observer\index.js
中定义,和响应式相关。
// src\core\observer\index.js
/**
* Delete a property and trigger change if necessary.
*/
export function del (target: Array<any> | Object, key: any) {
// 判断目标对象是否是undefined 或 原始值
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 是否是合法的数组索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 通过 splice 对 key 位置的元素进行删除
// array.js 重定义的 splice 修改数组元素可以触发响应式机制
target.splice(key, 1)
return
}
// 获取 target 中的 observer 对象
const ob = (target: any).__ob__
// 判断 target 是 Vue 实例 或 $data 根数据(ob.vmCount=1)
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
}
// 判断 target 自身属性中是否包含 key 属性
// 如果没有,不作任何处理
if (!hasOwn(target, key)) {
return
}
// 删除属性
delete target[key]
// 如果 target 不是响应式对象,不做任何处理
if (!ob) {
return
}
// 发送通知
ob.dep.notify()
}
vm.$watch
基本使用
vm.$watch(expOrFn, callback, [options])
用法:监听 expOrFn(Vue 实例上的一个表达式或者一个函数计算结果)的变化,触发callback (回调函数)。
- expOrFn:要监听的 Vue 实例上的数据,值可以为:
- 表达式:只接受简单的键路径。
- 函数:对于更复杂的表达式,用一个函数取代。
- 例如监听两个属性相加后的结果,结果发生变化时触发回调
- callback:数据变化后执行的函数,值可以为:
- 函数:回调函数,callback(newVal, oldVal) 接收 新值 和 旧值。
- 对象:具有 handler 属性的对象,handler可以为:
- 字符串:对应methods 中相应的方法
- 函数
- options:可选的选项
- deep:深度监听
- immediate:是否立即以表达式的当前值触发回调
<div id="app">
{{ user.fullName }}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
user: {
firstName: '诸葛',
lastName: '亮',
fullName: ''
}
},
});
vm.$watch(
'user',
function (newVal, oldVal) {
this.user.fullName = `${newVal.firstName} ${newVal.lastName}`
},
{
immediate: true,
deep: true
}
)
// 在控制台执行下面的代码
// vm.user.firsName = '张'
</script>
$watch 没有对应的静态方法,因为 $watch 方法中要使用 Vue 的实例。
Watcher 三种类型及创建顺序:
- 计算属性 Watcher
- 用户 Watcher (侦听器)
- 渲染 Watcher
$watch 中创建了 Watcher 对象,也就是用户 Watcher (侦听器)。
vm.$watcher 的定义位置:src\core\instance\state.js
调试三种 Watcher 的创建顺序
<div id="app">
{{ reversedMessage }}
{{ user.fullName }}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
message: 'Hello Vue',
user: {
firstName: '诸葛',
lastName: '亮',
fullName: ''
}
},
computed: {
reversedMessage() {
return this.message.split(''),reverse().join('')
}
},
watch: {
'user': {
handler: function(newVal, oldVal) {
},
immediate: true,
deep: true
}
}
});
</script>
在 Watcher 类的构造函数中打断点 src/core/observer/watcher.js
。
从调用栈(Call Stack)可以看到,第一个 Watcher 对象,是在 initComputed 中创建的。
它是计算属性 Watcher。
initComputed 是在 initState 中调用的。
接着进入 Watcher 类中,在更新 id 的位置打断点,查看每个 Watcher 的 id。
因为 Watcher 对象的 id 是自增的,所以通过 id 可以看到它们的创建顺序。
F8执行,此时 id 为 1。继续 F8 查看创建第二个 Watcher 的调用栈。它是在 $watch 方法中创建的。
它是用户 Watcher(侦听器)。
- initState 中先判断并初始化了 计算属性,然后调用了 initWatch 方法,将 options 的 watch 选项传入进去,初始化 侦听器。
- 内部调用了 createWatcher 方法,最终调用 vm.$watch 方法。
- vm.$watch 方法中创建了 Watcher 对象,也就是用户 Watcher。
F8 继续执行,此时 id 更新为 2。
继续执行,就是调用 mountComponent 创建渲染 Watcher 了,并在创建时标识了 isRenderWatcher。
F8继续执行,此时 id 为 3。
三种 Watcher 的执行顺序
调试发现,通过 Watcher 的 id 可以获取它们的创建顺序。
而执行 Watcher 的时候,是通过调用 flushSchedulerQueue 方法。
它会先将队列中的 Watcher 根据 id 排序,在依次执行。
所以 Watcher 的执行顺序也是:计算属性 Watcher -> 用户 Watcher(侦听器) -> 渲染 Watcher
$watch 源码
初始化侦听器 的时候 最终调用了 $watch 方法。
查看调用 $watch 的地方,initState中调用了initWatch方法。
内部调用 createWatcher。
createWatcher 方法:解析 watch 对象属性的值,用解析好的参数调用 vm.$watch
// src\core\instance\state.js
function initWatch (vm: Component, watch: Object) {
// 遍历选项的 watch 对象
for (const key in watch) {
// 获取 watch 对象的值
const handler = watch[key]
// 判断值是否是数组:回调数组
if (Array.isArray(handler)) {
// 遍历数组,进行处理
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
// 解析参数调用 vm.$watch
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 先判断 属性 的值,是否是原生对象
if (isPlainObject(handler)) {
// 将它拷贝给options,用于保留其他选项
options = handler
// 重新赋值为 handler
handler = handler.handler
}
// 如果 handler 是字符串,就去 Vue 实例(vm.methods)中找到这个字符串对应的方法
if (typeof handler === 'string') {
// 最终重新赋值为 回调函数
handler = vm[handler]
}
// 用解析好的参数调用 vm.$watch 方法
return vm.$watch(expOrFn, handler, options)
}
$watch 方法也是在当前文件中定义的。
// src\core\instance\state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 获取 Vue 实例(因为需要Vue实例,所以它没有对应的静态方法)
const vm: Component = this
// 判断 回调函数 是否是一个原始对象
// 侦听器中解析了这个参数,所以它是一个function
// 手动调用 vm.$watch 的时候可以传一个包含 handler 函数的对象
if (isPlainObject(cb)) {
// 如果传的是对象,就调用createWatcher对其解析
// 并返回取消监听的方法
return createWatcher(vm, expOrFn, cb, options)
}
// 如果options曾经解析过,直接赋值
// 否则设置为一个空对象
options = options || {}
// options.user 标记当前要创建的 Watcher 对象是 用户 Watcher
// 这也是为什么将 watch 选项和 vm.$watch 创建的 Watcher 对象称为 用户 Watcher
options.user = true
// 创建 用户 Watcher 对象
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// 立即执行一次 回调函数,并把当前值传入
// try catch 用于确保用户传入的函数(异常)不影响后面代码的执行
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回取消监听的方法
return function unwatchFn () {
watcher.teardown()
}
}
标记的 options.user 在调用 watcher的run方法时用于判断,如果当前是用户 Watcher, 就调用它的回调函数。
// src\core\observer\watcher.js
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
// 判断如果当前是用户watcher
if (this.user) {
// 就调用它的 cb 回调函数(例如侦听器对应的function)
// 使用try catch 是避免用户定义的回调发生异常
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
计算属性 Watcher
Watcher 中判断了 lazy 属性,这是计算属性 Watcher 用到的属性,它固定为 true。
initState 中调用了 initComputed 方法,它内部创建了计算属性对应的 Watcher 对象。
// src\core\instance\state.js
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
// _computedWatchers存储所有计算属性创建的 Watcher
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 遍历计算属性选项
for (const key in computed) {
// 获取属性的值
const userDef = computed[key]
// 获取属性的getter方法
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
// 创建 Watcher对象,传入属性的getter方法,get会调用 getter
// computedWatcherOptions 是一个常量:{lazy: true}
// Watcher对象在构造函数中判断 lazy 为 true 就不会调用 get 方法
// 因为计算属性是在渲染模板的时候使用的,所以创建 Watcher 的时候不需要调用
// render渲染或数据变更时会通知Watcher调用update,内部会调用 get 方法
// 把对象存入 _computedWatchers
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
// 下面是将计算属性注入到 Vue 实例,并收集依赖
// 内部通过 _computedWatchers 获取计算属性的 Watcher
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
总结:计算属性 Watcher 和 用户 Watcher
- 在 initState 中先后初始化(创建)了 计算属性 和 侦听器。
- 计算属性
- initComputed 中解析获取属性的getter,创建 Watcher 对象
- 最后将计算属性注入到 Vue 实例
- 侦听器
- 过程
- 调用 initWatch 遍历 watch 选项,获取属性的值
- 调用 createWatcher 解析属性的值作为参数调用 vm.$watch
- vm.$watch 中创建了 Watcher 对象
- 用户还可以手动调用 vm.$watch 创建侦听器
- 内部通过 options.user 标记,所以称呼为侦听器为 用户 Watcher
- 过程
- 计算属性
nextTick() 异步更新队列
数据变化时,等待数据更新到DOM后,会执行 nextTick(cb) 中传递的回调函数。
Vue 更新 DOM 是异步执行的,批量的:
- 更新数据的时候,每更新一个属性值,并不会立即更新视图
- 测试:此时通过 ref 获取 DOM上展示的内容,结果并不是最新的
- 方法:在函数内通过 nextTick 的回调获取
- Vue 会等到函数执行完,将所有变化的结果批量更新到视图。
nextTick 的目的是获取 DOM 上的最新数据。
nextTick 源码
nextTick 也有静态方法 Vue.nextTick 和 实例方法 vm.$nextTick。
它们的区别是实例方法中回调函数的 this 自动绑定到调用它的实例上。
-
静态方法 Vue.nextTick 在
src\core\global-api\index.js
中定义- 它直接赋值为 nextTick 函数。
// src\core\global-api\index.js // 定义一些外部常用的静态方法 Vue.set = set Vue.delete = del Vue.nextTick = nextTick
-
实例方法 vm.$nextTick 是在
src\core\instance\index.js
中调用的 renderMixin 方法中定义- 它定义为 nextTick 函数,并将Vue实例传入作为上下位(回调函数 this 的指向)
// src\core\instance\index.js // $nextTick/_render renderMixin(Vue) // src\core\instance\render.js // 定义$nextTick Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) }
-
nextTick
// src\core\util\next-tick.js
/**
* nextTick
* @param {*} cb 回调函数
* @param {*} ctx 上下文(回调函数的调用者)
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 把 cb 加上异常处理,存入 callbacks 数组中
callbacks.push(() => {
if (cb) {
// 所有用户传入的函数,Vue都认为是危险的,所以都要增加 try catch
try {
// 调用 cb(),更改this指向
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 如果没有传递回调函数,会判断_resolve
// 如果_resolve有值,就会执行
// _resolve 是 Promise 对象的 resolve
_resolve(ctx)
}
})
// pending 表示 callbacks 队列是否正在被处理
if (!pending) {
// 标记队列正在被处理
pending = true
// 开始处理
timerFunc()
}
// $flow-disable-line
// 2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise
if (!cb && typeof Promise !== 'undefined') {
// 返回一个 promise 对象,内部将 resolve 传递给 _resolve
return new Promise(resolve => {
_resolve = resolve
})
}
}
- timerFunc
// src\core\util\next-tick.js
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// timerFunc优先以Promise(微任务)的方式执行nextTick
// 也就是所有同步任务执行完成后,再执行
// 可是此时视图并没有更新完
// 这是因为 nextTick中的回调执行之前,数据其实已经发生改变
// 当数据发生变化,会立即通知 Watcher 重新渲染视图。
// Watcher 中首先做的是把DOM上的数据更新,也就是更改DOM树
// 至于DOM什么时候更新到浏览器上,是在这次事件循环结束之后,才会执行DOM的更新操作
// 而nextTick在微任务中获取数据,其实是在DOM树上获取数据,此时DOM还没有渲染到浏览器中
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// 兼容 IOS 一些不支持 Promise 的版本,改用 setTimeout
if (isIOS) setTimeout(noop)
}
// 标记当前使用的nextTick使用的是微任务
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
// 判断当前不是IE浏览器,并且支持 MutationObserver
// MutationObserver在IE10 11中才支持,并且还有些小问题
// 这里主要用于兼容这些环境:PhantomJS, iOS7, Android 4.4
// MutationObserver 监听DOM对象的改变
// 如果DOM对象发生改变,会执行回调,这个回调也是以微任务的形式执行的
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
// 标记微任务
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 如果不支持上面的两个方式,就降低为使用 setImmediate
// 它和 setTimeout 的区别是:只有IE和nodejs支持
// 优于setTimeout使用的原因是:setImmediate的性能更好
// setTimeout使用时即使延迟设为0,最快也要等4ms才会执行
// 而setImmediate会立即执行
// 可以通过同时调用 setImmediate setTimeout 查看打印结果确认执行顺序
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
- flushCallbacks
// src\core\util\next-tick.js
function flushCallbacks () {
// 先标记为结束
pending = false
// 备份回调函数数组
const copies = callbacks.slice(0)
// 清空callbacks,以便允许继续向数组中添加回调
callbacks.length = 0
// 遍历执行回调函数
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
总结
- nextTick在执行回调函数的时候,先会把回到函数放到callbacks数组中。
- 然后它会优先以微任务的方式进行处理回调函数
- 如果浏览器不支持微任务,就降低为宏任务。
PS:在执行 Watcher 队列(flushSchedulerQueue)时是包含在 nextTick 中的,所以 Watcher 的执行过程是在视图更新后才会执行,它是异步执行的。
PS:nextTick 用来异步获取 DOM 的最新数据,本身仅仅是用来开启一个微任务或者宏任务,所以它不会造成页面的卡顿,除非回调函数中的代码过于耗时会导致页面卡顿。