最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue响应式原理

    正文概述 掘金(controZL)   2021-02-13   385

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

    起源地下载网 » Vue响应式原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元