vue.js 的响应式原理依赖于 Object.defineProperty, 尤大大在 vue.JS 文档中就已经提到过, 这也是 Vue.JS 不支持 IE8 以及更低版本浏览器的原因. Vue 通过设定对象属性的 setter/getter 方法来监听数据的变化, 通过 getter 进行依赖收集, 而每个 setter 方法就是一个观察者, 在数据变更的时候通知订阅者更新视图.
Let data to observable
首先假定一种最简单的情况, 不去考虑其他情况. 在 initData 中会调用 observe 这个函数将 Vue 的数据设置成 observable 的. 当_data 数据发生改变的时候就会触发 set, 对订阅者进行回调 (在这里是 render).
- function observe(value, cb) {
- Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
- }
- function defineReactive (obj, key, val, cb) {
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: ()=>{
- /*.... 依赖收集等....*/
- /*GitHub:https://github.com/answershuto*/
- return val
- },
- set:newVal=> {
- val = newVal;
- cb();/* 订阅者收到消息的回调 */
- }
- })
- }
为了操作方便, 我们需要将 _data 上的数据代理到 vm 实例上.
- function proxy (data) {
- const that = this;
- Object.keys(data).forEach(key => {
- Object.defineProperty(that, key, {
- configurable: true,
- enumerable: true,
- get: function proxyGetter () {
- return that._data[key];
- },
- set: function proxySetter (val) {
- that._data[key] = val;
- }
- })
- });
- }
依赖收集
依赖收集的原因
按照上面的方法进行绑定会出现一个问题 -- 实际模板中未使用的数据被更改后也会进行重新渲染, 而这样无疑会消耗性能, 因此需要依赖收集来保证只渲染实际模板中使用到的数据.
Dep
当对 data 上的对象进行修改值的时候会触发它的 setter, 那么取值的时候自然就会触发 getter 事件, 所以我们只要在最开始进行一次 render, 那么所有被渲染所依赖的 data 中的数据就会被 getter 收集到 Dep 的 subs 中去. 在对 data 中的数据进行修改的时候 setter 只会触发 Dep 的 subs 的函数.
Dep.prototype.depend 方法是将观察者 Watcher 实例赋值给全局的 Dep.target, 然后触发 render 操作只有被 Dep.target 标记过的才会进行依赖收集. 有 Dep.target 的对象会将 Watcher 的实例 push 到 subs 中, 在对象被修改触发 setter 操作的时候 dep 会调用 subs 中的 Watcher 实例的 update 方法来重新获取数据生成虚拟节点, 再由服务端将虚拟节点渲染成真实 DOM.
- src/oberver/dep.JS
- var uid = 0;
- //dep 构造函数
- export default function Dep(argument) {
- this.id = uid++
- this.subs = []
- }
- // 添加一个观察者对象
- Dep.prototype.addSub = function(sub) {
- this.subs.push(sub)
- }
- // 移除一个观察者对象
- Dep.prototype.removeSub = function(sub) {
- remove(this.subs, sub)
- }
- // 依赖收集
- Dep.prototype.depend = function() {
- if(Dep.target) {
- Dep.target.addDep(this)
} 全栈学习交流; 582735936
- }
- // 通知所有订阅者
- Dep.prototype.notify = function() {
- var subs = this.subs.slice()
- for(var i = 0, l = subs.length; i <l; i++){
- subs[i].update()
- }
- }
- Dep.target = null
- function remove (arr, item) {
- if (arr.length) {
- const index = arr.indexOf(item)
- if (index> -1) {
- return arr.splice(index, 1)
- }
- }
实现
上面讲述响应式原理和依赖收集的原因, 接下来就来简单实现一下. 开始正式编程前, 照常先写测试用例.
- test/observer/observer.spec.JS
- import {
- Observer,
- observe
- } from "../../src/observer/index"
- import Dep from '../../src/observer/dep'
- describe('Observer test', function() {
- it('observing object prop change', function() {
- const obj = { a:1, b:{a:1}, c:NaN}
- observe(obj)
- // mock a watcher!
- const watcher = {
deps: [], 全栈学习交流; 582735936
- addDep (dep) {
- this.deps.push(dep)
- dep.addSub(this)
- },
- update: jasmine.createSpy()
- }
- // observing primitive value
- Dep.target = watcher
- obj.a
- Dep.target = null
- expect(watcher.deps.length).toBe(1) // obj.a
- });
- });
接下来正式实现数据绑定, 其中 observe 的作用是返回一个 observer 实例, 而 observer 则负责实现数据绑定.
- src/observer/index.JS
- import {
- def, //new
- hasOwn,
- isObject
- }
- from '../util/index'
- export function Observer(value) {
- this.value = value
- this.dep = new Dep()
- this.walk(value)
- def(value, '__ob__', this)
- }
- export function observe (value){
- if (!isObject(value)) {
- return
- }
- var ob
- if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
- ob = value.__ob__
- } else {
- ob = new Observer(value)
} 全栈学习交流; 582735936
- return ob
- }
- Observer.prototype.walk = function(obj) {
- var keys = Object.keys(obj)
- for (var i = 0; i < keys.length; i++) {
- defineReactive(obj, keys[i], obj[keys[i]])
- }
- }
- export function defineReactive (obj, key, val) {
- var dep = new Dep()
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter () {
- var value = val
- if (Dep.target) {
- dep.depend()
- }
- return value
- },
- set: function reactiveSetter (newVal) {
- var value = val
- if (newVal === value || (newVal !== newVal && value !== value)) {
- return
- }
- val = newVal
- dep.notify()
- }
- })
- }
在上面的代码中我们用到了一些工具函数, 下面我们就把这些工具函数在单独的文件中实现, 方便之后其他组件的调用.
- src/util/index.JS
- const hasOwnProperty = Object.prototype.hasOwnProperty
- // 必须对传入的参数进行判断, 不然 obj 为 null 时会报错
- export function hasOwn(obj, key) {
- if (!isObject(obj) && !Array.isArray(obj)) {
- return
- }
- return hasOwnProperty.call(obj, key)
- }
- export function isObject(obj) {
- return obj !== null && typeof obj === 'object'
- }
- // 给要观察的对象的_ob_属性存放 Observer 对象, 标记已观察
- export function def(obj, key, val, enumerable) {
- Object.defineProperty(obj, key, {
- value: val,
- enumerable: !!enumerable,
- writable: true,
- configurable: true
- })
- }
全栈学习交流
上面已经简单实现了数据绑定, 接下来不妨使用 NPM run test 命令来测试下项目吧.
来源: http://www.qdfuns.com/article/51116/88dca3e97667dae65bc49e7bc5fda97f.html