前言
本篇博客主要回顾了vue.js的两大核心之一,数据驱动。
参考博客:https://juejin.cn/post/6989106100582744072;https://juejin.cn/post/6844904067525935118
面试回答
1.数据驱动:vue的核心是数据驱动,vue2中是利用Object.defineproperty进行数据劫持,同时结合发布者订阅者模式来实现数据的双向绑定,首先有一个发布者类,它提供添加订阅者方法以及通知方法,这两个方法中都要求订阅者类需要有更新方法;然后有一个数据劫持类,它主要是将数据转换成getter、setter,并且在getter中触发添加订阅者类方法,在setter中触发通知方法,订阅者类中则提供更新方法。当数据改变时,就会触发数据劫持类中的setter方法,去调用发布者类中的通知方法,从而调用订阅者中的更新方法;当然还有模板编译类中,用来处理元素节点上的指令以及文本节点的差值表达式这些;最后汇总在vue.js中,new一个数据劫持类、模板编译类,并且把响应式的数据绑定到vue.js上,完成数据的双向绑定。
2.vue3和vue2的区别:从原理上来说,vue2 的双向数据绑定是利⽤ES5的Object.definePropert来对数据进⾏劫持再结合发布订阅模式来实现的。而vue3中使⽤了es6的Proxy对数据进行代理,通过reactive函数给每⼀个对象都包⼀层Proxy来监听属性的变化。vue3这种方式能够避免definePropery不能进行全对象监听并且需要对数组进行特异性操作的一个缺陷。从算法上来说,vue3优化了diff算法,主要从三个方向进行优化,第一个优化是节点更新添加标记,对动态数据进行标记,对于静态数据则直接使用缓存,就是不再对所有的dom进行对比。第二个优化是数据循环优化,主要是对vnode节点遍历循环的方法进行优化,比如头部循环优化、尾部循环优化以及最长链循环优化。最后一个是事件优化,对于各类事件用闭包进行缓存,从而不引起重新渲染,来达到加速的目的。从使用上来说,就是一个选项式Api与组合式Api的区别,vue2是选项式Api,将data、methods、watch、computed都分开管理,而vue3是组合式Api,将所有的逻辑放到setup里面。
知识点
vue核心的本质:利用Object.defineProperty(vue2)/Proxy(vue3)进行数据劫持各个属性的getter、setter,同时结合观察者模式,来实现数据双向绑定以及响应式。其中Proxy是JS2015(ES6)的一个新特性。Proxy的代理是针对整个对象的,而不是对象的某个属性,因此不同于Object.defineProperty的必须遍历对象每个属性,Proxy只需要做一层代理就可以监听统计结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外,Proxy支持代理数组的变化,不过Proxy兼容性不太好。
1.数据拦截
这个知识点应用于实现MVVM库/框架的数据绑定,即改变数据的同时,可以同步更改页面上对应的数据。
let test = {
a:1,
b:'zxp',
c:{abc:'abc'}
}
1.ES5:defineProperty()
Object.defineProperty(obj, prop, descriptor) :obj,要在其上定义属性的对象。prop,要定义或修改的属性的名称。descriptor,将被定义或修改的属性描述符,也就是方法。
- ES5出来的方法
- 三个参数:对象(必填)、属性值(必填)、描述符(可选)
- defineProterty的描述符属性
数据属性:value,writable,configurable,enumerable
访问器属性:get,set
注:不能同时设置value和writable,这两对属性是互斥的
拦截对象的两种情况
let obj = {name:'',age:'',sex:'' },
defaultName = ["这是姓名默认值1","这是年龄默认值1","这是性别默认值1"];
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
get() {
return defaultName;
},
set(value) {
defaultName = value;
}
});
});
console.log(obj.name);
console.log(obj.age);
console.log(obj.sex);
obj.name = "这是改变值1";
console.log(obj.name);
console.log(obj.age);
console.log(obj.sex);
let objOne={},defaultNameOne="这是默认值2";
Object.defineProperty(obj, 'name', {
get() {
return defaultNameOne;
},
set(value) {
defaultNameOne = value;
}
});
console.log(objOne.name);
objOne.name = "这是改变值2";
console.log(objOne.name);
拦截数组变化的情况
let a={};
bValue=1;
Object.defineProperty(a,"b",{
set:function(value){
bValue=value;
console.log("setted");
},
get:function(){
return bValue;
}
});
a.b;//1
a.b=[];//setted
a.b=[1,2,3];//setted
a.b[1]=10;//无输出
a.b.push(4);//无输出
a.b.length=5;//无输出
a.b;//[1,10,3,4,undefined];
//结论:defineProperty无法检测数组索引赋值,改变数组长度的变化; 但是通过数组方法来操作可以检测到
拦截数组变化的情况多级嵌套对象监听
let info = {};
function observe(obj) {
if (!obj || typeof obj !== "object") {
return;
}
for (var i in obj) {
definePro(obj, i, obj[i]);
}
}
function definePro(obj, key, value) {
observe(value);
Object.defineProperty(obj, key, {
get: function() {
return value;
},
set: function(newval) {
console.log("检测变化", newval);
value = newval;
}
});
}
definePro(info, "friends", { name: "张三" });
info.friends.name = "李四";
缺点:
1、只能监听属性,而不是监听对象本身,需要对对象的每个属性进行遍历(提取高度嵌套的对象里的属性可以采取递归的方法),对于原本不在对象中的属性难以监听。
2、无法监听数组的变化: 数组的这些方法是无法触发set的:push, pop, shift, unshift,splice, sort, reverse,vue中能监听是因为对这些方法进行了重写。
2.ES6:Proxy()
new Proxy(target,handler)。target,用Proxy包装的目标对象(可以是任何类型的对象)。handler,一个对象,其属性是当执行一个操作时定义代理的行为的函数。
实例:
let handler = {
get: function(target, key){
console.log('get:',key)
return key in target ? target[key] : 'error'
},
set: function(target,key,val){
console.log('set:',val)
target[key] = val
}
};
let p = new Proxy(test, handler);
p.a = 5
//set: 5
//5
p.b = 'qwe'
//set: qwe
//'qwe'
console.log(p.a, p.b);
//get: a
//get: b
//5 'qwe'
p.c.abc = 'chiji'
//get: c
//'chiji'
console.log('d' in p, p.d);
//get: d
//false 'error'
test 输出为
{
a:5,
b:"qwe",
c:{
abc:"chiji"
}
}
涉及到多级对象或者多级数组
//传递两个参数,一个是object, 一个是proxy的handler
//如果是不是嵌套的object,直接加上proxy返回,如果是嵌套的object,那么进入addSubProxy进行递归。
function toDeepProxy(object, handler) {
if (!isPureObject(object)) addSubProxy(object, handler);
return new Proxy(object, handler);
//这是一个递归函数,目的是遍历object的所有属性,如果不是pure object,那么就继续遍历object的属性的属性,如果是pure object那么就加上proxy
function addSubProxy(object, handler) {
for (let prop in object) {
if ( typeof object[prop] == 'object') {
if (!isPureObject(object[prop])) addSubProxy(object[prop], handler);
object[prop] = new Proxy(object[prop], handler);
}
}
object = new Proxy(object, handler)
}
//是不是一个pure object,意思就是object里面没有再嵌套object了
function isPureObject(object) {
if (typeof object!== 'object') {
return false;
} else {
for (let prop in object) {
if (typeof object[prop] == 'object') {
return false;
}
}
}
return true;
}
}
let object = {
name: {
first: {
four: 5,
second: {
third: 'ssss'
}
}
},
class: 5,
arr: [1, 2, {arr1:10}],
age: {
age1: 10
}
}
//这是一个嵌套了对象和数组的数组
let objectArr = [{name:{first:'ss'}, arr1:[1,2]}, 2, 3, 4, 5, 6]
//这是proxy的handler
let handler = {
get(target, property) {
console.log('get:' + property)
return Reflect.get(target, property);
},
set(target, property, value) {
console.log('set:' + property + '=' + value);
return Reflect.set(target, property, value);
}
}
//变成监听对象
object = toDeepProxy(object, handler);
objectArr = toDeepProxy(objectArr, handler);
//进行一系列操作
console.time('pro')
objectArr.length
objectArr[3];
objectArr[2]=10
objectArr[0].name.first = 'ss'
objectArr[0].arr1[0]
object.name.first.second.third = 'yyyyy'
object.class = 6;
object.name.first.four
object.arr[2].arr1
object.age.age1 = 20;
console.timeEnd('pro')
//问题和优点:reflect对象没有构造函数 可以监听数组索引赋值,改变数组长度的变化, 是直接监听对象的变化,不用深层遍历
defineProterty和proxy的对比
- defineProterty是es5的标准,proxy是es6的标准
- proxy可以监听到数组索引赋值,改变数组长度的变化
- proxy是监听对象,不用深层遍历,defineProterty是监听属性
- 利用defineProterty实现双向数据绑定(vue2.x采用的核心) ,利用proxy实现双向数据绑定(vue3.x会采用)
2.数据双向绑定
实现数据双向绑定步骤:
- 实现目标类(dep.js),包含添加观察者方法、通知方法。
- 实现数据劫持类(observer.js),完成对传入数据的响应式处理,如对数据对象所有属性进行监听,并在get方法里调用目标类的添加观察者方法以及在set中触发通知方法,更新视图。
- 实现观察者类(watcher.js),观察者类包含更新视图方法以及初始化时通过vue连接数据劫持类(observer.js)触发get方法、获取新值、将观察者类储存在目标类中,来实现收到每个属性变动的通知及执行相应函数来更新视图。
- 实现模板编译类(Compiler.js),通过传入的参数,主要对node节点以及指令进行遍历,若涉及文本变量则创建新的观察者类,如果是元素节点,则遍历处理指令如v-text、v-model,并执行相应操作,指令方法内都需要创建新的观察者类。
- 实现Vue.js类,整合上述类,首先获取数据、元素节点,再处理数据,比如将数据与vue实例绑定,以及创建数据劫持类、模板编译类。
1.Dep.js(发布者类)
每个响应式属性都会创建这么一个Dep对象 ,在使用响应式数据的时候,负责收集Watcher对象。当我们对响应式属性在setter中进行更新的时候,会调用Dep中notify方法发送更新通知,然后去调用 Watcher中的update实现视图的更新操作,该类主要需做以下三件事:
- 提供添加观察者的方法,让其在实例化时后往目标类(Dep.js)里面添加自己。
- 观察者类(watcher.js)必须有一个update()方法,因为在数据劫持类(observer.js)会将观察者类添加到subs中。
- 待属性变动dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调。
class Dep {
constructor() {
// 存储观察者
this.subs = []
}
// 添加观察者
addSub(sub) {
// 判断观察者是否存在 和 是否拥有update方法
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 通知方法
notify() {
// 触发每个观察者的更新方法
this.subs.forEach((sub) => {
sub.update()
})
}
}
2.Observer.js(数据劫持类)
数据劫持类主要实现以下逻辑:
- 使用 observer 把 data 中的属性转为 响应式 添加到 自身身上
- 使用 observer 方法监听 data 的所有属性变化来通过观察者模式,更新视图
- 当对象的属性值也是对象时,也要对其值进行劫持-----递归
- 当对象赋值与旧值一样,则不需要后续操作------防止重复渲染
- 当模板渲染获取对象属性会调用get添加target,对象属性改动通知订阅者更新----数据变化,视图更新
Tip:Vue通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,一般通过$set(obj,index,value)
或者 $delete(obj,index,value)
进行处理,常见的有,不支持数组的长度变化(arr.length-1);修改数组中的指定元素,也无法侦测数组的变化(arr[0] =1)
class Observer {
constructor(data) {
// 用来遍历 data
this.walk(data)
}
// 遍历 data 转为响应式
walk(data) {
// 判断 data是否为空 和 对象
if (!data || typeof data !== 'object') return
// 遍历 data
Object.keys(data).forEach((key) => {
// 转为响应式
this.defineReactive(data, key, data[key])
})
}
// 转为响应式
// 要注意的 和vue.js 写的不同的是
// vue.js中是将属性给了Vue转为 getter setter
// 这里是将data中的属性转为getter setter
defineReactive(obj, key, value) {
// 如果是对象类型的 也调用walk 变成响应式,不是对象类型的直接在walk会被return
this.walk(value)
// 保存一下 this
const self = this
// 创建 Dep 对象
let dep = new Dep()
Object.defineProperty(obj, key, {
// 设置可枚举
enumerable: true,
// 设置可配置
configurable: true,
// 获取值
get() {
// 在这里添加观察者对象 Dep.target 表示观察者
Dep.target && dep.addSub(Dep.target)
return value
},
// 设置值
set(newValue) {
// 判断旧值和新值是否相等
if (newValue === value) return
// 设置新值
value = newValue
// 赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的
self.walk(newValue)
// 触发通知 更新视图
dep.notify()
},
})
}
}
3.Watcher.js(订阅者类)
watcher的作用是数据更新后收到通知之后调用update 进行更新,当我们改变响应式属性的时候,就会触发Observer中的set()方法 ,然后调用notify 方法,拿到了所有的观察者watcher 实例去执行 update方法调用了回调函数 cb(newValue) 方法并把新值传递到了cb(),而cb方法的具体逻辑则在Watcher里(可以参考Compiler.js中的new Watcher),去更新视图。
class Watcher {
constructor(vm, key, cb) {
// vm 是 Vue 实例
this.vm = vm
// key 是 data 中的属性
this.key = key
// cb 回调函数 更新视图的具体方法
this.cb = cb
// 把观察者的存放在 Dep.target
Dep.target = this
// 在执行完下面这一条语句后,会触发observer中 get 方法,watcher通过vue连接到ovserver中
// observer中的get 把 观察者 通过dep.addSub(Dep.target) 添加到了 dep.subs中
this.oldValue = vm[key]
// Dep.target 就不用存在了 因为上面的操作已经存好了
Dep.target = null
}
// 观察者中的必备方法 用来更新视图
update() {
// 获取新值
let newValue = this.vm[this.key]
// 比较旧值和新值
if (newValue === this.oldValue) return
// 调用具体的更新方法
this.cb(newValue)
}
}
4.Compiler.js(模板编译类)
使用compiler编译元素节点上面指令和文本节点差值表达式
Comilper.js在这个文件里实现对文本节点和元素节点指令编译,比如v-text、v-model,他做了以下三件事:
- 将当前根节点所有子节点遍历放到内存中
- 编译文档碎片,替换模板(元素、文本)节点中属性的数据
- 将编译的内容回写到真实DOM上
【重点】:
- 先把真实的 dom 移入到内存中操作 --- 文档碎片
- 编译元素节点和文本节点
- 给模板中的表达式和属性添加观察者(Watcher.js)
class Compiler {
// vm 指 Vue 实例
constructor(vm) {
// 拿到 vm
this.vm = vm
// 拿到 el
this.el = vm.$el
// 编译模板
this.compile(this.el)
}
// 编译模板
compile(el) {
// 获取子节点 如果使用 forEach遍历就把伪数组转为真的数组
let childNodes = [...el.childNodes]
childNodes.forEach((node) => {
// 根据不同的节点类型进行编译
// 文本类型的节点
if (this.isTextNode(node)) {
// 编译文本节点
this.compileText(node)
} else if (this.isElementNode(node)) {
//元素节点
this.compileElement(node)
}
// 判断是否还存在子节点考虑递归
if (node.childNodes && node.childNodes.length) {
// 继续递归编译模板
this.compile(node)
}
})
}
// 判断是否是 文本 节点
isTextNode(node) {
return node.nodeType === 3
}
// 编译文本节点(简单的实现)
compileText(node) {
// 核心思想利用把正则表达式把{{}}去掉找到里面的变量
// 再去Vue找这个变量赋值给node.textContent
let reg = /\{\{(.+?)\}\}/
// 获取节点的文本内容
let val = node.textContent
// 判断是否有 {{}}
if (reg.test(val)) {
// 获取分组一 也就是 {{}} 里面的内容 去除前后空格
let key = RegExp.$1.trim()
// 进行替换再赋值给node
node.textContent = val.replace(reg, this.vm[key])
// 创建观察者
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
// 编译元素节点这里只处理指令
compileElement(node) {
// 获取到元素节点上面的所有属性进行遍历
// 问题:这里为什么要加一个!
![...node.attributes].forEach((attr) => {
// 获取属性名
let attrName = attr.name
// 判断是否是 v- 开头的指令
if (this.isDirective(attrName)) {
// 除去 v- 方便操作
attrName = attrName.substr(2)
// 获取 指令的值就是 v-text = "msg" 中msg
// msg 作为 key 去Vue 找这个变量
let key = attr.value
// 指令操作 执行指令方法
// vue指令很多为了避免大量个 if判断这里就写个 uapdate 方法
//问题:这一步是干嘛的
this.update(node, key, attrName)
}
})
}
// 判断元素的属性是否是 vue 指令
isDirective(attr) {
return attr.startsWith('v-')
}
// 添加指令方法 并且执行
update(node, key, attrName) {
// 比如添加 textUpdater 就是用来处理 v-text 方法
// 我们应该就内置一个 textUpdater 方法进行调用
// 加个后缀加什么无所谓但是要定义相应的方法
let updateFn = this[attrName + 'Updater']
// 如果存在这个内置方法 就可以调用了
updateFn && updateFn.call(this, node, key, this.vm[key])
}
// 实例
// 提前写好 相应的指定方法比如这个 v-text
// 使用的时候 和 Vue 的一样
textUpdater(node, key, value) {
node.textContent = value
// 创建观察者
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// v-model
modelUpdater(node, key, value) {
node.value = value
// 创建观察者
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 这里实现双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
}
5.Vue.js
vue.js以及observer.js都对数据进行劫持的原因是,obsever.js中是把data的所有属性加到data自身,变为响应式转成getter 、setter方式;在vue.js 中 也把data的所有属性加到Vue上,是为了以后方面操作可以用Vue的实例直接访问到或者在 Vue 中使用 this 访问,这样在Vue
和 $data
中都存在了 所有的data属性了并且是响应式的。
class Vue {
constructor(options) {
// 获取到传入的对象 没有默认为空对象
this.$options = options || {};
this.$el = typeof options.el === 'string' ?
document.querySelector(options.el) :
options.el;
//获取data
this.$data = options.data || {};
// 调用 _proxyData 处理 data中的属性,这里的目的是让用户可以直接通过vue.js去拿到值
this._proxyData(this.$data); //走到这里直接去看_proxyData方法
// 使用 Obsever 把data中的数据转为响应式,走到这里直接去处理Observer类
new Observer(this.$data);
// 编译模板
new Compiler(this)
}
_proxyData(data) {
Object.keys(data).forEach(key => {
// 进行数据劫持
// 把每个data的属性 到添加到 Vue 转化为 getter setter方法
Object.defineProperty(this, key, {
// 设置可以枚举
enumerable: true,
// 设置可以配置
configurable: true,
// 获取数据
get() {
return data[key];
},
// 设置数据
set(newValue) {
// 判断新值和旧值是否相等
if (newValue === data[key]) return;
// 设置新值
data[key] = newValue;
},
});
});
}
}
6.HTML
<body>
<div id="app">
{{msg}} <br />
{{age}} <br />
<div v-text="msg"></div>
<input v-model="msg" type="text" />
</div>
<script src="./js/dep.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: '123',
age: 21,
},
})
</script>
</body>
最后
走过路过,不要错过,点赞、收藏、评论三连~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。