Vue响应式原理及双向数据绑定示例分析
作者:一拳不是超人
前言
之前公司招人,面试了一些的前端同学,因为公司使用的前端技术是Vue
,所以免不了问到其响应式原理和Vue
的双向数据绑定。但是这边面试到的80%的同学会把两者搞混,通常我要是先问响应式原理再问双向数据绑定原理,来面试的同学大都会认为是一回事,那么这里我们就说一下二者的区别。
响应式原理
是Vue的核心特性之一,数据驱动视图,我们修改数据视图随之响应更新,就很优雅~
Vue2.x
是借助Object.defineProperty()
实现的,而Vue3.x
是借助Proxy
实现的,下面我们先来看一下2.x的实现。
Object.defineProperty(obj, key, { enumerable: true, configurable: true, //拦截get,当我们访问data.key时会被这个方法拦截到 get: function getter () { //我们在这里收集依赖 return obj[key]; }, //拦截set,当我们为data.key赋值时会被这个方法拦截到 set: function setter (newVal) { //当数据变更时,通知依赖项变更UI } })
我们通过Object.defineProperty
为对象obj
添加属性,可以设置对象属性的getter
和setter
函数。之后我们每次通过点语法获取属性都会执行这里的getter
函数,在这个函数中我们会把调用此属性的依赖收集到一个集合中 ;而在我们给属性赋值(修改属性)时,会触发这里定义的setter
函数,在次函数中会去通知集合中的依赖更新,做到数据变更驱动视图变更。
3.x的与2.x的核心思想一致,只不过数据的劫持使用Proxy
而不是Object.defineProperty
,只不过Proxy相比Object.defineProperty在处理数组和新增属性的响应式处理上更加方便。
let nObj=new Proxy(obj,{ //拦截get,当我们访问nObj.key时会被这个方法拦截到 get: function (target, propKey, receiver) { console.log(`getting ${propKey}!`); return Reflect.get(target, propKey, receiver); }, //拦截set,当我们为nObj.key赋值时会被这个方法拦截到 set: function (target, propKey, value, receiver) { console.log(`setting ${propKey}!`); return Reflect.set(target, propKey, value, receiver); } })
Proxy
的详细使用方法参考ES6教程。
Vue
的响应式原理的实现细节相信大多数同学已经很熟悉了,这里就不在展开细谈了,如果还想更详细的了解,或者想要做一个简易的Vue
实现,可以参考这篇Vue原理,相信你会有不小收获。
双向数据绑定
双向数据绑定通常是指我们使用的v-model
指令的实现,是Vue
的一个特性,也可以说是一个input
事件和value
的语法糖。 Vue
通过v-model
指令为组件添加上input
事件处理和value
属性的赋值。
<template> <input v-model='localValue'/> </template>
上述的组件就相当于如下代码
<template> <!-- 这里添加了input时间的监听和value的属性绑定 --> <input @input='onInput' :value='localValue' /> <span>{{localValue}}</span> </template> <script> export default{ data(){ return { localValue:'', } }, methods:{ onInput(v){ //在input事件的处理函数中更新value的绑定值 this.localValue=v.target.value; console.log(this.localValue) } } } </script>
<template> <div> <input @input='onInput' :value='localValue' /> <span>{{localValue}}</span> </div> </template> <script> // import Vue from 'vue'; export default{ data(){ return { localValue:'hello', } }, methods:{ onInput(v){ this.localValue=v.target.value; console.log(this.localValue) } } } </script> <style> .count { color: red; } </style>
因此当我们修改input输入框中的值时,我们通过v-model绑定的值也会同步修改,基于上述原理,我们可以很容易的实现一个数据双向绑定的组件。
v-model实践
首先我们定义一个Vue
组件,相信大家已经很熟悉了。
<tempalte> <div class="count" @click="addCount">click me {{value}}</div> </template> <script> export default{ props:{ //关键的第一步:设置一个value属性 value:{ type:Number, default:0 } }, watch:{ //监听value变化,更新组件localvalue状态 value(v){ this.localvalue=v; } }, methods:{ //关键的第二步:事件触发localvalue变更,通过事件同步父组件状态变更 addCount(){ this.localvalue++; this.$emit('input',this.localvalue); } }, data(){ return{ //组件状态,遵守单项数据流原则,不直接修改props中的属性 localvalue:0 } }, created(){ //初始化获取value值 this.localvalue=this.value; } } </script>
上面的组件定了我们通过在props
中添加value
属性,并且在值更新时触发input
事件。created
钩子和watch
中为localvalue
赋值是为了同步父组件状态到子组件中。
通过上面👆的组件定义,我们就可以在组件上使用v-model
指令做双向数据绑定了。
<template> <add-one v-model="count"></add-one> <span>父组件{{count}}</span> </tempalte> <script> export default{ data() { return { count: 0, }; }, methods: { }, created(){ } } </script>
下面是实际效果
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js' Vue.directive('mymodel', { bind(el, binding, vnode, oldVnode) { //组件和原生input标签需要分别处理, el.value = binding.value; if (vnode.tag == 'input') { //监听绑定的变量 vnode.context.$watch(binding.expression, (v) => { el.value = v; }) //添加inout事件监听 el.addEventListener('input', (e) => { //context是input所在的父组件,这一步是同步数据 vnode.context[binding.expression] = e.target.value; }) } else { //组件 //vnode的结构可以参见文档。不过我觉得最直观的方法就是直接在控制台打印处理 let { componentInstance, componentOptions, context } = vnode; const { _props } = componentInstance; //处理model选项 if (!componentOptions.Ctor.extendOptions.model) { componentOptions.Ctor.extendOptions.model = { value: 'value', event: 'input' } } let modelValue = componentOptions.Ctor.extendOptions.model.value; let modelEvent = componentOptions.Ctor.extendOptions.model.event; //属性绑定,这里直接修改了属性,没有想到更好的办法,友好的意见希望可以提出 console.log(binding) _props[modelValue] = binding.value; context.$watch(binding.expression, (v) => { _props[modelValue] = v; }) //添加事件处理函数,做数据同步 componentInstance.$on(modelEvent, (v) => { context[binding.expression] = v; }) } }, inserted() {}, update() {}, componentUpdated() {}, unbind() {}, }) Vue.component('add-one', { template: '<div class="count" @click="addCount">click me {{localvalue}}</div>', props: { value: { type: Number | String, default: -1 } }, //自定义value和事件 // model: { // value: 'count', // event: 'change' // }, watch: { //监听value变化,更新组件localvalue状态 value(v) { this.localvalue = v; } }, methods: { //事件触发localvalue变更,通过事件同步父组件状态变更 addCount() { this.localvalue++; this.$emit('input', this.localvalue); } }, data() { return { localvalue: 0 } }, created() { //初始化获取value值 console.log(this.value) this.localvalue = this.value; } }) new Vue({ el: '#app', data() { return { count: 0, }; }, methods: {}, created() { } })
当然我们也可以不使用value
和input
事件这样的组合,为了更使得组件的定义更加符合语义,我们也可以自定义要实现双向绑定的属性和事件。 我们在组件的model
选项中设置value
和event
即可。如下:
export default{ //这里做了一个value和event的映射 model:{ value:'count', event:'change' }, props:{ //关键的第一步:设置一个value属性 count:{ type:Number, default:0 } }, methods:{ //关键的第二步:事件触发localvalue变更,通过事件同步父组件状态变更 addCount(){ this.localvalue++; this.$emit('change',this.localvalue); } }, }
通过上面的组件定义
<add-one v-model="count"></add-one>
就相当于
<template> <add-one @change='onChange' :count='count'></add-one> <span>{{count}}</span> </template> <script> export default{ data(){ return { count:0, } }, methods:{ onChange(v){ this.count=v; console.log(this.count) } } } </script>
只不过v-model
指令帮我们做上面的事件添加,属性绑定和状态同步操作罢了。
以上就是Vue响应式原理及双向数据绑定示例分析的详细内容,更多关于Vue响应式双向数据绑定的资料请关注脚本之家其它相关文章!