Vue 进阶系列之响应式原理及实现

什么是响应式Reactivity

Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。

需求

现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?

简单尝试1:

let a = 3;
let b = a * 10;
console.log(b); // 30

乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。

let a = 4;console.log(a); // 4
console.log(b); // 30
let b = a * 10;
console.log(b); // 40

简单尝试2:

将a和b的关系定义在函数内,那么在改变a之后执行这个函数,b的值就会改变。伪代码如下。

onAChanged(() => {
    b = a * 10;
})

所以现在的问题就变成了实现onAChanged函数,当a改变之后自动执行onAChanged,请看后续.

结合view层

现在把a、b和view页面相结合,此时a对应于数据,b对应于页面。业务场景很简单,改变数据a之后就改变页面b。

<span class="cell b"></span>
onStateChanged(() => {
	document.querySelector('.cell.b').textContent = state.a * 10
})

再次抽象之后如下所示。

<span class="cell b">
	{{ state.a * 10 }}
</span>

onStateChanged(() => {
	view = render(state)
})

view = render(state)是所有的页面渲染的高级抽象。这里暂时不考虑view = render(state)的实现,因为需要涉及到DOM结构及其实现等一系列技术细节。这边需要的是onStateChanged的实现

实现

实现方式是通过Object.defineProperty中的gettersetter方法。具体使用方法参考如下链接。

MDN之Object.defineProperty

需要注意的是get和set函数是存取描述符,value和writable函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。

实例1:实现 convert() 函数

要求如下:

  • 1、传入对象obj作为参数
  • 2、使用Object.defineProperty转换对象的所有属性
  • 3、转换后的对象保留原始行为,但在get或者set操作中输出日志

示例:

const obj = { foo: 123 }
convert(obj)

obj.foo //输出 getting key "foo": 123
obj.foo = 234 // 输出 setting key "foo" to 234
obj.foo // 输出 getting key "foo": 234

在了解Object.definePropertygettersetter的使用方法之后,通过修改getset函数就可以实现onAChangedonStateChanged

实现:

function convert (obj) { //迭代对象的所有属性
	//并使用Object.defineProperty()转换成getter/setters
	Object.keys(obj).forEach(key => {
		//保存原始值
		let internalValue = obj[key]
		Object.defineProperty(obj, key, {
			get () {
				console.log(`getting key "${key}": ${internalValue}`)    
            	return internalValue
			},
			set(newValue) {
				console.log(`setting key "${key}" to: ${newValue}`)
            	internalValue = newValue
			}
		})
	})
}

实例2:实现Dep类

要求如下:

  • 1、创建一个Dep类,包含两个方法:dependnotify
  • 2、创建一个autorun函数,传入一个update函数作为参数
  • 3、在update函数中调用dep.depend(),显式依赖于Dep实例
  • 4、调用dep.notify()触发update函数重新运行

示例:

const dep = new Dep()

autorun(() => {
  dep.depend()
  console.log('updated')
})// 注册订阅者,输出 updated
dep.notify()// 通知改变,输出 updated

首先需要定义autorun函数,接收update函数作为参数。因为调用autorun时要在Dep中注册订阅者,同时调用dep.notify()时要重新执行update函数,所以Dep中必须持有update引用,这里使用变量activeUpdate表示包裹update的函数。

实现代码如下:

let activeUpdate = null
function autorun(update) {
	const wrappedUpdate = () => {
    	activeUpdate = wrappedUpdate    // 引用赋值给activeUpdate
      	update()                        // 调用update,即调用内部的dep.depend
		activeUpdate = null             // 绑定成功之后清除引用
	}
	wrappedUpdate()                   // 调用
}

wrappedUpdate本质是一个闭包,update函数内部可以获取到activeUpdate变量,同理dep.depend()内部也可以获取到activeUpdate变量,所以Dep的实现就很简单了。

实现代码如下:

class Dep{ //初始化
	constructor () {
		this.subscribers = new Set()
	} //订阅update函数列表
	depend () {
		if(activeUpdate) {
			this.subscribers.add(activeUpdate)
		}
	} //所有update函数重新运行
	notify () {
		this.subscribers.forEach(sub => sub())	
	}
}

结合上面两部分就是完整实现。

实例3:实现响应式系统

要求如下:

  • 1、结合上述两个实例,convert()重命名为观察者observe()
  • 2、observe()转换对象的属性使之响应式,对于每个转换后的属性,它会被分配一个Dep实例,该实例跟踪订阅update函数列表,并在调用setter时触发它们重新运行
  • 3、autorun()接收update函数作为参数,并在update函数订阅的属性发生变化时重新运行。

示例:

const state = { count: 0 }
observe(state)
autorun(() => {
	console.log(state.count)
}) //输出 count is:0
state.count++ //输出 count is: 1

结合实例1和实例2之后就可以实现上述要求,observe中修改obj属性的同时分配Dep的实例,并在get中注册订阅者,在set中通知改变。autorun函数保存不变。

实现如下:


    class Dep {
      // 初始化
      constructor() {
        this.subscribers = new Set()
      }
      // 订阅update函数列表
      depend() {
        if (activeUpdate) {
          this.subscribers.add(activeUpdate)
        }
      }
      // 所有update函数重新运行
      notify() {
        this.subscribers.forEach(sub => sub())
      }
    }
    function observe(obj) {
      // 迭代对象的所有属性
      // 并使用Object.defineProperty()转换成getter/setters
      Object.keys(obj).forEach(key => {
        let internalValue = obj[key]
        // 每个属性分配一个Dep实例
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          // getter负责注册订阅者
          get() {
            dep.depend()
            return internalValue
          },
          // setter负责通知改变
          set(newVal) {
            const changed = internalValue !== newVal
            internalValue = newVal
            // 触发后重新计算
            if (changed) {
              dep.notify()
            }
          }
        })
      })
      return obj
    }
    let activeUpdate = null
    function autorun(update) {
      // 包裹update函数到"wrappedUpdate"函数中,
      // "wrappedUpdate"函数执行时注册和注销自身
      const wrappedUpdate = () => {
        activeUpdate = wrappedUpdate
        update()
        activeUpdate = null
      }
      wrappedUpdate()
    }

结合Vue文档里的流程图就更加清晰了。

在这里插入图片描述

本文内容参考自VUE作者尤大的付费视频

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值