zl程序教程

您现在的位置是:首页 >  前端

当前栏目

Vue响应式原理

2023-09-11 14:18:53 时间

码字不易,有帮助的同学希望能关注一下我的微信公众号:Code程序人生,感谢!代码自用自取。

在这里插入图片描述

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])
  }
}

有微信小程序课设、毕设需求联系个人QQ:505417246

关注下面微信公众号,可以领取微信小程序、Vue、TypeScript、前端、uni-app、全栈、Nodejs、Python等实战学习资料
最新最全的前端知识总结和项目源码都会第一时间发布到微信公众号,请大家多多关注,谢谢

在这里插入图片描述