重要的vue响应式原理(数据双向绑定)

   vue响应式的实现主要是通过数据拦截(拦截数据的set和get)以及发布者订阅者模式。主要有observer,dep,watcher这几个模块。首先创建vue实例时,会传入一个对象,内部包含el,data, method, computed等属性。

observer是咋来的呢?

  observer主要是进行数据拦截,用defineProperty递归拦截传入对象的data属性的set和get,以确保每个对象的set,get都被拦截。

  observer.js代码:

export class Observer {
  constructor(obj) {
    this.obj = obj;
    this.transform(obj);
  }
  // 将 obj 里的所有层级的 key 都用 defineProperty 重新定义一遍, 使之 reactive 
  transform(obj) {
    const _this = this;
    for (let key in obj) {
      const value = obj[key];
      makeItReactive(obj, key, value);
    }
  }
}
function makeItReactive(obj, key, val) {
  // 如果某个 key 对应的 val 是 object, 则重新迭代该 val, 使之 reactive 
  if (isObject(val)) {
    const childObj = val;
    new Observer(childObj);
  }
  // 如果某个 key 对应的 val 不是 Object, 而是基础类型,我们则对这个 key 进行 defineProperty 定义 
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.info(`get ${key}-${val}`)
      return val;
    },
    set: (newVal) => {
      // 如果 newVal 和 val 相等,则不做任何操作(不执行渲染逻辑)
      if (newVal === val) {
        return;
      }
      // 如果 newVal 和 val 不相等,且因为 newVal 为 Object, 所以先用 Observer迭代 newVal, 使之 reactive, 再用 newVal 替换掉 val, 再执行对应操作(渲染逻辑)
      else if (isObject(newVal)) {
        console.info(`set ${key} - ${val} - ${newVal} - newVal is Object`);
        new Observer(newVal);
        val = newVal;
      }
      // 如果 newVal 和 val 不相等,且因为 newVal 为基础类型, 所以用 newVal 替换掉 val, 再执行对应操作(渲染逻辑)
      else if (!isObject(newVal)) {
        console.info(`set ${key} - ${val} - ${newVal} - newVal is Basic Value`);
        val = newVal;
      }
    }
  })
}

function isObject(data) {
  if (typeof data === 'object' && data != 'null') {
    return true;
  }
  return false;
}

dep是咋来的呢?

   dep是订阅者,在observer阶段,每个从data里遍历出来的属性key都会创建新的dep实例,dep负责管理对应的订阅者,当key的get被某个watcher实例调用时,就将watcher加到对应dep实例创建的数组中,当set被调用时,就通知对应的dep,让dep通知对应的watcher实例更新。

watcher是咋来的呢?

  在compile解析el阶段,解析到代码中包含{{mustache}},v-model,v-bind,computed,watch的语法,都会生成一个watcher实例。这时dep.target会指向这个新生成的对象,就能把这个watcher添加到对应的dep中:dep.addSub(Dep.target)。

  watcher更新,执行各自的回调函数

代码(有些我也没看懂,先贴着慢慢看):

dep.js

export class Dep {
  constructor() {
    this.subs = [];
  }
  // 将 watcher 实例置入队列
  addSub(sub) {
    this.subs.push(sub);
  }
  // 通知队列里的所有 watcher 实例,告知该 key 的 对应的 val 被改变
  notify() {
    this.subs.forEach((sub, index, arr) => sub.update());
  }
}

// Dep 类的的某个静态属性,用于指向某个特定的 watcher 实例.
Dep.target = null
observer.js

import {Dep} from './dep'
function makeItReactive(obj, key, val) {
 var dep = new Dep()
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: () => {
    // 收集依赖! 如果该 key 被某个 watcher 实例依赖,则将该 watcher 实例置入该 key 对应的 dep 实例里
    if(Dep.target){
      dep.addSub(Dep.target)
    }
    return val
  },
  set: (newVal) => {
    if (newVal === val) {
      return;
    }
    else if (isObject(newVal)) {
      new Observer(newVal);
      val = newVal;
    // 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 实例发送通知
    dep.notify()
    }
    else if (!isObject(newVal)) {
      val = newVal;
    // 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 发送通知
    dep.notify()
    }
  }
})
     }    

watcher.js

import { Dep } from './Dep.js';

export class Watcher {
  constructor(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.value = this.get();
  }
  get() {
    // 在实例化某个 watcher 的时候,会将Dep类的静态属性 Dep.target 指向这个 watcher 实例
    Dep.target = this;
    // 在这一步 this.vm._data[this.expOrFn] 调用了 data 里某个 key 的 getter, 然后 getter 判断类的静态属性 Dep.target 不为null, 而为 watcher 的实例, 从而把这个 watcher 实例添加到 这个 key 对应的 dep 实例里。 巧妙!
    const value = this.vm._data[this.expOrFn];
    // 重置类属性 Dep.target 
    Dep.target = null;
    return value;
  }

  // 如果 data 里的某个 key 的 setter 被调用,则 key 会通知到 该 key 对应的 dep 实例, 该Dep实例, 该 dep 实例会调用所有 依赖于该 key 的 watcher 实例的 update 方法。
  update() {
    this.run();
  }
  run() {
    const value = this.get();
    if (value !== this.value) {
    this.value = value;
    // 执行 cb 回调
    this.cb.call(this.vm);
    }
  }
}

贴张示意图方便理解:

小栗子1:简单实现响应式

<input type = 'text' id = 'input'>
<p id = 'text'></p>

<script>
var oinput = document.getElementById('input')
var op = document.getElementById('text')
var obj = {}

Object.defineProperty(obj,'text',{
    get(e){
        console.log(e)
    }

    set(newValue){
        op.innerHTML = newValue
    }
}

oinput.onKeyUp = function(e){
     obj.text = event.target.value
}

小栗子二:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>双向绑定demo</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <div id="app">
            <div>双向绑定demo</div>
            <input type="text" id="input">
            <div>
				<span>input:</span>
				<span id="output"></span>
			</div>
        </div>
    </body>
    <script>
        var obj={}
        Object.defineProperty(obj,'input',{
            get:function(){
                return obj.input
            },
            set:function(value){
                document.getElementById('input').value = value
                document.getElementById('output').innerHTML = value
            }
        })
        
        //view->model
        document.addEventListener('keyup',function(e){
            //model->view
            obj.input = e.target.value
        })
    </script>
</html>

  由以上两个例子可以看出都是通过定义一个中间变量空的object对象,先对这个对象进行数据劫持,然后通过DOM元素的监听,将输入的值赋值给被数据劫持的obj对象的属性,实现model的数据改变

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值