1、Vue项目中为什么要在列表组件中写key,作用是什么?
我们在业务组件中,会经常使用循环列表,当时用v-for
命令时,会在后面写上:key
,那么为什么建议写呢?
key
的作用是更新组件时判断两个节点是否相同。相同则复用,不相同就删除旧的创建新的。正是因为带唯一key
时每次更新都不能找到可复用的节点,不但要销毁和创建节点,在DOM
中还要添加移除节点,对性能的影响更大。所以才说,当不带key
时,性能可能会更好。 因为不带key
时,节点会复用(复用是因为Vue
使用了Diff
算法),省去了销毁或创建节点的开销,同时只需要修改DOM
文本内容而不是移除或添加节点。既然如此,为什么我们还要建议带key
呢?因为这种不带key
的模式只适合渲染简单的无状态的组件。对于大多数场景来说,列表都得必须有自己的状态。避免组件复用引起的错误。 带上key
虽然会增加开销,但是对于用户来说基本感受不到差距,为了保证组件状态正确,避免组件复用,这就是为什么建议使用key。
2、Vue的双向绑定,Model如何改变View,View又是如何改变Model的?
我们先看一幅图,下面一幅图就是Vue双向绑定的原理图。
第一步,使数据对象变得“可观测”
我们要知道数据在什么时候被读或写了。
let person = { 'name': 'maomin', 'age': 23 } let val = 'maomin'; Object.defineProperty(person, 'name', { get() { console.log('name属性被读取了') return val }, set(newVal) { console.log('name属性被修改了') val = newVal } }) // person.name // name属性被读取了 // "maomin" // person.name='xqm' // name属性被修改了 // "xqm"
通过Object.defineProperty()
方法给person
定义了一个name
属性,并把这个属性的读和写分别使用get()
和set()
进行拦截,每当该属性进行读或写操作的时候就会触发get()
和set()
。这样数据对象已经是“可观测”的了。
核心是利用es5
的Object.defineProperty
,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。
Object.defineProperty
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
Object.defineProperty( obj, // 定义属性的对象 prop, // 要定义或修改的属性的名称 descriptor // 将要定义或修改属性的描述符【核心】 )
写一个简单的双向绑定:
<!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>Document</title> </head> <body> <input type="text" id="input"/> <div id="text"></div> </body> <script> let input = document.getElementById('input'); let text = document.getElementById('text'); let data = {value:''}; Object.defineProperty(data,'value',{ set:function(val){ text.innerHTML = val; input.value = val; }, get:function(){ return input.value; } }); input.onkeyup = function(e){ data.value = e.target.value; } </script> </html>
第二步,使数据对象的所有属性变得“可观测”
上面,我们只能观测person.name
的变化,那么接下来我们要让所有的属性都变得可检测。
let person = observable({ 'name': 'maomin', 'age': 23 }) /** * 把一个对象的每一项都转化成可观测对象 * @param { Object } obj 对象 */ function observable(obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); //返回一个表示给定对象的所有可枚举属性的字符串数组 keys.forEach((key) => { defineReactive(obj, key, obj[key]) }) return obj; } /** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */ function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log(`${key}属性被读取了`); return val; }, set(newVal) { console.log(`${key}属性被修改了`); val = newVal; } }) } // person.age // age属性被读取了 // 23 // person.age=24 // age属性被修改了 // 24
我们通过Object.keys()
将一个对象返回一个表示给定对象的所有可枚举属性的字符串数组,然后遍历它,使得所有对象可以被观测到。
第三步,依赖收集,制作一个订阅器
我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。 创建一个依赖收集容器,也就是消息订阅器Dep
,用来容纳所有的“订阅者”。订阅器Dep
主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。
设计了一个订阅器Dep类:
class Dep { constructor(){ this.subs = [] }, //增加订阅者 addSub(sub){ this.subs.push(sub); }, //判断是否增加订阅者 depend () { if (Dep.target) { this.addSub(Dep.target) } }, //通知订阅者更新 notify(){ this.subs.forEach((sub) =>{ sub.update() }) } } Dep.target = null;
创建完订阅器,然后还要修改一下defineReactive
function defineReactive (obj,key,val) { let dep = new Dep(); Object.defineProperty(obj, key, { get(){ dep.depend(); //判断是否增加订阅者 console.log(`${key}属性被读取了`); return val; }, set(newVal){ val = newVal; console.log(`${key}属性被修改了`); dep.notify() //数据变化通知所有订阅者 } }) }
我们将订阅器Dep
添加订阅者的操作设计在get()
里面,这是为了让订阅者初始化时进行触发,因此需要判断是否要添加订阅者。
第四步,订阅者Watcher
设计一个订阅者Watcher类:
class Watcher { // 初始化 constructor(vm,exp,cb){ this.vm = vm; // 一个Vue的实例对象 this.exp = exp; // 是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name; this.cb = cb; // 是Watcher绑定的更新函数; this.value = this.get(); // 将自己添加到订阅器的操作 }, // 更新 update(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); }, get(){ Dep.target = this; // 缓存自己 let value = this.vm.data[this.exp] // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; } }
订阅者Watcher
在初始化的时候需要将自己添加进订阅器Dep
中,如何添加呢?我们已经知道监听器Observer
是在get()
执行了添加订阅者Wather
的操作的,所以我们只要在订阅者Watcher
初始化的时候触发对应的get()
去执行添加订阅者操作即可。那要如何触发监听器get()
,再简单不过了,只要获取对应的属性值就可以触发了。
订阅者Watcher运行时,首先进入初始化,就会执行它的 this.get()
方法, 执行Dep.target = this;
,实际上就是把Dep.target
赋值为当前的渲染 Watcher
,接着又执行了let value = this.vm.data[this.exp];
。在这个过程中会对数据对象上的数据访问,其实就是为了触发数据对象的get()
。
每个对象值的get()
都持有一个dep
,在触发 get()
的时候会调用 dep.depend()
方法,也就会执行this.addSub(Dep.target)
,即把当前的 watcher
订阅到这个数据持有的dep.subs
中,这个目的是为后续数据变化时候能通知到哪些 subs
做准备。完成依赖收集后,还需要把 Dep.target
恢复成上一个状态Dep.target = null;
因为当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target
也需要改变。
而update()
是用来当数据发生变化时调用Watcher
自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];
获取到最新的数据,然后将其与之前get()
获得的旧数据进行比较,如果不一样,则调用更新函数cb
进行更新。
总结:
实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer
,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher
看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep
来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。
实现一个Vue数据绑定:
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1 id="name"></h1> <input type="text"> <input type="button" value="改变data内容" onclick="changeInput()"> <script src="observer.js"></script> <script src="watcher.js"></script> <script> function myVue (data, el, exp) { this.data = data; observable(data); //将数据变的可观测 el.innerHTML = this.data[exp]; // 初始化模板数据的值 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this; } var ele = document.querySelector('#name'); var input = document.querySelector('input'); var myVue = new myVue({ name: 'hello world' }, ele, 'name'); //改变输入框内容 input.oninput = function (e) { myVue.data.name = e.target.value } //改变data内容 function changeInput(){ myVue.data.name = "改变后的data" } </script> </body> </html>
observer.js(为了方便,这里将订阅器与监听器写在一块)
// 监听器 // 把一个对象的每一项都转化成可观测对象 // @param { Object } obj 对象 function observable (obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); keys.forEach((key) =>{ defineReactive(obj,key,obj[key]) }) return obj; } // 使一个对象转化成可观测对象 // @param { Object } obj 对象 // @param { String } key 对象的key // @param { Any } val 对象的某个key的值 function defineReactive (obj,key,val) { let dep = new Dep(); Object.defineProperty(obj, key, { get(){ dep.depend(); console.log(`${key}属性被读取了`); return val; }, set(newVal){ val = newVal; console.log(`${key}属性被修改了`); dep.notify() //数据变化通知所有订阅者 } }) } // 订阅器Dep class Dep { constructor(){ this.subs = [] } //增加订阅者 addSub(sub){ this.subs.push(sub); } //判断是否增加订阅者 depend () { if (Dep.target) { this.addSub(Dep.target) } } //通知订阅者更新 notify(){ this.subs.forEach((sub) =>{ sub.update() }) } } Dep.target = null;
watcher.js
class Watcher { constructor(vm,exp,cb){ this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 将自己添加到订阅器的操作 } get(){ Dep.target = this; // 缓存自己 let value = this.vm.data[this.exp] // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; } update(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } } }