资讯专栏INFORMATION COLUMN

Vue源码解析(二)Vue的双向绑定讲解及实现

ckllj / 2325人阅读

摘要:上篇文章,我们讲解了的属性映射和方法的重定义,链接地址如下源码解析一属性映射和函数引用的重定义这篇文章给大家带来的是的双向绑定讲解。这就是的双向绑定。使用定时器定时检查的值,发生变化就通知订阅者。这个方法不好,定时器不能实时反应变化。

文章中的代码时阶段,可以下载源码测试一下。
git项目地址:https://github.com/xubaodian/...

项目使用webpack构建,下载后先执行:

npm install

安装依赖后使用指令:

npm run dev

可以运行项目。

上篇文章,我们讲解了Vue的data属性映射和方法的重定义,链接地址如下:

Vue源码解析(一)data属性映射和methods函数引用的重定义

这篇文章给大家带来的是Vue的双向绑定讲解。

什么是双向绑定

我们看一张图:



可以看到,输入框上方的内同和输入框中的值是一致的。输入框的之变化,上方的值跟着一起变化。

这就是Vue的双向绑定。

对象属性监听实现

我们先不着急了解Vue时如何实现这一功能的,如果我们自己要实现这样的功能,如何实现呢?

我的思路是这样:



可以分为几个步骤,如下:

1、首先给输入框添加input事件,监视输入值,存放在变量value中。

2、监视value变量,确保value变化时,监视器可以发现。

3、若value发生变化,则重新渲染视图。

上面三个步骤,1(addEventListener)和3(操作dom)都很好实现,对于2的实现,可能有一下两个方案:

1、使用Object.defineProperty()重新定义对象set和get,在值发生变化时,通知订阅者。

2、使用定时器定时检查value的值,发生变化就通知订阅者。(这个方法不好,定时器不能实时反应value变化)。

Vue源码中采用了方案1,我们首先用方案1实现对对象值的监听,代码如下:

function defineReactive(obj, key, val, customSetter) {
  //获取对象给定属性的描述符
  let property = Object.getOwnPropertyDescriptor(obj, key);
  //对象该属性不可配置,直接返回
  if (property && property.configurable === false) {
    return;
  }

  //获取属性get和set属性,若此前该属性已经进行监听,则确保监听属性不会被覆盖
  let getter = property && property.get;
  let setter = property && property.set;
  
  if (arguments.length < 3) {
    val = obj[key];
  }

  //监听属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val;
      console.log(`读取了${key}属性`);
      return value;
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val;
      //如果值没有变化,则不做改动
      if (newVal === value) {
        return;
      }
      //自定义响应函数
      if (customSetter) {
        customSetter(newVal);
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      console.log(`属性${key}发生变化:${value} => ${newValue}`);
    }
  })
}

下面我们测试下,测试代码如下:

let obj = {
    name: "xxx",
    age: 20
};
defineReactive(obj, "name");
let name = obj.name;
obj.name = "1111";

控制台输出为:

读取了name属性
test.html:51 属性name发生变化:xxx => 1111

可见,我们已经实现了对obj对象name属性读和写的监听。

实现了监听,这没问题,但是视图怎么知道这些属性发生了变化呢?可以使用发布订阅模式实现。

发布订阅模式

什么是发布订阅模式呢?

我画了一个示意图,如下:


发布订阅模式有几个部分构成:

1、订阅中心,管理订阅者列表,发布者发消息时,通知相应的订阅者。

2、订阅者,这个是订阅消息的主体,就像关注微信公众号一样,有文章就会通知关注者。

3、发布者,类似微信公众号的文章发布者。

订阅中心的代码如下:

export class Dep {
  constructor() {
    this.id = uid++;
    //订阅列表
    this.subs = [];
  }

  //添加订阅
  addSub(watcher) {
    this.subs.push(watcher);
  }

  //删除订阅者
  remove(watcher) {
    let index = this.subs.findIndex(item => item.id === watcher.id);
    if (index > -1) {
      this.subs.splice(index, 1);
    }
  }


  depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

//通知订阅者
  notify() {
    this.subs.map(item => {
      item.update();
    });
  }
}

//订阅中心  静态变量,订阅时使用
Dep.target = null;
const targetStack = [];

export function pushTarget (target) {
  targetStack.push(target);
  Dep.target = target;
}

export function popTarget () {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}

订阅中心已经实现,还有发布者和订阅者,先看下发布者,这里谁是发布者呢?

没错,就是defineReactive函数,这个函数实现了对data属性的监听,它可以检测到data属性的修改,发生修改时,通知订阅中心,所以defineReactive做一些修改,如下:

//属性监听
export function defineReactive(obj, key, val, customSetter) {
  //获取对象给定属性的描述符
  let property = Object.getOwnPropertyDescriptor(obj, key);
  //对象该属性不可配置,直接返回
  if (property && property.configurable === false) {
    return;
  }

  //订阅中心
  const dep = new Dep();

  //获取属性get和set属性,若此前该属性已经进行监听,则确保监听属性不会被覆盖
  let getter = property && property.get;
  let setter = property && property.set;
  
  if (arguments.length < 3) {
    val = obj[key];
  }

  //如果监听的是一个对象,继续深入监听
  let childOb = observe(val);
  //监听属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val;
      //这段代码时添加订阅时使用的
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
      }
      return value;
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val;
      //如果值没有变化,则不做改动
      if (newVal === value) {
        return;
      }
      //自定义响应函数
      if (customSetter) {
        customSetter(newVal);
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      //如果新的值为对象,重新监听
      childOb = observe(newVal);
      /**
       * 订阅中心通知所有订阅者
       **/
      dep.notify();
    }
  })
}

这里设计到闭包的概念,我们在函数里定义了:

 const dep = new Dep();

由于set和get函数一直都存在的,所有dep会一直存在,不会被回收。

当值发生变化后,利用下面的代码通知订阅者:

dep.notify();

订阅中心和发布者都有了,我们何时订阅呢?或者什么时间订阅合适呢?

我们是希望实现当读取data属性时候,实现订阅。所以在defineReactive函数的get监听中添加了如下代码:

    if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
    }
    return value;

Dep.target是一个静态变量,用来存储订阅者的,每次订阅前指向订阅者,订阅者置为null。

订阅者代码如下:

let uid = 0;
//订阅者类
export class Watcher{
  //构造器,vm是vue实例
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.id = uid++;
    this.deps = [];
    if (typeof expOrFn === "function") {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.get();
  }

  //将订阅这添加到订阅中心
  get() {
    //订阅前,设置Dep.target变量,指向自身
    pushTarget(this)
    let value;
    const vm = this.vm;
    /**
     * 这个地方读取data属性,触发下面的订阅代码,
     *  if (Dep.target) {
     *      dep.depend();
     *     if (childOb) {
     *       childOb.dep.depend();
     *     }
     *   }
     *   return value;
     **/
    value = this.getter.call(vm, vm);
    //订阅后,置Dep.target为null
    popTarget();
    return value
  }

  //值变化,调用回调函数
  update() {
    this.cb(this.value);
  }

  //添加依赖
  addDep(dep) {
    this.deps.push(dep);
    dep.addSub(this);
  }
}

//解析类属性的路径,例如obj.sub.name,返回实际的值
export function parsePath (path){
  const segments = path.split(".");
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return;
      obj = obj[segments[i]];
    }
    return obj;
  }
}

除了发布订阅以外,双向绑定还需要编译dom。

dom编译和input绑定

主要实现两个功能:

1、将dom中的{{key}}元素替换为Vue中的属性。

2、检测带有v-model属性的input元素,添加input事件,有修改时,修改Vue实例的属性。

检测v-model,绑定事件的代码如下:

export function initModelMixin(Vue) {
    Vue.prototype._initModel = function () {
        if (this._dom == undefined) {
            if (this.$options.el) {
                let el = this.$options.el;
                let dom = document.querySelector(el);
                if (dom) {
                    this._dom = dom;
                } else {
                    console.error(`未发现dom: ${el}`);
                }
           } else {
               console.error("vue实例未绑定dom");
           }
        } 
        bindModel(this._dom, this);
    } 
}

