分享内容与目的
彻底搞懂vue2的数据更新原理,手写相关代码。
\
什么是MVVM模式
数据变化,视图会自动变化。
// 模版
<p>我{{ age }}岁了</p>
// 数据变化
this.age++;
model:表示数据模型;
view:表示视图;
view-model:表示桥梁。
\
侵入式和非侵入式
// Vue 数据变化 (非侵入式)
this.age++;
// React 数据变化 (侵入式)
this.setState({
age: this.state.age + 1
})
侵入式:响应式将视图的更新封装到了setState中,调取函数改版数据与视图;
非侵入式:修改数据自动触发更新视图。
\
Vue使用非侵入式的钥匙
Object.defineProperty();
数据劫持/数据代理
\
利用javascript引擎赋予的功能检测对象属性变化。(IE8+)
\
Object.defineProperty()方法
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
const obj = {};
Object.defineProperty(obj, "a", {
value: 3
})
Object.defineProperty(obj, "b", {
value: 5
})
console.log(obj);
\
Object.defineProperty方法可以设置一些额外是隐藏属性。
Object.defineProperty(obj, "a", {
value: 3,
configurable: false, // 是否可被改变与删除
writable: false, // 是否可写
emumerable: false // 是否可被枚举
})
Object.defineProperty(obj, "a", {
get() {
console.log("试图访问obj的a的属性")
},
set() {
console.log("试图改变obj的a属性")
}
})
configurable
当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
默认为 ****false。
enumerable
当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
默认为 ****false。
\
数据描述符还具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
默认为 ****undefined。
writable
当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符(en-US)改变。
默认为 false 。
\
存取描述符还具有以下可选键值:
get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 ****undefined。
set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined。
\
defineReactive函数
为什么需要定义defineReactive
let temp = 1;
Object.defineProperty(obj, "a", {
get() {
console.log("试图访问obj的a的属性");
return temp;
},
set(newVal) {
console.log("试图改变obj的a属性");
temp = newVal;
}
})
getter/setter 需要变量中转。
function defineReactive(data, key, value) {
const property = Object.getOwnPropertyDescriptor(data, key);
if (property && property.configurable) {
return
}
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log(`试图访问obj的${key}属性`)
return value;
},
set(newVal) {
console.log(`试图改变obj的${key}属性`)
if (value === newVal) return;
value = newVal;
}
})
}
\
递归侦测对象全部属性
\
Observer类
创建一个正常的object转换为每一层的属性都是响应式(可以被侦测的)的object。
class Observer {
constructor(data) {
this.walk(data);
}
// 遍历
walk(data) {
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
}
\
observe函数
创建一个observe函数返回响应式对象
function def(obj, key, val, enumerable = false) {
Object.defineProperty(obj, key, {
value: val,
enumerable,
writable: true,
configurable: true
})
}
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
export function typeOf(val) {
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}
class Observer {
constructor(data) {
this.value = data;
def(data, "__ob__", this); // this表示实例
this.walk(data);
}
walk(data) {
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
}
function observe(data) {
const type = typeOf(data);
if (!["array", "object"].includes(type)) {
return
}
let ob;
if (hasOwn(data, "__ob__")) {
ob = data.__ob__;
} else {
ob = new Observer(data);
}
return ob;
}
\
改造defineReactive
循环创建observe函数;
function defineReactive(data, key, value) {
const property = Object.getOwnPropertyDescriptor(data, key);
if (property && !property.configurable) {
return
}
// 递归调用
observe(value);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log(`试图访问obj的${key}属性`)
return value;
},
set(newVal) {
console.log(`试图改变obj的${key}属性`)
if (value === newVal) return;
value = newVal;
observe(newVal);
}
})
}
\
思考
- 这样的数据劫持方式对数组有什么影响?
这样递归的方式其实无论是对象还是数组都进行了观测,但是比如此时 data包含数组[1,2,3,4,5]那么我们根据下标可以直接修改数据也能触发 set 但是如果一个数组里面有上千上万个元素 每一个元素下标都添加 get 和 set 方法 这样对于性能来说是承担不起的 所以此方法只用来劫持对象。
\
- 根据上面问题在vue中如果对象内容过多依然会出现此性能问题,该怎么办?
例:使用Object.freeze对象做数据冻结。
\
Object.defineProperty缺点?
对象新增或者删除的属性无法被监听到,只有对象本身存在的属性修改才会被劫持。
\
数组的响应式处理
使用侵入式数据侦测。
vue中使用改写push pop shift unshift splice reverse sort这7种方式主动触发。
\
- 备份数组自身方法
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
- 重写
array7个方法
const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "reverse", "sort"];
methodsToPatch.forEach(function(method) {
def(arrayMethods, method, function(...arg) {
const result = arrayProto[method].apply(this, args);
return result;
})
})
- Observer类中改写真实数组方法
const isProto = "__proto__" in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
class Observer {
constructor(data) {
def(data, "__ob__", this);
if (Array.isArray(data)) {
rewriteArrayMethods(data, arrayMethods);
this.observeArray(data);
} else {
this.walk(data);
}
}
observeArray(items) {
for (let i = 0; i < items.length; i++) {
observe(items[i]);
}
}
walk(data) {
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
}
function rewriteArrayMethods(target, src) {
if (isProto) {
target.__proto__ = src; // ie 11+
} else {
arrayKeys.forEach(key => {
def(target, key, src[key]);
})
}
}
- 获取新增数据手动触发响应式绑定
def(arrayMethods, method, function(...arg) {
const result = arrayProto[method].apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
// [].push(1); => arg = [1];
case "push":
// [].unshift(2); => arg = [2]
case "unshift":
inserted = args;
break;
// [].splice(0, 1, 1); => arg = [0, 1, 1];
case "splice":
inserted = args.slice(2);
default:
break;
}
// 判断有没有插入的新项,让新项也变为响应式
if(inserted) {
ob.observeArray(inserted);
}
return result;
})
\
优化observe处理冻结对象问题
function observe(data) {
const type = typeOf(data);
if (!["array", "object"].includes(type)) {
return
}
let ob;
if (hasOwn(data, "__ob__")) {
ob = data.__ob__;
} else if (type === "array" || type === "object" &&Object.isExtensible(data)){
ob = new Observer(data);
}
return ob;
}
\
处理对象添加与删除事件
function set (target, key, val) {
if (Array.isArray(target)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val);
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = target.__ob__
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val);
// 通知
return val;
}
export function del (target, key) {
if (Array.isArray(target)) {
target.splice(key, 1);
return;
}
if (!hasOwn(target, key)) {
return;
}
delete target[key];
// 通知
}
\
依赖收集
\
什么是依赖?
- 需要用到数据的地方
- 在getter中收集依赖,在setter中触发依赖
\
Dep类与Watcher类
- 把依赖收集得代码封装成一个Dep类,它专门用来管理依赖
- Watcher是一个中介,数据发生变化时通过Watcher中转通知组件
- 依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
- Dep采用发布订阅模式,当数据方式变化时,循环遍历依赖列表,通知所有的Watcher。
- 将Wacher设置到全局的一个指定位置,然后读取数据,因为读取数据,所有触发getter。在getter中就能得到当前正在读取数据的Watcher,并把整个Watcher收集到Dep中。
- 发生setter与getter通知dep实例
class Dep{
constructor() {
console.log("创建一个Dep实例");
}
depend() {
console.log("依赖添加");
}
notify() {
console.log("数据发生改变通知");
}
}
function defineReactive(data, key, value) {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(data, key);
if (property && !property.configurable) {
return
}
let childOb = observe(value);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(childOb)) {
dependArray(childOb);
}
}
console.log(`试图访问obj的${key}属性`)
return value;
},
set(newVal) {
console.log(`试图改变obj的${key}属性`)
if (value === newVal) return;
value = newVal;
childOb = observe(newVal);
// 通知dep
dep.notify();
}
})
}
function dependArray(value) {
for (const e of value) {
if (e && e.__ob__ && e.__ob__.depend) {
e.__ob__.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
}
特例:数组
数组因为使用侵入式通知,需单独改造数组通知
arrayMethods[method] = function (...args) {
const result = arrayProto[method].apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
if (inserted) {
ob.observeArray(inserted);
}
// 但是ob并不存在dep所以需要为Observer实例添加dep
ob.dep.notify();
return result;
}
class Observer {
constructor(data) {
// 添加外部手动触发
this.dep = new Dep();
def(data, "__ob__", this);
if (Array.isArray(data)) {
rewriteArrayMethods(data, arrayMethods);
this.observeArray(data);
} else {
this.walk(data);
}
}
}
- Watcher类的创建
假如我们希望实现这样的一个例子:当obj.a.m 的m发生变化时执行回调。
const obj = {
a: {
m: 1
},
b: [1,2,3,4],
c: 10
};
observe(obj);
new Watcher(obj, "a.m", function (newVal, oldVal) {
console.log("发生变化回调", newVal, oldVal);
})
- Watcher实例化时处理
a.m始其能正常解析; - 手动触发 一个value获取,使其触发observe中的getter方法;
- 添加一个到Dep实例中的方法;
- 添加
update方法触发Watcher实例中的callback方法。
Watcher:
import Dep from "./dep";
function parsePath(str) {
const arr = str.split(".");
return function(obj) {
for (const key of arr) {
if (!obj) return;
obj = obj[key];
}
return obj;
}
}
export default class Watcher {
constructor(target, expOrFn, cb) {
this.target = target;
this.getter = typeof expOrFn === "function" ? expOrFn : parsePath(expOrFn);
this.cb = cb;
// 手动触发data的getter
this.value = this.get();
console.log("我是一个Watcher实例")
}
get() {
Dep.target = this;
const value = this.getter(this.target);
Dep.target = null;
return value;
}
addDep(dep) {
dep.addSub(this);
}
update() {
this.run();
}
run() {
const value = this.get();
if (value !== this.value) {
const oldValue = this.value;
this.value = value;
this.cb(value, oldValue);
}
}
}
Dep
export default class Dep{
static target = null;
constructor() {
// 用数组存储自己的订阅者。
this.subs = [];
console.log("创建一个Dep实例");
}
// 添加订阅
addSub(sub) {
this.subs.push(sub);
}
// 添加依赖
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
console.log("数据发生改变通知");
// 克隆subs 避免执行时数据发生变化
const subs = this.subs.slice();
for (const watcher of subs) {
watcher.update();
}
}
}