稍微学一下 Vue 的数据响应式(Vue2 及 Vue3)

什么是数据响应式

从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。
换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。

因此实现数据响应式有两个重点问题:

  1. 如何知道数据发生了变化?
  2. 如何知道数据变化后哪里需要修改?

对于第一个问题,如何知道数据发生了变化,Vue3 之前使用了 ES5 的一个 API Object.defineProperty Vue3 中使用了 ES6 的 Proxy,都是对需要侦测的数据进行 变化侦测 ,添加 getter 和 setter ,这样就可以知道数据何时被读取和修改。

第二个问题,如何知道数据变化后哪里需要修改,Vue 对于每个数据都收集了与之相关的 依赖 ,这里的依赖其实就是一个对象,保存有该数据的旧值及数据变化后需要执行的函数。每个响应式的数据变化时会遍历通知其对应的每个依赖,依赖收到通知后会判断一下新旧值有没有发生变化,如果变化则执行回调函数响应数据变化(比如修改 dom)。

下面详细分别介绍 Vue2 及 Vue3 的数据变化侦测及依赖收集。

Vue2

变化侦测

Object 的变化侦测

转化响应式数据需要将 Vue 实例上 data 属性中定义的数据通过递归将所有属性都转化为 getter/setter 的形式,Vue 中定义了一个 Observer 类来做这个事情。

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

class Observer {
  constructor(value) {
    this.value = value;
    def(value, '__ob__', this);
    if (!Array.isArray(value)) {
      this.walk(value);
    }
  }
  walk(obj) {
    for (const [key, value] of Object.entries(obj)) {
      defineReactive(obj, key, value);
    }
  }
}

直接将一个对象传入 new Observer() 后就对每项属性都调用 defineReactive 函数添加变化侦测,下面定义这个函数:

function defineReactive(data, key, val) {
  let childOb = observe(val);
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 读取 data[key] 时触发
      console.log('getter', val);
      return val;
    },
    set: function (newVal) {
      // 修改 data[key] 时触发
      console.log('setter', newVal);
      if (val === newVal) {
        return;
      }
      val = newVal;
    }
  })
}

function observe(value, asRootData) {
  if (typeof val !== 'object') {
    return;
  }
  let ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else {
    ob = new Observer(val);
  }
  return ob;
}

函数中判断如果是对象则递归调用 Observer 来实现所有属性的变化侦测,根据 __ob__ 属性判断是否已处理过,防止多次重复处理,Observer 处理过后会给数据添加这个属性,下面写一个对象试一下:

const people = {
  name: 'c',
  age: 12,
  parents: {
    dad: 'a',
    mom: 'b'
  },
  mates: ['d', 'e']
};
new Observer(people);
people.name; // getter c
people.age++; // getter 12  setter 13
people.parents.dad; // getter {}  getter a

打印 people 可以看到所有属性添加了 getter/setter 方法,读取 name 属性时打印了 people.age++ 修改 age 时打印了 getter 12 setter 13 说明 people 的属性已经被全部成功代理监听。

Array 的变化侦测

可以看到前面 Observer 中仅对 Object 类型个数据做了处理,为每个属性添加了 getter/setter,处理后如果属性值中有数组,通过 属性名 + 索引 的方式(如:this.people.mates[0])获取也是会触发 getter 的。但是如果通过数组原型方法修改数组的值,如 this.people.mates.push('f'),这样是无法通过 setter 侦测到的,因此,在 Observer 中需要对 Object 和 Array 分别进行单独的处理。

为侦测到数组原型方法的操作,Vue 中是通过创建一个拦截器 arrayMethods,并将拦截器重新挂载到数组的原型对象上。

下面是拦截器的定义:

