什么是响应式ReactivityReactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变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才会变化。 a = 4;console.log(a); // 4console.log(b); // 30b = 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>document .querySelector('.cell.b') .textContent = state.a * 10
现在建立数据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 中的getter 和setter 方法。具体使用方法参考如下链接。 MDN之Object.defineProperty
需要注意的是get 和set 函数是存取描述符,value 和writable 函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。 实例1:实现convert() 函数要求如下: 示例: const obj = { foo: 123 } convert(obj)
obj.foo // 输出 getting key 'foo': 123obj.foo = 234 // 输出 setting key 'foo' to 234obj.foo // 输出 getting key 'foo': 234
在了解Object.defineProperty 中getter 和setter 的使用方法之后,通过修改get 和set 函数就可以实现onAChanged 和onStateChanged 。 实现: 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 类,包含两个方法:depend 和notify 2、创建一个autorun 函数,传入一个update 函数作为参数 3、在update 函数中调用dep.depend() ,显式依赖于Dep 实例 4、调用dep.notify() 触发update 函数重新运行
示例: const dep = new Dep()
autorun(() => { dep.depend() console.log('updated') })// 注册订阅者,输出 updateddep.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: 0state.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 = nullfunction autorun (update) { // 包裹update函数到'wrappedUpdate'函数中, // 'wrappedUpdate'函数执行时注册和注销自身 const wrappedUpdate = () => { activeUpdate = wrappedUpdate update() activeUpdate = null } wrappedUpdate() }
结合Vue文档里的流程图就更加清晰了。 Job Done!!! 本文内容参考自VUE作者尤大的付费视频
●编号1040,输入编号直达本文
●输入m获取到文章目录
|