Vue源码之响应式数据原理
数据驱使着页面的变化,手动实现Vue底层的响应式数据原理
1. 侵入式与非侵入式
在Vue中我们修改值不需要API,例如 this.a++ 这就是非侵入式
而在React中,必须要使用React给我们提供的API 例如
this.setState({
a: this.state.a + 1
})
这就是侵入式
2. Object.defineProperty()方法
const conf = {
name: "小明",
age: 18,
}
Object.defineProperty(conf, "sex", {
value: "男",
//是否可写
writable: false,
//是否可以被枚举
enumerable: false,
//是否可以被改变和删除
configurable:false,
})
Object.defineProperty(conf,"sex",{
//当有人访问sex值的时候被访问
get(){
return conf.sex
},
//当有人修改sex的值的时候被调用
set(newValue){
}
})
3.defineReactive函数
Object.defineProperty()方法有一个很大的问题,就是当我们用get和set时必须需要一个中转变量
这时我们就可以定义一个defineReactive函数,来构造一个闭包环境
let obj = {}
function defineProperty(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(v) {
if (v === value) return
value = v
}
})
}
defineProperty(obj, "a", 10)
console.log(obj.a)
obj.a = 11
console.log(obj.a)
接下里手动实现Vue响应式数据原理
4.index.js入口文件
4.1配置webpack环境实现模块式开发
我们使用webpack的版本如下
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3"
webpack.config.js文件
const path = require("path")
module.exports = {
mode: "development",
entry: './src/index.js',
output: {
filename: "bundle.js"
},
devServer: {
contentBase: path.join(__dirname, "www"),
compress: false,
port: 8080,
publicPath: "/xuni/"
}
}
4.2 index.js文件
import {observe} from "./observe";
let obj = {
a: {
m: {
n: 5
}
},
b: 18,
g: [22, 33, 44, 55, [11, 22, 33]]
}
observe(obj)
我们要把obj这个对象里的所有数据都变成响应式的
5. observe函数
//Create an observe function to give an aid to the Observer class
import Observer from "./Observer";
export const observe = function (value) {
//This function serves only objects
if (typeof value !== 'object') return
let ob
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
这个函数的主要功能是给数据添加一个Observer类,且这个函数只为对象服务
6. Observer类
import {def} from './utils'
import defineReactive from "./defineReactive";
import {arrayMethods} from "./array";
import {observe} from "./observe";
//The purpose of the Observer class
//Converting a normal Object to Objects that are responsive (can be detected) at each level
export default class Observer {
constructor(value) {
def(value, "__ob__", this, false)
if (Array.isArray(value)) {
//Force a change in the prototype chain
Object.setPrototypeOf(value, arrayMethods)
this.observeArray(value)
}else {
this.walk(value)
}
}
walk(value) {
for (let k in value) {
defineReactive(value, k)
}
}
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
其实响应式数据原理的底层就是递归,但不是普通的递归,是几个js文件相互调用就形成了递归
6.1首先看对象的响应式
在类的构造器里将对象传入进去,如果是非数组的数据变为响应式的话,调用自身的walk方法,然后调用defineReactive函数
6.2 而数组的响应式比较复杂
在学习Vue是我们知道,如果想让Vue知道数组的侦测的话,只能有7种方法才可以被侦测的到,分别是
'push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"
Vue底层的方法是重写这几个方法,强制改变他们的原型链,然后再调用数组自己的方法实现响应式
7. defineReactive函数
import {observe} from "./observe";
export default function defineReactive(data, key, value) {
if (arguments.length === 2) {
value = data[key]
}
let childOb = observe(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newValue) {
if (value === newValue) return
value = newValue
childOb = observe(newValue)
}
})
}
这个文件的作用就是别别人调用时,依次为传入的值匹配 getter和setter实现响应式
在此时数组非常的特殊,如果有人修改了数组的值就会调用setter方法,在setter我们再重新调用observe函数,重新将数组的数据转为响应式,实现递归
8. array.js
这个文件集中处理数组的问题
import {def} from "./utils";
//To make the array responsive, we will rewrite the seven methods of the array
// ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
methodsNeedChange.forEach(methodName => {
let original = arrayPrototype[methodName]
def(arrayMethods, methodName, function () {
const args = [...arguments]
const ob = this.__ob__
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) {
ob.observeArray(inserted)
}
return original.apply(this, arguments)
}, false)
})
首先将数组的原型关系复制一遍,创造一个新的对象,将复制的原型转化为arrayMethods对象的原型,再将上面的7个方法重写,我们只是添加一些功能,我们要将原始的功能original返回出去,这样就不会干扰数组方法的正常功能
我们导入def模块,就是Object.defineProperty()的作用,将新写的这7个方法放入arrayMethods这个对象中,然后对外暴露
但其中这3个方法比较特殊,分别是 push unshift spilce,他们的功能其实就是新加数据,所以当调用时我们就调用自身身上的observeArray方法,将新入的数据变为响应式,其实就是这些具有功能的类和函数,相互调用,实现递归
9. def函数
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
其功能就是给 array.js其辅助作用,就是往arrayMethods身上写入你重写的数组的方法
10.小结:
Vue源码的中将数据变为响应式,也就是我上面写的,但Vue写的一定更加完善,他的方法就是这些具有功能的类和函数,相互调用,实现循环递归
11. 依赖
11.1 什么是依赖:
- 就是需要用到数据的地方,就是依赖
11.2 Vue的思路:
- 在getter时收集依赖,在setter时触发依赖
12. Dep类和Watcher类
- 依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中
- Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍
- Vue设计的巧妙之处就是把Watcher设置到全局的一个指定位置,然后读取数据,因为读取了数据,就会触发数据的getter方法,在getter中就能得到当前数据的Watcher,并把这个Watcher收集到Dep中
13. Dep类
let uid = 0
export default class Dep {
constructor() {
this.id = uid++
//The array holds its own subscribers
//It's also an instance of Watcher
this.subs = []
}
//Add a subscription
addSub(sub) {
this.subs.push(sub)
}
//Add the dependent
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
//Inform the update
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep类中存放的就是Watcher类实例
14.混入Dep类的defineReactive函数
import {observe} from "./observe";
import Dep from "./Dep";
export default function defineReactive(data, key, value) {
const dep = new Dep()
if (arguments.length === 2) {
value = data[key]
}
let childOb = observe(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set(newValue) {
if (value === newValue) return
value = newValue
childOb = observe(newValue)
dep.notify()
}
})
}
在getter时收集依赖,再接管数据,将数据变为响应式时,同时实例Dep类,当有人调用getter类时,就会收集依赖
15.混入Dep类的 Observer类
import {def} from './utils'
import defineReactive from "./defineReactive";
import {arrayMethods} from "./array";
import {observe} from "./observe";
import Dep from "./Dep";
//The purpose of the Observer class
//Converting a normal Object to Objects that are responsive (can be detected) at each level
export default class Observer {
constructor(value) {
//Every instance of an Observer has a DEP on it
this.dep = new Dep()
def(value, "__ob__", this, false)
if (Array.isArray(value)) {
//Force a change in the prototype chain
Object.setPrototypeOf(value, arrayMethods)
this.observeArray(value)
} else {
this.walk(value)
}
}
walk(value) {
for (let k in value) {
defineReactive(value, k)
}
}
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
在调用Observer类时同时也也实例化Dep类
16.混入Dep类的array.js文件
在响应化数组的时候也要实例Dep类
import {def} from "./utils";
//To make the array responsive, we will rewrite the seven methods of the array
// ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
methodsNeedChange.forEach(methodName => {
let original = arrayPrototype[methodName]
def(arrayMethods, methodName, function () {
const args = [...arguments]
const ob = this.__ob__
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) {
ob.observeArray(inserted)
}
ob.dep.notify()
return original.apply(this, arguments)
}, false)
})
注:ob对象里有dep实例,因为在数组调用混入Dep类的 Observer类时就以添加,所以直接使用就可
17. Weather类
import Dep from "./Dep";
let 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()
}
update() {
this.run()
}
get() {
Dep.target = this
const obj = this.target
let value
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
run() {
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
const value = this.get()
if (value !== this.value || typeof value === "object") {
const oldValue = this.value
this.value = value
cb.call(this.target, value, oldValue)
}
}
}
function parsePath(str) {
let segments = str.split(".")
return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
当数据发生更新时,Dep类的update()函数触发Weather类的run方法,此时Weather类被调用,没有人引用Weather类,它别Vue的指令例如v-model等调用,Weather类得到最新的结果通知渲染,在进行后面虚拟DOM和diff算法,最小量更新,最后完成重新解析模板
这也就是响应式数据的原理
18. 注:
代码含webpack构建工具已传至github仓库
https://github.com/Bald-heads/dataResponsiveness.git