关于前端:vue响应式原理底层超详细的解读手写响应式原理

55次阅读

共计 10659 个字符,预计需要花费 27 分钟才能阅读完成。

vue 响应式原理

对于 vue 响应式原理(底层)原理,明天和大家一起探讨钻研,结尾附上手敲代码以及 git 下载地址,如有有余或不精确请及时留言斧正,期待共同进步~

本文将采纳 webpack 环境进行编写, 我的项目目录如下。
index.js: 入口文件
arrar.js: 数组文件
def.js: 定义一个对象属性
defineReactive.js: 给对象 data 的属性 key 定义监听
Dep.js: Dep 类专门帮忙咱们治理依赖,能够收集依赖,删除依赖,向依赖发送告诉等。
Observe.js: 监听 value,试图创立 bserver 实例,如果 value 曾经是响应式数据(依据是否具备__ob__属性判断),就不须要再创立 Observer 实例,间接返回曾经创立的 Observer 实例即可。
Observer.js: 将一个失常的 object 转换为每个层级的属性都是响应式(能够被侦测)的 object。
Wather.js: Watcher 是一个中介的角色,数据发生变化时告诉它,而后它再告诉其余中央.

先从 index.js 代码编写,环境搭建完后咱们编写代码,输入就会在控制台展现,定义 obj,Object.defineProperty 进行数据劫持。

// index.js 入口文件

let obj = {
  a: 1,
  b: {
    c: {d: 4,},
  },
  e: [22, 33, 44, 55],
};
// 定义一个函数 defineReactive 
let val;
function defineReactive(data, key, value) {
  Object.defineProperty(obj, key, {
    // 收集依赖 getter
    get() {console.log('您试图拜访' + key  + '属性');
      return val
    },
    // 触发依赖 setter
    set (newVal) {console.log('你试图拜访' + key + '属性', newVal);
      if (val === newVal) {return}
      val = newVal
      
    }
  })
}


defineReactive(obj, 'a', 10)
console.log('obj.a', obj.a); // 1
obj.a = 8
console.log('obj.a 扭转后', obj.a); // 8 

在 Vue2.X 响应式中应用到了 Object.defineProperty 进行数据劫持,所以咱们对它必须有肯定的理解,Object.defineProperty 中有两个十分重要的函数,getter 和 setter,getter 负责收集依赖,简略来说,getter 就是收集以后的 obj 对象的依赖,setter 函数则是当你的数据扭转时进行触发的函数。

接下来咱们思考 obj.a 是一个简略简略的数据类型,那如果我想要简单的对象 obj.b.c.d 的数值呢,打印一下控制台发现尽管 Object.defineProperty 拜访了 getter, 然而并未监测到 obj.b.c.d, 只是说监测到 b 属性。如图 2

那咱们该怎么做能力实现对深层次的对象进行监听呢? 咱们是否能够讲每一层对象都循环调用,增加监听,这时是不是听到循环调用就想到了递归,没错咱们能够应用 —递归侦听

为了代码参差,首先咱们将 defineReactive 函数提出来,造成一个独立文件,代码和 index.js 拆散。

// defineReactive.js
/**
 * 给对象 data 的属性 key 定义监听
 * @param {*} data 传入的数据
 * @param {*} key 监听的属性
 * @param {*} value 闭包环境提供的周转变量
 */
export default function defineReactive (data, key , val) {if (arguments.length === 2) {val = data[key]
  }
    Object.defineProperty(data, key, {
      // 可枚举 能够循环
      enumerable: true,
      // 可被配置,比方能够被删除
      configurable: true,
      get() {console.log('您试图拜访' + key  + '属性');
        return val
      },
      set (newVal) {console.log('你试图拜访' + key + '属性', newVal);
        if (val === newVal) {return}
        val = newVal
      }
    })
}

在 index.js 中引入该文件,代码如下

// index.js
import defineReactive from "./defineReactive";
let obj = {
  a: 1,
  b: {
    c: {d: 4,},
  },
  e: [22, 33, 44, 55],
};
defineReactive(obj, 'a')
defineReactive(obj, 'b')

console.log(obj.b.c.d);

接下来开始写递归侦听,新建一个 observe 类,这个类的作用就是判断是否须要试创立新的 Observer 类,检测 value 身上是否有__ob__属性,如果有能够了解为 value 为响应式,响应式数据,就不须要再创立 Observer 实例,间接返回曾经创立的 Observer 实例即可,防止反复侦测 value 变动的问题,否则,创立 Observer 实例。

// observe.js
import Observer from "./Observer";

/**
 * 监听 value
 * 尝试创立 Observer 实例,如果 value 曾经是响应式数据,就不须要再创立 Observer 实例,间接返回曾经创立的 Observer 实例即可,防止反复侦测 value 变动的问题
 * @param {*} value 
 * @returns 
 */
