Vue3将要使用Proxy作为数据驱动,不想进来看看吗?

2020/3/4 11:01:39

本文主要是介绍Vue3将要使用Proxy作为数据驱动,不想进来看看吗?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

尤大大在开发者大会上说新版的Vue会采用Proxy作为数据驱动,来代替原本的 defineProperty。今天聊一聊使用Proxy的好处和Vue是怎样实现数据驱动的。 最后会带着大家手动实现一个Proxy版的MVVM

  • 目前的 Vue 通过什么实现数据驱动?

    • 目前官网使用的版本是Vue 2.5.X

    • 这个版本的底层数据驱动是通过Object.defineProperty 方法来实现了getter/setter。那这个方法是做什么的呢? 我们都知道js对象 都会具备get/set方法,这个方法就是用来定义对象里的get/set方法(虽然也可以定义其他属性 比如是否遍历 是否允许修改等)

    •   Object.defineProperty(obj, prop, descriptor)
        其中obj代表定义属性的对象;prop代表定义的属性名称;descriptor代表定义的内容。举个🌰大家就明白啦
        const obj = {
            name: 'jack',
            sex: 'male',
            age: 20
        }
        for(let key in obj) {
            let val = obj[key]
            Object.defineProperty(obj, key, {
                get() {
                    return val // 触发get函数就返回对象里的值
                    // others...
                },
                set(newVal) {
                    val = newVal // 触发set函数就修改对象
                    // others...
                }
            })
        }
        console.log(obj.name) // getter
        obj.name = 'rose' // setter
        上述代码就利用get/set完成一个简单的访问着,这个🌰也是MVVM中的核心内容
      
      复制代码
    • 为什么要使用Proxy代替defineProperty?

      • 通过上面的🌰大家能看出来要实现get/set就要遍历到每一个对象里的属性,在实际开发时我们的数据结构都比较复杂而且嵌套层级很多时,要监听到所有的数据就需要深层次的递归遍历
      • 第二点就是该方法不支持数组的操作
      • 在Vue中对数组的方法进行了hack,当触发数组方法时(push,pop)会手动触发一下数据驱动,让数组也具备数据驱动的能力
  • 在新版本中Vue使用什么来实现数据驱动?

    • 在今后将要推出的Vue3.X版本中会使用ES6中新的API Proxy来作为数据驱动的核心。相比于上一版的API它有如下好处:
      •   let obj = new Proxy(target, handler)
          // 其中target是要用Proxy包装的对象;handler是包装时执行的操作。还是举个🌰
          let obj = {
              name: 'jack',
              sex: 'male',
              age: 20
          }
          let newObj = new Proxy(obj, {
              get: (target, property, receiver) => {
                  // get方法的参数,target:目标对象;property:获取的属性名;receiver:当前的Proxy
                  比如我访问了 obj.name  这里的target = obj;property = name
                  return obj[property]
                  // others...
              },
              set:(target, property, value, receiver) => {
                  // 参数同上,不同的是 value是新值
                  obj[property] = value
                  /// others...
              }
          })
        复制代码
  • 如果我们想要实现Vue的数据驱动需要做什么事情?

    • 首先我们来想一下一个Vue实例渲染都经过了哪些大的步骤:

      • 初始化Data数据,将data编译为可追踪和可观察的对象
      • 编译dom,将data里的数据渲染到dom上
      • 编译完成,Vue实例渲染完成
      • 当触发修改,观察者将新数据同步至 Data 和 dom
      • re-render
    • 所以我们只要将上述步骤全部编写成代码就可以得到一个数据驱动的源码,事不宜迟下面就直接进入主题

  • Vue数据驱动实现步骤

    • 1.初始化Data数据,将data编译为可追踪和可观察的对象

          首先需要创建Vue类,并解构一些所需对象
          class Vue {
              constructor(options) {
                  const { data } = options 
                  this.$data = data
                  initObserve.call(this, this.$data)
              }
              function initObserve(data) {
                  // 初始化data对象,将data里的对象转化为可观察的对象
                  this.$data = observe(data);
              }
              function observe(data) {
                  // 判断是否为对象类型,否则则返回原对象
                  if(typeof data !== 'object') return data;
                  return new Observe(data);
              }
              class Observe {
                  //实现观察者,递归监听data里的数据
                  constructor(data) {
                  // 使用 for in 做深层次递归,保证data里嵌套格式也能正确为转化
                      for(let key in data) {
                          data[key] = observe(data[key]);
                      }
                      return this.proxy(data);
                  }
                  proxy(data) {
                      // 这里就是观察者的核心部分,对接收的data附加getter/setter
                      return new Proxy(data, {
                          get(target, property, receiver) {
                              return Reflect.get(target, property);
                          },
                          set(target, property, value, receiver) {
                              const result = Reflect.set(target, property, observe(value));
                              return result;
                          } 
                      })
                  }
              }
          }
          
          const mvvm = new Vue({
              data: {}
          })
      复制代码
      我们的第一步已经完成了,总结一句话:遍历$data并使用Proxy函数对data进行加工处理
    • 2. 代理属性到this上

      • 做完第一步我们已经拥有了可观察的数据驱动模型。但是只能通过this.$data来触发,我们想要的效果是所有操作全部集中在this关键字上,所以我们要将data代理到this上去
          // 为了保持代码整洁,每次只展示所需要的代码,其他代码并不是不需要了
          class Vue {
              constructor(options) {
                  const { data } = options 
                  this.$data = data
                  let vm =  initVm.call(this);            ++
                  // others code
                  return this.$vm                         ++
              }
              function initVm() {                         ++
                /* 
                  这一步主要是代理数据 
                  相当于将this.$data上的数据代理到this上 访问this.name === 访问 this.$data.name
                 */
                  this.$vm = new Proxy(this, {            
                      get: (target, property, receiver) => {
                          return this.$data[property]
                          },
                      set:(target, property, value, receiver) => {
                        return Reflect.set(this.$data, property, value);
                      }
                  });
                  return this.$vm;
              }
          }
          
          const mvvm = new Vue({
              data: {}
          })
      复制代码
    • 3.编译渲染,将data上的数据渲染到dom上

      • 我们已经可以在js里操作这个数据模型了,但是光通过js操作不行,在mvvm里面我们忘记了最重要的 视图。下一步就是把处理好的data渲染到dom中。能让用户看到
          class Vue {
              constructor(options) {
                  // 首先获取指定渲染的dom根节点
                  let { data, el } = options;                 ++
                  this.$el = document.querySelector(el);      ++
                  new Compile(el, vm)                         ++
                  // others code
              }
              class Compile {                                 ++
                /* 
                  编译数据
                  将data的数据渲染到页面上
                 */
                constructor(el, vm) {
                  this.vm = vm;
                  let fragment = document.createDocumentFragment();
                  fragment.append(document.querySelector(el));
                  this.replace(fragment);
                  document.body.appendChild(fragment);
                }
                replace(arr) {
                  Array.from(arr.childNodes).forEach(node => {
                    const reg = /\{\{(.*?)\}\}/g;
                    let txt = node.textContent;
                    // nodeType === 3表示该节点为文本节点
                    if(node.nodeType === 3 && reg.test(node.textContent)) {
                      let vm = this.vm;
                      updateTxt();
                      function updateTxt() {
                        // 去除首尾空格,把符合条件的目标替换为data里的对象
                        // 🌰:{{ name }} => {{name}} => $data.name => jack
                        // 使用reduce函数是为了防止:$data.user.sex 这种嵌套情况
                        const val = txt.replace(reg, (matched, arrs) => {
                          return arrs.split('.').map(el => el.trim()).reduce((obj, key) => {
                            return obj[key] === undefined? node.textContent : obj[key]; // 例如:去vm.makeUp.one对象拿到值
                          }, vm);
                        });
                        if(val != node.textContent) {
                          node.textContent = val;
                        }
                      }
                    }
                    // 递归遍历dom节点
                    if(node.childNodes && node.childNodes.length > 0) {
                      this.replace(node);
                    }
                  });  
                }
              }
          }
      复制代码
    • 4.修改$data并驱动观察者更新页面dom视图

      • 写到这里我们已经完成了大半工作,现在我们在js层可以修改data 在视图层我们可以把data渲染到dom。但是这两者还没有联系起来。下一步我们要做到更改data就能驱动dom发生改变
          // 还记得第一步实现的初始化data吗?
          // 首先我们来想一个问题:视图怎么才能知道我何时要更新呢?
          // 每当用户修改了$data时就应该更新视图,不然的话视图和$data就会不一致了。其实我们已经知道如何解决了,我们只要在$data所触发的 getter/setter里注册函数就好了,在相应时刻调用函数就能更新视图了
          function initObserve(data) {
              this.$data = observe(data);
          }
          
          function observe(data) {
              if(typeof data !== 'object') return data;
              return new Observe(data);
          }
          class Observe {
            constructor(data) {
              this.dep = new Dep();                       ++
              for(let key in data) {
                data[key] = observe(data[key]);
              }
              return this.proxy(data);
            }
            proxy(data) {
              let dep = this.dep;                         ++
              return new Proxy(data, {
                get(target, property, receiver) {
                  // 在getter里注册监听方法
                  if(Dep.target) {                        ++
                    if(!dep.subs.includes(Dep.exp)) {
                      dep.addSub(Dep.exp);
                      dep.addSub(Dep.target);
                    }
                  }  
                  return Reflect.get(target, property);
                },
                set(target, property, value, receiver) {
                  // 触发setter时一并触发修改方法
                  const result = Reflect.set(target, property, observe(value));
                  dep.notify();                           ++
                  return result;
                } 
              });
            }
          }
          class Compile {
              // 找到编译函数,在替换数据这个函数里加一行 Watcher的监听
              // 这样我们就在所有需要编译的地方实例了观察函数,Watcher还接收了编译的值
              replace() {
                  function updateTxt() {
                      new Watcher(vm, arrs, updateTxt);
                  }
              }
          }
          class Dep {
            /* 
              发布订阅
              监听setter 当触发setter会调用注册过的函数 依次调用函数
             */
            constructor() {
              // 需要更新数据放在这个数组里
              this.subs = [];
            }
            addSub(sub) {
              this.subs.push(sub);
            }
            notify() {
              // 当setter时遍历数组执行所有函数
              this.subs.filter(fn => typeof fn !== 'string').forEach(sub => sub.update());
            }            
          }
          class Watcher {
            // 修改dom最核心的函数,接收了当前Vue实例、更新的字段和更新函数
            constructor(vm, exp, fn) {
              // 每一次的Watcher都能对应一次Dep
              // 所以说在getter函数里的Dep都
              this.fn = fn;
              this.vm = vm;
              this.exp = exp;
              Dep.exp = exp;
              Dep.target = this;
              const arr = exp.split('.').map(el => el.trim());
              let val = vm;
              arr.forEach(key => {
                val = val[key] || val;
              });
              Dep.target = null;
            }
            update() {
              const arr = this.exp.split('.').map(el => el.trim());
              let val = this.vm;
              arr.forEach(key => {
                val = val[key];
              });
              this.fn(val);
            }
          }
      
      复制代码

      请注意上述代码中的 Dep和Watcher函数一定要结合起来看,每次触发$data中的getter时都会相应触发一次Dep和Wacther,又因为没有异步的关系所以说Dep和Watcher中的变量一定是相互对应关联的。

    • 到这为止核心篇已经讲完了,现在我们知道如何更新数据和如何替换dom节点。知道这些还是不够的,下面讲一讲Vue中好用的功能点
  • v-model

    • 双向绑定可谓是开发中使用频率最最最高的api之一了,那她是如何实现的呢? 从官网我们可以知道,v-model是语法糖,即:
        <input value="val" onChange="(e) => val = e.target.value" />
    复制代码
    知道v-model的原理我们就很好实现了,首先找到 Compile类,在replace中加一行代码
        replace(arr) {
        // 这行代码首先判断node是不是元素节点,因为只有元素上才会有v-model
            if(node.nodeType === 1) {
                this.directives(node);      +++
            }
        }
        directives(node) {                  +++
        // 遍历元素上是否存在叫v-model的名字。如果找到,就将v-model扩展成 value+onChange的形式
            const vm = this.vm;
            Array.prototype.slice.call(node.attributes).forEach(el => {
              if(el.name === 'v-model') {
                node.value = vm[el.value];
                node.addEventListener('input', e => {
                  vm[el.value] = e.target.value;
                });
              }
            });
        }
    复制代码
  • v-bind

    • 这个其实和v-model是一样的道理,只要相对应寻找到v-bind的名字
    • 不过有个弊端,为了偷懒我只判断了v-bind的简写形式 @
    • 有了v-bind,就可以正常使用click这种事件了
        // 就像上面一样,在v-model的下面增加相应代码
        if(el.name.includes('@')) {         +++
            const eventName = el.name.split('@')[1];
            node.setAttribute(`v-bind:${ eventName }`, el.value);
            node.addEventListener(eventName, vm.$methods[el.value].bind(vm));
        }
    复制代码
  • computed

    • 本文章最后介绍的是使用频率很高的计算属性
    • 代码很简洁,思路还是给computed对象增加Watcher监听
        class Vue {
            function initComputed() {       +++
              let computed = this.$options.computed;
              this.$computed = {};
              if(!computed) return;
              Object.keys(computed).forEach(key => {
                this.$computed[key] = computed[key].call(this.$vm);
                new Watcher(this.$vm, key, val => {
                  this.$computed[key] = computed[key].call(this.$vm);
                });
              });
            }
        }
    复制代码
  • initVm

    • 还差最后一步,我们已经在Vue内部封装好了 v-model、v-bind、computed等等。应该如何通过this来访问呢?
    • 首先来看initVm函数,这里面使用proxy代理了this的访问规则,增加一下访问规则:
        function initVm() {
          this.$vm = new Proxy(this, {          +++
            get: (target, property, receiver) => {
              return this[property] || this.$data[property] || this.$computed[property] || this.$methods[property];     
            },
            set:(target, property, value, receiver) => {
              return Reflect.set(this.$data, property, value);
            }
          });
          return this.$vm;
        }
    复制代码
  • 总结

    • 如果客观大人看到这就证明一个可以使用的Vue实例就创建好啦。这篇文章从年前开始写磨磨唧唧写到了现在。发现自己的文笔确实不行,如果有的地方讲的不明白还请大人留言或移步我的github查看源码哈~
    • 最最后想说一下所有代码都没有用到Vnode或diff的概念,性能上不能保证。不过也是抛砖引玉,能让大家有一丝丝收获我就满足了~

另附上github源码~

谢谢观看!



这篇关于Vue3将要使用Proxy作为数据驱动,不想进来看看吗?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程