1. 预期效果

当数据变动时,触发自定义的回调函数。

2. 思路

对对象 objectsetter 进行设置,使 setter 在赋值之后执行回调函数 callback()

3.细节

3.1 设置 setter 和 getter

JS提供了 [Object.defineProperty()](Object.defineProperty() - JavaScript | MDN (mozilla.org)) 这个API来定义对象属性的设置,这些设置就包括了 gettersetter。注意,在这些属性中,如果一个描述符同时拥有 valuewritablegetset 键,则会产生一个异常。

Object.defineProperty(obj, "key", {
enumerable: false, // 是否可枚举
configurable: false, // 是否可配置
writable: false, // 是否可写
value: "static"
});

我们可以利用JS的 [闭包](闭包 - JavaScript | MDN (mozilla.org)),给 gettersetter 创造一个共同的环境,来保存和操作数据 valuecallback 。同时,还可以在 setter 中检测值的变化。

// task1.js
const defineReactive = function(data, key, value, cb) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('getter')
return value
},
set(newValue) {
if (newValue !== value) {
value = newValue
console.log('setter: value change')
cb(newValue)
}
}
});
} const task = function() {
console.log('running task 1...')
const obj = {}
const callback = function(newVal) {
console.log('callback: new value is ' + newVal)
}
defineReactive(obj, 'a', 1, callback)
console.log(obj.a)
obj.a = 2
obj.a = 3
obj.a = 4
} task()

至此我们监控了 value ,可以感知到它的变化并执行回调函数。

3.2 递归监听对象的值

上面的 defineRective()value 为对象的时候,当修改深层键值,则无法响应到。因此通过循环递归的方法来对每一个键值赋予响应式。这里可以通过 observe()Observer 类来实现这种递归:

// observe.js
import { Observer } from "./Observer.js" // 为数据添加响应式特性
export default function(value) {
console.log('type of obj: ', typeof value)
if (typeof value !== 'object') {
// typeof 数组 = object
return
}
if (typeof value.__ob__ !== 'undefined') {
return value.__ob__
}
return new Observer(value)
}
// Observer.js
import { defineReactive } from './defineReactive.js'
import { def } from './util.js'; export class Observer {
constructor(obj) {
// 注意设置成不可枚举,不然会在walk()中循环调用
def(obj, '__ob__', this, false)
this.walk(obj)
}
walk(obj) {
for (const key in obj) {
defineReactive(obj, key)
}
}
}

在这里包装了一个 def() 函数,用于配置对象属性,把 __ob__ 属性设置成不可枚举,因为 __ob__ 类型指向自身,设置成不可枚举可以放置遍历对象时死循环

// util.js
export const def = function(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}

3.3 检测数组

从需求出发,对于响应式,我们对数组和对象的要求不同,对于对象,我们一般要求检测其成员的修改;对于数组,不仅要检测元素的修改,还要检测其增删(比如网页中的表格)

对由于数组没有 key ,所以不能通过 defineReactive() 来设置响应式,同时为了满足响应数组的增删改,所以 Vue 的方法是,通过包装 Array 的方法来实现响应式,当调用 push()poll()splice() 等方法时,会执行自己设置的响应式方法

使用 Object.create(obj) 方法可以 obj 对象为原型(prototype)创建一个对象,因此我们可以以数组原型 Array.prototype 为原型创建一个新的数组对象,在这个对象中响应式包装原来的 push()pop()splice()等数组

// array.js
import { def } from "./util.js"
export const arrayMethods = Object.create(Array.prototype)
const methodNameNeedChange = [
'pop',
'push',
'splice',
'shift',
'unshift',
'sort',
'reverse'
] methodNameNeedChange.forEach(methodName => {
const original = Array.prototype[methodName]
def(arrayMethods, methodName, function() {
// 响应式处理
console.log('call ' + methodName)
const res = original.apply(this, arguments)
const args = [...arguments]
let inserted = []
const ob = this.__ob__
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
case 'splice':
inserted = args.slice(2)
}
ob.observeArray(inserted) return res
})
})
// Observer.js
import { arrayMethods } from './array.js'
import { defineReactive } from './defineReactive.js'
import observe from './observe.js'
import { def } from './util.js' export class Observer {
constructor(obj) {
console.log('Observer', obj)
// 注意设置成不可枚举,不然会在walk()中循环调用
def(obj, '__ob__', this, false)
if (Array.isArray(obj)) {
// 将数组方法设置为响应式
Object.setPrototypeOf(obj, arrayMethods)
this.observeArray(obj)
} else {
this.walk(obj)
}
}
// 遍历对象成员并设置为响应式
walk(obj) {
for (const key in obj) {
defineReactive(obj, key)
}
}
// 遍历数组成员并设置为响应式
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}

3.5 Watcher 和 Dep 类

设置多个观察者检测同一个数据

// Dep.js

var uid = 0
export default class Dep {
constructor() {
this.id = uid++
// console.log('construct Dep ' + this.id)
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
depend() {
if (Dep.target) {
if (this.subs.some((sub) => { sub.id === Dep.target.id })) {
return
}
this.addSub(Dep.target)
}
}
notify() {
const s = this.subs.slice();
for (let i = 0, l = s.length; i < l; i++) {
s[i].update()
}
}
}
// Watcher.js

import Dep from "./Dep.js"

var uid = 0
export default class Watcher {
constructor(target, expression, callback) {
this.id = uid++
this.target = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()
}
get() {
Dep.target = this
const obj = this.target
let value
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
update() {
this.run()
}
run() {
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
const obj = this.target
const newValue = this.get()
if (this.value !== newValue || typeof newValue === 'object') {
const oldValue = this.value
this.value = newValue
cb.call(obj, newValue, newValue, oldValue)
} }
} function parsePath(str) {
var segments = str.split('.'); return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]]
}
return obj;
};
}
// task2.js
import observe from "../observe.js";
import Watcher from "../Watcher.js"; const task2 = function() {
const a = {
b: {
c: {
d: {
h: 1
}
}
},
e: {
f: 2
},
g: [ 1, 2, 3, { k: 1 }]
}
const ob_a = observe(a)
const w_a = new Watcher(a, 'b.c.d.h', (val) => {
console.log('1111111111')
})
a.b.c.d.h = 10
a.b.c.d.h = 10 console.log(a)
} task2()