//input输入框有V-model属性,则绑定input事件
function bindModel(dom, vm) {
    if (dom) {
        if (dom.tagName === "INPUT") {
            let attrs = Array.from(dom.attributes);
            attrs.map(item => {
                if (item.name === "v-model") {
                    let value = item.value;
                    dom.value = getValue(vm, value);
                    //绑定事件,暂不考虑清除绑定,因此删除dom造成的内存泄露我们暂不考虑,这些问题后续解决
                    dom.addEventListener("input", (event) => {
                        setValue(vm, value, event.target.value);
                    });
                }
            })
        }
        let children = Array.from(dom.children);
        if (children) {
            children.map(item => {
                bindModel(item, vm);
            });
        }
    }
}

替换dom中{{key}}类似的属性代码:

export function renderMixin(Vue) {
    Vue.prototype._render = function () {
        if (this._dom == undefined) {
            if (this.$options.el) {
                let el = this.$options.el;
                let dom = document.querySelector(el);
                if (dom) {
                    this._dom = dom;
                } else {
                    console.error(`未发现dom: ${el}`);
                }
           } else {
               console.error("vue实例未绑定dom");
           }
        } 
        replaceText(this._dom, this);
    } 
}

//替换dom的innerText
function replaceText(dom, vm) {
    if (dom) {
        let children = Array.from(dom.childNodes);
        children.map(item => {
            if (item.nodeType === 3) {
                if (item.originStr === undefined) {
                    item.originStr = item.nodeValue;
                }
                let str = replaceValue(item.originStr, function(key){
                    return getValue(vm, key);
                });
                item.nodeValue = str;
            } else if (item.nodeType === 1) {
                replaceText(item, vm);
            }
        });
    }
}

到此位置,就实现了双向绑定。

测试代码如下,因为我用webpack构建的前端项目,html模板如下:




  
  
  test
  
  


  
{{name}}

main.js代码:

import { Vue } from "../src/index";

let options = {
    el: "#app",
    data: {
        name: "xxx",
        age: 18
    },
    methods: {
        sayName() {
            console.log(this.name);
        }
    }
}


let vm = new Vue(options);

效果如下:



可以下载源码尝试,git项目地址:https://github.com/xubaodian/...

项目使用webpack构建,下载后先执行:

npm install

安装依赖后使用指令:

npm run dev

可以运行项目。

如有疑问,欢迎留言或发送邮件至472784995@qq.com。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100593.html

相关文章

  • Vue原理】VModel - 白话版

    摘要:执行的时候,会绑定上下文对象为组件实例于是中的就能取到组件实例本身,的代码块顶层作用域就绑定为了组件实例于是内部变量的访问,就会首先访问到组件实例上。其中的获取,就会先从组件实例上获取,相当于。 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得...

    keke 评论0 收藏0
  • 剖析Vue原理&实现双向绑定MVVM

    摘要:所以无需太过介怀是实现的单向或双向绑定。监听数据绑定更新函数的处理是在这个方法中,通过添加回调来接收数据变化的通知至此,一个简单的就完成了,完整代码。 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也...

    melody_lql 评论0 收藏0
  • 剖析Vue实现原理 - 如何实现双向绑定mvvm(转载)

    摘要:接下来要看看这个订阅者的具体实现了实现订阅者作为和之间通信的桥梁,主要做的事情是在自身实例化时往属性订阅器里面添加自己自身必须有一个方法待属性变动通知时,能调用自身的方法,并触发中绑定的回调,则功成身退。 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,...

    nemo 评论0 收藏0
  • Vue原理】VModel - 源码版 之 表单元素绑定流程

    摘要:首先,兄弟,容我先说几句涉及源码很多,篇幅很长,我都已经分了上下三篇了,依然这么长,但是其实内容都差不多一样,但是我还是毫无保留地给你了。 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也...

    sarva 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<