背景
动态修改对象的属性时,vue2视图template不会跟新渲染的数据,来一探究竟
动态添加属性,不使用$set
测试案例:
config属性被我动态添加name:yma16
结论:
如果template动态添加则说明对象的属性添加删除具有响应式
<template> <div class="container"> <h1>{{ msg }}</h1> <span> config渲染: </span> <span> {{ config }} </span> <hr /> <div style="width: 600px;margin: 0 auto"> <span> 添加config属性 </span> <el-form label-width="80px" :model="form" ref="formRef"> <el-form-item label="key" :rules="{ required: true, message: 'key不能为空!', trigger: 'blur' }" prop="key" > <el-input v-model="form.key" placeholder="请输入key"></el-input> </el-form-item> <el-form-item label="value"> <el-input v-model="form.value" placeholder="请输入value"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submit"> 添加保存 </el-button> </el-form-item> </el-form> </div> </div> </template> <script> export default { name: 'Dashboard', data () { return { msg: '$set用法', config: { key: 'value' }, form: { key: '', value: '' } } }, methods: { submit () { this.$refs['formRef'].validate().then(valid => { console.log('valid', valid) if (valid) { this.config[this.form.key] = this.form.value this.$message.success('添加属性成功!') console.log('this.config', this.config) } else { this.$message.warning('请检查配置!') } }) } } } </script>
效果
如下图所示。
结论:
vue2动态添加对象属性不具有响应式
动态添加属性,使用$set
改动点:
submit使用$set添加替换属性
<template> <div class="container"> <h1>{{ msg }}</h1> <span> config渲染: </span> <span> {{ config }} </span> <hr /> <div style="width: 600px;margin: 0 auto"> <span> 添加config属性 </span> <el-form label-width="80px" :model="form" ref="formRef"> <el-form-item label="key" :rules="{ required: true, message: 'key不能为空!', trigger: 'blur' }" prop="key" > <el-input v-model="form.key" placeholder="请输入key"></el-input> </el-form-item> <el-form-item label="value"> <el-input v-model="form.value" placeholder="请输入value"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submit"> 添加保存 </el-button> </el-form-item> </el-form> </div> </div> </template> <script> export default { name: 'Dashboard', data () { return { msg: '$set用法', config: { key: 'value' }, form: { key: '', value: '' } } }, methods: { submit () { this.$refs['formRef'].validate().then(valid => { console.log('valid', valid) if (valid) { this.$set(this.config, this.form.key, this.form.value) this.$message.success('添加属性成功!') console.log('this.config', this.config) } else { this.$message.warning('请检查配置!') } }) } } } </script>
效果
结论:
vue2动态添加对象属性使用$set可以让对象具有响应式渲染
$set做了啥?
对象defineProperty
给对象添加属性
var myObject = {}; Object.defineProperty( myObject, "a", { value: 2, writable: false, // 不可写! configurable: true, enumerable: true } ); myObject.a = 3; myObject.a; // 2
对象的Getter和Setter
object获取和配置属性(用get达实现监听)
定义新属性b设置为a的2倍,达到监听变量a变化的效果
当a变量地址改变时属性b也会改变
var myObject = { // 给 a 定义一个 getter get a() { return 2; } }; Object.defineProperty( myObject, "b", { get: function(){ return this.a * 2 }, enumerable: true } ); myObject.a; // 2 myObject.b; // 4
$set的源码
vue的原型链挂了set
set里面定义属性且可枚举
使用了枚举enumerable属性和注册configurable属性
/** * Define a reactive property on an Object. */ export function defineReactive( obj: object, key: string, val?: any, customSetter?: Function | null, shallow?: boolean, mock?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ( (!getter || setter) && (val === NO_INIITIAL_VALUE || arguments.length === 2) ) { val = obj[key] } let childOb = !shallow && observe(val, false, mock) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { const value = getter ? getter.call(obj) : val if (Dep.target) { if (__DEV__) { dep.depend({ target: obj, type: TrackOpTypes.GET, key }) } else { dep.depend() } if (childOb) { childOb.dep.depend() if (isArray(value)) { dependArray(value) } } } return isRef(value) && !shallow ? value.value : value }, set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val if (!hasChanged(value, newVal)) { return } if (__DEV__ && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else if (getter) { // #7981: for accessor properties without setter return } else if (!shallow && isRef(value) && !isRef(newVal)) { value.value = newVal return } else { val = newVal } childOb = !shallow && observe(newVal, false, mock) if (__DEV__) { dep.notify({ type: TriggerOpTypes.SET, target: obj, key, newValue: newVal, oldValue: value }) } else { dep.notify() } } }) return dep }
重点
当一个数据为响应式时,vue会给该数据添加一个__ob__属性,因此可以通过判断target对象是否存在__ob__属性来判断target是否是响应式数据
dep.notify({ type: TriggerOpTypes.SET, target: obj, key, newValue: newVal, oldValue: value }) } else { dep.notify() }
当target对象是响应式数据时,我们将target的属性key也设置为响应式并手动触发通知其属性值的更新
defineReactive(ob.value, key, val) ob.dep.notify()
notify 触发update更新
承接$set的notify
notify(info?: DebuggerEventExtraInfo) { // stabilize the subscriber list first const subs = this.subs.filter(s => s) as DepTarget[] if (__DEV__ && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { const sub = subs[i] if (__DEV__ && info) { sub.onTrigger && sub.onTrigger({ effect: subs[i], ...info }) } sub.update() } }
update 继承 DebuggerOptions
/** * @internal */ export interface DepTarget extends DebuggerOptions { id: number addDep(dep: Dep): void update(): void }
onTrack和onTrigger
watch update
_update更新渲染节点
function _update(oldVnode, vnode) { const isCreate = oldVnode === emptyNode const isDestroy = vnode === emptyNode const oldDirs = normalizeDirectives( oldVnode.data.directives, oldVnode.context ) const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) const dirsWithInsert: any[] = [] const dirsWithPostpatch: any[] = [] let key, oldDir, dir for (key in newDirs) { oldDir = oldDirs[key] dir = newDirs[key] if (!oldDir) { // new directive, bind callHook(dir, 'bind', vnode, oldVnode) if (dir.def && dir.def.inserted) { dirsWithInsert.push(dir) } } else { // existing directive, update dir.oldValue = oldDir.value dir.oldArg = oldDir.arg // update 更新 vnode callHook(dir, 'update', vnode, oldVnode) if (dir.def && dir.def.componentUpdated) { dirsWithPostpatch.push(dir) } } } if (dirsWithInsert.length) { const callInsert = () => { for (let i = 0; i < dirsWithInsert.length; i++) { callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode) } } if (isCreate) { mergeVNodeHook(vnode, 'insert', callInsert) } else { callInsert() } } if (dirsWithPostpatch.length) { mergeVNodeHook(vnode, 'postpatch', () => { for (let i = 0; i < dirsWithPostpatch.length; i++) { callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode) } }) } if (!isCreate) { for (key in oldDirs) { if (!newDirs[key]) { // no longer present, unbind callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy) } } } }
update的调用地方太多
找了一个监听的update
/** * @internal since we are not exposing this in Vue 2, it's used only for * internal testing. */ export function effect(fn: () => any, scheduler?: (cb: any) => void) { const watcher = new Watcher(currentInstance, fn, noop, { sync: true }) if (scheduler) { watcher.update = () => { scheduler(() => watcher.run()) } } }
结束
感谢阅读,如有不足或者错误欢迎指正!