vue双向绑定原理

438 阅读10分钟

总结这块:断断续续花了1个多月的时间才成型最终稿

先申明:图都是盗的,为了节省时间没自己画

MVVM和MVM

blog.csdn.net/u014346301/…

  1. MVC —— Model-View-Controller的缩写

工作原理:用户发送事件的时候,发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上

View——毫无疑问是操作视图,比如点击、输入,用户发送事件的时候,发送指令到controller层

Model——包含api(网络请求相关、对数据库、缓存操作)和变量

Controller——对于数据逻辑的操作,包含对View的操作,又包含调用Model的变量或者函数

结合自己的经验:当时写ios逻辑的时候,就是用MVC的思想, 比如(举例代码语法不一定对,只是象征性的举例): 在controller中

label.text = 'hello'btn.click = function(){}
这就是操作view,还可以调用model中的函数来更新view中的值

想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,这样都没法写在view层,而是写在controller层,如果逻辑很复杂,这样controller层的代码非常多甚至混乱。由此这是它很明显的缺点,总结如下:

  • 在controller和view逻辑不能分离

前端上有这方面的实例吗?

2 . MVVM

结合MVC的缺点,所以变种另一种思想,得出MVVM,它最早由微软提出的。

工作原理:view层和model层完全解耦,view层发出的事件传递到VM层中,VM层去操作model层,并且将数据返回给view层

View —— 视图中

viewModel —— 调用model中的函数,可以调用view中的函数,把数据传给view

Model —— 包含api(网络请求相关、对数据库、缓存操作)

  1. 双向绑定:数据更新视图,视图更新数据

MVVM怎么实现数据双向绑定?

通过Object.defineProperty( )对属性设置一个set函数,把更新方法放到此处。

双向绑定

View更新变化Data,可以通过事件监听的方式来实现,比如input,checkbox,select;

Data更新变化View,重点是如何知道数据变了,通过几个步骤:

1)实现一监听器Observer:劫持和监听data属性值,当属性发生变化时(set中通过新值和旧值比较判断是否变化),调用dep.notify();

我的剖析总结:
- 遍历data对象,每个属性会实例化订阅器var dep = new Dep(),如果是对象遍历对象
- 初始化订阅器dep,每个属性实例化一个订阅器dep
- get————收集订阅者Watcher,dep.addSub(Dep.target)
想到Watcher实例怎么赋值到Dep.target上? 订阅器为什么在get中收集订阅者?

由此联想到data为什么要返回对象?

原因:对象为引用类型,当重用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题

  // 1.对象方式(所有重用的实例中的data均为同一个对象)
  var data = {
    x: 1
  }
  var vm1 = {
    data: data
  }
  var vm2 = {
    data: data
  }
  vm1.data === vm2.data // true,指向同一个对象
 
  // 2.函数方式(所有重用的实例中的data均为同一个函数)
  var func = function () {
    return {
      x: 1
    }
  }
  var vm3 = {
    data: func
  }
  var vm4 = {
    data: func
  }
  vm3.data() === vm4.data() // false,指向不同对象
  
  console.log({x:1} === {x:1} ) // false

  console.log(new Object({x:1}) === new Object({x:1}))  //false
  
  函数方式中data都指向同一个函数,但这个函数每次的返回值都是一个新的对象,所以都是false
  
  console.log(String({x:1})===String({x:1}))  //true

  console.log(({x:1}).toString()===({x:1}).toString())  //true
  
  var data = {
    x: 1
  }
  console.log(data === data )  //true

=== 涉及到类型转换,参考数据类型转换营

/**
 * 循环遍历数据对象的每个属性
 */
function observable(obj) {
   if (!obj || typeof obj !== 'object') {
       return;
   }
   let keys = Object.keys(obj);
   keys.forEach((key) => {
       defineReactive(obj, key, obj[key])
   })
   return obj;
}
/**
* 将对象的属性用 Object.defineProperty() 进行设置
*/
function defineReactive(obj, key, val) {
   Object.defineProperty(obj, key, {
       get() {
           console.log(`${key}属性被读取了...`);
           return val;
       },
       set(newVal) {
           console.log(`${key}属性被修改了...`);
           val = newVal;
       }
   })
}


defineReactive: function(data, key, val) {
   var dep = new Dep();
   Object.defineProperty(data, key, {
   	enumerable: true,
   	configurable: true,
   	get: function getter () {
   		if (Dep.target) {
   			dep.addSub(Dep.target);
   		}
   		return val;
   	},
   	set: function setter (newVal) {
   		if (newVal === val) {
   			return;
   		}
   		val = newVal;
   		dep.notify();
   	}
   });
}

2)实现一订阅者watcher:收到属性变化的通知,并且执行相应更新的方法,从而更新视图(怎么跟视图关联起来?)

我的剖析总结:
- 订阅者Watcher结构函数中参数包含:vue实例vm, data中的某一属性exp, 更新函数cb;
- 一个模板指令相对于一个订阅者Watcher;
- 触发监听器Observer里的get函数,将自己添加到订阅器Dep中; 
- 包含update更新函数; 
- 更新View界面;
function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this; // 全局变量 订阅者 赋值
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null; // 全局变量 订阅者 释放
        return value;
    }
};