export default function observe(value) {
  // 如果 value 不是对象,就什么都不做
  if (typeof value != "object") return;

  let ob; // Observer 的实例
  if (typeof value.__ob__ !== "undefined") {ob = value.__ob__;} else {ob = new Observer(value);
  }
  
  return ob;
}

接下来写 observer 类。

// observer.js

import observe from "./observe";
export default class Observer {constructor (value) {console.log('我是 observer 结构器', value);
    // 给实例增加__ob__属性,值是以后 Observer 的实例,不可枚举 
    // def 被独自拎进去了 次要作用就是为了增加__ob__属性带哦
    // this 是以后 new 的实例
    def(value, "__ob__", this, false);
    this.walk(value);
  }
  // 遍历
  walk(value) {for (let k in value) {defineReactive(value, k)
    }
  } 
}

// def.js
/**
 * 定义一个对象属性
 * @param {*} obj 
 * @param {*} key 
 * @param {*} value 
 * @param {*} enumerable 
 */
export default function def(obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true,
  });
}

在 observer 类中,咱们在结构器内执行 def 函数,def 函数次要作用是为以后实例(obj)增加__ob__属性,后面说过了这个属性是代表实例是否是响应式的标记。而后调用 walk 办法循环实例,在循环里调用 defineReactive 函数。至此,外层的属性(obj.a)曾经成为响应式了
在 index.js 中 创立 observe 函数 observe(obj),能够看见控制台打印如下,并发现__ob__属性已存在。

然而咱们会发现在 obj.b obj.c 的身上没有__ob__属性。

咱们写 Observer 的目标是为了递归侦听,当初咱们对外层的元素曾经实现了监测,思考下咱们当初只剩下对外部的属性进行侦听了,那么该怎么做呢?

拆解一下,首先要监测外部元素,少不了循环,那如果在循环中对每一层进行监测不就 ok 了吗?循环咱们写过了,剩下的就是须要在 efineReactive 函数外部调用 observe 类,observe 子元素,而 observe 中又会调用 observer 类,而后循环,最初在 setter 中监测新的子元素的值即可。看下整体代码。

// index.js
import observe from "./observe";
import defineReactive from "./defineReactive";
let obj = {
  a: 1,
  b: {
    c: {d: 4,},
  },
  e: [22, 33, 44, 55],
};
// 创立 observe 函数 
observe(obj)



// observe.js
import Observer from "./Observer";
/**
 * 监听 value
 * 尝试创立 Observer 实例,如果 value 曾经是响应式数据,就不须要再创立 Observer 实例,间接返回曾经创立的 Observer 实例即可,防止反复侦测 value 变动的问题
 * @param {*} value 
 * @returns 
 */
export default function observe(value) {
  // 如果 value 不是对象,就什么都不做
  if (typeof value != "object") return;

  let ob; // Observer 的实例
  if (typeof value.__ob__ !== "undefined") {ob = value.__ob__;} else {ob = new Observer(value);
  }
  
  return ob;
}


// observer.js

def from "./def";
import defineReactive from "./defineReactive";
export default class Observer {constructor (value) {console.log('我是 observer 结构器', value);
    // 给实例增加__ob__属性,值是以后 Observer 的实例,不可枚举 
    def(value, "__ob__", this, false);
    this.walk(value)
  }
  // 遍历
  walk(value) {for (let k in value) {defineReactive(value, k)
    }
  }
  
}


// def.js
/**
 * 定义一个对象属性
 * @param {*} obj 
 * @param {*} key 
 * @param {*} value 
 * @param {*} enumerable 
 */
export default function def(obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true,
  });
}


// defineReactive.js
import observe from "./observe";

/**
 * 给对象 data 的属性 key 定义监听
 * @param {*} data 传入的数据
 * @param {*} key 监听的属性
 * @param {*} value 闭包环境提供的周转变量
 */
export default function defineReactive (data, key , val) {console.log('递归侦听属性',key);
  if (arguments.length === 2) {val = data[key]
  }
  // 子元素要进行 observe,造成递归
  let childOb = observe(val);
    Object.defineProperty(data, key, {
      // 可枚举 能够循环
      enumerable: true,
      // 可被配置,比方能够被删除
      configurable: true,
      get() {console.log('您试图拜访' + key  + '属性');
        return val
      },
      set (newVal) {console.log('你试图拜访' + key + '属性', newVal);
        if (val === newVal) {return}
        // 当设置了新值,新值也要被 observe
        childOb = observe(newVal);
        val = newVal
      }
    })
}

