一、知识准备
Object.defineProperty( )方法可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。
Object.defineProperty(obj,prop,descriptor),重点是第三个参数,对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。
数据描述符是一个拥有可写或不可写值的属性,存取描述符是由一对getter-setter函数功能来描述的属性。描述符必须二选一,不能同时是两者。

数据描述符和存取描述符均具有以下可选键值:

configurable:当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false。
enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:

value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable:当且仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false。

存取描述符同时具有以下可选键值:

get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为undefined。
set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。

二、监听对象变动
Vue监听数据变化的机制是把一个普通JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。

// 观察者构造函数
function Observer (value) {
    this.value = value
    this.walk(value)
}

// 递归调用,为对象绑定getter/setter
Observer.prototype.walk = function (obj) {
    var keys = Object.keys(obj)
    for (var i = 0, l = keys.length; i < l; i++) {
        this.convert(keys[i], obj[keys[i]])
    }
}

// 将属性转换为getter/setter
Observer.prototype.convert = function (key, val) {
    defineReactive(this.value, key, val)
}

// 创建数据观察者实例
function observe (value) {
    // 当值不存在或者不是对象类型时,不需要继续深入监听
    if (!value || typeof value !== 'object') {
        return
    }
    return new Observer(value)
}

// 定义对象属性的getter/setter
function defineReactive (obj, key, val) {
    var property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }

    // 保存对象属性预先定义的getter/setter
    var getter = property && property.get
    var setter = property && property.set

    var childOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            var value = getter ? getter.call(obj) : val
            console.log("访问:"+key)
            return value
        },
        set: function reactiveSetter (newVal) {
            var value = getter ? getter.call(obj) : val
            if (newVal === value) {
                return
            }
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            // 对新值进行监听
            childOb = observe(newVal)
            console.log('更新:' + key + ' = ' + newVal)
        }
    })
}

【测试】定义一个对象作为数据模型,并监听这个对象。

let data = {
    user: {
        name: 'camille',
        age: '94'
    },
    address: {
        city: 'shagnhai'
    }
}
observe(data)

console.log(data.user.name)
// 访问:user
// 访问:name

data.user.name = 'Camille Hou'
// 访问:user
// 更新:name = Camille Hou

三、监听数组变动
数组对象无法通过Object.defineProperty实现监听,Vue包含观察数组的变异方法,来触发视图更新。

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

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

// 数组的变异方法
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]
.forEach(function (method) {
    // 缓存数组原始方法
    var original = arrayProto[method]
    def(arrayMethods, method, function mutator () {
        var i = arguments.length
        var args = new Array(i)
        while (i--) {
            args[i] = arguments[i]
        }
        console.log('数组变动')
        return original.apply(this, args)
    })
})

【测试】定义一个数组作为数据模型,并对这个数组调用变异的七个方法实现监听。

let skills = ['JavaScript', 'Node.js', 'html5']
// 原型指针指向具有变异方法的数组对象
skills.__proto__ = arrayMethods

skills.push('java')
// 数组变动
skills.pop()
// 数组变动

四、数组监听优化
我们可以在上面Observer观察者构造函数中添加对数组的监听。

const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

// 观察者构造函数
function Observer (value) {
    this.value = value
    if (Array.isArray(value)) {
        var augment = hasProto
            ? protoAugment
            : copyAugment
        augment(value, arrayMethods, arrayKeys)
        this.observeArray(value)
    } else {
        this.walk(value)
    }
}

// 观察数组的每一项
Observer.prototype.observeArray = function (items) {
    for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i])
    }
}

// 将目标对象/数组的原型指针__proto__指向src
function protoAugment (target, src) {
    target.__proto__ = src
}

// 将具有变异方法挂在需要追踪的对象上
function copyAugment (target, src, keys) {
    for (var i = 0, l = keys.length; i < l; i++) {
        var key = keys[i]
        def(target, key, src[key])
    }
}

五、发布订阅模式
Vue的Watcher订阅者作为Observer和Compile之间通信的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。

/**
 * 观察者对象
 */
