【Vue2】2. 文本响应式更新(响应式原理)

994 阅读3分钟

概述

Vue是数据驱动视图模式,数据变更后,会自动更新视图。开发很便利,不要手动管理视图更新。哪 Vue 是如何实现自动更新,也就是响应式的?

以原生 JS 为例,数据变化了需要手动获取 Dom 节点,然后使用相关 Api 去修改视图

而 Vue 数据变化后需要自动更新视图,所以流程应该:

从图可以大致感受到,响应式更新有两个难点需要解决:

  1. 如何监听数据的变化?
  2. 如何触发引用有变化数据的视图镜像更新?

代码实现, 一共 373 行JS代码,包含Watcher/Observer的简单实现

DEMO展示

入口代码:

new Vue({
  el: '#app',
  data() {
    return {
      message: 'hello'
    }
  },
  render(h) {
    return h('div', null, this.message)
  },
})

渲染效果:

响应式更新:

监听数据变化

一般而言,对于基本类型的值是否变化,可以对比前后值是否相同。对于引用类型是否有变化,则可能需要递归判断属性是否都一致。

在 Vue 中,组件数据 data 是 Object 类型的,这样做的好处,我理解一是方便扩展属性、二是为了能方便对数据进行拦截、代理等。

针对 Object 类型的的数据,其相关key/value可以通过 数据属性/访问器属性 获取。两者可以是可以互换的:

数据属性访问简单直观,而访问器属性扩展性更好。在Vue种为了监听属性变化,使用访问器属性 覆写了 数据属性。

在Vue中定义的数据结构为

经过Vue解析处理后 data 函数的值会存储到 _data 私有属性中:
Vue 也是借助 Object.defineProperty 实现的:

function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get() {
      return target[sourceKey][key]
    },
    set(val) {
      target[sourceKey][key] = val
    }
  })
}

借助 getter/setter 即可感知到数据变化,而自动触发视图更新做准备

数据关联视图(依赖收集)

Vue视图渲染的过程为: 模板 + 数据 -> 虚拟Dom -> Diff ... Dom操作 -> 真实Dom(渲染到页面中),这个过程可以很自然的使用数据。但数据不仅包含视图展示数据、还会有一些视图不需要数据,如果每次检测到数据变化都重新执行一遍视图的渲染,这样可能造成极大的性能损耗。那如何才能识别哪些是视图所依赖的数据呢?

数据是在函数中引用的,故可以在访问数据时,记录当前执行的函数即可,整个思路如下:

简单实现步骤为:

  1. 设置一个全局变量记录当前执行的 render 函数
  2. render 函数会引用data,属性的 getter 访问器,在通过闭包保存当前的执行的 render 函数(此时记录在 全局变量中)
  3. 将全局变量置空
  4. 数据变化时,会触发属性的 setter

访问器,执行闭包的保存的render函数,触发视图重新渲染 在Vue中实现,为了更好的性能(异步渲染,可能多处修改属性值)、更好的扩展性(属性可能多处被引用,需要自动更新、以及支持watch、compute属性等),引入了 watcher/observer/dep 来实现依赖收集以及自动更新。

通过更多的分层,实现更好的扩展性。代码的思路:

  1. initData 属性时,对属性进行 observe 处理 <= observer概念
  2. mount 时,使用watcher管理更新
  3. 渲染时,设置一个全局变量,被引用属性的 getter 属性会通过该全部变量 收集对应的依赖
  4. 数据更新时,触发watcher 的 update 方法进行重新渲染操作

总结

通过 defineProperty 对数据属性进行拦截,再拦截的基础上 基于 JS 单线程原理进行依赖收集,数据变更时触发视图的重新渲染。