3)实现一订阅器dep:收集订阅者watcher,统一管理监听器observer和订阅者watcher(怎么统一管理?也就是提供函数给监听器observer调用,并调用订阅者watcher中的函数)

我的剖析总结:
- 充当通知和收集的角色
- 通知函数notify和收集订阅者addSub都在订阅器Dep中定义,只不过在监听器observer中调用;
- 通知函数notify要遍历订阅者watcher,并调用订阅者watcher各自的更新函数sub.update()
function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;

4)实现一解析器compile:解析每个节点的相关指令,对模板数据和订阅器进行初始化(怎么初始化?)

我的剖析总结:
- 做解析dom节点和绑定工作;
- 解析模板指令,并替换模板数据,初始化视图;
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅者Watcher;

为了解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,因此这个环节需要对dom操作比较频繁,所有可以先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理:

function nodeToFragment (el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
        // 将Dom元素移入fragment中
        fragment.appendChild(child);
        child = el.firstChild
    }
    return fragment;
}

接下来需要遍历各个节点,对含有相关指定的节点进行特殊处理,这里咱们先处理最简单的情况:

对 '{{变量}}' 这种形式的指令处理的关键代码进行分析
el为节点,比如跟节点#app
new Vue({
    el: '#app'
}

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/;
        var text = node.textContent;
 
        if (self.isTextNode(node) && reg.test(text)) {  
            // 判断是否符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
 
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    // 将这个指令exp初始化为一个订阅者Watcher并绑定更新函数,
    //后续 exp 改变时,就会触发这个更新回调,从而更新视图
    new Watcher(this.vm, exp, function (value) { 
    // value在Watcher中传入
        self.updateText(node, value);  
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

参考:

www.cnblogs.com/canfoo/p/68… (vue的双向绑定原理及实现)

juejin.cn/post/684490…

面试回答:通过MVVM的思想实现双向绑定。

Proxy

用于修改某些操作的默认行为,在对象之前架设一层"拦截",可以对外界的访问进行过滤和改写

let proxy = new Proxy({},{
    get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
})
  1. proxy支持13种拦截操作;
  2. Proxy可以直接监听对象而非属性,直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty;
  3. Proxy可以直接监听数组的变化;

definePrototype

Object.defineProperty(obj, prop, descriptor)

缺陷:

  1. 无法监听数组变化 vue中通过以下8种方法监听,但不能通过vm.items[indexOfItem] = newValue监听 push()、 pop()、 shift()、 unshift()、 splice()、 sort()、 reverse()
  2. 只能劫持对象的属性,需要深度遍历对象

双向绑定

主要实现以下四个:

  • 监听器Observer 对data数据对象变得“可监测”,vue2.0用Object.defineProperty()来劫持各个数据属性的 setter / getter;

  • 监听器 Observer ,用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者

  • 订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理

  • 订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图

  • 解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化

vue数据双向绑定的原理:vue内部把data对象里每个数据的读写转化为getter、setter,当数据变化时通知视图更新。 主要通过监听器observer、订阅器dep、订阅者watcher、解析器compile,这4个步骤实现

自己口头描述:双向绑定主要需要实现observer、dep、watcher、compile,他们分别的功能是。。。 首先要定义observer,对data中属性的setter / getter进行拦截,compile解析模板中指令,替换模板数据,初始化视图,初始化订阅者;watcher相当于每个指令,触发getter,添加到dep中;dep收集watcher,当收到属性变化通知时,通知更新函数。(我觉得赋值是在代码中赋值,就能监听到set)

口述:双向绑定原理 get :获取订阅者watcher (买家,一个订阅者相当于一个指令) set: 通知订阅器(中介),调用notify,通知订阅者watcher更新函数

源码加强分析

注意:每个data对象的属性的 getter 都持有一个 dep;源码中收集watcher会有判断保证同一数据不会被添加多次

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

这时候会做一些逻辑判断(保证同一数据不会被添加多次)后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。

生成顺序

data的初始化是在created时已经完成数据观测; props => methods =>data => computed => watch

自己构造的方法与vue生命周期的运行顺序

往往我们在开发项目时都经常用到 $refs 来直接访问子组件的方法,但是这样调用的时候可能会导致数据的延迟滞后的问题,则会出现bug。

解决方法则是推荐采取异步回调的方法,然后传参进去,严格遵守vue的生命周期就可以解决 推荐 es6 的promise。

handleAsync () {
    return new Promise(resolve=>{
       const res="";
        resolve(res)
	})
}
async handleShow() {
    await this.handleAsync().then(res=>{
    this.$refs.child.show(res);
})
}

vue2.0怎么对数组和对象拦截

数组:observe 方法 对象:遍历对象的key调用 defineReactive 方法,

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter,递归调用observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。

vue3.0的好处

juejin.cn/post/684490…

虚拟dom

juejin.cn/post/684490…

参考链接

www.jianshu.com/p/2df6dcddb…

www.cnblogs.com/canfoo/p/68…

juejin.cn/post/684490…github.com/fengshi123/…