前言
现在的面试中,只要技术栈中写到了Vue,面试官基本都会问一个点:“请说一下Vue实现数据相应式的原理~”,一些同学知道解决的核心步骤,一些同学知道数据响应式结合的设计模式,可如果要你直接思路清晰的手写一波,在某些方面可能还是会有些问题,所以今天不妨跟着我一起,看看我这版手写vue响应式的思路。
如果对您有所帮助,阔以 点波小赞 鼓励一下咯~ ~,当然,有写得不对的地方,请直接提出来,咱们共同进步!
1.什么是Vue的数据响应式
当一个 Vue 实例被创建时,data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
要理解Vue的响应式,一定要结合MVVM这种设计模式,Vue在基于MVVM的设计模式下,构建了一套响应式系统,如图所示:
model层(数据管理层)一般负责处理修改数据,并将发送数据变化发送至viewmodel层;<br>
viewmode层(之后简写为vm层) 一般用于
1. 接收viewmode层传递的数据,并渲染到视图层view中;
2. 监听view层用户操作后,改变的数据,并将model层中的数据修改,同时,触发view层有修改数据的视图更新
view层(视图层)一般负责渲染数据,提供用户交互,当用户操作后通知viewmode层,触发相应事件
复制代码
以上粗略的介绍了mvvm设计模式的三个模块,接下来将结合mvvm设计模式,为大家介绍手写实现Vue数据响应式的思路以及具体方法。
2.通过一个具体的需求实现Vue响应式
具体需求:
带者具体问题(非抽象)去开发,一般会有跟清晰的目击感,那我们先模拟一个初级的Vue的具体需求:
1.支持script标签中 vue的初始化(实例化),在传入根节点元素id和指定的data后,在body中通过v-model和v-text指令绑定的元素要有初始化的值显示出来。
2.见图二,区域二中通过v-model绑定的输入框修改值以后,对应区域一v-text指令绑定的对应值显示也要相应的改变。
实现思路及代码
针对上面的需求,我拆分成5个步骤来实现:
第1步 支持vue对象的实例化,包括绑定根节点元素;将创建vue实例时传入的data对象初始化成vue实例的上的data属性,并实现针对有v-text
、v-model
的元素进行依赖收集,获取到这些元素绑定的data属性。
1.1 思路分析:
第一步主要是构建vm
层,也就是实现vue的实例化;先将model
层的数据转化到vm
层,即将传入的data作为vue实例的参数$data
。
之后是对含v-text
、v-model
的元素进行依赖收集并渲染,这一步在设计模式上属于vm
层-view
层的初次渲染;同时要构建发布者-订阅模式,为之后vm-view
的动态发布更新做准备。
1.2 代码实现:
class Vue {
constructor(options) {
//绑定根节点元素
this.el = options.el;
if (this.el !== undefined || this.el !== "" || this.el !== null) {
this.rootnode = document.querySelector(this.el);
}
//将数据存入vm层
this.$data = options.data;
//依赖搜集(解析出v-model和v-text对应元素,并将data中对应绑定的那个元素值关联上)
//为了可以让根节点的各级子节点都能进行依赖搜集,采用递归传祖节点方式进行依赖搜集
this.complie(this.rootnode)
}
//对指令进行依赖收集
complie(el) {
let node = el;
if (node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
let childNode = node.children[i];
if (childNode.children.length > 0) {
//传入含有子节点的子节点元素
this.complie(childNode)
} else {
if (childNode.hasAttribute('v-text')) {
console.log(childNode, "通过v-text绑定了:", childNode.getAttribute('v-text'))
}
if (childNode.hasAttribute('v-model')) {
console.log(childNode, "通过v-model绑定了:", childNode.getAttribute('v-model'))
}
}
}
}
}
}
复制代码
1.3 步骤1执行结果:
实现了 model
–vm
,并完成了v-
指令的依赖搜集
第2步 通过观察者-订阅
模式将$data中的数据渲染到对应元素上
2.1 思路分析:这一步主要是将 vm
层中的数据渲染到view
层,其中需要建立观察者-订阅模式
,可以把Vue看成发布者,Watcher看成订阅者(负责view
层视图渲染)。
2.2 代码实现:
//发布者
class Vue {
constructor(options) {
//绑定根节点元素
this.el = options.el;
if (this.el !== undefined || this.el !== "" || this.el !== null) {
this.rootnode = document.querySelector(this.el);
}
//将数据存入vm层
this.$data = options.data;
//创建一个订阅者组成的容器,只要有元素用到了$data中的属性,
//那么就给容器中$data对应属性的那一项数组,添加一个订阅者,
//这样,整个页面只要用到了那个属性的,都能建立发布订阅,
//之后的步骤中只要vm中data一变,都能触发视图更新
this.$directive = {}
console.log(this.$data)
this.observer(this.$data)
console.log(this.$directive)
//依赖搜集(解析出v-model和v-text对应元素,并将data中对应绑定的那个元素值关联上)
//为了可以让根节点的各级子节点都能进行依赖搜集,采用递归传祖节点方式进行依赖搜集
this.complie(this.rootnode)
}
//主要用于数据劫持,并发布更新
observer(data) {
//初始化订阅者容器
console.log(data)
for (let key in data) {
this.$directive[key] = [];
}
}
//对指令进行依赖收集
complie(el) {
let node = el;
if (node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
let childNode = node.children[i];
if (childNode.children.length > 0) {
//传入含有子节点的子节点元素
this.complie(childNode)
} else {
if (childNode.hasAttribute('v-text')) {
let _this = this
this.$directive[childNode.getAttribute('v-text')].push(new Watcher({
el: childNode,
vm: _this,
attr: childNode.getAttribute('v-text'),
v_type: 'innerHTML'
}))
}
if (childNode.hasAttribute('v-model')) {
// console.log(childNode, "通过v-model绑定了:", childNode.getAttribute('v-model'))
let _this = this;
this.$directive[childNode.getAttribute('v-model')].push(new Watcher({
el: childNode,
vm: _this,
attr: childNode.getAttribute('v-model'),
v_type: 'value'
}))
}
}
}
}
}
}
//订阅者(主要用于更新view视图层)
class Watcher {
constructor(options) {
this.el = options.el;
this.vm = options.vm;
this.attr = options.attr;
this.v_type = options.v_type;
this.update();
}
//依据传入的元素,元素对应绑定的值,渲染到视图层
//订阅者不负责判断数据是否改变等,只负责渲染数据到视图层
update() {
// this.vm.$data[this.attr]
let {
el,
vm,
attr,
v_type
} = this;
console.log(vm, attr, el, v_type, vm.$data[attr])
el[v_type] = vm.$data[attr];
}
}
复制代码
2.3 步骤2 执行结果:
使页面能够在vue实例创建的时候将绑定的数据初始化渲染到视图层上。
第3步 在用户通过交互改变输入框数据之后,使视图层数据同步发生改变
3.1 思路分析:
这一步从设计模式的角度应该这样看:
1.首先监听用户在`view`视图层的交互,触发的钩子函数会直接改变`vm`实例中的$data值;
2.在vm层中通过数据劫持,监听到$data值改变时,触发订阅者的update方法,实现视图层对应数据的更新。
复制代码
3.2 代码实现:
//发布者
class Vue {
constructor(options) {
//绑定根节点元素
this.el = options.el;
if (this.el !== undefined || this.el !== "" || this.el !== null) {
this.rootnode = document.querySelector(this.el);
}
//将数据存入vm层
this.$data = options.data;
//创建一个订阅者组成的容器,只要有元素用到了$data中的属性,那么就给容器中$data对应属性的那一项数组,添加一个订阅者,这样,整个页面只要用到了那个属性的,都能建立发布订阅,之后的步骤中只要vm中data一变,都能触发视图更新
this.$directive = {}
this.observer(this.$data)
//依赖搜集(解析出v-model和v-text对应元素,并将data中对应绑定的那个元素值关联上)
//为了可以让根节点的各级子节点都能进行依赖搜集,采用递归传祖节点方式进行依赖搜集
this.complie(this.rootnode)
}
//主要用于数据劫持,并发布更新
observer(data) {
//初始化订阅者容器
for (let key in data) {
this.$directive[key] = [];
let value = data[key];
let _this = this;
Object.defineProperty(this.$data, key, {
get: function() {
return value
},
set: function(newValue) {
if (newValue !== value) {
value = newValue;
//触发跟这个数据绑定的所有订阅者的视图更新方法
_this.$directive[key].forEach((item) => {
item.update()
})
}
}
})
}
}
//对指令进行依赖收集
complie(el) {
let node = el;
if (node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
let childNode = node.children[i];
if (childNode.children.length > 0) {
//传入含有子节点的子节点元素
this.complie(childNode)
} else {
if (childNode.hasAttribute('v-text')) {
// console.log(childNode, "通过v-text绑定了:", childNode.getAttribute('v-text'))
let _this = this;
this.$directive[childNode.getAttribute('v-text')].push(new Watcher({
el: childNode,
vm: _this,
attr: childNode.getAttribute('v-text'),
v_type: 'innerHTML'
}))
}
if (childNode.hasAttribute('v-model')) {
this.$directive[childNode.getAttribute('v-model')].push(new Watcher({
el: childNode,
vm: this,
attr: childNode.getAttribute('v-model'),
v_type: 'value'
}))
//监听视图层的交互,让vm层中绑定的数据改变
childNode.addEventListener('input', (function() {
return function(val) {
console.log(val)
this.$data[childNode.getAttribute('v-model')] = val.target.value;
}
})().bind(this))
}
}
}
}
}
}
//订阅者(主要用于更新view视图层)
class Watcher {
constructor(options) {
this.el = options.el;
this.vm = options.vm;
this.attr = options.attr;
this.v_type = options.v_type;
this.update();
}
//依据传入的元素,元素对应绑定的值,渲染到视图层(订阅者不负责判断数据是否改变等,只负责渲染数据到视图层)
update() {
let {
el,
vm,
attr,
v_type
} = this;
console.log(vm, attr, el, v_type, vm.$data[attr])
el[v_type] = vm.$data[attr];
}
}
复制代码
3.3 代码实现结果
如下图所示,经过了第三步的开发后,已经实现通过监听view
层交互,修改vm
层中$data
的值,并在$data
修改后自动渲染到view
视图层中。在区域2输入框输入值时,区域1绑定的这个值也会同步改变。
3.实现vue响应式思路总结
这次实现主要是结合MVVM
的设计模式,总的说来分为以下几步;
1.构建vm层,实例化时将model层的数据转化到vm层,即将传入的data作为vue实例的参数$data。
2.实现`vm层-view层`中的渲染(初次时通过对含v-text、v-model的元素进行依赖收集并渲染);
3.vm层中的数据渲染到view层,其中需要建立观察者-订阅模式,可以把Vue看成发布者,Watcher看成订阅者(负责view层视图渲染)。
4.监听用户在`view`视图层的交互,触发的钩子函数会直接改变`vm`实例中的$data值;
5.在vm层中通过数据劫持,监听到$data值改变时,触发订阅者的update方法,实现视图层对应数据的更新。
进而最终实现了实例化Vue对象,根节点子元素中v-model、v-text指令以及model-vm-view的数据双向绑定,即数据响应式
复制代码
以上就是本文的全部内容,如果觉得写得还阔以,请高抬贵手,点波小赞咯~ ~