执行结果如下,可以看到成功响应了数据变化

【Vue原理模拟】模拟Vue实现响应式数据的更多相关文章

  1. 用vue和layui简单写一个响应式数据展示表

    在创建项目之前,先把我们需要的文件打包处理 <!DOCTYPE html> <html lang="en"> <head> <meta c ...

  2. vue 源码自问自答-响应式原理

    vue 源码自问自答-响应式原理 最近看了 Vue 源码和源码分析类的文章,感觉明白了很多,但是仔细想想却说不出个所以然. 所以打算把自己掌握的知识,试着组织成自己的语言表达出来 不打算平铺直叙的写清 ...

  3. Vue实现双向绑定的原理以及响应式数据

    一.vue中的响应式属性 Vue中的数据实现响应式绑定 1.对象实现响应式: 是在初始化的时候利用definePrototype的定义set和get过滤器,在进行组件模板编译时实现water的监听搜集 ...

  4. vue源码之响应式数据

    分析vue是如何实现数据响应的. 前记 现在回顾一下看数据响应的原因. 之前看了vuex和vue-i18n的源码, 他们都有自己内部的vm, 也就是vue实例. 使用的都是vue的响应式数据特性及$w ...

  5. 仿VUE创建响应式数据

    VUE对于前端开发人员都非常熟悉了,其工作原理估计也都能说的清个大概,具体代码的实现估计看的人不会太多,这里对vue响应式数据做个简单的实现. 先简单介绍一下VUE数据响应原理,VUE响应数据分为对象 ...

  6. angular,vue,react的基本语法—插值表达式,渲染数据,响应式数据

    基本语法: 1.插值表达式: vue:{{}} react:{} angular:{{}} 2.渲染数据 vue js: export default{ data(){ return{ msg:&qu ...

  7. vue响应式数据变化

    vue响应式数据变化 话不多说,先上代码: //拷贝一份数组原型,防止修改所有数组类型变量的原型方法 let arrayProto = Array.prototype;// 数组原型上的方法 let ...

  8. vue基础响应式数据

    1.vue 采用 v……vm……m,模式,v---->el,vm---->new Vue(实例),m---->data 数据,让前端从操作大量的dom元素中解放出来. 2.vue响应 ...

  9. 由浅入深,带你用JavaScript实现响应式原理(Vue2、Vue3响应式原理)

    由浅入深,带你用JavaScript实现响应式原理 前言 为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数 ...

  10. Vue2手写源码---响应式数据的变化

    响应式数据变化 数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作) 手写简单的响应式数据的实现(对象属性劫持.深度属性劫持.数组函数劫持).模板转成 ast ...

随机推荐

  1. Vue-数据代理

    Vue中的数据代理 数据代理定义 所谓数据代理,就是通过一个对象代理对另一个对象中的属性的操作(读/写).说白了就是操作一个对象上的属性可以读取和修改另一个对象上的属性,这种关系就叫做数据代理. 在V ...

  2. 基于MassTransit.RabbitMQ的延时消息队列

    1 nuget包 <PackageReference Include="MassTransit.RabbitMQ" Version="8.0.2" /&g ...

  3. software engineering homework 2

    博客信息 沈阳航空航天大学计算机学院2020软件工程作业 作业要求 https://edu.cnblogs.com/campus/sau/Computer1701-1705/homework/1058 ...

  4. 2、HTTP的消息格式

    概念 HTTP协议 Hyper Text Transfer Protocol 超文本传输协议 传输协议 传输协议定义了客户端和服务器端通信时,发送数据的格式. 特点 基于TCP/IP的高级协议 默认端 ...

  5. element ui 点击选中表头并改变表头样式

    前言: header-cell-style 表头单元格的 style 的回调方法,也可以使用一个固定的 Object 为所有表头单元格设置一样的 Style. Function({row, colum ...

  6. jQuery.extend 函数详解(转)

    地址:http://www.jb51.net/article/29591.htm JQuery的extend扩展方法: Jquery的扩展方法extend是我们在写插件的过程中常用的方法,该方法有一些 ...

  7. 记一次FusionCompute安装springboot应用过程

    客户给了一个地址,登录后发现是FusionCompute 的一个虚拟机,第一次使用,于是过了一下在线帮助文档 ,大概明白了. 因为客户已经创建完了裸机虚拟机,前面的过程我也不会太多关注. 在概要里,可 ...

  8. Python中的join函数用法

    函数:string.join()Python中有join()和os.path.join()两个函数,具体作用如下:    join():    连接字符串数组.将字符串.元组.列表中的元素以指定的字符 ...

  9. 什么是5G垂直行业?

    什么是垂直行业呢? 感觉"垂直行业"这个词在太多地方遇到,但是这个词的涵盖范围到底是什么呢? 垂直这一概念源于两条直线(或平面)的直角交叉,两条直线是相互作为参照物的.比如,我们可 ...

  10. leetcode-1072 Flip Columns For Maximum Number of Equal Rows

    Given a matrix consisting of 0s and 1s, we may choose any number of columns in the matrix and flip e ...