阅读 917

Vue响应式原理

vue实现数据响应式,是通过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新,换句话说是Observe,Watcher以及Compile三者相互配合,

  • Observe实现数据劫持,递归给对象属性,绑定setter和getter函数,属性改变时,通知订阅者
  • Compile解析模板,把模板中变量换成数据,绑定更新函数,添加订阅者,收到通知就执行更新函数
  • Watcher作为Observe和Compile中间的桥梁,订阅Observe属性变化的消息,触发Compile更新函数

数据劫持/代理 Observer

实现响应式的第一步就是能侦测数r据的变化,在Vue2.x是通过ES5的方法Object.defineProperty()实现对象属性的侦听,在Vue3.x中使用了ES6提供的Proxy对对象进行代理

Object.defineProperty

function observe(obj) {
  if (!obj || typeof obj !== "object") {
    return;
  }
  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key]);
  });
  function defineReactive(obj, key, value) {
    //递归子属性
    observe(value);
    //订阅器
    const dp = new Dep();
    Object.defineProperty(obj, key, {
      configurable: true, //可删除
      enumerable: true, //可枚举遍历
      get: function () {
        /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
        dp.addSub(Dep.target);
        return value;
      },
      set: function (newValue) {
        //递归新的子属性
        observe(newValue);
        if (value !== newValue) {
          value = newValue;
          /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
          dp.notify();
        }
      },
    });
  }
}
复制代码

Proxy实现代理


let target = { name: " xiao" };

let handler = {
  get(target, key) {
    if (typeof target[key] === "object" && target[key] !== "null") {
      return new Proxy(target[key], handler);
    }
    return target[key];
  },
  set: function (target, key, value) {
    target[key] = value;
  },
};

target = new Proxy(target, handler);
复制代码

依赖收集Dep

//Dep订阅者,依赖收集器
class Dep {
  constructor() {
    /* 用来存放Watcher对象的数组 */
    this.subs = [];
  }
  /* 在subs中添加一个Watcher对象 */
  addSub(sub) {
    this.subs.push(sub);
  }
  /* 在subs中添加一个Watcher对象 */
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}
//用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
//用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。
复制代码

Watcher订阅者

class Watcher {
  constructor(obj, key, cb) {
    /* 在new一个Watcher对象时将该对象赋值给Dep.target,在observe get中会用到 */
    Dep.target = this;
    this.obj = obj;
    this.key = key;
    this.cb = cb;
    //触发getter,依赖收集
    this.value = obj[key];
    //收集完置空Dep.target,防止重复收集
    Dep.target = null;
  }
  update() {
    //获得新值
    this.value = obj[this.key];
    console.log("视图更新");
  }
}
复制代码

Compile模板编译

  • 正则匹配解析vue指令、表达式
  • 把变量替换成数据初始化渲染
  • 创建Watcher订阅更新函数
//指令处理类
const compileUtile = {
    getVal(expr,vm){
        //reduce用的好啊
        return expr.split('.').reduce((data,curentval)=>{
            return data[curentval];
        },vm.$data)
    },
    html(node,expr,vm){
        new Watcher(vm,expr,(newVal)=>{
            this.updater.htmlUpdate(node,newVal);
        })
        const value = this.getVal(expr,vm);
        this.updater.htmlUpdate(node,value);
    },
  
    //更新函数
    updater:{
        htmlUpdate(node,value){
            node.innerHTML= value;
        },
    }
}
//Compile指令解析器
class Compile{
//各种正则匹配vue指令和表达式,替换数据
}


复制代码

Object.defineProperty与Proxy的区别?

  • Proxy可以直接监听对象,而非属性,可以监听属性的增加
  • Proxy可以监听数组
  • Proxy有很多Object.defineProperty不具备的拦截方法
  • Proxy返回一个新对象,可以直接操作新对象达到目的,Object.defineProperty只能遍历对象属性修改

为什么要依赖收集?

数据劫持的目的是在属性变化的时候触发视图更新,依赖收集可以收集到哪些地方使用到了相关属性,属性变化时,就可以通知到所有的地方去更新视图,对于没有使用的属性,也可以避免无用的数据比对更新

Dep和Watcher的关系(多对多)

  • data中一个key对应一个Dep实例, 一个Dep实例对应多个Watcher实例(一个属性在多个表达式中使用)
  • 一个表达式对应一个Watcher实例,一个Watcher对用多个Dep实例(一个表达式中有多个属性)

watcher和Dep何时创建

  • Dep在初始化data的属性进行数据劫持时创建的
  • Watcher是在初始化时解析大括号表达式/一般指令时创建

如何实现对数组的监听

因为Object.defineProperty不能监听数组长度变化,所以Vue使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

 1 // src/core/observer/array.js
 2 
 3 // 获取数组的原型Array.prototype,上面有我们常用的数组方法
 4 const arrayProto = Array.prototype
 5 // 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
 6 export const arrayMethods = Object.create(arrayProto)
 7 
 8 // 列出需要重写的数组方法名
 9 const methodsToPatch = [
10   'push',
11   'pop',
12   'shift',
13   'unshift',
14   'splice',
15   'sort',
16   'reverse'
17 ]
18 // 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
19 methodsToPatch.forEach(function (method) {
20   // 保存一份当前的方法名对应的数组原始方法
21   const original = arrayProto[method]
22   // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
23   def(arrayMethods, method, function mutator (...args) {
24     // 调用数组原始方法,并传入参数args,并将执行结果赋给result
25     const result = original.apply(this, args)
26     // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
27     const ob = this.__ob__
28     let inserted
29     switch (method) {
30       case 'push':
31       case 'unshift':
32         inserted = args
33         break
34       case 'splice':
35         inserted = args.slice(2)
36         break
37     }
38     if (inserted) ob.observeArray(inserted)
39     // 将当前数组的变更通知给其订阅者
40     ob.dep.notify()
41     // 最后返回执行结果result
42     return result
43   })
44 })
复制代码

def就是通过Object.defineProperty重写value,也就是自定义的几个数组方法


function def(obj,key,val,enumble){
Object.defineProperty(obj,key,{
 enumble:!!enumble,
 configrable:true,
 writeble:true,
 val:val
 
})
}
复制代码

observe方法里面加入数组的处理,

  • 能获取到__proto__属性,就把__protp__属性指向重写的方法
  • 获取不到__proto__属性,就把重写的方法定义到对象上实例上

// src/core/observer/index.js
export class Observer {
  ...
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  ...
}
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

复制代码
文章分类
前端
文章标签