在具体解说一下下面这写代码逻辑,将代码串联一下。
首先,在 index.js 中 let 一个对象 obj, 调用 obeserve 函数(留神不是 observer)在 observe 函数外部,首先查看是否为响应式,如果是,则不须要创立 observer 实例,防止反复监听,节约资源。显然以后的 obj 是非响应式的,那么就须要创立一个 observer 实例, 进入 observer.js 文件,进入 observer 外部执行 defineReactive 办法,进 defineReactive 文件,最要害一步,子元素要进行 observe,造成递归(这个递归不是本人调用本人,而是多个函数嵌套调用)—〉这行代码 let childOb = observe(val);实现了递归的重要一步,接下来要在 setter 函数中,更新值,childOb = observe(newVal);能够看见控制台会输出 obj 的每个属性,b、c、d

咱们设置 obj.b.c.d = 110, 打印控制台看下是否会失效。

以上递归侦听能够演绎为下图

上面,咱们在已有函数的根底上将数组的响应式原理补上去。数组的响应式原理。尤雨溪老师讲数组的七个办法进行了改写(“push”, “pop”, “shift”, “unshift”, “splice”,
“sort”, “reverse”,),数组的侦听能够简略的了解为如下原理

咱们将数组的隐式原型链__proto__指向 arrayMethods,而 arrayMethods 是以 Array.prototype 为原型创立的,咱们在 arrayMethods 上改写办法即可,新建 array.js

首先备份一份,而后调用 def.js, 增加属性__ob__。小伙伴们是否记得咱们在 observer 外部应用 walk 办法循环了实例,而后调用 defineReactive 办法,就是这里咱们须要补充一下数组的办法,减少代码如下,判断实例是对象还是数组,数组的话,就将这个数组的原型指向 arrayMethods,去改写数组办法,而后执行 observeArray(数组的遍历办法),简略来说就是去侦测数组中的每一项,一一进行 observe。

// arrar.js

import def from "./def";

const arrayPrototype = Array.prototype;

// 以 Array.prototype 为原型创立 arrayMethod
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的 7 个数组办法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 批量操作这些办法
methodsNeedChange.forEach((methodName) => {
  // 备份原来的办法
  const original = arrayPrototype[methodName];

  // 定义新的办法
  def(
    arrayMethods,
    methodName,
    function () {console.log("array 数据曾经被劫持");

      // 复原原来的性能(数组办法)
      const result = original.apply(this, arguments);
      // 把类数组对象变成数组
      const args = [...arguments];

      // 把这个数组身上的__ob__取出来
      // 在拦截器中获取 Observer 的实例
      const ob = this.__ob__;

      // 有三种办法 push、unshift、splice 能插入新项,要劫持(侦测)这些数据(插入新项)let inserted = [];
      switch (methodName) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // 查看有没有新插入的项 inserted,有的话就劫持
      // ob.observeArray 实例外部办法
      if (inserted) {ob.observeArray(inserted);
      }

      return result;
    },
    false
  );
});


// observer.js
// 在 observer 外部判断是否为数组

import def from "./def";
import defineReactive from "./defineReactive";
import {arrayMethods} from "./array";
export default class Observer {constructor (value) {console.log('我是 observer 结构器', value);
    // 给实例增加__ob__属性,值是以后 Observer 的实例,不可枚举 
    def(value, "__ob__", this, false);
    // 判断是数组还是对象
    if (Array.isArray(value)) {
      // 是数组,就将这个数组的原型指向 arrayMethods
      Object.setPrototypeOf(value, arrayMethods);
      // 晚期实现是这样
      // value.__proto__ = arrayMethods;
      
      // observe 数组
      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
      observe(arr[i]);
    }
  }
  
}

自此数组的侦测曾经实现。

上面进行收集依赖和 watcher 的解说。
新建一个 Dep 类,Dep 类专门帮忙咱们治理依赖,能够收集依赖,删除依赖,向依赖发送告诉等, 新建文件 Dep.js,Dep 类中蕴含 addSub,depend,notify 办法。watcher 则是中转站,依赖的变动须要告诉 watcher。

新建 dep 类

let uid = 0;
/**
 * Dep 类专门帮忙咱们治理依赖,能够收集依赖,删除依赖,向依赖发送告诉等
 */
export default class Dep {constructor() {console.log("Dep 结构器", this);
    this.id = uid++;
    // 用数组存储本人的订阅者,放的是 Watcher 的实例
    this.subs = [];}

  // 增加订阅
  addSub(sub) {this.subs.push(sub);
  }

  // 删除订阅
  removeSub(sub) {remove(this.subs, sub);
  }

  // 增加依赖
  depend() {
    // Dep.target 是一个咱们指定的全局的地位,用 window.target 也行,只有是全局惟一,没有歧义就行
    if (Dep.target) {this.addSub(Dep.target);
    }
  }