function Watcher(vm, expOrFn, cb) {
    this.vm = vm
    this.cb = cb
    this.depIds = {}
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn
    } else {
        this.getter = this.parseExpression(expOrFn)
    }
    this.value = this.get()
}

/**
 * 收集依赖
 */
Watcher.prototype.get = function () {
    // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
    Dep.target = this
    // 触发getter,将自身添加到dep中
    const value = this.getter.call(this.vm, this.vm)
    // 依赖收集完成,置空,用于下一个Watcher使用
    Dep.target = null
    return value
}

Watcher.prototype.addDep = function (dep) {
    if (!this.depIds.hasOwnProperty(dep.id)) {
        dep.addSub(this)
        this.depIds[dep.id] = dep
    }
}

/**
 * 依赖变动更新
 *
 * @param {Boolean} shallow
 */
Watcher.prototype.update = function () {
    this.run()
}

Watcher.prototype.run = function () {
    var value = this.get()
    if (value !== this.value) {
        var oldValue = this.value
        this.value = value
        // 将newVal, oldVal挂载到MVVM实例上
        this.cb.call(this.vm, value, oldValue)
    }
}

Watcher.prototype.parseExpression = function (exp) {
    if (/[^\w.$]/.test(exp)) {
        return
    }
    var exps = exp.split('.')

    return function(obj) {
        for (var i = 0, len = exps.length; i < len; i++) {
            if (!obj) return
            obj = obj[exps[i]]
        }
        return obj
    }
}

Dep是一个数据结构,其本质是维护了一个watcher队列,负责添加watcher,更新watcher,移除 watcher,通知watcher更新。

let uid = 0

function Dep() {
  this.id = uid++
  this.subs = []
}

Dep.target = null

/**
 * 添加一个订阅者
 *
 * @param {Directive} sub
 */
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub)
}

/**
 * 移除一个订阅者
 *
 * @param {Directive} sub
 */
Dep.prototype.removeSub = function (sub) {
  let index = this.subs.indexOf(sub);
  if (index !== -1) {
    this.subs.splice(index, 1);
  }
}

/**
 * 将自身作为依赖添加到目标watcher
 */
Dep.prototype.depend = function () {
  Dep.target.addDep(this)
}

/**
 * 通知数据变更
 */
Dep.prototype.notify = function () {
  var subs = toArray(this.subs)
  // stablize the subscriber list first
  for (var i = 0, l = subs.length; i < l; i++) {
    // 执行订阅者的update更新函数
    subs[i].update()
  }
}

六、模板编译
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

function Compile(el, value) {
  this.$vm = value
  this.$el = this.isElementNode(el) ? el : document.querySelector(el)
  if (this.$el) {
    this.compileElement(this.$el)
  }
}

Compile.prototype.compileElement = function (el) {
  let self = this
  let childNodes = el.childNodes

  ;[].slice.call(childNodes).forEach(node => {
    let text = node.textContent
    let reg = /\{\{((?:.|\n)+?)\}\}/
    // 处理element节点
    if (self.isElementNode(node)) {
      self.compile(node)
    } else if (self.isTextNode(node) && reg.test(text)) { // 处理text节点
      self.compileText(node, RegExp.$1.trim())
    }
    // 解析子节点包含的指令
    if (node.childNodes && node.childNodes.length) {
      self.compileElement(node)
    }
  })
}

Compile.prototype.compile = function (node) {
  let nodeAttrs = node.attributes
  let self = this

  ;[].slice.call(nodeAttrs).forEach(attr => {
    var attrName = attr.name
    if (self.isDirective(attrName)) {
      let exp = attr.value
      let dir = attrName.substring(2)
      if (self.isEventDirective(dir)) {
        compileUtil.eventHandler(node, self.$vm, exp, dir)
      } else {
        compileUtil[dir] && compileUtil[dir](node, self.$vm, exp)
      }
      node.removeAttribute(attrName)
    }
  });
}

Compile.prototype.compileText = function (node, exp) {
  compileUtil.text(node, this.$vm, exp);
}

Compile.prototype.isDirective = function (attr) {
  return attr.indexOf('v-') === 0
}

Compile.prototype.isEventDirective = function (dir) {
  return dir.indexOf('on') === 0;
}