const ArrayProto = Array.prototype;
const arrayMethods = Object.create(ArrayProto);
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(method => {
  const original = ArrayProto[method];
  Object.defineProperty(arrayMethods, method, {
    value: function mutator(...args) {
      console.log('mutator:', this, args);
      return original.apply(this, args);
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})

这里 arrayMethods 继承了 Array 的原型对象 Array.prototype,并给它添加了 push pop shift unshift splice sort reverse 这些方法,因为数组是可以通过这些方法进行修改的。添加的 push pop... 方法中重新调用 original(缓存的数组原型方法),这样就不会影响数组本身的操作。

最后给 Observer 中添加数组的修改:直接将拦截器挂载到数组原型对象上

class Observer {
  constructor(value) {
    this.value = value;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods;
    } else {
      this.walk(value);
    }
  }
  walk(obj) {
    for (const [key, value] of Object.entries(obj)) {
      defineReactive(obj, key, value);
    }
  }
}

再来验证一下:

const people = {
  name: 'c',
  age: 12,
  parents: {
    dad: 'a',
    mom: 'b'
  },
  mates: ['d', 'e']
};
new Observer(people);
people.mates[0]; // getter (2) ["d", "e"]
people.mates.push('f'); // mutator: (2) ["d", "e"] ["f"]

现在数组的修改也能被侦测到了。

依赖收集

目前已经可以对 ObjectArray 数据的变化进行截获,那么开始考虑一开始提到的 Vue 响应式数据的第二个问题:如何知道数据变化后哪里需要修改?

最开始已经说过,Vue 中每个数据都需要收集与之相关的依赖,用来表示该数据变化时需要进行的操作行为。

通过数据的变化侦测我们可以知道数据何时被读取或修改,因此可以在数据读取时收集依赖,修改时通知依赖更新,这样就可以实现数据响应式了。

依赖收集在哪

为每个数据都创建一个收集依赖的对象 dep,对外暴露 depend(收集依赖)、notify(通知依赖更新)的两个方法,内部维护了一个数组用来保存该数据的每项依赖。

对于 Object,可以在 getter 中收集,setter 中通知更新,对 defineReactive 函数修改如下:

function defineReactive(data, key, val) {
  let childOb = observe(val);
  // 处理每个响应式数据时都创建一个对象用来收集依赖
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依赖
      dep.depend();
      return val;
    },
    set: function (newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
      // 通知依赖更新
      dep.notify();
    }
  })
}

上面代码中依赖是收集在一个 Dep 实例对象上的,下面看一下 Dep 这个类。

class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  removeSub(sub) {
    if (this.subs.length) {
      const index = this.subs.indexOf(sub);
      this.subs.splice(index, 1);
    }
  }
  depend() {
    if (window.target) {
      this.addSub(window.target);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0; i < subs.length; i++) {
      subs[i].update();
    }
  }
}

Dep 的每个实例都有一个保存依赖的数组 subs,收集依赖时是从全局的一个变量上获取到并插入 subs,通知依赖时就遍历所有 subs 成员并调用其 update 方法。

Object 的依赖收集和触发都是在 defineProperty 中进行的,因此 Dep 实例定义在 defineReactive 函数中就可以让 getter 和 setter 都拿到。

而对于 Array 来说,依赖可以在 getter 中收集,但触发却是在拦截器中,为了保证 getter 和 拦截器中都能访问到 Dep 实例,Vue 中给 Observer 实例上添加了 dep 属性。

class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods;
    } else {
      this.walk(value);
    }
  }
  walk(obj) {
    for (const [key, value] of Object.entries(obj)) {
      defineReactive(obj, key, value);
    }
  }
}

Observer 在处理数据响应式时也将自身实例添加到了数据的 __ob__ 属性上,因此在 getter 和拦截器中都能通过响应式数据本身的 __ob__.dep 拿到其对应的依赖。修改 defineReactive 和 拦截器如下:

function defineReactive(data, key, val) {
  let childOb = observe(val);
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend();
      // 给 Observer 实例上的 dep 属性收集依赖
      if (childOb) {
        childOb.dep.depend();
      }
      return val;
    },
    ...
  })
}

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(method => {
  const original = ArrayProto[method];
  def(arrayMethods, method, (...args) => {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    ob.dep.notify();
    return result;
  })
})

依赖长什么样

现在已经知道了依赖保存在每个响应式数据对应的 Dep 实例中的 subs 中,通过上面 Dep 的代码可以知道,收集的依赖是一个全局对象,且该对象对外暴露了一个 update 方法,记录了数据变化时需要进行的更新操作(如修改 dom 或 Vue 的 Watch)。

首先这个依赖对象的功能主要有两点:

  1. 需要主动将自己收集到对应响应式数据的 Dep 实例中;
  2. 保存数据变化时要进行的操作并在 update 方法中调用;

其实就是一个中介角色,Vue 中起名为 Watcher。

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    // 保存通过表达式获取数据的方法
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get();
  }
  get() {
    // 将自身 Watcher 实例挂到全局对象上
    window.target = this;
    // 获取表达式对应的数据
    // 会自动触发该数据的 getter
    // getter 中收集依赖时从全局对象上拿到这个 Watcher 实例
    let value = this.getter.call(this.vm, this.vm);
    window.target = undefined;
    return value;
  }
  update() {
    const oldValue = this.value;
    this.value = this.get();
    // 将旧值与新值传递给回调函数
    this.cb.call(this.vm, this.value, oldValue);
  }
}

对于第一点,主动将自己收集到 Dep 实例中,Watcher 中设计的非常巧妙,在 get 中将自身 Watcher 实例挂到全局对象上,然后通过获取数据触发 getter 来实现依赖收集。

第二点实现很简单,只需要将构造函数参数中的回调函数保存并在 update 方法中调用即可。

构造函数中的 parsePath 方法就是从 Vue 实例的 data 上通过表达式获取数据,比如表达式为 "user.name" 则需要解析该字符串然后获取 data.user.name 数据。

