共计 5052 个字符,预计需要花费 13 分钟才能阅读完成。
作者简介:
道格
格物钛 Infra 团队 运维开发工程师
深入浅出 Vue 数据响应式原理
在应用 Vue 框架进行开发的过程中,经常会遇到更新数据然而视图无奈更新的 bug,从而对开发的进度造成阻塞。
为了进步开发效率,咱们能够通过恪守最佳实际来缩小相似的 bug 的频率。
除此之外,若开发者对数据响应式的过程有更好的了解,也能在性能实现的过程中对代码有更好的把控,进而缩小相似问题的产生。
由此,为了帮忙晋升团队搭档的开发效率,我对 vue 数据响应式的实现进行了摸索,并将其实现简化成了可被执行的样例代码,以帮忙大家更好的了解 视图更新 和依赖收集 的过程。
无论你是前端开发者还是后端开发者,亦或者是算法工程师,置信浏览这篇文章当前都会有所播种。
何为数据响应式
数据响应式的扭转了前端渲染视图的代码,绝对于传统 web 开发,使得代码更可读性更强,我会分为 3 个步骤介绍如何实现繁难的响应式数据。
- 无响应式
- Observer 模式
- 数据响应式的解决方案
首先,简略介绍一下两个贯通全文的类:
– State & View 类
// State 负责管理页面状态
class State {
text = "hello"
constructor(initText){this.text = initText}
}
// TextView 负责承受一个参数,并且裸露一个 render 办法用于渲染视图
class TextView {state = {}
// 在初始化 TextView 时就会主动调用 render 函数,以渲染到视图
constructor(s){
this.state = s
this.render()}
render(){
// 用 console.log 输入到终端来模仿渲染函数
console.log(this.state.text)
}
}
基于这两个 class,咱们能够通过如下的代码模仿前端渲染:
let state = new State("hello world");
// 渲染出 hello world
let node = new TextView(state);
无响应式
当节点状态扭转,并且须要从新渲染时分为两个步骤:
state.text = "foo bar";
node.render();
假如有 3 个节点都订阅了这个 state,则代码如下:
state.text = "foo bar";
node.render();
node0.render();
node1.render();
显然,可扩展性很差,并且难以保护。
Observer 模式
对上述的代码稍加改变,即可实现更好的可扩展性。
class State {
/** 省略与上局部反复代码 **/
watchList = []
watchText(view){this.watchList.push(view)
}
updateText(text){
this.text = text;
this.watchList.forEach(v => {v.render()
})
}
}
class TextView {
/** 其余代码放弃不变 **/
constructor(s){s.watchText(this)
this.state = s
this.render()}
}
如此,在初始化 TextView 实例的时候,其会将本人注册到 State 的监听列表内。当有人通过 updateText 函数更新的数据的时候,就会调用所有监听 text 类的 View 实例,进行从新渲染:
let state = new State("hello world");
let node = new View(state);
let node0 = new View(state);
let node1 = new View(state);
// 终端输入 3 次 foo bar
state.updateText("foo bar")
仿佛,可扩展性的问题解决了?
NO,NO,NO!
假如咱们当初有了一个 HeaderView 视图类,代码如下:
class HeaderView {state = {}
constructor(s){s.watchText(this)
this.state = s
this.render()}
render(){
// 仅此处不同
console.log(this.state.header)
}
}
那咱们的 State 代码大略会变成这样:
class StateWithHeader{
text = "hello"
header = "header"
watchTextList = []
watchHeaderList = []
constructor(header,text){}
updateText(text){/** 省略实现 **/}
watchText(view){/** 省略实现 **/}
watchHeader(view){/** 省略实现 **/}
updateHeader(header){/** 省略实现 **/}
}
如果字段变得越来越多,则办法也会越来越多,反复代码越来越多。
你可能会想,这不是个问题,依然能解决:
class StateWithManyFields {
// 存入许多状态
state = {}
// 一个 map,key 为 string 类型代表监听的字段,val 为一个 Array<View> 类型
fieldWatchers = {}
constructor(initState){this.state = initState;}
// 只 watch 对应的字段,并且传入 View 实例
watch(field,view){if (this.fieldWatchers[field] == undefined){this.fieldWatchers[field] = [];}
this.fieldWatchers[field].push(view)
}
// 更新字段,并且调用所有 View 实例的 render 函数
update(field,val){this.state[field] = val;
if (this.fieldWatchers[field] != undefined){this.fieldWatchers[field].forEach(v => v.render());
}
}
}
Bravo !
你回退到了传统 web 开发模式,并且失去了类型提醒。
假如你有 20 个状态,因为这些状态变成动静的了,ide 无奈给你提醒,你可能得靠记忆去记住这些字符串的值。假如你有 20 个这样多状态的组件,并且 ide 无奈给你类型提醒。Good luck my friend!
所以数据响应式是如何解决的
在答复这个问题之前,须要先引入 javascript 的 Proxy 类,咱们利用它对咱们的 State 实例进行代理:
let state = new State("hello world");
let proxyedState = new Proxy(state,{
// 当有人调用 someVar = proxyedState.<field> 时,会打印出对应的 <field>
get:(obj,field) => {console.log(field)
return obj[field]
},
set:(obj,field,val) => {
// 当有人调用 proxyedState.<field> = <newVal> 时,会打印出对应的 <field> 和 传入的 <newVal>
console.log(field,val)
obj[field] = val;
return true
}
})
上面是调用形式:
// 触发了 Proxy 类的 get 代理
// 终端输入 "text"
let a = proxyedState.text;
// 触发了 Proxy 类的 set 代理
// 终端输入 "text foo bar"
proxyedState.text = "foo bar"
如果了解了上述 Proxy 的代码,那么依据 Proxy,咱们能够优化咱们的 state,省去 updateField 的代码,同时不失去类型提醒。
class State {reactive = {}
// 一个 map,key 为 string 类型代表监听的字段,val 为一个 Array<View> 类型
watchers = {}
constructor(initState){
this.reactive = initState;
this.reactive = new Proxy(
this.reactive,
{
// 假如当有人调用 reactive.text 时,则会更新所有监听 text 状态的视图
set: (obj,field,val) => {obj[field]=val;
if (this.watchers[field] != undefined){this.watchers[field].forEach(v => v.render() )
}
return true;
},
}
)
}
}
好了,咱们离胜利只差一步之遥了:
View 类如何不通过 Watch 办法就能将本人增加到 State 的 Watcher 中?
往下浏览之前,大家能够先想想解决方案。
接下来就简略介绍 Vue 中的依赖收集是怎么做的,首先在 ReactiveState 的 class 申明前,退出一个全局变量的申明:
let currentCreatingView = undefined;
class ReactiveState {/** 省略 **/}
接下来,批改 View 类的 constructor 代码;
class TextView {state = {}
constructor(s){
// 将本人注册到全局变量中
currentCreatingView = this;
this.state = s
// 触发 getter
this.render()
// 初始化实现
currentCreatingView = undefined;
}
render(){console.log(this.state.text)
}
}
最初欠缺 State 的代码:
class State {
/** 省略其余字段 **/
constructor(initState){
this.reactive = initState;
this.reactive = new Proxy(
this.reactive,
{get: (obj,field) => {
// 当有视图正在初始化时,将这个正在初始化的视图增加到 watcher 中
if (currentCreatingView != undefined){if (this.watchers[field]==undefined){this.watchers[field] = []}
this.watchers[field].push(currentCreatingView)
}
return obj[field]
}
/** 省略 get **/
}
)
}
}
在持续解析前,先试试看成绩。
let s = new State({header:"hello",text:"world"}).reactive;
console.log("---- init views ----")
let textView = new TextView(s)
console.log("---- init complete ----")
console.log("modifying header")
// 咱们没有调用 State.updateField 办法,而是间接更改数据就让视图从新渲染
// 同时,咱们也没有调用 State.watch 办法,将 View 类注册到监听列表中
s.text = "foo";
输入:
---- init views ----
world /* print by TextView.render */
---- init complete ----
modifying header
foo /* print by TextView.render */
当初,先咱们终于让 State 变得 Responsive:
- 实现了 Observer 模式,在更改数据的时候主动调用 render 函数。
- 防止了传统开发可读性差的问题,防止了诸如 update 和 watch 这样的函数,而是间接通过 class 的 getter 和 setter 对其状态进行监听和批改。
如果了解了后面 Proxy 的演示代码,那么对通过 setter 进行状态更新的告诉很好了解,艰难的是如何通过 getter 将 View 类注册到对应的 State 中,上面通过流程图解析:
总结
通过下面的样例代码,咱们理清了 Vue 数据响应式的过程,分为两个步骤:
- 视图更新:通过 Observe 模式从新渲染。
- 依赖收集:通过肯定的机制收集有谁订阅了数据中的特定字段。
理论 Vue 的实现必定会更为简单,但心愿本文所介绍的内容能让你对其原理和过程有一个更好的了解。
心愿你能有所播种~
Reference:
https://www.infoq.cn/article/…