Vue响应式原理
码字不易,有帮助的同学希望能关注一下我的微信公众号:Code程序人生,感谢!代码自用自取。
vue实现数据响应式,是通过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新,换句话说是Observe,Watcher以及Compile三者相互配合。
- Observe实现数据劫持,递归给对象属性,绑定setter和getter函数,属性改变时,通知订阅者
- Compile解析模板,把模板中
变量
换成数据
,绑定更新函数,添加订阅者,收到通知就执行更新函数 - Watcher作为Observe和Compile中间的桥梁,订阅Observe属性变化的消息,触发Compile更新函数
数据劫持/代理 Observer
实现响应式的第一步就是能侦测数r据的变化,在Vue2.x是通过ES5的方法Object.defineProperty()实现对象属性的侦听,在Vue3.x中使用了ES6提供的Proxy对对象进行代理。
Object.defineProperty
function observe(obj) {
if (!obj || typeof obj !== "object") {
return;
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
function defineReactive(obj, key, value) {
//递归子属性
observe(value);
//订阅器
const dp = new Dep();
Object.defineProperty(obj, key, {
configurable: true, //可删除
enumerable: true, //可枚举遍历
get: function () {
/* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
dp.addSub(Dep.target);
return value;
},
set: function (newValue) {
//递归新的子属性
observe(newValue);
if (value !== newValue) {
value = newValue;
/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
dp.notify();
}
},
});
}
}
Proxy实现代理
let target = { name: " xiao" };
let handler = {
get(target, key) {
if (typeof target[key] === "object" && target[key] !== "null") {
return new Proxy(target[key], handler);
}
return target[key];
},
set: function (target, key, value) {
target[key] = value;
},
};
target = new Proxy(target, handler);
依赖收集Dep
//Dep订阅者,依赖收集器
class Dep {
constructor() {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub(sub) {
this.subs.push(sub);
}
/* 在subs中添加一个Watcher对象 */
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
//用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
//用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。
Watcher订阅者
class Watcher {
constructor(obj, key, cb) {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在observe get中会用到 */
Dep.target = this;
this.obj = obj;
this.key = key;
this.cb = cb;
//触发getter,依赖收集
this.value = obj[key];
//收集完置空Dep.target,防止重复收集
Dep.target = null;
}
update() {
//获得新值
this.value = obj[this.key];
console.log("视图更新");
}
}
Compile模板编译
- 正则匹配解析vue指令、表达式
- 把变量替换成数据初始化渲染
- 创建Watcher订阅更新函数
//指令处理类
const compileUtile = {
getVal(expr,vm){
//reduce用的好啊
return expr.split('.').reduce((data,curentval)=>{
return data[curentval];
},vm.$data)
},
html(node,expr,vm){
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdate(node,newVal);
})
const value = this.getVal(expr,vm);
this.updater.htmlUpdate(node,value);
},
//更新函数
updater:{
htmlUpdate(node,value){
node.innerHTML= value;
},
}
}
//Compile指令解析器
class Compile{
//各种正则匹配vue指令和表达式,替换数据
}
Object.defineProperty与Proxy的区别?
- Proxy可以直接监听对象,而非属性,可以监听属性的增加
- Proxy可以监听数组
- Proxy有很多Object.defineProperty不具备的拦截方法
- Proxy返回一个新对象,可以直接操作新对象达到目的,Object.defineProperty只能遍历对象属性修改
为什么要依赖收集?
数据劫持的目的是在属性变化的时候触发视图更新,依赖收集可以收集到哪些地方使用到了相关属性,属性变化时,就可以通知到所有的地方去更新视图,对于没有使用的属性,也可以避免无用的数据比对更新
Dep和Watcher的关系(多对多)
- data中一个key对应一个Dep实例, 一个Dep实例对应多个Watcher实例(一个属性在多个表达式中使用)
- 一个表达式对应一个Watcher实例,一个Watcher对用多个Dep实例(一个表达式中有多个属性)
watcher和Dep何时创建
- Dep在初始化data的属性进行数据劫持时创建的
- Watcher是在初始化时解析大括号表达式/一般指令时创建
如何实现对数组的监听
因为Object.defineProperty
不能监听数组长度变化,所以Vue使用了函数劫持
的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
1 // src/core/observer/array.js
2
3 // 获取数组的原型Array.prototype,上面有我们常用的数组方法
4 const arrayProto = Array.prototype
5 // 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
6 export const arrayMethods = Object.create(arrayProto)
7
8 // 列出需要重写的数组方法名
9 const methodsToPatch = [
10 'push',
11 'pop',
12 'shift',
13 'unshift',
14 'splice',
15 'sort',
16 'reverse'
17 ]
18 // 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
19 methodsToPatch.forEach(function (method) {
20 // 保存一份当前的方法名对应的数组原始方法
21 const original = arrayProto[method]
22 // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
23 def(arrayMethods, method, function mutator (...args) {
24 // 调用数组原始方法,并传入参数args,并将执行结果赋给result
25 const result = original.apply(this, args)
26 // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
27 const ob = this.__ob__
28 let inserted
29 switch (method) {
30 case 'push':
31 case 'unshift':
32 inserted = args
33 break
34 case 'splice':
35 inserted = args.slice(2)
36 break
37 }
38 if (inserted) ob.observeArray(inserted)
39 // 将当前数组的变更通知给其订阅者
40 ob.dep.notify()
41 // 最后返回执行结果result
42 return result
43 })
44 })
def就是通过Object.defineProperty重写value,也就是自定义的几个数组方法
function def(obj,key,val,enumble){
Object.defineProperty(obj,key,{
enumble:!!enumble,
configrable:true,
writeble:true,
val:val
})
}
observe方法里面加入数组的处理,
- 能获取到
__proto__
属性,就把__protp__
属性指向重写的方法 - 获取不到
__proto__
属性,就把重写的方法定义到对象上实例上
// src/core/observer/index.js
export class Observer {
...
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
...
}
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
有微信小程序课设、毕设需求联系个人QQ:505417246
关注下面微信公众号,可以领取微信小程序、Vue、TypeScript、前端、uni-app、全栈、Nodejs、Python等实战学习资料
最新最全的前端知识总结和项目源码都会第一时间发布到微信公众号,请大家多多关注,谢谢
相关文章
- Vue笔记:安装配置node.js及使用vue-cli创建项目
- JS框架_(Vue.js)带有星期日期的数字时钟
- Vue - 定义使用组件
- 深入浅出Vue基于“依赖收集”的响应式原理(转)
- [Vue @Component] Simplify Vue Components with vue-class-component
- [Vue] Get up and running with vue-router
- vue.js3:色调色相调整并保存(vue@3.2.37)
- vue.js3.2.20: 在打包时取消生成.map文件
- 使用vue搭建应用四引入axios
- vue.js3:分割图片为四宫格九宫格等形式(vue@3.2.37)
- vue.js3:多张图片转pdf(jspdf@2.5.1 / vue@3.2.37)
- springboot+vue实现前后端分离之前端vue部分(spring boot 2.5.4/vue.js 3.2.4)
- Vue+Spring boot前后端响应流程总结
- standalone vue initialization process - Vue应用的初始化过程
- vue 的 render 函数的用法:new Vue({ render: h => h(App), }).$mount(‘#app‘)
- Vue CLI 4.x安装成功,但是无法运行vue-V等指令。报错vue -V 不是内部或外部命令,也不是可运行的程序或批处理文件解决方案!!!
- vue项目报错:migrating.js?2bb5:56 [Element Migrating][ElInput][Event]: click is removed.
- Vue+ElementUI项目使用webpack输出MPA【华为云分享】
- Springboot+Vue实现将图片和表单一起提交到后端,同时将图片地址保存到数据库、再次将存储的图片展示到前端vue页面
- vue3 报错解决:找不到模块‘xxx.vue’或其相应的类型声明。(Vue 3 can not find module)
- 前端MVC Vue2学习总结(二)——Vue的实例、生命周期与Vue脚手架(vue-cli)
- 123:vue+openlayers 动态显示鼠标位置坐标2 (示例代码)
- 118:vue+openlayers 加载geoserver普通layer的数据(示例代码)
- 在Vue项目中引入 ECharts 3D 路径图 Flights GL(需安装echarts、echarts-gl、jQuery依赖,已踩坑)
- Vue(一)vue 概述与用法、MVVM、绑定语法、vue 指令
- Vue解析markdown字符串,并且将其展示到页面(代码高亮+代码行号显示)
- Vue安装
- 【三十天精通Vue 3】第六天 Vue 3 计算属性和监听器详解
- Vue: vue-router路由
- Vue:第一个vue-cli项目