评论

收藏

[jQuery] Vue响应式原理

开发技术 开发技术 发布于:2021-07-21 13:47 | 阅读数:453 | 评论:0

DSC0000.png

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])
  }
}
DSC0001.png



关注下面的标签,发现更多相似文章