Vue2.x 响应式原理, MVVM 实现双向绑定

250 阅读2分钟

1. 实现效果

2. 关于 Object.defineProperty(obj, prop, descriptor)

defineProperty 用于新增, 修改对象属性, 并返回新的对象
参数: obj 操作对象, prop 操作属性, descriptor 属性描述符

descriptor分为数据描述符存取描述符, 两种形式不能混合使用

  • 二者均可选的键值:
    configurable: descriptor是否可修改以及property是否可删除, 默认为false
    enumerable: property是否可被枚举, 比如Object.keys(), 默认为false
  • 数据描述符可选键值:
    value: 属性值, 默认undefined
    writable: value是否可修改, 默认为false
  • 存取描述符可选键值:
    get: 提供 getter 方法, 默认为undefined
    set: 提供 setter 方法, 默认为undefined
let obj = {}

Object.defineProperty(obj, 'name', {
  value : 'Tom',
  writable : true,
  enumerable : true,
  configurable : false
})

let email = 'hellozouzh@163.com'
Object.defineProperty(obj, "email", {
  get(){
    return email
  },
  set(newValue){
    email = newValue
  },
  enumerable : true,
  configurable : false
})

Object.keys(obj) // ['name', 'email']
obj.email // ''hellozouzh@163.com'

3. 关于 document.createDocumentFragment()

appendChild() 每一次对文档的插入都会引起页面重绘, 对于复杂的节点插入操作会导致性能问题

var ul = document.getElementById("ul")
for (var i = 0; i < 20; i++) {
    var li = document.createElement("li")
    li.innerHTML = "index: " + i
    ul.appendChild(li)
}

DocumentFragment可以在内存之中先构建出一个复杂文档片段, 然后将这个片段一次性插入到当前文档中, DocumentFragment节点相当于一个占位符, 本身不会被插入当前文档, 实际插入的是它的所有子孙节点, 值得注意的是一旦DocumentFragment节点被添加进当前文档, 它自身就变成了空节点

var ul = document.getElementById("ul")
var fragment = document.createDocumentFragment()
for (var i = 0; i < 20; i++) {
    var li = document.createElement("li")
    li.innerHTML = "index: " + i
    fragment.appendChild(li)
}
ul.appendChild(fragment)

4. 实现步骤

4.1 数据劫持

使用Object.defineProperty()new Mvvm(optios)传递进来的data中的所以属性挂载到实例vm$data上,
通过属性描述符的getset进行数据劫持, 监听属性的变动

4.2 数据代理

通过属性描述符的getsetvm.$data全部代理到实例vm上, 即通过vm.price代理vm.$data.price

4.3 模板编译

通过DocumentFragment将文档树#app读取到内存当中完成变量表达式的替换 data.price -> {{ price }}

4.4 计算属性

计算属性的值依赖于data中的属性, 只要通过Object.defineProperty()把计算属性挂载到实例 vm 上即可

4.5 发布订阅

data 中的每个属性创建一个订阅中心data.price, 在模板编译时进行依赖收集,当属性值发生改变时通知所有依赖更新模板{{price}}

4.6 整体逻辑

5. 传送门

源码地址