  // 告诉更新
  notify() {console.log("告诉更新 notify");
    // 浅拷贝一份
    const subs = this.subs.slice();
    // 遍历
    for (let i = 0, l = subs.length; i < l; i++) {subs[i].update();}
  }
}

/**
 * 从 arr 数组中删除元素 item
 * @param {*} arr
 * @param {*} item
 * @returns
 */
function remove(arr, item) {if (arr.length) {const index = arr.indexOf(item);
    if (index > -1) {return arr.splice(index, 1);
    }
  }
}


那么收集依赖应该在哪里收集呢,答案很显然是在 Object.defineProperty 中,因为 Object.defineProperty 天生能够进行数据劫持,故咱们在 Object.defineProperty 中创立 Dep 数组。在 getter 中判断以后是否处于依赖收集阶段(Dep.target 为 true),还须要对子元素进行判断。

// defineReactive.js

// 能够了解为所有的依赖收集工作都有 Dep 实现,而后在告诉 watcher

import Dep from "./Dep";
import observe from "./observe";

/**
 * 给对象 data 的属性 key 定义监听
 * @param {*} data 传入的数据
 * @param {*} key 监听的属性
 * @param {*} value 闭包环境提供的周转变量
 */
export default function defineReactive(data, key, value) {console.log('执行 defineReactive()', key)
  
  // 每个数据都要保护一个属于本人的数组,用来寄存依赖本人的 watcher
  const dep = new Dep();

  if (arguments.length === 2) {value = data[key];
  }

  // 子元素要进行 observe,造成递归
  let childOb = observe(value);

  Object.defineProperty(data, key, {

    // 可枚举 能够 for-in
    enumerable: true,
    // 可被配置,比方能够被 delete
    configurable: true,

    // getter  收集依赖
    get() {console.log(`getter 试图拜访 ${key}属性 侦测中 `);

      // 收集依赖 Dep.target 就是以后的 wather 实例
      if (Dep.target) {dep.depend();

        // 判断有没有子元素
        if (childOb) {
          // 数组收集依赖
          childOb.dep.depend();}
      }

      return value;
    },

    // setter 触发依赖
    set(newValue) {console.log(`setter 试图扭转 ${key}属性 侦测中 `, newValue);

      if (value === newValue) return;
      value = newValue;

      // 当设置了新值,新值也要被 observe
      childOb = observe(newValue);

      // 触发依赖
      // 公布订阅模式,告诉 dep
      dep.notify();},
  });
}

最初咱们还差一个 watcher 类,用来承受 dep 的音讯,并更新。

// watcher.js

import Dep from "./Dep";

let uid = 0;
/**
 * Watcher 是一个中介的角色,数据发生变化时告诉它,而后它再告诉其余中央
 */
export default class Watcher {constructor(target, expression, callback) {console.log("Watcher 结构器");
    this.id = uid++;
    this.target = target;
    // 按点拆分  执行 this.getter()就能够读取 data.a.b.c 的内容
    this.getter = parsePath(expression);
    this.callback = callback;
    this.value = this.get();}

  get() {
    // 进入依赖收集阶段。// 让全局的 Dep.target 设置为 Watcher 自身
    Dep.target = this;
    const obj = this.target;
    var value;
    // 只有能找就始终找
    try {value = this.getter(obj);
    } finally {Dep.target = null;}
    return value;
  }

  update() {this.run();
  } 

  run() {this.getAndInvoke(this.callback);
  }
  getAndInvoke(callback) {const value = this.get();
    if (value !== this.value || typeof value === "object") {
      const oldValue = this.value;
      this.value = value;
      callback.call(this.target, value, oldValue);
    }
  }
}

/**
 * 将 str 用. 宰割成数组 segments,而后循环数组,一层一层去读取数据,最初拿到的 obj 就是 str 中想要读的数据
 * @param {*} str
 * @returns
 */
function parsePath(str) {let segments = str.split(".");
  return function (obj) {for (let key of segments) {if (!obj) return;
      obj = obj[key];
    }
    return obj;
  };
}

Watcher 类中须要留神的是结构器中的 getter,getter 接管 parsePath 函数的返回值,parsePath 函数的次要作用是用. 宰割成数组 segments,而后循环数组,一层一层去读取数据,最初拿到的 obj 就是 str 中想要读的数据,对于以后例子来说就是当咱们对 obj 在 data 中定义时,内层 obj.b.c.d : 4(初始值),这里的拿到的就是 obj.b.c.d 的初始值 4。

到此响应式原理根本实现。如图

结尾处画了一张便于了解的图。

心愿能够帮忙到大家,也心愿大家踊跃留言点赞,欢送交换,前端小白,请各位大佬多点容纳~~ 一起提高

参考文件:https://www.bilibili.com/vide…

正文完
 0