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

posted @ 2021-07-07 16:16  Dreamsqin  阅读(198)  评论(0编辑  收藏  举报