Compile.prototype.isElementNode = function (node) {
  return node.nodeType === 1
}

Compile.prototype.isTextNode = function (node) {
  return node.nodeType === 3
}

// 指令处理集合
var compileUtil = {
  text: function (node, vm, exp) {
    this.bind(node, vm, exp, 'text')
  },
  html: function (node, vm, exp) {
    this.bind(node, vm, exp, 'html')
  },
  model: function (node, vm, exp) {
    this.bind(node, vm, exp, 'model')

    let self = this, val = this._getVMVal(vm, exp)
    node.addEventListener('input', function (e) {
      var newValue = e.target.value
      if (val === newValue) {
        return
      }
      self._setVMVal(vm, exp, newValue)
      val = newValue
    });
  },
  bind: function (node, vm, exp, dir) {
    var updaterFn = updater[dir + 'Updater']
    updaterFn && updaterFn(node, this._getVMVal(vm, exp))
    new Watcher(vm, exp, function (value, oldValue) {
      updaterFn && updaterFn(node, value, oldValue)
    })
  },
  eventHandler: function (node, vm, exp, dir) {
    var eventType = dir.split(':')[1],
        fn = vm.$options.methods && vm.$options.methods[exp];

    if (eventType && fn) {
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },
  _getVMVal: function (vm, exp) {
    var val = vm
    exp = exp.split('.')
    exp.forEach(function (k) {
      val = val[k]
    })
    return val
  },
  _setVMVal: function (vm, exp, value) {
    var val = vm;
    exp = exp.split('.')
    exp.forEach(function (k, i) {
      // 非最后一个key,更新val的值
      if (i < exp.length - 1) {
        val = val[k]
      } else {
        val[k] = value
      }
    })
  }
}

var updater = {
  textUpdater: function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value
  },
  htmlUpdater: function (node, value) {
    node.innerHTML = typeof value == 'undefined' ? '' : value
  },
  modelUpdater: function (node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value
  }
}

七、MVVM实例

/**
 * @class 双向绑定类 MVVM
 * @param {[type]} options [description]
 */
function MVVM(options) {
  this.$options = options || {}
  // 简化了对data的处理
  let data = this._data = this.$options.data
  // 监听数据
  observe(data)
  new Compile(options.el || document.body, this)
}

MVVM.prototype.$watch = function (expOrFn, cb) {
  new Watcher(this, expOrFn, cb)
}

为了能够直接通过实例化对象操作数据模型,我们需要为 MVVM 实例添加一个数据模型代理的方法。

MVVM.prototype._proxy = function (key) {
  Object.defineProperty(this, key, {
    configurable: true,
    enumerable: true,
    get: () => this._data[key],
    set: (val) => {
      this._data[key] = val
    }
  })
}

八、举个例子

<div id="J_box">
  <h3>{{user.name}}</h3>
  <input type="text" v-model="modelValue">
  <p>{{modelValue}}</p>
