第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的?
<div>
name: {{name}}<br/>
<input type="text" v-model = 'name'>
</div>
<script>
const el = document.getElementById('content');
const template = el.innerHTML;
const _data = {
name: 'mark',
}
//new Proxy(target, handler);
let changeName = new Proxy(_data, {
set(obj, name, value){
obj[name] = value;
render()
}
})
render();
function render(){
el.innerHTML = template.replace(/{{w+}}/g, str=>{
str = str.substring(2, str.length-2);
return _data[str];
})
Array.from(el.getElementsByTagName('input')).filter(ele => {
return ele.getAttribute('v-model');
}).forEach(input=>{
let name = input.getAttribute('v-model');
input.value = changeName[name];
input.oninput = function(){
changeName[name] = this.value;
}
})
}
</script>
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(14)
想问一下Vue的双向绑定和vue的响应式原理有什么不同啊
双向数据绑定不应该指v-model吗?而Object.defineProperty那一坨流程是Vue响应式系统吧?
这样会不会更简洁一些
input 中输入内容,焦点会消失
其实都是单向数据流,v-model也只是语法糖。主要思想还是 数据驱动 视图,按照这个去开发就完事了。不要写的不伦不类
v-model是v-bind和v-on的语法糖。
v-bind即model=>view,当model数据发生变化,在setter中,去触发对应组件重新生成Vnode,对比新旧虚拟树,更新差异。
v-on即view=>model,view操作后,触发事件,调用回调函数,在回调函数中更新model
Model 改变 View的过程: 依赖于ES5的object.defindeProperty,通过 defineProperty 实现的数据劫持,getter 收集依赖,setter 调用更新回调(不同于观察者模式,是发布订阅 + 中介)
View 改变 Model的过程: 依赖于 v-model ,该语法糖实现是在单向数据绑定的基础上,增加事件监听并赋值给对应的Model
附带补一个问题, 如果react也要双向绑定(虽然推荐是单向的),其实早期mixin【React.addons.LinkedStateMixin】可以实现, 也就是搞一些简单的setstate封装吧
从页面渲染到加载数据,依赖于ES5的object.defindeProperty 此时收益数据依赖并当数据reactive,当修改数据的时候触发set 调用以来的notify并触发更新
vue通过Object.defineProperty 劫持传进来的数据, 然后在数据getter的时候订阅重新编译模板的消息,然后通过js监听元素的事件,这里input事件也好,keyup事件也好,总是监听输入框值变化,将新的值重新赋值给被劫持的data,这样就会触发setter函数,再setter函数中就会去发布重新编译模板的消息;
一个比较直观的图,看了就明白了 https://seawind8888.github.io/2019/07/15/%E4%B8%80%E5%9B%BE%E7%9C%8B%E6%87%82Vue%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A%E5%8E%9F%E7%90%86/
核心是利用ES5的Object.defineProperty,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。
依赖收集getter(重点关注以下两点)
*const dep = new Dep() // 实例化一个Dep实例
*在get函数中通过dep.depend做依赖收集
Dep是一个Class,它定义了一些属性和方法,它有一个静态属性target,这是一个全局唯一Watcher【同一时间内只能有一个全局的Watcher被计算】。Dep实际上就是对Watcher的一种管理,Dep脱离Watcher单独存在是没有意义的。Watcher和Dep就是典型的观察者设计模式。
Watcher是一个Class,在它的构造函数中定义了一些和Dep相关的属性:
收集过程:当我们实例化一个渲染watcher的时候,首先进入watcher的构造函数逻辑,然后执行他的
this.get()
方法,进入get函数把Dep.target赋值为当前渲染watcher并压栈(为了恢复用)。接着执行vm._render()
方法,生成渲染VNode,并且在这个过程对vm上的数据访问,这个时候就触发数据对象的getter(在此期间执行Dep.target.addDep(this)
方法,将watcher订阅到这个数据持有的dep的subs中,为后续数据变化时通知到哪些subs做准备)。然后递归遍历添加所有子项的getter。考虑到Vue是数据驱动的,所以每次数据变化都会重写Render,那么
vm._render()
方法会再次执行,并再次触发数据收集依赖的目的是为了当这些响应式数据发生变化,触发它们的setter的时候,能知道应该通知哪些订阅者去做相应的逻辑处理【派发更新】
派发更新setter(重点关注以下两点)
*childOb = !shallow && observe(newVal) // 如果shallow为false的情况,会对新设置的值变成一个响应式对象
*dep.notify() // 通知所有订阅者
派发过程:当我们组件中对响应的数据做了修改,就会触发setter的逻辑,最后调用
dep.notify()
方法,它是Dep的一个实例方法。具体做法是遍历依赖收集中建立的subs,也就是Watcher的实例数组【subs数组在依赖收集getter中被添加,期间通过一些逻辑处理判断保证同一数据不会被添加多次】,然后调用每一个watcher的update方法。update函数中有个
queueWatcher(this)
方法引入了队列的概念,是vue在做派发更新时优化的一个点,它并不会每次数据改变都会触发watcher回调,而是把这些watcher先添加到一个队列中,然后在nextTick后执行watcher的run函数队列排序保证:
watcher.run()
。run函数解析:先通过
this.get()
得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep模式任何一个条件,则执行watcher的回调,注意回调函数执行的时候会把第一个参数和第二个参数传入新值value和旧值oldValue,这就是当我们自己添加watcher时候可以在参数中取到新旧值的来源。对应渲染watcher而言,在执行this.get()
方法求值的时候,会执行getter方法。因此在我们修改组件相关数据时候,会触发组件重新渲染,接着重新执行patch的过程手写一个数据绑定:
底层就是defineProperty get是读取之前的旧数据,set中如果发现数据没改 直接return 原始值 ,如果改了就直接修改为NewValue
这个是一个简单的demo
这个图是 vue源码
他是最后通知 dep.notify() notify 调用 组件的watcher 中的update函数 大致就是这样
始终感觉这个问题有点问题,明明是单向绑定,只是 m -> v,在 vue 2.x 中 通过 defineProperty 实现的数据劫持,getter 收集依赖,setter 调用更新回调,这个过程是 vue 黑盒提供的,也就是说数据驱动视图,开发人员只需关注数据的变更即可;再说 v -> m,通过 v-model 的方式,指令添加是开发人员加的吧,如果一个组件有多个 v-model ,你要自己写 v-on 和 data 的修改吧。
了解更多