vue的响应式原理

Vue 的响应式是通过 Object.defineProperty 对数据进行劫持,并结合发布订阅者模式实现。 Vue 利用 Object.defineProperty 创建一个 observe 来劫持监听所有的属性,把这些属性全部转为 gettersetter。Vue 中每个组件实例都会对应一个 watcher 实例,它会在组件渲染的过程中把使用过的数据属性通过 getter 收集为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
vue的响应式原理_第1张图片

一、reduce

1.数值的累加

作用:将****前一项**后一项****的值进行运算,返回累积的结果

格式:数组.reduce(function(prev,next){…})

其中,prev表示前一项,next表示后一项。

运算规则:

默认情况下,会把数组的第一个元素作为prev的初始值。

每循环一次,把累积的结果赋给prev,next就变为下一个数组元素

var arr3 = [10,22,23,25,50];
	
  const total = arr3.reduce(function(pre,next){
		console.log(pre+"----"+next);
		return pre+next;
	})
	console.log(total);

实际上,reduce方法还有第二个参数,****如果传递了第二个参数,就作为prev的初始值****。同时next就是数组的第一个元素。


2.链式获取对象的值

const person={
		name:"尼古拉斯赵四",
		info:{
			address:{
				location:"东北铁岭",
				work:"二人转"
			}
		}
	}
	
 const arrs=["info","address","location"];
	
  const result=arrs.reduce((newobj,k)=>{
		console.log(newobj)
		return newobj[k]
		
	},person)
		
	console.log(result);
//如果不是一个数组而是一个字符串呢
const str="info.address.location";
//console.log(str.split("."))
 const result2 = str.split(".").reduce((newobj,k)=>{
	  return newobj[k];
},person)
console.log(result2);

二、Object.defineProperty

Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性

Object.defineProperty(obj, prop, desc)
  1. obj 需要定义属性的当前对象
  2. prop 当前需要定义的属性名
  3. desc 属性描述符

一般通过为对象的属性赋值的情况下,对象的属性可以修改也可以删除,但是通过Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性。

属性描述符

通过Object.defineProperty()为对象定义属性,有两种形式,且不能混合使用,分别为数据描述符,存取描述符,下面分别描述下两者的区别:

数据描述符 --特有的两个属性(value,writable)
let Person = {}
Object.defineProperty(Person, 'name', {
   value: 'Lucky',
   writable: true // 是否可以改变
})
存取描述符 --是由一对 getter、setter 函数功能来描述的属性

get:一个给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为undefined
set:一个给属性提供setter的方法,如果没有setter则为undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined

let Person = {}
let temp = null
Object.defineProperty(Person, 'name', {
  get: function () {
    return temp
  },
  set: function (val) {
    temp = val
  }
})
数据描述符和存取描述均具有以下描述符
  1. configrable 描述属性是否配置,以及可否删除
  2. enumerable 描述属性是否会出现在for in 或者 Object.keys()的遍历中

**注意:**configurable: false 时,不能删除当前属性,且不能重新配置当前属性的描述符(但是可以把writable的状态由true改为false,但是无法由false改为true),但是在writable: true的情况下,可以改变value的值。

其他属性:Object.seal()、Object.freeze()不再说。

configurable: true时,可以删除当前属性,可以配置当前属性所有描述符。

var fun={
		name:"kelly",
		age:'123'
	}
	var funage='哈哈';
	Object.defineProperty(fun,'address',{
		get(){
			return funage;
		},
		set(val){
			console.log('触发fun')
			funage=val; 
		}
	})
	// fun.name="张三";
	// console.log(fun);
	// fun.age=12;
	// console.log(fun.age)
	fun.address="中山西路"
	console.log(fun.address)	 

三、发布-订阅者模式

1.vue响应原理:

vue.js采用数据劫持结合发布-订阅者模式,通过Object.defineProperty()来劫持data中各个属性的setter、getter,在数据变动时,发布消息给订阅者,触发响应的监听回调。

(setter和getter是对象的存储器属性,是一个函数(属性),用来获取和设置值)

2、发布-订阅者模式的作用:

处理一对多的场景,应用于不同情况下的不同函数调用

优点:低耦合性,易于代码维护;

缺点:若订阅的消息未发生,需消耗一定的时间和内存。

发布订阅者模式:

其定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知

vue的响应式原理_第2张图片

举个栗子

这里面以微信公众号为例:

  • 假如用户大脚订阅了 胡吃海喝这个公众号,那么当公众号胡吃海喝推送消息的时候,用户大脚就会收到相关的推送,点开可以查看推送的消息内容。
  • 但是公众号胡吃海喝并不关心订阅的它的是男人、女人还是未成年,它只负责发布自己的主体,只要是订阅公众号的用户均会收到该消息。
  • 作为用户大脚,不需要时刻打开手机查看公众号胡吃海喝是否有推动消息,因为在公众号推送消息的那一刻,大脚就会收到相关推送。
  • 当然了,用户大脚如果不想继续关注公众号胡吃海喝,那么可以取消关注,取关以后,公众号胡吃海喝再推送消息,大脚就无法收到了。

还有一个生活中的栗子,就是买房子的情景。

