Vue2的响应式原理 -- 源码剖析
vue2的响应式原理一直是面试基本必考的,今天我们通过手写来剖析响应式原理的实现
手写响应式原理
- vue首先把所有data中的属性和方法耦合到了一起
js
//响应式的数据变化,数据变化了我可以监控到数据的变化
//数据的取值 和 更改值我们要监控到
const vm = new Vue({
data() {
return { //代理数据
name: 'zf',
age: 20,
address: {
content: 'DDD',
num: 56
},
hobby: ['eat', 'slipp', {
a: 1
}]
}
}
})
//将所有的方法都耦合到一起
function Vue(options){//options就是用户的选项
this._init(options) //默认就调用了init
}
initMixin(Vue) //扩展了init方法- 将用户选项挂载到vm实例上
js
function initMixin(Vue){
Vue.prototype._init = function(options){//用于初始化操作
//vue vm.$options 就是获取用户的配置
//我们使用的vue的时候 $nextTick $data $attr.....
const vm = this
vm.$options = options//将用户的选项挂载到实例上
//初始化状态
initState(vm);
}
}- 对属性进行劫持
js
function initState(vm){
const opts = vm.$options //获取所有的选项
if(opts.data){
initData(vm)
}
}
function initData(vm){
let data = vm.$options.data // 可能是函数或对象
data = typeof data === 'function' ? data.call(vm) : data
vm._data = data //我将返回的对象放到了_data上
//对数据进行劫持 vue2里采用了一个api defineProperty
observe(data)
//将vm._data用vm来代理就可以了
for(let key in data){
proxy(vm,'_data',key)
}
}- 为了获取属性方便,进行数据代理
js
function proxy(vm,target,key){
//把_data中的属性再vm上在添加一遍,这样我们就可以直接this.xxx获取数据,不用this._data.xxx来获取数据了
Object.defineProperty(vm,key,{
get(){
return vm[target][key]
}
})
}- Observe观测者 进行递归,将所有对象属性加上了get和set
js
class Observer {
constructor(data) {
this.dep = new Dep(); // 此时的dep挂在被observe的数据的__ obj__属性上,在对象或数组增删元素时调用$set等api时发布更新的;
this.vmCount = 0 //记录实例使用次数
//Object.defineProperty只能劫持已经存在的属性(vue2里会为此单独写一些api $set $delete)
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false //将__ob__变成不可枚举(循环时无法获取,防止死循环)
})
data.__ob__ = this //给数据加了一个标识
if (Array.isArray(data)) {
//重写数组的方法,7个变异方法是可以修改数组本身的
data.__proto__ = newArrayProto //需要保留原数组原有的特性,并且重写部分方法
this.observeArray(data) //如果数组中放的是对象,可以监控到对象的变化
} else {
this.walk(data)
}
}
walk(data) { //循环对象 对属性依次劫持
//"重新定义"属性 性能差
Object.keys(data).forEach(key => defineReactive(data, key, data[key])) //会根据__ob__来判断对象是否已经被劫持了
}
observeArray(data) {
for (let i = 0, l = data.length; i < l; i++) {
observe(data[i]); // 逐项进行observe
}
}
}
export function defineReactive(target, key, value) { //闭包 //外层函数包裹,避免使用全局变量接收newVal的问题
const dep = new Dep();//存在在getter/setter闭包函数的作用域链上,是为对象属性服务的。在Watcher获取属性的值的时候收集订阅者,在设置属性值的时候发布更新。
let childOb = observe(value) //子元素要进行observe,递归调用深度监听
Object.defineProperty(target, key, {
get() {
if (Dep.target) { // 如果现在处于依赖收集阶段,render函数/computed/watcher创建的阶段
dep.depend();
if (childOb) {
childOb.dep.depend();
}
if(Array.isArray(value)){
dependArray(value)
}
}
return value //不是依赖收集阶段 直接返回值
},
set(newValue) {
// value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
if (newValue === value) return
observe(newVal) //若赋值成为对象,也要进行深度监听
value = newValue
dep.notify() // 发布订阅模式,通知dep去告诉watcher触发更新
}
})
}
export function observe(data) {
if (typeof data !== 'object' || data == null) {
return //只有对象需要劫持
}
//如果一个对象被劫持过了,那就不需要再被劫持了(要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)
if (data.__ob__ instanceof Observer) {
return data.__ob__ //说明这个对象被代理过了
}
return new Observer(data)
}- 重写数组中的部分方法
js
let oldArrayProto = Array.prototype // 获取数组的原型
// 创建新对象,原型指向oldArrayProperty
let newArrayProto = Object.create(oldArrayProto)
let methods = [
//找到所有的变异方法
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
] // concat slice 不会改变原数组
methods.forEach(methods=>{
newArrayProto[method] = function(...args){//这里重写了数组的方法
const result = oldArrayProto[method].call(this,...args)//内部调用原来的方法,函数的劫持,切片编程
//我们需要对新增的数据再次进行劫持
let inserted
let ob = this.__ob__
switch(method){
case 'push':
case'unshift':
inserted =args;
break
case 'splice':
inserted = args.slice(2)
default:
break
}
console.log(inserted) //新增的内容
if(inserted){
//对新增的内容再次进行观测
ob.observeArray(inserted)
}
return result
}
})- dep
js
let id = 0
class Dep {
constructor() {
this.id = id++
// 依赖收集,收集当前属性对应的观察者 watcher
this.subs = []
}
// 通知 watcher 收集 dep
depend() {
Dep.target.addDep(this)
}
// 让当前的 dep收集 watcher
addSub(watcher) {
this.subs.push(watcher)
}
// 通知subs 中的所有 watcher 去更新
notify() {
const subs = this.subs.slice(); // 浅克隆一份
this.subs.forEach(watcher => watcher.update())
}
}
// 当前渲染的 watcher,静态变量
Dep.target = null //渲染/更新完毕后我们会立即清空 Dep.target 保证了只有在模版渲染/更新阶段的取值操作 才会进行依赖收集。之后我们手动进行数据访问时,不会触发依赖收集,因为此时 Dep.target 已经重置为 null
export default Dep- Watcher
js
import Dep from './dep'
let id = 0
class Watcher {
constructor(vm, fn) {
this.id = id++
this.getter = fn
this.deps = [] // 收集当前 watcher 对应被观察者属性的 dep
this.depsId = new Set()
this.get()
}
// 收集 dep
addDep(dep) {
let id = dep.id
// 去重,一个组件 可对应 多个属性 重复的属性不用再次记录
if (!this.depsId.has(id)) {
this.deps.push(dep)
this.depsId.add(id)
dep.addSub(this) // watcher已经收集了去重后的 dep,同时让 dep也收集 watcher
}
}
// 执行 watcher 回调
get() {
Dep.target = this // Dep.target 是一个静态属性
this.getter() // 执行vm._render时,会劫持到数据访问,调用 dep.depend() 进行依赖收集
Dep.target = null // 渲染完毕置空,保证了只有在模版渲染阶段的取值操作才会进行依赖收集
}
// 重新渲染
update() {
this.get()
}
}- 总结 :
- vue2中对每一个属性都进行"重新定义"属性,这也是相对性能差的原因,
- 数组如果数据量过大,那么将会有溢出栈的问题,所以对数组部分方法进行改造,
__ob__- 我们给数组/对象的实例都增加一个 dep。可以通过响应式数据的
__ob__获取到实例,进而访问实例上的属性和方法,可以为重写数组方法中拿到class类中的observeArray方法 - 又可以给检测过的属性加上标识,防止重复监测的问题.
- 我们给数组/对象的实例都增加一个 dep。可以通过响应式数据的
- 将
__ob__变成不可枚举(循环时无法获取,防止死循环) - Dep有一个静态属性
target,全局唯一,Dep.target 是当前正在执行的watcher实例,这是一个非常巧妙的设计!因为在同一时间只能有一个全局的 watcher - 实例化 dep 的时机
- Observer类:在 Observer 类中实例化 dep,可以给每个数组/对象的实例都增加一个 dep
- defineReactive函数:在 defineReactive 方法中实例化 dep,可以让每个被劫持的属性都拥有一个 dep,这个 dep 是被闭包读取的局部变量,会驻留到内存中且不会污染全局