Vue2.X实现
一、理论知识
Vue响应式数据原理:
Vue内部通过Object.defineProperty方法属性拦截的方式,把目标对象中的每个数据读写转换为getter/setter,当数据变化时通知视图更新。
二、基本流程
实现一个类似于vue响应式数据绑定的响应式系统,以下是简易流程图
由上面的图,关于监听器(Observer)、订阅器(Dep)、订阅者(Watcher)可能对没有学过设计模式的人有一点点抽象,那换个简单的例子说下他们的关系
小明要买房子,于是他加了一个售楼部销售员小红的微信,小红有很多关于住房的信息,小明想要的户型A恰好没有了,就拜托小红注意最新的消息,一旦这个户型又有了就立马通知小明,小明就马上付钱买下这个户型。小红有很多的客户其实都想买这个户型A,为了方便之后通知这些有意向的购买者,小红把这些人的微信记录在一个小本本上并且和户型A关联在一起。
在以上信息中,我们抽离出小明这个人他就是一个订阅者实例,小红手上的户型A信息就是数据,小红就是监听器实例,而这个记录这些A户型意向购买者信息的小本本就是Dep实例。当户型A有开始售卖了,小红就赶紧通知小本本上的人。
这里是举的一个小小的例子,可能不太恰当,详细具体的可以去看发布-订阅模式
接下来,我们一点一点的实现一个响应式系统
1.实现监听器Observer
在这里我们将通过Object.defineProperty方法实现数据劫持,也就是说我们能感知数据对象的读写,使得数据对象变得可观测。
- 关于ObjectdefineProperty
这里就不展开细说了 详细的使用方法查看MDN。需要注意的就是
属性描述符和存取描述符的使用
接下来是coding时间:
基本实现
- defineReactive()方法实现属性的数据拦截
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get: function getter() {
console.log("获取")
return value;
},
set: function setter(newValue) {
if (newValue === value) return;
value = newValue
console.log("设置")
}
})
}
以上函数通过设置setter和getter方法来实现数据的监听
- Observer类实现对象属性的遍历拦截
class Observer {
constructor(obj) {
this.value = obj
this.trave()
}
trave() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key]))
}
}
以上通过Object.keys方法遍历对象的所有属性,从而实现每个属性的数据拦截
优化提升
通过以上代码我们基本可以实现对一个对象obj所有属性的数据劫持,但是有一个小小的问题,如果对象属性是一个对象呢,也就是说obj内有嵌套属性,因此我们可以进行遍历,因此将代码优化为
class Observer {
constructor(obj) {
this.value = obj
this.trave()
}
trave() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key]))
}
}
function observe(obj){
if(!obj||typeof obj!=='object'){
return;
}
//监听器实例化
new Observer(obj);
}
function defineReactive(obj, key, value) {
//对每个属性值进行判断
observe(value)
Object.defineProperty(obj, key, {
get: function getter() {
console.log("获取")
return value;
},
set: function setter(newValue) {
if (newValue === value) return;
value = newValue
//对新设置的属性也需要进行判断
observe(newValue)
console.log("设置")
}
})
}
以上通过observe函数判断对象是否有嵌套属性,有的话就递归遍历,实现每一个属性的数据劫持
2.实现订阅者Watcher
先来了解Watcher需要实现的功能是什么?
- 当实例化一个订阅者的时候,意味着需要获取它订阅的属性
- 当订阅者收到数据变化后,需要去调用相应的回调函数
接下来coding时间:
基本实现
class Watcher{
constructor(obj,exp,cb){
this.obj = obj;
//exp 为string类型 类似于'person.name'这样的string
this.exp = exp;
this.cb = cb;
//实例化Watcher实例时获取它订阅的属性
this.value = this.get()
}
//订阅属性
get(){
var value = getValue(this.obj,this.exp);
return value;
}
//数据变化时,执行回调函数
update(){
var newVal = getValue(this.obj,this.exp);
var oldVal = this.value;
if(newVal!==oldVal){
//数据更新
this.value = newVal;
//执行回调函数
this.cb();
}
}
}
function getValue(obj,exp) {
var keys= exp.split(".");
var value;
for(let i of keys){
if(!obj) return;
value = obj[i];
}
return value;
}
关于Watcher的实例有很多地方需要去优化,先将订阅器Dep实现之后,我们再来探讨关于Watcher的优化
3.实现订阅器Dep
我们先来了解一下它需要实现的功能是什么?
- 每一个属性对应一个dep实例,dep实例存储每一个订阅该属性的Watcher
- 当数据发生变化时,对应的dep实例要去通知存储的每一个watcher
基本实现
class Dep{
constructor(){
this.subs = [];
}
//将Watcher实例添加进subs数组
addSub(sub){
this.subs.push(sub);
}
//派发消息 使得每个watcher实例调用update方法
notify(){
this.subs.forEach(function (sub) {
sub.update();
})
}
}
以上只是实现了一个Dep类,有添加和通知watcher实例的方法
优化提升
经过上述的coding,我们只是进行了基本类的实现,而他们之间的联系还没有进行实现,具体是以下功能:
- 属性什么时候添加订阅器,属性改变时怎么通知对应的dep实例
通过改变数据劫持函数,在遍历每个属性的时候,添加对应的dep实例。
在setter方法更改属性值时,就需要去通知dep实例
function defineReactive(obj, key, value) {
//实例化dep
var dep = new Dep();
observe(value)
Object.defineProperty(obj, key, {
get: function getter() {
return value;
},
set: function setter(newValue) {
if (newValue === value) return;
value = newValue;
observe(newValue);
//派发消息
dep.notify();
}
})
}
- 怎么将实例化watcher实例时添加到对应属性的dep实例中?
Watcher的get方法 当实例化一个watcher,获取对应属性值时,同时也触发了defineReactive函数的getter方法,因此在getter方法中将watcher实例添加到对应的dep数组中。
- 怎么getter方法中获取watcher实例?
很显然 将watcher实例放在getter方法能访问的地方就行了 在这里我们将它放到Dep.target属性上。
- 修改Watcher
get(){
//将watcher实例挂靠在getter方法能访问到地方
Dep.target = this
var value = getValue(this.obj,this.exp);
//需要重置Dep.target
Dep.target = null;
return value;
}
- 修改defineReactive函数
get: function getter() {
//判断Dep.target是否有值,如果有的话 添加watcher实例
if(Dep.target){
dep.addSub(Dep.target);
return value;
}
}
三、完整代码
class Observer {
constructor(obj) {
this.value = obj
this.trave()
}
trave() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key]))
}
}
function observe(obj){
if(!obj||typeof obj!=='object'){
return;
}
new Observer(obj);
}
function defineReactive(obj, key, value) {
observe(value)
//实例化dep
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function getter() {
if(Dep.target){
dep.addSub(Dep.target);
}
return value;
},
set: function setter(newValue) {
if (newValue === value) return;
value = newValue
observe(newValue);
dep.notify()
}
})
}
//watcher
class Watcher{
constructor(obj,exp,cb){
this.obj = obj;
this.exp = exp;
this.cb = cb;
//实例化Watcher实例时获取它订阅的属性
this.value = this.get()
}
//订阅属性
get(){
Dep.target = this;
var value = getValue(this.obj,this.exp);
Dep.target = null
return value;
}
//数据变化时
update(){
var newVal = getValue(this.obj,this.exp);
var oldVal = this.value;
if(newVal!==oldVal){
//数据更新
this.value = newVal;
this.cb(newVal,oldVal);
}
}
}
function getValue(obj,exp) {
var keys= exp.split(".");
console.log(keys)
var value;
for(let i of keys){
if(!obj) return;
value = obj[i];
}
return value;
}
class Dep{
constructor(){
this.subs = [];
}
//将Watcher实例添加进subs数组
addSub(sub){
this.subs.push(sub);
}
//派发消息 使得每个watcher实例调用update方法
notify(){
this.subs.forEach(function (sub) {
sub.update();
})
}
}
以上是一个简易版本的数据响应,具体的双向数据绑定需要加上compile解析器
Vue3.X实现
一、理论知识
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
关于Proxy就不细说了,直接看MDN或者阮一峰老师的文档
vue3中响应式处理都放在reactive类中,因此接下来就模仿vue3来实现数据的响应式处理
二、基本流程
基于Proxy的强大,接下来的代码主要体现代码的封装性
import {reactive} from './vue3/reactivity';
const state = reactive({
name:'beatrix',
age:'21',
hobby:{
"a":1,
"b":2
}
})
很显然,接下来我们需要去实现这样一个reactive方法,使得我们传入的对象能够被监听
1.实现reactive
import {mutableHandler} from './mutableHandler'
function reactive(target) {
return createReactiveObject(target,mutableHandler);
}
function createReactiveObject(target,baseHandler) {
if(!isObject(target)){
return target
}
const observer = new Proxy(target,baseHandler);
//返回一个代理对象
return observer;
}
function isObject(obj){
return typeof obj ==='object' && obj!== null
}
export{
reactive
}
以上代码主要注意一点
- mutableHandler 它是我们在执行各种操作时代理新创建对象的行为,由很多的函数作为属性的对象
2.实现mutableHandler.js
import { reactive } from "./reactive"
function hasOwnProperty(target,key){
return Object.prototype.hasOwnProperty.call(target,key)
}
function isEqual(newVal,oldVal) {
return newVal === oldVal
}
const get = createGetter(),
set = createSetter();
function createGetter () {
return function get (target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// todo ......
console.log('响应式获取:' + target[key]);
if (isObject(res)) {
//进行递归
return reactive(res);
}
return res;
}
}
function createSetter () {
return function set (target, key, value, receiver) {
const isKeyExist = hasOwnProperty(target, key),
oldValue = target[key],
res = Reflect.set(target, key, value, receiver);
// todo ......
if (!isKeyExist) {
console.log('响应式新增:' + value);
} else if (!isEqual(value, oldValue)) {
console.log('响应式修改:' + key + '=' + value);
}
return res;
}
}
const mutableHandler = {
get,
set
}
export {
mutableHandler
}
以上主要注意通过Reflect来实现对属性的操作。
- Reflect 关于Reflect可以直接看MDN
总结
关于vue2/3响应式数据的原理在这里只是做了简单的实现。
比如关于Vue3的reactive它当然不会有那么简单,只是实现了一点点而已,具体感兴趣的可以去查看源码.