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

发布于 2022-07-22 11:14:03 字数 1094 浏览 222 评论 14

<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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(14

活雷疯! 2022-05-04 13:57:19

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

北城挽邺 2022-05-04 13:57:19

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

猥︴琐丶欲为 2022-05-04 13:57:18
<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>
el.innerHTML = template.replace(/{{(w+)}}/g, ($0,$1)=>{
       return _data[$1];
     }) 

这样会不会更简洁一些

私藏温柔 2022-05-04 13:57:14
<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>

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

盗梦空间 2022-05-04 13:57:10

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

汐鸠 2022-05-04 13:57:09

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

紫南 2022-05-04 13:57:04

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

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

终弃我 2022-05-04 13:56:58

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

[浮城] 2022-05-04 13:56:53

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

遇到 2022-05-04 13:56:44

核心是利用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 type="text" />
<div></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;
};
野侃 2022-05-04 13:56:03

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

<body>
    <div>
        <input type="text"><br />
        <div></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源码

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

格子衫的從容 2022-05-04 05:18:12

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

~没有更多了~

关于作者

倾城花音

暂无简介

0 文章
0 评论
25 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文