总结

  • 数据先通过调用 new Observer() 为每项属性添加变化侦测,并创建一个 Dep 实例用来保存相关依赖。在读取属性值时保存依赖,修改属性值时通知依赖;
  • Dep 实例的 subs 属性为一个数组,保存依赖是向数组中添加,通知依赖时遍历数组一次调用依赖的 update 方法;
  • 依赖是一个 Watcher 实例,保存了数据变化时需要进行的操作,并将实例自身放到全局的一个位置,然后读取数据触发数据的 getter,getter 中从全局指定的位置获取到该 Watcher 实例并收集在 Dep 实例中。

以上就是 Vue2 中的响应式原理,在 Observer 处理完后,外界只需要通过创建 Watcher 传入需要监听的数据及数据变化时的响应回调函数即可。

Vue3

Vue3 中每个功能单独为一个模块,并可以单独打包使用,本文仅简单讨论 Vue3 中与数据响应式相关的 Reactive 模块,了解其内部原理,与 Vue2 相比又有何不同。

因为该模块可以单独使用,先来看一下这个模块的用法示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>vue3 demo</title>
</head>
<body>
  <div id="app">
    <div id="count"></div>
    <button id="btn">+1</button>
  </div>
  <script src="./vue3.js"></script>
  <script>
    const countEl = document.querySelector('#count')
    const btnEl = document.querySelector('#btn')
    // 定义响应式数据
    const state = reactive({
      count: 0,
      man: {
        name: 'pan'
      }
    })
    // 定义计算属性
    let double = computed(() => {
      return state.count * 2
    })
    // 回调函数立即执行一次,内部使用到的数据更新时会重新执行回调函数
    effect(() => {
      countEl.innerHTML = `count is ${state.count}, double is ${double.value}, man's name is ${state.man.name}`
    })
    // 修改响应式数据触发更新
    btnEl.addEventListener('click', () => {
      state.count++
    }, false)
  </script>
</body>
</html>

通过示例可以看到实现 Vue3 这个数据响应式需要有 reactive、computed、effect 这几个函数,下面仍然通过从变化侦测及依赖收集两个方面介绍,简单实现这几个函数。

变化侦测

示例中的 reactive 函数是对数据进行响应式化的,因此该函数的功能就类似于 Vue2 中的 defineReactive 函数的 getter/setter 处理,处理后能够对数据的获取及修改操作进行捕获。

const toProxy = new WeakMap()
const toRaw = new WeakMap()

