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

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

前言

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

准备工作

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

VSCode
VSCode

Code Runner
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、如果对你有帮助的话,记得给个赞赏加关注,鼓励一下。
相关文章
|
4天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
4天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
4天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
10天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
10天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
10天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
10天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
9天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
11天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
9天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
下一篇
无影云桌面