数据双向绑定简易原理

<input type="text" id="username">
<span id="uName"></span>
  let obj = {}Object.defineProperty(obj, 'username', {// 使用 defineProperty 中的 get 来双向绑定数据set (v) {document.getElementById('uName').innerText = v;document.getElementById('username').value = v;},get () {console.log('get')}})document.getElementById('username').addEventListener('keyup', function (e) {obj.username = e.target.value})

defineProperty 实现原理

const vm = {name: 'bob',age: 11,enjoy: ['1', 'a', 3],hobby: { a: 'eat' }
}const orginalProto = Array.prototype
const arrayProto = Object.create(orginalProto) // 先克隆份 Array 原型
const methodToPatch = ['push','pop','shift','unshift','splice','sort','reverse'
]
methodToPatch.forEach(method => {arrayProto[method] = function () {console.log('method changed: ', method, arguments)orginalProto[method].apply(this, arguments)}
})const observe = (data) => {if (!data || typeof data !== 'object') returnif (Array.isArray(data)) {// 如果是数组,重写原型链 data.__proto__ = arrayProtoObject.setPrototypeOf(data, arrayProto);for (let i = 0; i < data.length; i++) {observe(data[i])}} else {Object.keys(data).forEach(key => {dr(data, key, data[key])})}
}const dr = (data, key, val) => {observe(val) // 递归制造响应式数据Object.defineProperty(data, key, {enumerable: true,configurable: true,set(nv) {console.log('val changed: ', nv)val = nv},get() {return val}})
}observe(vm)vm.name = 'lucy'
vm.age = 11
vm.hobby.a = 'play'
vm.enjoy.push(123)
vm.sex = 'female' // 监听不到// 问题:直接 watch 对象下某个属性监听不到
@Watch('searchList', { deep: true, immediate: true })
onSearchListWatch(newVal, oldVal) {const getSubBu = (list) => list?.find((item) => item.index === 1)?.subBuCode;if (getSubBu(newVal) !== getSubBu(oldVal)) {this.initLineList();}
}// 解决
@Watch('oneSearchList', { deep: true, immediate: true })
onSearchListWatch(newVal, oldVal) {if (newVal !== oldVal) {this.initLineList();}
}get oneSearchList() {return this.searchList?.find((item) => item.index === 1)?.subBuCode;
}

缺陷

Object.defineProperty 无法监听新增加的属性
Object.defineProperty 无法一次性监听对象所有属性,如对象属性的子属性
Object.defineProperty 无法响应数组操作
Proxy 拦截方式更多, Object.defineProperty 只有 get 和 set

const vm = {name: 'bob',age: 11,enjoy: ['1', 'a', 3],hobby: { a: 'eat' }
}const observe = (data) => {if (!data || typeof data !== 'object') returnObject.keys(data).forEach(key => {if (typeof data[key] === 'object') {data[key] = proxyData(data[key])observe(data[key])}})return proxyData(data);
}// Reflect
// Reflect 是一个内建对象,可简化 Proxy 的创建。
// 前面所讲过的内部方法,例如 [[Get]] 和 [[Set]] 等,都只是规范性的,不能直接调用。
// Reflect 对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。
const proxyData = (data) => new Proxy(data, {get(target, propKey, receiver) {return Reflect.get(target, propKey, receiver)},set(target, propKey, value, receiver) {console.log('val changed: ', value)// target[propKey] = valueReflect.set(target, propKey, value, receiver)return true}
})const proxyVm = observe(vm);proxyVm.name = 'lucy'
proxyVm.age = 11
proxyVm.hobby.a = 'play'
proxyVm.enjoy.push(123)
proxyVm.sex = 'female'

3.0 改进

为什么使用 Proxy 可以解决上面的问题呢?主要是因为Proxy是拦截对象,对对象进行一个"拦截",外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中
vue 实现简易原理,其内部主要由 Observer、Compile、Watcher 组成,中间包括 Dep(变化通知)以及 Updater(视图更新)等模块,最终再由 MVVM(new Vue)去汇合

区分 Proxy 和 Decorator 的使用场景,可以概括为:Proxy 的核心作用是控制外界对被代理者内部的访问,Decorator 的核心作用是增强被装饰者的功能

Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
Proxy 用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改

Proxy 性能问题

Proxy 的性能比 Promise 还差
Proxy 作为新标准,从长远来看,JS 引擎会继续优化 Proxy
Proxy 兼容性差(Vue 3.0 中放弃了对于IE的支持)

完整 github 地址

Observer

  • 利用 Obeject.defineProperty() 来监听属性变动
class Observer {constructor(data) {this.data = data;this.walk(data);}walk(data) {var me = this;Object.keys(data).forEach(function (key) {me.convert(key, data[key]);});}convert(key, val) {this.defineReactive(this.data, key, val);}defineReactive(data, key, val) {var dep = new Dep();observe(val); // 监听子属性Object.defineProperty(data, key, {enumerable: true, // 可枚举configurable: false, // 不能再defineget() {if (Dep.target) {dep.depend();}return val;},set(newVal) {if (newVal === val) {return;}val = newVal;// 新的值是 object 的话,进行监听if (typeof newVal === 'object') observe(newVal);// 通知订阅者dep.notify();}});}
}
  • 那么将需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter
function observe(value, vm) {if (!value || typeof value !== 'object') {return;}return new Observer(value);
};
  • 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化,其中通过 Dep 来作为调度者,对 Watcher 通知变化以及添加订阅者
var uid = 0;class Dep {constructor() {this.id = uid++;this.subs = [];}addSub(sub) { // 添加订阅this.subs.push(sub);}depend() {// 此时 Dep.target 指向 Watcher 的 thisDep.target.addDep(this);}removeSub(sub) { // 移除订阅var index = this.subs.indexOf(sub);if (index != -1) {this.subs.splice(index, 1);}}notify() { // 通知订阅者this.subs.forEach(function (sub) {// sub 为 getter 时添加到 subs 的 Watcher 的前一个状态sub.update();});}
}Dep.target = null;

Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

  • 1、在自身实例化时往属性订阅器 (dep) 里面添加自己
  • 2、自身必须有一个 update() 方法
  • 3、待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退
class Watcher {constructor(vm, expOrFn, cb) {this.cb = cb;this.vm = vm;this.expOrFn = expOrFn;this.depIds = {};if (typeof expOrFn === 'function') {this.getter = expOrFn;} else {this.getter = this.parseGetter(expOrFn.trim());}// 此处为了触发属性的 getter,从而在 dep 添加自己,结合 Observer 更易理解this.value = this.get();}update() {this.run();}run() {var value = this.get(); // 收到最新值var oldVal = this.value;if (value !== oldVal) {this.value = value;this.cb.call(this.vm, value, oldVal);}}addDep(dep) {// 1. 每次调用run()的时候会触发相应属性的getter// getter里面会触发dep.depend(),继而触发这里的addDep// 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已// 则不需要将当前watcher添加到该属性的dep里// 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里// 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性// 则需要将当前watcher(child.name)加入到新的 child.name 的dep里// 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中// 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了// 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep// 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update// 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter// 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep// 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher// dep 指向 Observer 的 thisif (!this.depIds.hasOwnProperty(dep.id)) {dep.addSub(this); // 添加当前状态的 this 对象(value、expOrFn 等等)到 Observerthis.depIds[dep.id] = dep;}}get() {Dep.target = this; // 将当前订阅者指向自己var value = this.getter.call(this.vm, this.vm); // 为 getter 传入自身的 obj,从而获取值Dep.target = null; // 添加完毕,重置return value;}parseGetter(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;}}
}

compile

  • compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,
  • 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
const compileUtil = {text(node, vm, exp) { // v-textthis.bind(node, vm, exp, 'text');},html(node, vm, exp) { // v-htmlthis.bind(node, vm, exp, 'html');},bind(node, vm, exp, dir) { // 绑定更新节点var updaterFn = updater[dir + 'Updater'];// 第一次初始化视图updaterFn && updaterFn(node, this._getVMVal(vm, exp));// 实例化订阅者,此操作会在对应的属性消息订阅器(有绑定的对象值)中添加了该订阅者 watchernew Watcher(vm, exp, function (value, oldValue) {// 一旦属性值有变化,会收到通知执行此更新函数,更新视图updaterFn && updaterFn(node, value, oldValue);});},// ...省略
}

更新 updater

const updater = { // 更新节点textUpdater(node, value) {node.textContent = typeof value == 'undefined' ? '' : value;},htmlUpdater(node, value) {node.innerHTML = typeof value == 'undefined' ? '' : value;},classUpdater(node, value, oldValue) {var className = node.className;className = className.replace(oldValue, '').replace(/\s$/, '');var space = className && String(value) ? ' ' : '';node.className = className + space + value;},modelUpdater(node, value, oldValue) {node.value = typeof value == 'undefined' ? '' : value;}
};

compile

class Compile {constructor(el, vm) {this.$vm = vm;this.$el = this.isElementNode(el) ? el : document.querySelector(el);if (this.$el) {this.$fragment = this.node2Fragment(this.$el);this.init();this.$el.appendChild(this.$fragment);}}node2Fragment(el) { // 虚拟 domvar fragment = document.createDocumentFragment(),child;// 将原生节点拷贝到fragmentwhile (child = el.firstChild) {// fragment.appendChild() 具有移动性,此操作是 move dom// 将 el.children[0] 被抽出,在下次操作从 el.children[1] 开始,以达到循环的目的fragment.appendChild(child);}return fragment;}init() {this.compileElement(this.$fragment);}// 递归遍历所有节点及其子节点,进行扫描解析编译,调用对应指令渲染,并调用对应指令更新函数进行绑定compileElement(el) {var childNodes = el.childNodes,me = this;[].slice.call(childNodes).forEach(function (node) {var text = node.textContent;var reg = /\{\{(.*)\}\}/;// 按元素节点方式编译if (me.isElementNode(node)) { // 是否元素节点me.compile(node);} else if (me.isTextNode(node) && reg.test(text)) { // 是否文本节点并符合 {{xxx}} 形式// 指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串// 以此类推,RegExp.$2,RegExp.$3,..RegExp.$99 总共可以有99个匹配me.compileText(node, RegExp.$1.trim());}// 遍历编译子节点if (node.childNodes && node.childNodes.length) {me.compileElement(node);}});}// 修改元素节点对应属性的赋值操作compile(node) {var nodeAttrs = node.attributes,me = this;[].slice.call(nodeAttrs).forEach(function (attr) {var attrName = attr.name;// 规定:指令以 v-xxx 命名(v-text、v-html 等)if (me.isDirective(attrName)) {var exp = attr.value;var dir = attrName.substring(2);if (me.isEventDirective(dir)) {// 事件指令,如 v-on:clickcompileUtil.eventHandler(node, me.$vm, exp, dir);} else {// 普通指令compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);}node.removeAttribute(attrName);}});}compileText(node, exp) {compileUtil.text(node, this.$vm, exp);}isDirective(attr) {return attr.indexOf('v-') == 0;}isEventDirective(dir) {return dir.indexOf('on') === 0;}isElementNode(node) {return node.nodeType == 1;}isTextNode(node) {return node.nodeType == 3;}
}

MVVM

  • MVVM作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,
  • 通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;
  • 视图交互变化 (input) -> 数据 model 变更的双向绑定效果
class MVVM {constructor(options) {this.$options = options || {};var data = this._data = this.$options.data;var me = this;// 属性代理,实现 vm.xxx -> vm._data.xxxObject.keys(data).forEach(function (key) {me._proxyData(key);});this._initComputed();// 对所有对象启动监听observe(data, this); // new Observer// 启动编译this.$compile = new Compile(options.el || document.body, this)}// 监听需要用到的值$watch(key, cb, options) {new Watcher(this, key, cb);}// 代理 this 中的 data_proxyData(key, setter, getter) {var me = this;setter = setter ||Object.defineProperty(me, key, {configurable: false,enumerable: true,get: function proxyGetter() {return me._data[key];},set: function proxySetter(newVal) {me._data[key] = newVal;}});}// 代理 this 中的 computed_initComputed() {var me = this;var computed = this.$options.computed;if (typeof computed === 'object') {Object.keys(computed).forEach(function (key) {Object.defineProperty(me, key, {get: typeof computed[key] === 'function'? computed[key]: computed[key].get,set: function () { }});});}}
}

vue——ViewModel 简易原理相关推荐

  1. 【2019 前端进阶之路】深入 Vue 响应式原理,活捉一个 MVVM

    作者:江三疯,专注前端开发.欢迎关注公众号前端发动机,第一时间获得作者文章推送,还有各类前端优质文章,致力于成为推动前端成长的引擎. 前言 作为 Vue 面试中的必考题之一,Vue 的响应式原理,想必 ...

  2. vue 数组删除 dome没更新_详解Vue响应式原理

    摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...

  3. 什么是Vue?Vue的工作原理是什么?

    Vue(读音/Vju:/,类似于View)是一套用于构建用户界面的渐进式框架,与其他大型框架相比,Vue被设计为可以自底向上逐层应用.其他大型框架往往一-开始就对项 目的技术方案进行强制性的要求,而V ...

  4. vue双向绑定原理及实现

    vue双向绑定原理及实现 一.MVC模式 二.MVVM模式 三.双向绑定原理 1.实现一个Observer 2.实现一个Watcher 3.实现一个Compile 4.实现一个MVVM 四.最后写一个 ...

  5. Vue响应式原理的简单模型

    1.前言 最近在梳理vue响应式的原理,有一些心得,值得写一篇博客出来看看. 其实之前也尝试过了解vue响应式的原理,毕竟现在面试看你用的是vue的话,基本上都会问你几句vue响应式的原理.以往学习这 ...

  6. Vue响应式原理(看这一篇就够了)

    你肯定听说过Object.denfineProperty或是Proxy\reflect,这的确是在VUE响应式原理中起重要作用的一部分代码,但这远远不能代表整个流程的精妙.上图: 不懂没关系,请往下看 ...

  7. 详解Vue响应式原理

    摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...

  8. vue双向数据绑定原理分析--Mr.Ember

    vue双向数据绑定原理分析 摘要 vue常用,但原理常常不理解,下面我们来具体分析下vue的双向数据绑定原理. (1)创建一个vue对象,实现一个数据监听器observer,对所有数据对象属性进行监听 ...

  9. Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理?

    Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理? 我们在Vue里面,定义在Data里的属性,叫做响应式属性. 每一个vue组件被创建的时候,同时还有一个对象被创建出来了,这个对象我们是看不到 ...

最新文章

  1. 死磕Java并发:J.U.C之并发工具类:Exchanger
  2. 数据库查询性能优化之利器—索引(二)
  3. 颠覆:链表在删除和插入的效率一定优于数组吗?
  4. 数据仓库介绍与实时数仓案例
  5. JDK+SDK 环境变量记录
  6. 微信小程序的零食商城
  7. android图片上加有汉字,Android 为图片添加文字水印
  8. 第103篇Python:Python爬虫系列之书籍爬取,细节拉满
  9. python 成员运算符_Python的“ in”和“ not in”成员资格运算符
  10. Android 8.1 SystemUI之状态栏、下拉菜单通知、导航栏分析(一)
  11. amoeba实现mysql主从读写分离_MySQL+Amoeba实现数据库主从复制和读写分离
  12. 他们都说springboot是懒人神器,你觉得呢?
  13. 3分钟,把你的安卓手机/平板变成你的电脑副屏
  14. Android 录制桌面视频 screenrecord
  15. 论 致命错误c0000005
  16. Windows Installer和即点即用版本的Office程序不能并行的问题
  17. 耶鲁大学《博弈论》课程——最佳对策
  18. 三招破解禁用鼠标右键的网站
  19. Day12--介绍搜索功能并创建serach分支
  20. G1-Card Table和Remember Set

热门文章

  1. 忧桑三角形,调了半天,真忧桑TAT
  2. NumPy 数组的维度变换
  3. 原生JS实现任意数据的动态表格
  4. R语言︱SNA-社会关系网络 R语言实现专题(基础篇)(一)
  5. APP规范实例(详细的UI设计方法)
  6. YOLO中对IOU、GIOU、DIOU、CIOU的理解
  7. (原创)直观了解通道混和器的校色作用
  8. 微信小程序分页加载列表
  9. 【IoT】 产品设计之结构设计:材料工艺选择及特点(PP、PVC、PE、PS、ABS、PC)
  10. 2023.02.14草图大师 卧室房间 效果图