const baseHandler = {
  get(target, key) {
    console.log('Get', target, key)
    const res = Reflect.get(target, key)
    // 递归寻找
    return typeof res == 'object' ? reactive(res) : res
  },
  set(target, key, val) {
    console.log('Set', target, key, val)
    const res = Reflect.set(target, key, val)
    return res
  }
}
function reactive(target) {
  console.log('reactive', target)
  // 查询缓存
  let observed = toProxy.get(target)
  if (observed) {
    return observed
  }
  if (toRaw.get(target)) {
    return target
  }
  observed = new Proxy(target, baseHandler)
  // 设置缓存
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

reactive 中使用 Proxy 对目标进行代理,代理的行为是 baseHander ,然后对目标对象及代理后的对象进行缓存,防止多次代理。

baseHandler 中就是对数据的获取及修改进行拦截,并通过 Reflect 执行 get/set 的原本操作,并在获取值为 Object 时递归进行响应式处理。很简单地就完成了数据的响应式处理。

依赖收集

依赖收集与 Vue2 类似,在 getter 中收集依赖,setter 中触发依赖,修改 baseHandler 如下:

const baseHandler = {
  get(target, key) {
    const res = Reflect.get(target, key)
    // 收集依赖
    track(target, key)
    return typeof res == 'object' ? reactive(res) : res
  },
  set(target, key, val) {
    const info = {
      oldValue: target[key],
      newValue: val
    }
    const res = Reflect.set(target, key, val)
    // 触发更新
    trigger(target, key, info)
    return res
  }
}

track 函数收集依赖,trigger 函数触发依赖更新。

首先需要两个全局变量,用于保存当前待收集的依赖对象的 effectStack 及一个记录所有数据及其对应依赖的表 targetMap 。

const effectStack = []
const targetMap = new WeakMap()

接下来定义这收集依赖及触发依赖更新这两个函数:

function track(target, key) {
  // 从栈中拿到待收集的依赖对象
  let effect = effectStack[effectStack.length - 1]
  if (effect) {
    // 通过 target 及 key 从依赖映射表中拿到对应的依赖列表(Set类型)
    // 首次需要对依赖映射表初始化
    let depsMap = targetMap.get(target)
    if (depsMap === undefined) {
      depsMap = new Map()
      targetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (dep === undefined) {
      dep = new Set()
      depsMap.set(key, dep)
    }
    // 若 target.key 对应的依赖列表中不存在该依赖则收集
    if (!dep.has(effect)) {
      dep.add(effect)
    }
  }
}
function trigger(target, key, info) {
  // 依赖映射表中取出 target 相关数据
  const depsMap = targetMap.get(target)
  if (depsMap === undefined) {
    return
  }
  // 普通依赖对象的列表
  const effects = new Set()
  // 计算属性依赖对象的列表
  const computedRunners = new Set()
  if (key) {
    // 取出 key 相关的依赖列表遍历分类存入 effects 及 computedRunners
    let deps = depsMap.get(key)
    deps.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
  // 遍历执行所有依赖对象
  const run = effect=> effect()
  effects.forEach(run)
  computedRunners.forEach(run)
}

track 及 trigger 的大致代码也很简单,track 是拿到待收集的依赖对象 effect 后收集到 effectStack,trigger 是从 effectStack 拿到对应的依赖列表遍历执行。

到现在就差这个依赖对象了,根据上面 trigger 函数可以知道,这个依赖 effect 首先是个函数可以执行,并且还有自身属性,如 computed 表示其为一个计算属性的依赖,有时会根据该标识进行写特殊处理。

下面开始介绍这个依赖对象是如何产生的:

// 创建依赖对象
function createReactiveEffect(fn, options) {
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  effect.computed = options.computed
  effect.lazy = options.lazy
  return effect
}

function run(effect, fn, args) {
  if (!effectStack.includes(effect)) {
    try {
      effectStack.push(effect)
      return fn(...args)
    } finally {
      effectStack.pop()
    }
  }
}

createReactiveEffect 是一个高阶函数,内部创建了一个名为 effect 的函数,函数内部返回的是一个 run 函数,run 函数中将依赖 effect 对象存入全局的待收集依赖栈 effectStack 中,并执行传入的回调函数,该回调函数其实就是一开始示例中 effect 函数传入的修改 Dom 的函数。也就是说依赖对象作为函数直接执行就会添加依赖到全局栈并执行回调函数。

回调函数中如果有读取了响应式数据的话则会触发 proxy 的 get 收集依赖,这时就能从 effectStack 上拿到该依赖对象了。

然后给 effect 增加了 computed lazy 属性后返回。

最后就是对外暴露的 effect 及 computed 函数了:

// 创建依赖对象并判断非计算属性则立即执行
function effect(fn, options = {}) {
  let e = createReactiveEffect(fn, options)
  if (!options.lazy) {
    e()
  }
  return e
}

// computed 内部调用 effect 并添加计算属性相关的 options
function computed(fn) {
  const runner = effect(fn, {
    computed: true,
    lazy: true
  })
  return {
    effect: runner,
    get value() {
      return runner()
    }
  }
}

computed 就不多说了,effect 就是将传入的回调函数传给 createReactiveEffect 创建依赖对象,然后执行依赖对象就会执行回调函数并收集该依赖对象。

总结

  • reactive 将传入的数据对象使用 proxy 包装,通过 proxy 的 get set 拦截数据的获取及修改,与 Vue2 的 defineProperty 一样,在 get 中收集依赖,在 set 中触发依赖;
  • effect 函数接受一个回调函数作为参数,将回调函数包装一下作为依赖对象后执行回调函数,回调函数执行时触发相关数据的 get 后进行依赖收集;

到此 Vue2 及 Vue3 中的数据响应式原理都分析完了。

Vue2 及 Vue3 数据响应式的对比

本次 Vue 对于数据响应式的升级主要在变化侦测部分。

Vue2 中的变化侦测实现对 Object 及 Array 分别进行了不同的处理,Objcet 使用了
Object.defineProperty API ,Array 使用了拦截器对 Array 原型上的能够改变数据的方法进行拦截。虽然也实现了数据的变化侦测,但存在很多局限 ,比如对象新增属性无法被侦测,以及通过数组下边修改数组内容,也因此在 Vue2 中经常会使用到 $set 这个方法对数据修改,以保证依赖更新。

Vue3 中使用了 es6 的 Proxy API 对数据代理,没有像 Vue2 中对原数据进行修改,只是加了代理包装,因此首先性能上会有所改善。其次解决了 Vue2 中变化侦测的局限性,可以不使用 $set 新增的对象属性及通过下标修改数组都能被侦测到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你...
    浪里行舟阅读 1,978评论 0 16
  • 这方面的文章很多,但是我感觉很多写的比较抽象,本文会通过举例更详细的解释。(此文面向的Vue新手们,如果你是个大牛...
    Ivy_2016阅读 15,376评论 8 64
  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    world_7735阅读 950评论 0 2
  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    NARUTO_86阅读 37,437评论 8 86
  • 摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归...
    Fundebug阅读 5,487评论 0 9