</div>
<script>
  let vm = new MVVM({
    el: '#J_box',
    data: {
      modelValue: '',
      user: {
        name: 'camille',
        age: '94'
      },
      address: {
        city: 'shanghai'
      },
      skills: ['JavaScript', 'Node.js', 'html5']
    }
  })

  vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`))
</script>

vue实现数据双向绑定的原理的更多相关文章

  1. 【Vue】-- 数据双向绑定的原理 --Object.defineProperty()

    Object.defineProperty()方法被许多现代前端框架(如Vue.js,React.js)用于数据双向绑定的实现,当我们在框架Model层设置data时,框架将会通过Object.def ...

  2. vue数据双向绑定的原理、虚拟dom的原理

    vue数据双向绑定的原理https://www.cnblogs.com/libin-1/p/6893712.html 虚拟dom的原理https://blog.csdn.net/u010692018/ ...

  3. Vue的数据双向绑定和Object.defineProperty()

    Vue是前端三大框架之一,也被很多人指责抄袭,说他的两个核心功能,一个数据双向绑定,一个组件化分别抄袭angular的数据双向绑定和react的组件化思想,咱们今天就不谈这种大是大非,当然我也没到达那 ...

  4. 原生js实现 vue的数据双向绑定

    原生js实现一个简单的vue的数据双向绑定 vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时 ...

  5. 对象的属性类型 和 VUE的数据双向绑定原理

    如[[Configurable]] 被两对儿中括号 括起来的表示 不可直接访问他们 修改属性类型:使用Object.defineProperty()  //IE9+  和标准浏览器  支持 查看属性的 ...

  6. vue中数据双向绑定的实现原理

    vue中最常见的属v-model这个数据双向绑定了,很好奇它是如何实现的呢?尝试着用原生的JS去实现一下. 首先大致学习了解下Object.defineProperty()这个东东吧! * Objec ...

  7. Vue的数据双向绑定原理——Object-defineProperty

    一.定义 ①方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象. ②vue.js的双向数据绑定就是通过Object.defineProperty方法实现的,俗称属性拦截 ...

  8. 一、vue的数据双向绑定的实现

    响应式系统 一.概述 Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图 ...

  9. vue中数据双向绑定注意点

    最近一个vue和element的项目中遇到了一个问题: 动态生成的对象进行双向绑定是失败 直接贴代码: <el-form :model="addClass" :rules=& ...

随机推荐

  1. Meterpreter命令详解

      0x01初识Meterpreter 1.1.什么是Meterpreter Meterpreter是Metasploit框架中的一个扩展模块,作为溢出成功以后的攻击载荷使用,攻击载荷在溢出攻击成功以 ...

  2. Vue(小案例_vue+axios仿手机app)_首页(底部导航栏+轮播图+九宫格)

    ---恢复内容开始--- 一.前言                        1.底部导航(两种做法)                                         2.轮播图 ...

  3. 前端工程师必须要知道的HTTP部分

    1. IETF组织制定的标准 rfc7234: https://tools.ietf.org/html/rfc7234 --- 原来的2616以被废弃 2. 格式 HTTP分为 请求Request 和 ...

  4. 前向分步算法 && AdaBoost算法 && 提升树(GBDT)算法 && XGBoost算法

    1. 提升方法 提升(boosting)方法是一种常用的统计学方法,在分类问题中,它通过逐轮不断改变训练样本的权重,学习多个分类器,并将这些分类器进行线性组合,提高分类的性能 0x1: 提升方法的基本 ...

  5. 【ASP.NET】website转webapplication

    *以下操作都以VS2013为参考: #新建两种web项目 1.添加webapplication项目: 2.添加website项目: #比较两种web项目新建的webform页面的不同点: 1.文件目录 ...

  6. openstack项目【day23】:keystone组件基础

    本节内容 一 什么是keystone 二 为何要有keystone 三 keystone的功能 四 keystone概念详解 五 keystone内包含的组件 六 keystone与openstack ...

  7. kubernetes云平台管理实战: 集群部署(一)

    一.环境规划 1.架构拓扑图 2.主机规划 3.软件版本 [root@k8s-master ~]# cat /etc/redhat-release CentOS Linux release 7.4.1 ...

  8. EF CodeFirst系列(9)---添加初始化数据和数据库迁移策略

    1.添加初始化数据(Seed) 我们可以在初始化数据库的过程中给数据库添加一些数据.为了实现初始化数据(seed data)我们必须创建一个自定义的数据库初始化器(DB initializer),并重 ...

  9. [物理学与PDEs]第5章习题9 伴随矩阵的特征值

    设 $3\times 3$ 阵 ${\bf A}$ 的特征值为 $\lm_1,\lm_2,\lm_3$, 证明 $\cof {\bf A}$ 的特征值为 $$\bex \lm_2\lm_3,\quad ...

  10. [物理学与PDEs]第1章第4节 电磁能量和电磁动量, 能量、动量守恒与转化定律 4.3 电磁能量 (动量) 密度, 电磁能量流 (动量流) 密度

    1. 电磁能量密度: $\cfrac{1}{2}\sex{\ve_0E^2+\cfrac{1}{\mu_0}B^2}$. 2. 电磁能量流密度向量: ${\bf S}=\cfrac{1}{\mu_0} ...