乐趣区

关于javascript:技术博客|深入浅出-Vue-数据响应式原理

作者简介:
道格
格物钛 Infra 团队 运维开发工程师

深入浅出 Vue 数据响应式原理

在应用 Vue 框架进行开发的过程中,经常会遇到更新数据然而视图无奈更新的 bug,从而对开发的进度造成阻塞。

为了进步开发效率,咱们能够通过恪守最佳实际来缩小相似的 bug 的频率。

除此之外,若开发者对数据响应式的过程有更好的了解,也能在性能实现的过程中对代码有更好的把控,进而缩小相似问题的产生。

由此,为了帮忙晋升团队搭档的开发效率,我对 vue 数据响应式的实现进行了摸索,并将其实现简化成了可被执行的样例代码,以帮忙大家更好的了解 视图更新 依赖收集 的过程。

无论你是前端开发者还是后端开发者,亦或者是算法工程师,置信浏览这篇文章当前都会有所播种。

何为数据响应式

数据响应式的扭转了前端渲染视图的代码,绝对于传统 web 开发,使得代码更可读性更强,我会分为 3 个步骤介绍如何实现繁难的响应式数据。

  1. 无响应式
  2. Observer 模式
  3. 数据响应式的解决方案

首先,简略介绍一下两个贯通全文的类:

– 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:

  1. 实现了 Observer 模式,在更改数据的时候主动调用 render 函数。
  2. 防止了传统开发可读性差的问题,防止了诸如 update 和 watch 这样的函数,而是间接通过 class 的 getter 和 setter 对其状态进行监听和批改。

如果了解了后面 Proxy 的演示代码,那么对通过 setter 进行状态更新的告诉很好了解,艰难的是如何通过 getter 将 View 类注册到对应的 State 中,上面通过流程图解析:

总结

通过下面的样例代码,咱们理清了 Vue 数据响应式的过程,分为两个步骤:

  1. 视图更新:通过 Observe 模式从新渲染。
  2. 依赖收集:通过肯定的机制收集有谁订阅了数据中的特定字段。

理论 Vue 的实现必定会更为简单,但心愿本文所介绍的内容能让你对其原理和过程有一个更好的了解。
心愿你能有所播种~

Reference:

https://www.infoq.cn/article/…

退出移动版