Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的 #34

Open
long-joan opened this issue Mar 8, 2019 · 15 comments
Labels

Comments

@long-joan
Copy link

long-joan commented Mar 8, 2019

<div id="content">
    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>
@cleverboy32
Copy link

了解更多

@yygmind yygmind changed the title 第29题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的 第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的 Apr 26, 2019
@negativeentropy9
Copy link

始终感觉这个问题有点问题,明明是单向绑定,只是 m -> v,在 vue 2.x 中 通过 defineProperty 实现的数据劫持,getter 收集依赖,setter 调用更新回调,这个过程是 vue 黑盒提供的,也就是说数据驱动视图,开发人员只需关注数据的变更即可;再说 v -> m,通过 v-model 的方式,指令添加是开发人员加的吧,如果一个组件有多个 v-model ,你要自己写 v-on 和 data 的修改吧。

@db46rt00ors
Copy link

db46rt00ors commented Jul 12, 2019

底层就是defineProperty get是读取之前的旧数据,set中如果发现数据没改 直接return 原始值 ,如果改了就直接修改为NewValue
这个是一个简单的demo

<body>
    <div id="app">
        <input type="text" id="model"><br />
        <div id="modelText"></div>
    </div>
    <script>
        var model = document.querySelector("#model");
        var modelText = document.querySelector("#modelText");
        var defaultName = "defaultName";
        var userInfo = {}
        model.value = defaultName;
        Object.defineProperty(userInfo, "name", {
            get: function () {
                return defaultName;
            },
            set: function (value) {
                defaultName = value;
                model.value = value;
                console.log(value);
                modelText.textContent = value;
            }
        })

        userInfo.name = "new value";
        var isEnd = true;

        model.addEventListener("keyup", function () {
            if (isEnd) {
                userInfo.name = this.value;
            }
        }, false)
        //加入监听中文输入事件
        model.addEventListener("compositionstart", function () {
            console.log("开始输入中文");
            isEnd = false;
        })
        model.addEventListener("compositionend", function () {
            isEnd = true;
            console.log("结束输入中文");
        })
    </script>
</body>

这个图是 vue源码
image
他是最后通知 dep.notify() notify 调用 组件的watcher 中的update函数 大致就是这样

@seawind8888
Copy link

一个比较直观的图,看了就明白了 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/

@Forever17s
Copy link

核心是利用ES5的Object.defineProperty,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。

  • Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

