(关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导)前端
Vue进阶系列汇总以下,欢迎阅读,欢迎加高级前端进阶群一块儿学习(文末)。vue
Vue 进阶系列(二)之插件原理及实现github
Reactivity表示一个状态改变以后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。前端工程师
如今有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么作?闭包
let a = 3; let b = a * 10; console.log(b); // 30
乍一看好像知足要求,但此时b的值是固定的,无论怎么修改a,b并不会跟着一块儿改变。也就是说b并无和a保持数据上的同步。只有在a变化以后从新定义b的值,b才会变化。app
a = 4; console.log(a); // 4 console.log(b); // 30 b = a * 10; console.log(b); // 40
将a和b的关系定义在函数内,那么在改变a以后执行这个函数,b的值就会改变。伪代码以下。函数
onAChanged(() => { b = a * 10; })
因此如今的问题就变成了如何实现onAChanged
函数,当a改变以后自动执行onAChanged
,请看后续。post
如今把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
函数是数据描述符。描述符必须是这两种形式之一,但两者不能共存,否则会出现异常。
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 } }) }) }
Dep
类要求以下:
Dep
类,包含两个方法:depend
和notify
autorun
函数,传入一个update
函数做为参数update
函数中调用dep.depend()
,显式依赖于Dep
实例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()) } }
结合上面两部分就是完整实现。
要求以下:
convert()
重命名为观察者observe()
observe()
转换对象的属性使之响应式,对于每一个转换后的属性,它会被分配一个Dep
实例,该实例跟踪订阅update
函数列表,并在调用setter
时触发它们从新运行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做者尤大的付费视频
本人Github连接以下,欢迎各位Star
http://github.com/yygmind/blog
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!
若是你想加群讨论每期面试知识点,公众号回复[加群]便可