zoukankan      html  css  js  c++  java
  • Vue底层学习2——手撸数据响应化

    全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/14982040.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)

    作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上一篇聊了聊大家熟知的理论部分,本篇就来手撸数据响应化代码,即数据遍历并重写settergetter

    Object.defineProperty(obj, prop, descriptor)

    它是实现数据响应式的核心,该方法可以直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

    • obj:要定义或修改的属性对象;
    • prop:要定义或修改的属性名称;
    • descriptor:要定义或修改的属性描述,详细说明可参见我之前写的《Javascript基础巩固系列——标准库Object对象》中属性描述对象章节;
      下面是一个自定义setter和getter的简单例子:
    function Archiver() {
      var temperature = null;
      var archive = [];
    
      Object.defineProperty(this, 'temperature', {
        get: function() {
          console.log('get!');
          return temperature;
        },
        set: function(value) {
          temperature = value;
          archive.push({ val: temperature });
        }
      });
    
      this.getArchive = function() { return archive; };
    }
    
    var arc = new Archiver();
    arc.temperature; // 访问temperature属性时会调用get方法,控制台打印:'get!'
    arc.temperature = 11 // 修改temperature属性时会调用set方法,为archive数组添加日志条目:{val: 11};
    arc.temperature = 13; // 修改temperature属性时会调用set方法,为archive数组添加日志条目:{val: 13};
    arc.getArchive(); //调用getArchive方法返回archive数组:[{ val: 11 }, { val: 13 }]
    

    通过Object.defineProperty读取和设置DOM节点内容

    在上一篇做原理解析的时候有提到Vue是通过Object.defineProperty重写data对象中各个属性的settergetter,用于实现【响应式】和【依赖收集】。那么我们先做一件事,通过Object.defineProperty读取和设置DOM节点内容,以此体会一下数据驱动视图的概念。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>defineProperty</title>
    </head>
    <body>
      <div id="app">
        <p id="name"></p>
      </div>
    
      <script>
        var obj = {};
        Object.defineProperty(obj, 'name', {
          get: function() {
          	// 访问obj对象的name属性时,获取id为name的节点内容 
            return document.querySelector('#name').innerHTML;
          },
          set: function(value) {
          	// 修改obj对象的name属性时,设置id为name的节点内容为修改后的值
            document.querySelector('#name').innerHTML = value;
          }
        })
        // 数据驱动视图变更
        obj.name = 'dreamsyang';
      </script>
    </body>
    </html>
    

    运行结果如下,可以看到obj对象的name属性值被修改后DOM节点内容也同步更新:

    自建数据响应式框架

    有了上面的例子做铺垫应该对响应式有些许感觉,接下来我们自己搭建一个Vue响应式框架,需要达到的效果就是在new一个Vue实例之后实现数据初始化,达到响应式效果~为了区别于Vue,我重新命名为MVue

    新建MVue.js文件用于MVue类封装,参数接收配置对象

    /*** MVue.js ***/
    // new MVue({ data: {...} })
    
    class MVue {
      constructor(options) {
        // 数据缓存
        this.$options = options;
        this.$data = options.data;
    
        // 数据遍历
        this.observe(this.$data);
      }
    }
    

    通过observe实现data数据遍历

    /*** MVue.js ***/
    // new MVue({ data: {...} })
    
    class MVue {
      constructor(options) {...}
    
      observe(data) {
        // 确定data存在并且为对象
        if (!data || typeof data !== 'object') {
          return;
        }
    
        // 遍历data对象
        Object.keys(data).forEach(key => {
        	// 重写对象属性的getter和setter,实现数据的响应化
            this.defineReactive(data, key, data[key]);
        })
      }
    }
    
    

    通过defineReactive重写gettersetter实现数据响应化

    /*** MVue.js ***/
    // new MVue({ data: {...} })
    
    class MVue {
      constructor(options) {...}
    
      observe(data) {...}
    
      defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
          get: function() {
            return val;
          },
          set: function(newVal) {
            // 判断属性值是否发生变化
            if (newVal === val) {
              return;
            }
            val = newVal;
            // 预留视图更新
            console.log(`${key}属性更新了:${val}`);
          }
        })
      }
    }
    

    自建框架测试demo1

    完成上述步骤后先看看目前的效果,写个小demo测试一下:

    <!-- demo1.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>demo1</title>
    </head>
    <body>
      <script src="MVue.js"></script>
      <script>
        const app = new MVue({
          data: {
            name: 'dreamsyang',
            infoObj: {
              location: 'chongqing',
            }
          }
        })
        app.$data.name = 'hello, dreamsyang!';
        app.$data.infoObj.location = 'oh, chongqing!';
      </script>
    </body>
    </html>
    
    

    运行结果如下,可以看到app.$data.infoObj.location = 'oh, chongqing!'并未触发setter中的打印,其主要原因是我们在遍历data时不是深度遍历:

    通过递归实现深度遍历

    我们只需要在defineReactive执行的开始再次调用observe即可,如果val不为对象,就会结束执行,如果为对象就会深度遍历。

    /*** MVue.js ***/
    // new MVue({ data: {...} })
    
    class MVue {
      constructor(options) {
        // 数据缓存
        this.$options = options;
        this.$data = options.data;
    
        // 数据遍历
        this.observe(this.$data);
      }
    
      observe(data) {
        // 确定data存在并且为对象
        if (!data || typeof data !== 'object') {
          return;
        }
    
        // 遍历data对象
        Object.keys(data).forEach(key => {
            // 重写对象属性的getter和setter,实现数据的响应化
            this.defineReactive(data, key, data[key]);
        })
      }
    
      defineReactive(obj, key, val) {
        // 解决数据嵌套,递归实现深度遍历
        this.observe(val);
    
        Object.defineProperty(obj, key, {
          get: function() {
            return val;
          },
          set: function(newVal) {
            // 判断属性值是否发生变化
            if (newVal === val) {
              return;
            }
            val = newVal;
            // 预留视图更新
            console.log(`${key}属性更新了:${val}`);
          }
        })
      }
    }
    

    再次执行demo1结果如下,可以看到正常打印了:

    参考资料

    1、Object.definePropertyhttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
    2、Vue源码:https://github.com/vuejs/vue

  • 相关阅读:
    快速排序及其优化
    JVM基础:深入学习JVM堆与JVM栈(转)
    java 反射简介(转载)
    java 泛型简介(转载)
    Java 注解简介
    JVM入门必看——JVM结构
    Java多线程详解(转载)
    SpringMVC 实现文件的上传与下载
    死锁简介
    SQL的模糊查询(转载)
  • 原文地址:https://www.cnblogs.com/dreamsqin/p/14982040.html
Copyright © 2011-2022 走看看