Vue 进阶系列之响应式原理及实现
栏目: JavaScript · 发布时间: 6年前
内容简介:Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。
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才会变化。
a = 4; console.log(a); // 4 console.log(b); // 30 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> 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
方法。具体使用方法参考如下链接。
需要注意的是 get
和 set
函数是存取描述符, value
和 writable
函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。
实例1:实现 convert()
函数
要求如下:
obj Object.defineProperty
示例:
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.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') }) // 注册订阅者,输出 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文档里的流程图就更加清晰了。
Job Done!!!
本文内容参考自VUE作者尤大的付费视频
以上所述就是小编给大家介绍的《Vue 进阶系列之响应式原理及实现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mastering Regular Expressions, Second Edition
Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95
Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!