Object.defineProperty(
obj, // 定义属性的对象
prop, // 要定义或修改的属性的名称
descriptor, // 将被定义或修改属性的描述符【核心】

  • observe的功能就是用来监测数据的变化。实现方式是给非VNode的对象类型数据添加一个Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer对象实例。

Observer是一个类,它的作用是给对象属性添加getter和setter,用于 依赖收集派发更新

依赖收集getter(重点关注以下两点)

  • *const dep = new Dep() // 实例化一个Dep实例
  • *在get函数中通过dep.depend做依赖收集

Dep是一个Class,它定义了一些属性和方法,它有一个静态属性target,这是一个全局唯一Watcher【同一时间内只能有一个全局的Watcher被计算】。Dep实际上就是对Watcher的一种管理,Dep脱离Watcher单独存在是没有意义的。Watcher和Dep就是典型的观察者设计模式。

Watcher是一个Class,在它的构造函数中定义了一些和Dep相关的属性:

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()				

收集过程:当我们实例化一个渲染watcher的时候,首先进入watcher的构造函数逻辑,然后执行他的this.get()方法,进入get函数把Dep.target赋值为当前渲染watcher并压栈(为了恢复用)。接着执行vm._render()方法,生成渲染VNode,并且在这个过程对vm上的数据访问,这个时候就触发数据对象的getter(在此期间执行Dep.target.addDep(this)方法,将watcher订阅到这个数据持有的dep的subs中,为后续数据变化时通知到哪些subs做准备)。然后递归遍历添加所有子项的getter。

Watcher在构造函数中初始化两个Dep实例数组。newDeps代表新添加的Dep实例数组,deps代表上一次添加的Dep实例数组。
依赖清空:在执行清空依赖(cleanupDeps)函数时,会首先遍历deps,移除对dep的订阅,然后把newDepsIds和depIds交换,newDeps和deps交换,并把newDepIds和newDeps清空。考虑场景,在条件渲染时,及时对不需要渲染数据的订阅移除,减少性能浪费。

考虑到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函数

队列排序保证:

  1. 组件的更新由父到子。父组件创建早于子组件,watcher的创建也是
  2. 用户自定义watcher要早于渲染watcher执行,因为用户自定义watcher是在渲染watcher前创建的
  3. 如果一个组件在父组件watcher执行期间被销毁,那么它对应的watcher执行都可以被跳过,所以父组件的watcher应该先执行。
  • 队列遍历:排序完成后,对队列进行遍历,拿到对应的watcher,执行watcher.run()

run函数解析:先通过this.get()得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep模式任何一个条件,则执行watcher的回调,注意回调函数执行的时候会把第一个参数和第二个参数传入新值value和旧值oldValue,这就是当我们自己添加watcher时候可以在参数中取到新旧值的来源。对应渲染watcher而言,在执行this.get()方法求值的时候,会执行getter方法。因此在我们修改组件相关数据时候,会触发组件重新渲染,接着重新执行patch的过程

手写一个数据绑定:

<input id="input" type="text" />
<div id="text"></div>

let input = document.getElementById("input");
let text = document.getElementById("text");
let data = { value: "" };
Object.defineProperty(data, "value", {
  set: function(val) {
    text.innerHTML = val;
    input.value = val;
  },
  get: function() {
    return input.value;
  }
});
input.onkeyup = function(e) {
  data.value = e.target.value;
};

@Mrfujianfei
Copy link

vue通过Object.defineProperty 劫持传进来的数据, 然后在数据getter的时候订阅重新编译模板的消息,然后通过js监听元素的事件,这里input事件也好,keyup事件也好,总是监听输入框值变化,将新的值重新赋值给被劫持的data,这样就会触发setter函数,再setter函数中就会去发布重新编译模板的消息;

@Primise
Copy link

Primise commented Jun 10, 2020

从页面渲染到加载数据,依赖于ES5的object.defindeProperty 此时收益数据依赖并当数据reactive,当修改数据的时候触发set 调用以来的notify并触发更新

@CodingMeUp
Copy link

CodingMeUp commented Jun 24, 2020

Model 改变 View的过程: 依赖于ES5的object.defindeProperty,通过 defineProperty 实现的数据劫持,getter 收集依赖,setter 调用更新回调(不同于观察者模式,是发布订阅 + 中介)
View 改变 Model的过程: 依赖于 v-model ,该语法糖实现是在单向数据绑定的基础上,增加事件监听并赋值给对应的Model

附带补一个问题, 如果react也要双向绑定(虽然推荐是单向的),其实早期mixin【React.addons.LinkedStateMixin】可以实现, 也就是搞一些简单的setstate封装吧

@ndfhsledk
Copy link

v-model是v-bind和v-on的语法糖。
v-bind即model=>view,当model数据发生变化,在setter中,去触发对应组件重新生成Vnode,对比新旧虚拟树,更新差异。
v-on即view=>model,view操作后,触发事件,调用回调函数,在回调函数中更新model

@XuedaoYuan
Copy link

其实都是单向数据流,v-model也只是语法糖。主要思想还是 数据驱动 视图,按照这个去开发就完事了。不要写的不伦不类

@Maricaya
Copy link

<div id="content">
    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>

input 中输入内容,焦点会消失

@Liu-10004
Copy link

Liu-10004 commented Nov 23, 2020

<div id="content">
    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>
el.innerHTML = template.replace(/\{\{(\w+)\}\}/g, ($0,$1)=>{
       return _data[$1];
     }) 

这样会不会更简洁一些

@Vevean
Copy link

Vevean commented Feb 3, 2022

想问一下Vue的双向绑定和vue的响应式原理有什么不同啊

@861621821
Copy link

双向数据绑定不应该指v-model吗?而Object.defineProperty那一坨流程是Vue响应式系统吧?

@gookyn
Copy link

gookyn commented Jun 27, 2023

Vue 的双向数据绑定和响应式数据是两个不同的概念。

双向数据绑定

  • Vue 中的双向数据绑定指的是当一个数据发生变化时,视图也会随之更新;而当用户在视图中修改了数据时,数据模型也会相应地更新
  • 双向数据绑定是一种语法糖,它允许将表单元素(如input、textarea等)上的值与 Vue 实例中的数据属性进行双向绑定

这种双向绑定可以通过 v-model 指令来实现,例如:

<input type="text" v-model="message">

上面的代码表示将输入框与 Vue 实例中的 message 属性进行双向绑定,当用户在输入框中输入内容时,message 属性的值也会随之改变。

响应式数据

Vue 中的响应式数据指的是当一个组件的数据发生变化时,Vue 会自动检测到这些变化,并且重新渲染视图。

Vue 通过 ProxyObject.defineProperty() 对数据对象进行代理或劫持,从而能够监听到数据的变化,并通知相关的组件进行重新渲染。例如:

const data = { message: 'Hello, Vue!' }
const vm = new Vue({
  el: '#app',
  data: data
})

上面的代码创建了一个 Vue 实例,并将 data 对象传入其中作为其数据模型。当 data.message 发生变化时,Vue 会自动检测到并通知相关的组件进行重新渲染。


因此:

  • 双向数据绑定主要用于处理表单等交互操作,让用户能够方便地修改数据
  • 响应式数据则是 Vue 实现数据驱动视图的核心机制,通过它使得视图能够自动地响应数据的变化

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests