从设计理念到数据响应式

简介: 本文从vue系列的基本设计思路开始,到手写基本api的实现。让大家从实践中体会vue的数据驱动的神秘之处。

网络异常,图片无法展示
|


本文从vue系列的基本设计思路开始,到手写基本api的实现。让大家从实践中体会vue的数据驱动的神秘之处。


设计理念


大家都知道,vue是一个典型的MVVM框架。那什么是MVVM、在vue中又是怎么体现的呢?


MVVM


网络异常,图片无法展示
|

M

M代表的是模型Model。是展示在页面中的数据,在vue中指向的是data中的数据模型。


V

V代表的是视图View。是展示的页面,指向的是vue中的模板引擎(template模板)。


VM

VM代表的是ViewModel。不需要通过我们的操作将数据解析展示到视图上,以及数据发生改变,页面上的视图自动会发生相应改变。


那么vue是如何将视图和逻辑操作分开,这就很有必要提到vue中数据驱动的特点。那么这些数据又是如何在视图中展示的呢? vue通过数据响应式监听数据的变化并在视图中更新;


模板引擎提供描述视图的模板语法(类html。提供一些vue特有指令、插值);

渲染:将模板语法转为html(AST=>vdom=>dom)。


数据响应式


简单响应式


vue中的监听数据变化:


vue2: Object.defineProperty()

vue3: Proxy


两者简单的响应数据案例可以查看这篇文章

<div id="app"></div>
<script>
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      return val
    },
    set(newVal) {
      if (val != newVal)
        val = newVal
      // 数据发生变化通知视图更新
      update()
    }
  })
}
function update() {
  content.innerHTML = `<h1>${obj.name}</h1>`
}
let obj = { name: 'clying' }
let content = document.getElementById('app')
update()
// 响应式处理
defineReactive(obj, 'name', 'clying')
setTimeout(() => {
obj.name = 'deng'
console.log(obj.name);
}, 1000)
</script>

运行代码,我们可以看到页面一开始展示clying,经过1秒之后变为deng。 通过这个简单的案例,我们可以实现简单的obj对象响应式地对页面进行渲染。


递归响应式


上述案例中只是对deep=1的对象进行了数据监听。那么具有深度的obj对象又是如何监听变化的呢?


这时候就需要遍历obj对象,对对象中的每个属性进行数据监听。通过Object.keys返回一个obj中所有元素为字符串的数组,对其进行setter和getter拦截。

// 先来一个具有深度的对象
let obj = {
  name: 'clying',
  arr: [
    1,
    {
      namearr: '2',
    },
  ],
  children: {
    name1: 'deng',
    children: {
      name2: 'clying deng',
    },
  },
}
function defineReactive(obj, key, val) {
  observe(val) // 子属性可能仍为对象,在对其进行拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('获取', key)
      return val
    },
    set(newVal) {
      if (newVal !== val) val = newVal
      console.log('设置新值', key, obj[key])
    },
  })
}
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return
  Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))
}
observe(obj)
obj.children.name1 = 'l'

网络异常,图片无法展示
|


当设置obj.children.name1时,先获取到children属性,发现children仍是一个对象,继续遍历。获取到children中的name1属性时,发现不是对像,对name1进行拦截,设置新值。


动态添加属性


当我想要根据上例,对obj动态添加一个age属性时,其实是没有作用的。defineProperty无法检测新增属性,这也涉及到vue2的一个弊端。这就要额外使用到vue2中的set API方法。

function set(obj, key, val) {
  defineReactive(obj, key, val)
}
set(obj, 'age', 22)
obj.age


可以看出set方法其实也是利用defineProperty去添加新属性,只是需要用户手动调用。通过调用set,使obj中新属性age可以被拦截到。

网络异常,图片无法展示
|


注意set用法:


set方法对于接收的目标参数必须是响应式的,可以在源码set方法中看到,一开始就会去判断传入的目标值是否是原始值、undefined或null,如果是这些情况直接警告。如果想要删除属性,相同的需要手动调用delete方法。


注:observer文件夹index.js 201行


数组响应式


defineProperty方法其实是可以拦截到像arr[0] = 1这种,通过index下标赋值的数组。但是它无法支持数组中push、pop等数组的原型方法。我们需要拦截数组的7个方法,重写他们,就是干!


因为我们只是简单的模仿,没有写Observe类。在此我将Observe类中拆成observe方法(判断是数组还是对象,分别监听)、observeArray循环遍历监听数组属性。


function observeArray(arr) {
  arr.forEach((_) => observe(_))
}
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) return
  if (Array.isArray(obj)) {
    obj.__proto__ = arrayMethods // 继承原型方法属性
    observeArray(obj)
  } else {
    Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))
  }
}


比较核心的还是在 obj.__proto__ = arrayMethods中,使当前遍历到的数组的原型链可以指向我们重写的数组方法。


const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]
let oldArrayMethods = Array.prototype
let arrayMethods = Object.create(oldArrayMethods) //arrayMethods的原型指向Array数组的原型,可以获取数组原型方法
methodsToPatch.forEach((method) => {
  arrayMethods[method] = function (...args) {
    const result = oldArrayMethods[method].apply(this, args)
    let inserted // 当前用户插入的元素
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      // 3个 新增属性 splice 有删除 新增的功能 arr.splice(0,1,{name:1})
      case 'splice':
        inserted = args.slice(2)
      default:
        break
    }
    // let ob = this.__ob__;
    // ob.observeArray(inserted); // 插入的是对象或者数组的话还需要再次递归监听
    // update通知更新
    return result
  }
})


当我们通过obj.arr.push(1);obj.arr[1].namearr = 2时,可以看到控制台输出:

网络异常,图片无法展示
|


说明在push数组方法,和修改数组值时,数组都可以走到defineProperty中,被其拦截。


在此,数组插入的值可能是对象或数组时,仍需要对其插入的值进行监听。应该在Observe类中,先将这个实例保存(内部会含有observeArray方法)。然后,在arrayMethods中使用其observeArray方法,继续进行深度劫持。


至此,关于数据响应式就可以告一段落拉。如有不足,欢迎大家指正。

目录
相关文章
|
6月前
|
编解码 前端开发 UED
前端开发中的响应式设计原则与技巧
【2月更文挑战第10天】在当今数字化时代,响应式设计已经成为前端开发的重要组成部分。本文将介绍响应式设计的基本原则和一些实用技巧,帮助开发人员创建适应不同设备和屏幕尺寸的网页界面。从媒体查询到流体布局,从移动优先到图像优化,我们将探索如何以用户为中心,提供优质的用户体验。
90 1
|
6月前
|
编解码 移动开发 前端开发
探索现代前端开发中的响应式设计原则
在现代前端开发中,响应式设计成为了一个关键概念。本文将深入探讨响应式设计的原则和应用,在不同屏幕尺寸和设备上提供一致的用户体验。通过学习如何创建灵活适应的界面,我们可以为用户提供更好的浏览和交互体验。
|
28天前
|
前端开发 JavaScript 物联网
组件化设计适用于哪些场景
【10月更文挑战第22天】组件化设计适用于哪些场景
|
28天前
|
前端开发 JavaScript UED
什么是组件化设计
【10月更文挑战第22天】什么是组件化设计
|
2月前
|
前端开发 开发者 UED
探索现代Web开发中的响应式设计原则
【9月更文挑战第21天】 在数字化时代,Web页面的响应式设计已成为开发者必备的技能之一。本文将通过浅显易懂的语言和生动的比喻,带你了解响应式设计的精髓,并结合代码示例,展示如何在实际项目中运用这些原则。我们将从基础出发,逐步深入,确保你能够掌握并应用这些知识,让你的网页在不同设备上都能展现出最佳的用户体验。
|
4月前
|
编解码 前端开发 UED
现代前端开发中的响应式设计原则与实践
随着移动设备的普及和互联网内容的多样化,响应式设计已成为现代前端开发的重要组成部分。本文将探讨响应式设计的基本原则,并结合实际案例介绍其在现代网页开发中的应用和优化策略。
|
6月前
|
前端开发 UED 开发者
《前端开发中的响应式设计原则与实践》
【2月更文挑战第4天】 在当今数字化时代,移动设备的普及使得响应式设计成为前端开发中不可或缺的重要环节。本文将探讨响应式设计的基本原则,并结合实际案例详细介绍在前端开发中如何应用这些原则,以提升用户体验和网站性能。
|
6月前
|
前端开发 UED 开发者
探索前端开发中的响应式设计原则与实践
【2月更文挑战第2天】在当今移动设备普及的时代,响应式设计成为了前端开发中不可或缺的一环。本文将探索响应式设计的基本原则和实践技巧,帮助开发者更好地适配不同屏幕尺寸,并提供优质的用户体验。
|
6月前
|
前端开发 UED 开发者
探索前端开发中的响应式设计原则
在当今互联网时代,响应式设计已成为前端开发中的重要技术之一。本文将深入探讨响应式设计的原则和实践,帮助开发者更好地适应不同设备和屏幕尺寸,提升用户体验。
|
6月前
|
前端开发 JavaScript UED
现代前端开发中的响应式设计原则
在现代前端开发中,响应式设计已经成为一种必备的技术。本文将介绍响应式设计的基本原则,并探讨如何使用HTML、CSS和JavaScript来实现一个优秀的响应式网站。