vue的发布订阅模式可以用下图简单描述:

vue的响应式原理_第3张图片
接下来用代码实现简单的发布订阅者

//收集依赖/收集订阅
  class Dep{
	  constructor(){
		  //这个subs数组,用来存放所有订阅者的信息
		  this.subs=[]
	  }
	  //向subs数组中,添加订阅者的信息
	  addSubs(watcher){
	  	  this.subs.push(watcher)
	  }
	  //发布通知的方法
	  notify(){
		 this.subs.forEach((watcher)=>watcher.update()) 
	  }
  }
  
  //订阅者的类
  class Watcher{
	  constructor(cb) {
	      this.cb=cb;
	  }
	//触发回调的方法
	  update(){
		  this.cb();
	  }
  }
  
  const w1=new Watcher(()=>{
	  console.log("我是第一个订阅者");
  })
  
  const w2=new Watcher(()=>{
	  console.log("我是第二个订阅者!")
  })
  
 const d1=new Dep();
 d1.addSubs(w1);
 d1.addSubs(w2);
 
 //只要我们为vue中的数据重新赋值了,这个赋值的操作,会被vue监听到
 //然后vue把数据的变化,通知到每个订阅者!!
 //接下来,订阅者(DOM元素)要根据最新的数据,更新自己的内容
 //1.谁是订阅者  2.为什么要订阅
 d1.notify();		

四、简单实现双向数据绑定

1.创建一个js文件,vue.js

class Vue {
  constructor(options) {
    this.$data = options.data

    // 调用数据劫持的方法
    Observe(this.$data)

    // 属性代理
    Object.keys(this.$data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this.$data[key]
        },
        set(newValue) {
          this.$data[key] = newValue
        },
      })
    })

    // 调用模板编译的函数
    Compile(options.el, this)
  }
}

// 定义一个数据劫持的方法
function Observe(obj) {
  // 递归终止条件
  if (!obj || typeof obj !== 'object') return
  const dep = new Dep()

  // 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
  Object.keys(obj).forEach((key) => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key]
    // 把 value 这个子节点,进行递归
    Observe(value)
    // 需要为当前的 key 所对应的属性,添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
        // 就被放到了 dep.subs 这个数组中了
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newVal) {
        value = newVal
        Observe(value)
        // 通知每一个订阅者更新自己的文本
        dep.notify()
      },
    })
  })
}

// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el)

  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment()

  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }

  // 进行模板编译
  replace(fragment)

  vm.$el.appendChild(fragment)

  // 负责对 DOM 模板进行编译的方法
  function replace(node) {
    // 定义匹配插值表达式的正则
    const regMustache = /\{\{\s*(\S+)\s*\}\}/

    // 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
    if (node.nodeType === 3) {
      // 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
      const text = node.textContent
      // 进行字符串的正则匹配与提取
      const execResult = regMustache.exec(text)
      console.log(execResult)
      if (execResult) {
        const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
        node.textContent = text.replace(regMustache, value)
        // 在这个时候,创建 Watcher 类的实例
        new Watcher(vm, execResult[1], (newValue) => {
          node.textContent = text.replace(regMustache, newValue)
        })
      }
      // 终止递归的条件
      return
    }

    // 判断当前的 node 节点是否为 input 输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
      // 得到当前元素的所有属性节点
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find((x) => x.name === 'v-model')
      if (findResult) {
        // 获取到当前 v-model 属性的值   
        const expStr = findResult.value
        const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
        node.value = value

        // 创建 Watcher 的实例
        new Watcher(vm, expStr, (newValue) => {
          node.value = newValue
        })

        // 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
        node.addEventListener('input', (e) => {
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
          const leafKey = keyArr[keyArr.length - 1]
          obj[leafKey] = e.target.value
        })
      }
    }

    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}

// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 所有的 watcher 都要存到这个数组中
    this.subs = []
  }

  // 向 subs 数组中,添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 负责通知每个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}

// 订阅者的类
class Watcher {
  // cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
  //   但是,只知道如何更新自己还不行,还必须拿到最新的数据,
  //    需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
  //   必须在 new Watcher 期间,指定 watcher 对应的数据的名字
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb

    // 把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 
    Dep.target = this
    key.split('.').reduce((newObj, k) => newObj[k], vm)
    Dep.target = null
  }

  // watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
  update() {
    const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
    this.cb(value)
  }
}



2.在页面中引入vue.js 然后实例化一个实例

<script src="./vue.js"></script>
	</head>
	<body>
		<div id="app">
			<p>姓名:{{name}}</p>
			<p>年龄:{{age}}</p>
			<p>工作:{{info.work}}</p>
			<p>住址:{{info.address}}</p>
			<input type="text" v-model="phone" />
			<p>双向绑定的iPhone:{{phone}}</p>
		</div>
	<script>
		const vm=new Vue({
			el:"#app",
			data:{
				name:"海绵宝宝",
				age:3,
				info:{
					work:"高级厨师",
					address:"太平洋比奇堡"
				},
				phone:'123'
			}
		})
	</script>	

vue的响应式原理_第4张图片

你可能感兴趣的:(vue基本知识,vue,面试)