美文网首页Vue随笔
从零手写实现Vue响应式源码之数据响应

从零手写实现Vue响应式源码之数据响应

作者: 站在大神的肩膀上看世界 | 来源:发表于2019-11-28 18:55 被阅读0次

    前言

    作为vue的使用者,一直想要探究它真正的原理,今天我终于开始对它下了手。本文暂时是对2.0版本的实现。后续会有3.0的,毕竟是大势所趋。但真正全面到来之前,也要知道前身,这样才能知道两者的区别。
    本文内容不多,毕竟太多也不容易消化。但依旧会涉及到以下的知识点:对象相关属性的使用、对象的继承、数据劫持、面向切面(aop)编程等知识点。不要怕,并没有那么可怕(╹▽╹)

    准备工作

    VSCode、以及运行单个js文件的插件 Code Runner(在VSCode的插件商店搜索安装,运行时右键文件Run code即可)

    VSCode Code Runner

    开撸代码

    响应式原理即数据改变,能触发视图更新
    以下将讲解我将按照编码思路进行编写

    • 1.模拟数据更改
    let data = { x: '1' };
    data.x = '2';
    console.log(data);  // 打印{ x: '2' }
    
    • 2.假定有一个更新页面的方法
    // 更新视图的方法
    function updateView () {
      console.log('更新页面视图');
      /**
       更新需要使用的一个方法Object.defineProperty,
       该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 
       并返回这个对象,同时会给属性添加getter  和setter。
       所以当数据改变的时候就会触发updateView的方法
    */
    }
    
    let data = { x: '1' };
    data.x = '2';
    console.log(data);  // 打印{ x: '2' }
    
    • 3.既然是响应,就必须要有一个监听数据的方法,同时需要考虑这个监听的数据如果不是对象或者为null则就不继续往下进行了,直接返回数值。原因是设置不了getter和setter
    /**
     * 监听数据的方法
     * @param {any} targer 
     */
    function observer (targer) {
     if (typeof targer !== 'object' || targer === null) {
        return targer;
      }
    }
    
    function updateView () {
      console.log('更新页面视图');
      /**
       更新需要使用的一个方法Object.defineProperty,
       该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 
       并返回这个对象,同时会给属性添加getter  和setter。
       所以当数据改变的时候就会触发updateView的方法
    */
    }
    
    let data = { x: '1' };
    observer(data); // 观察并监听data
    data.x = '2';
    console.log(data);  // 打印{ x: '2' }
    
    • 4.当参数是对象的话就需要属性循环设置getter和setter,定义设置getter和setter的方法,在setter方法中触发更新视图,并完善一下observer方法
    /**
     * 监听数据的方法
     * @param {any} targer 
     */
    function observer (targer) {
     if (typeof targer !== 'object' || targer === null) {
        return targer;
      }
      for (let key in targer) {
        defineReactive(targer,key,targer[key])
      }
    }
    
    
    /**
     * 设置getter和setter属性的方法
     * @param {any} targer 
     * @param {any} key 
     * @param {any} value 
     */
    function defineReactive(targer,key,value) {
      Object.defineProperty(targer, key, {
        get () {
          console.log('获取', value)
          return value;
        },
        set (newValue) {
          if (newValue !== value) {  // 处理数据相同还要赋值和更新视图的问题
            updateView();
            value = newValue;
          }
        }
      })
    }
    
    // 更新视图的方法
    function updateView () {
      console.log('更新页面视图');
      /**
       更新需要使用的一个方法Object.defineProperty,
       该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 
       并返回这个对象,同时会给属性添加getter  和setter。
       所以当数据改变的时候就会触发updateView的方法
    */
    }
    
    let data = { x: '1' };
    observer(data); // 观察并监听data
    data.x = '2';
    console.log(data);  // 打印   更新页面视图 { x: '2' }
    

    以上基本实现了对数据的响应了并触发更新视图的方法,但考虑情况怎么能考虑一种

    一、当对象层次较深的时候

    let data = { x: '1', y: {z: '1'} };
    data.y.z = '2';
    

    此时数据不会触发视图更新,因为什么呢 ?
    因为只是给x和y对应设置了getter 和setter
    那么我们就需要再循环设置一下了。也就是递归。
    优化

    /**
     * 设置getter和setter属性的方法
     * @param {any} targer 
     * @param {any} key 
     * @param {any} value 
     */
    function defineReactive(targer,key,value) {
      observer(value);  // 递归,解决数据层级问题
      Object.defineProperty(targer, key, {
        get () {
          console.log('获取', value)
          return value; // 返回原值
        },
        set (newValue) {
          if (newValue !== value) {
            updateView();
            value = newValue;
          }
        }
      })
    }
    

    二、当属性赋值对象的时候

    let data = { x: '1', y: {z: '1'} };
    data.y =  {z:'2'};
    data.y.z = '3'
    

    此时数据本应该会触发视图更新两次,但为什么只有一次 ?
    因为只是给data的y属性新赋的值,没有getter 和setter
    也就是设置的属性也可能是对象
    那么我们就优化一下。
    优化

    /**
     * 设置getter和setter属性的方法
     * @param {any} targer 
     * @param {any} key 
     * @param {any} value 
     */
    function defineReactive(targer,key,value) {
      observer(value);  // 递归,解决数据层级问题
      Object.defineProperty(targer, key, {
        get () {
          console.log('获取', value);
          return value; // 返回原值
        },
        set (newValue) {
          if (newValue !== value) {
            observer(value);
            updateView();
            value = newValue;
          }
        }
      })
    }
    

    三、当属性值为数组的时候

    let data = [1, 2, 3];
    data.push(4);
    

    为什么也不会触发更新视图?
    因为push的方法里没有getter 和 setter
    那我们能想到什么,重写他们,但不能直接覆盖原型上的重写,因为之后不观察的数据也被更新。

    let oldArrayPrototype = Array.prototype;  // 获取原对象
    let proto = Object.create(oldArrayPrototype); // 继承
    var ArrayFn = ['push','shift']; // 自己拓展
    ArrayFn.forEach(e => {
      proto[e] = function () {   // 函数劫持  把函数进行重写  内部 继承调用老的方法
        updateView();  // 面向切面编程(aop)不影响代码的业务逻辑
        oldArrayPrototype[e].call(this, ...arguments);
      }
    });
    

    再在监听里判断是否是数组

    /**
     * 监听数据的方法
     * @param { any } targer 
     */
    function observer (targer) {
      if (typeof targer !== 'object' || targer === null) {
        return targer;
      }
      if (Array.isArray(targer)) {
        targer.__proto__ = proto;
      }
      for (let key in targer) {
        defineReactive(targer,key,targer[key])
      }
    }
    

    这样再试试,数组方法也被更新了!!!
    最终代码

    let oldArrayPrototype = Array.prototype;  // 获取原对象
    let proto = Object.create(oldArrayPrototype); // 继承
    var ArrayFn = ['push','shift']; // 自己拓展
    ArrayFn.forEach(e => {
      proto[e] = function () {   // 函数劫持  把函数进行重写  内部 继承调用老的方法
        updateView();  // 面向切面编程(aop)不影响代码的业务逻辑
        oldArrayPrototype[e].call(this, ...arguments);
      }
    });
    
    /**
     * 监听数据的方法
     * @param { any } targer 
     */
    function observer (targer) {
      if (typeof targer !== 'object' || targer === null) {
        return targer;
      }
      if (Array.isArray(targer)) {
        targer.__proto__ = proto;
      }
      for (let key in targer) {
        defineReactive(targer,key,targer[key])
      }
    }
    
    
    /**
     * 设置getter和setter属性的方法
     * @param {any} targer 
     * @param {any} key 
     * @param {any} value 
     */
    function defineReactive(targer,key,value) {
      observer(value);  // 递归,解决数据层级问题
      Object.defineProperty(targer, key, {
        get () {
          console.log('获取', value);
          return value; // 返回原值
        },
        set (newValue) {
          if (newValue !== value) {
            observer(value);
            updateView();
            value = newValue;
          }
        }
      })
    }
    
    // 更新视图的方法
    function updateView () {
      console.log('更新页面视图');
      /**
       更新需要使用的一个方法Object.defineProperty,
       该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 
       并返回这个对象,同时会给属性添加getter  和setter。
       所以当数据改变的时候就会触发updateView的方法
    */
    }
    ...各种测试例子
    let data = { x: '1' };
    observer(data); // 观察并监听data
    data.x = '2';
    ...
    

    总结

    通过上面代码,能意识到2.0的版本对于数据的响应是有缺点的,那就是数据层级深的时候,递归方法的弊病就展现了。
    而且对于不存在的属性也不会响应不会触发视图的更新。

    // 例  data没有name属性
    data.name = '前端扫地僧'
    
    

    结尾

    • 1、如果对你有帮助的话,记得给个赞赏加关注,鼓励一下。

    相关文章

      网友评论

        本文标题:从零手写实现Vue响应式源码之数据响应

        本文链接:https://www.haomeiwen.com/subject/hsnbwctx.html