跟着尚硅谷学vue-day7
列表,vue2的监测机制,动态新增深层监测,数据劫持
根据输入框的值,自动更新列表
案例1:根据输入框的值,自动更新列表
1.首先得到输入框的值。利用v-model进行绑定数据
2.当输入框发生变化的时候,采用watch或者computed进行监测。根据需求过滤列表
3.判断一个字符串当中是否包含字符,可以用indexOf来判断。
<body><div id="root"><input type="text" placeholder="请输入名字" v-model="keyword"/><ul><li v-for="(p,index) in personArr" :key="index" >{{p.name}}-{{p.age}}--{{p.sex}}</li></ul></div><script type="text/javascript">Vue.config.productionTip = false;new Vue({el:'#root',data:{keyword:'',personArr:[{id:'001',name:'周小红',age:20,sex:'女'},{id:'002',name:'小红',age:30,sex:'女'},{id:'003',name:'刘小绿',age:25,sex:'女'},{id:'004',name:'小绿',age:25,sex:'女'}]},watch:{keyword(val){this.personArr=this.personArr.filter((p)=>{return p.name.indexOf(val) !== -1})}}})</script>
</body>
会出现越搜索越少的这种情况,原因就是上一次的搜索结果会影响原数组,导致原数组的数据越来越少
利用监视器属性实现,首先,需要新增一个filPerson空数组,用来存放搜索结果,接着,修改keyword的handler,将搜索结果赋值给filPerson,遍历filPerson,呈现给用户。
<div id="root"><input type="text" placeholder="请输入名字" v-model="keyword"/><ul><li v-for="(p,index) in filPerson" :key="index" >{{p.name}}-{{p.age}}--{{p.sex}}</li></ul>
</div>
<script type="text/javascript">Vue.config.productionTip = false;new Vue({el:'#root',data:{keyword:'',personArr:[{id:'001',name:'周小红',age:20,sex:'女'},{id:'002',name:'小红',age:30,sex:'女'},{id:'003',name:'刘小绿',age:25,sex:'女'},{id:'004',name:'小绿',age:25,sex:'女'}],filPerson:[]},watch:{keyword(val){this.filPerson=this.personArr.filter((p)=>{return p.name.indexOf(val) !== -1})}}})
</script>
但是这样做会有一个问题,就是进入页面,首次出现的就是一个空白数组,所以,可以用watch的immediate来实现。而且,当搜索框中没有数据的时候,即是''的时候,filter过滤结果会是全数组。因为'abc'.indexOf('')=0,即每个字符串的首位都是空。
watch:{keyword:{immediate:true,handler(val){this.filPerson=this.personArr.filter((p)=>{return p.name.indexOf(val) !== -1})}}
}
计算属性是一上来就调用一下,然后依赖的属性发生变化的时候调用一下。所以当keyword发生变化时,filPerson会重新调用一次。
computed:{//简写方式filPerson(){return this.personArr.filter((p)=>{return p.name.indexOf(this.keyword) !== -1}) }//keyword参与计算属性 }
对搜索出来的数组进行排序或者不用搜索,直接排序
案例升级:对搜索出来的数组进行排序或者不用搜索,直接排序
1.记录点击了哪一个按钮,通过给每个按钮绑定一个数字的方式来记录
2.利用arr.sort进行排序。
<body><div id="root"><input type="text" placeholder="请输入名字" v-model="keyword"/><button @click="keysort=2">年龄升序</button><button @click="keysort=1">年龄降序</button><button @click="keysort=0">原顺序</button><ul><li v-for="(p,index) in filPerson" :key="index" >{{p.name}}-{{p.age}}--{{p.sex}}</li></ul></div><script type="text/javascript">Vue.config.productionTip = false;new Vue({el:'#root',data:{keyword:'',personArr:[{id:'001',name:'周小红',age:20,sex:'女'},{id:'002',name:'小红',age:30,sex:'女'},{id:'003',name:'刘小绿',age:25,sex:'女'},{id:'004',name:'小绿',age:25,sex:'女'}],keysort:0 },computed:{//简写方式filPerson(){const arr = this.personArr.filter((p)=>{return p.name.indexOf(this.keyword) !== -1}) if(this.keysort){arr.sort((a,b)=>{return this.keysort===1?a.age-b.age:b.age-a.age;})}return arr;}//keyword参与计算属性 }})</script>
</body>
更新时的问题,索引值作为key,如果有升序降序操作,并且每一条数据之后有输入框,那么就会造成输入框内容和数据不对版的情况发生,正确操作是一id作为key,但是为什么id作为key就不会有这种情况发生
<div id="root"><button @click="updatemei">更新周小红的信息</button><ul><li v-for="(p,index) in personArr" :key="p.id" >{{p.name}}-{{p.age}}--{{p.sex}}</li></ul></div><script type="text/javascript">Vue.config.productionTip = false;new Vue({el:'#root',data:{personArr:[{id:'001',name:'周小红',age:20,sex:'女'},{id:'002',name:'小红',age:30,sex:'女'},{id:'003',name:'刘小绿',age:25,sex:'女'},{id:'004',name:'小绿',age:25,sex:'女'}]},methods: {updatemei(){this.personArr[0].name='周红'}},})</script>
更新列表问题
这种情况下,点击按钮,可以实现更新周小红的信息,但是如果将methods当中的updatemei修改成为
methods: {updatemei(){this.personArr[0]= {id:'001',name:'周红',age:20,sex:'女'}}}
会发现点击按钮,点开vue控制台,root根节点数据会发生变化,但是页面不会更新
反之,如果先点开vue控制台,再点击按钮,root根节点数据不会发生变化,页面也不会更新。
vue到底如何监测数据改变的
Vue 监测数据变化的核心机制是通过数据劫持(Data Observation)实现的,其底层原理在 Vue 2 和 Vue 3 中有所不同。以下是详细的技术解析,结合 _data
的加工过程说明:
Vue 2 的监测机制:Object.defineProperty
1. _data
的加工过程
当你在组件中定义 data
时,Vue 会进行以下处理:
// 用户定义的 data
data() {return {message: 'Hello',user: { name: 'Alice' }};
}// Vue 内部处理后的 _data
vm._data = {message: 'Hello', // 被劫持的属性user: { // 嵌套对象也会被递归劫持name: 'Alice'}
};
关键步骤:
- 递归遍历
data
:Vue 会递归遍历data
的所有层级,将每个属性转换为响应式属性。 - 使用
Object.defineProperty
劫持属性:- 对每个属性定义
getter
和setter
。 getter
用于依赖收集(记录哪些组件/Watcher 依赖该属性)。setter
用于触发更新(当属性变化时通知所有依赖更新视图)。
- 对每个属性定义
2. _data
的响应式实现
function observe(data) {if (!data || typeof data !== 'object') return;Object.keys(data).forEach(key => {defineReactive(data, key, data[key]);});
}function defineReactive(obj, key, val) {const dep = new Dep(); // 依赖管理器// 递归处理嵌套对象observe(val);Object.defineProperty(obj, key, {get() {if (Dep.target) { // 当前正在计算的 Watcherdep.addSub(Dep.target); // 收集依赖}return val;},set(newVal) {if (newVal === val) return;val = newVal;observe(newVal); // 新值如果是对象,继续劫持dep.notify(); // 通知所有依赖更新}});
}
例:
const vm = new Vue({data: { message: 'Hello' }
});// 访问 message 时,触发 getter,收集依赖
console.log(vm.message); // Dep 记录当前 Watcher// 修改 message 时,触发 setter,通知更新
vm.message = 'World'; // 视图自动更新
3. 局限性
- 无法检测新增/删除属性:需用
Vue.set()
或vm.$set()
。 - 数组索引赋值不响应:需用
push()
、pop()
等方法。
修改属性,就会有个自己加的提示(定时器)。
案例:修改name,就会有个自己加的提示。
<script type="text/javascript">let data = {name:'尚硅谷',address:'北京'}let tmp = '尚硅谷'setInterval(() => {if(data.name !== tmp){tmp = data.nameconsole.log('尚硅谷被改了')}}, 100);</script>
Object.defineProperty(data,'name',{get(){return data.name;},set(val){data.name = val}})
Object.defineProperty(data,'name',{get(){return data.name;},set(val){data.name = val}})
get方法导致,data.name会多次调用get()方法。get又会返回data.name;
假如data只有一层对象方法,代码
const obs = new observe(data);let vm = {};vm._data = data = obs;function observe(data){const keys = Object.keys(data);keys.forEach((key)=>{Object.defineProperty(this,key,{get(){return data[key]},set(val){console.log(`${key}被修改了,我要去解析模版,生成虚拟dom,我要开始忙了`)data[key]=val;}})})}
对于data当中有多层对象的,vue会一直往下找,一直往下找。值得说明的是Object.defineProperty(this,key,)没有利用data的key,用的是另一个对象的key。避免递归。
Vue.config.productionTip = false;const vm = new Vue({el:'#root',data:{name:'尚硅谷',address:'北京',student:{name:'tom',age:{rAge:20,sAge:21},friends:[{name:'jerry',age:13}]}}})
Vue 2 的响应式系统确实会对对象及其初始存在的嵌套属性递归地劫持(分配 getter/setter
),但动态新增的深层属性或嵌套对象需要特殊处理才能保证响应式。
动态新增深层监测或嵌套对象
对于在student当中新增sex属性,可以利用Vue.set(vm._data.student,'sex','男')/vm.$set(vm.student,'sex','女')/vm.$set(vm._data.student,'sex','未知')可以添加新属性,添加的新属性可以有响应式
在代码当中的体现就是
methods:{addSex(){Vue.set(this.student,'sex','男')}}
在代码当中,this代表的是vm。Vue.set只能给data中的某一个对象添加属性,但是不能给data添加属性。
data下的student对象当中包含数组
因为如果对象当中包含数组,那么数组的元素没有对应的get,set,所以如果修改数组,页面不会触发响应式。但是可以用针对数组的一些方法,来触发响应式
所以周小红 this.personArr.splice(0,1,{id:'001',name:'周红',age:20,sex:'女'}),使用这个就可以奏效。
原因:
hobby被vue管理了,hobby用的push不等于array用的push
Vue.set也可以用数据代理来操作数组。
总结
<body><div id="root"><button @click="student.age.rAge++">年龄+1岁</button><!-- <button @click="Vue.set(student,'sex','男')">添加性别属性,默认为男</button> -->错的因为vue不是全局<button @click="this.$set(student,'sex','男')">添加性别属性,默认为男</button> 错的不需要this<!-- 移除this:在模板中直接使用$set,不需要this --><button @click="$set(student, 'sex', '男')">添加性别属性,默认为男</button> 对的<button @click="addFriend">在列表首位添加一个盆友</button><button @click="updatefirstFriend">修改第一个盆友的名字为张三</button><button @click="addhobby">添加一个爱好</button><button @click="updatehobby">修改第一个爱好为开车</button><ul><li v-for = "(f,index) in student.hobby" :key = "index">{{f}}</li></ul><h2>学校:{{name}}</h2><h2>地址:{{address}}</h2><hr/><button @click="addSex">点我添加性别,默认是男</button><h2>学生姓名:{{student.name}}</h2><h2>学生年龄:真实{{student.age.rAge}},对外真实{{student.age.sAge}}</h2><h2 v-if="student.sex">学生性别:{{student.sex}}</h2><h2>朋友们</h2><ul><li v-for = "(f,index) in student.friends" :key = "index">{{f.name}}--{{f.age}}</li></ul></div><script type="text/javascript">Vue.config.productionTip = false;const vm = new Vue({el:'#root',data:{name:'尚硅谷',address:'北京',student:{name:'tom',age:{rAge:20,sAge:21},friends:[{name:'jerry',age:13}],hobby:['抽烟','喝酒','烫头','打麻将']}},methods:{addSex(){Vue.set(this.student,'sex','男')},addFriend(){this.student.friends.unshift({name:'jerry',age:12})},updatefirstFriend(){// this.student.friends.splice(0,1,{// name:'张三',age:12// })//奏效this.student.friends[0].name = '张三'//因为this.student.friends[0]这个对象的name有getset },addhobby(){this.student.hobby.push('学习')},updatehobby(){// this.student.hobby.splice(0,1,'开车')//奏效this.$set(this.student.hobby,0,'开车')}}})// 如果想要什么就先想好,如果自己填充的话,在data,或者vm上添加新属性,vm.data.sex='男',这样子是错的。因为不会有响应式。</script>
</body>
</html>
Vue监视数据的原理:
1.vue会监视data中所有层次的数据。
2.如何监测对象中的数据?
通过setter实现监视,且要在newVue时就传入要监测的数据。(1).对象中后追加的属性,Vue默认不做响应式处理
(2).如需给后添加的属性做响应式,请使用如下API:Vue.set(target,propertyName/index,value)或vm.$set(target,propertyName/index,value)
3.如何监测数组中的数据?通过包裹数组(包装)更新元素的方法实现,本质就是做了两件事:(1).调用原生对应的方法对数组进行更新。
(2).重新解析模板,进而更新页面。
4.在Vue修改数组中的某个元素一定要用如下方法:1.使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
2.Vue.set()或vm.$set()
特别注意:Vue.set()和vm.$set()不能给vm 或 vm的根数据对象 添加属性!!filter,concat,不在包裹数组内,所以直接将结果赋值
数据劫持
数据劫持是Vue框架实现响应式数据绑定的核心技术机制,其核心原理是通过拦截对象属性的访问和修改操作,实现数据变化时视图的自动更新