vue双向绑定原理
object.defineProperty() —vue2
这个API是实现双向绑定的核心,通过它对data的属性进行重写,主要是重新数据的get、set方法。
调用get方法时,绑定对应的依赖(dep)和监听器(watcher);更新数据时,调用set方式时,会通知依赖中相关的监听进行值修改。
不足之处:
-
不能检测到对象属性的增加和删除,即新增属性不是响应式的
解决方法: vm.$set -
不能通过索引操作数组
proxy —vue3
proxy可以理解成在目标对象之前架设了一层"拦截",外界对该对象的访问都需要通过这层拦截,所以就可以利用这层拦截对外界访问进行过滤和改写。
proxy的优点:
-
可以用来处理一些非核心的逻辑,从而让对象关注于核心逻辑,达到关注点分离,降低对象复杂度等目的
-
可以劫持整个对象,并返回一个新对象(弥补了vue2中的不足1)
-
proxy有13中劫持操作
-
get、set设置时不能设置writable和value,交叉设置或同时存在,会报错
-
通过defineProperty设置的属性,默认不能删除,不能遍历,当然你可以通过设置进行更改
-
get、set是函数,可以在里面进行各种操作
实现思路的关键步骤:
-
实现数据监听器observer,用Object.defineProperty()重写数据的get、set,值更新就在set中通知订阅者更新数据
-
实现模板编译Compile,深度遍历dom树,对每个元素节点的指令模板进行替换数据以及订阅数据。
-
实现Watch用于连接Observer和Compile,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
-
mvvm入口函数,整合以上三者
流程图
以下为手动实现简易vue的代码:
<body>
<div id="app">
{{msg}}
<h1>{{msg}}</h1>
<div v-html="htmltxt" class='abc' @click='fn'></div>
<div v-text='htmltxt' class='123'></div>
</div>
<script src="./qiyuVue.js"></script>
<script>
let app = new Vue({
el: "#app",
data: {
msg: "offer",
htmltxt: '<img src="https://www.baidu.com/img/flexible/logo/pc/result.png">'
},
methods: {
fn(e){
console.log(e)
}
}
})
</script>
</body>
qiyuvue.js文件
class Vue{
constructor(options){
let vm = this
this.$options = options
this.$data = this.$options.data
// 将data中的属性通过代理绑定到实例上
Object.keys(this.$data).forEach((key)=>{
this._proxy(key)
})
// 劫持data中的属性,当值发生改变的时候,会重新更新依赖的节点
new Observer(this.$data, vm)
this.$compile = new Compile(options.el, vm)
}
_proxy(key){
let vm = this
Object.defineProperty(vm, key, {
enumerable: true,
get: ()=>vm.$data[key],
set: (value)=> vm.$data[key] = value
})
}
}
// 创建编译类
class Compile{
constructor(el, vm){
this.$vm = vm;
// 根据选择器查找出需要解析编译的元素
this.$el = document.querySelector(el);
if(this.$el){
// 1、将元素节点取出
this.$fragment = this.createFragmentObj(this.$el)
console.log(this.$fragment)
// 2、生成相应的dom对象
this.createVdom(this.$fragment)
// 3、将生成的dom插入到页面中
this.$el.appendChild(this.$fragment)
}
}
createFragmentObj(el){
// 创建虚拟节点对象,节点对象包含节点的属性和方法
let fragment = document.createDocumentFragment();
let child;
while(child = el.firstChild){
fragment.appendChild(child)
}
console.log(fragment)
return fragment;
}
createVdom(fragment){
let childNodes = fragment.childNodes;
Array.prototype.slice.call(childNodes).forEach(childNode =>{
// 1、取出文本节点
let text = childNode.textContent;
// 2、匹配大括号
let reg = /\{\{(.*)\}\}/;
// 3、根据节点类型进行编译,3代表节点类型是文本,1代表节点类型是元素
if(childNode.nodeType === 3 && reg.test(text)){
this.compileText(childNode, RegExp.$1)
}else if(childNode.nodeType === 1 && !childNode.hasChildNodes()){
// childNode.hasChildNodes()判断是否有子节点,没有的话编译其innerHrml,有的话递归生成vdom
this.compileInnerHTML(childNode)
}else if(childNode.nodeType === 1 && childNode.hasChildNodes()){
this.createVdom(childNode)
}
})
}
compileText(node,temp){
this.updateDomVal(node, temp, "textContent", this.$vm[temp]);
}
updateDomVal(node, exp, domType, domValue){
// 此处创建监听器,数据发生改变的时候,就会触发监听器发生修改
new Watcher(this.$vm, node, exp, (newVal, oldVal)=>{
node[domType] = newVal;
})
node[domType] = domValue;
}
compileInnerHTML(node){
Object.keys(node.attributes).forEach(index => {
let nodeAttrName = node.attributes[index]['nodeName'];
let exp = node.attributes[index]['nodeValue']
let value;
switch(nodeAttrName){
case 'v-html':
value = this.$vm[exp]
this.updateDomVal(node, exp, 'innerHTML', value)
break;
case 'v-text':
value = this.$vm[exp]
this.updateDomVal(node, exp, 'textContent', value)
break;
case '@click':
node.addEventListener('click', (event)=>{
let eventMethod = this.$vm.$options.methods[exp]
eventMethod.bind(this.$vm)(event)
})
}
})
}
}
// 创建依赖类, 捕获每一个监听点的变化
class Dep{
constructor(){
this.subList = []
}
// 建立依赖给Dep和watcher
depend(){
// Dep.target是监听watcher
Dep.target.addDep(this)
}
// 添加监听点watch到sublist
addSub(sub){
this.subList.push(sub)
}
// 通知所有的watcher值发生改变
notify(newVal){
this.subList.forEach(sub => {
sub.update(newVal)
})
}
}
Dep.target = null;
// 创建监听类
let uid=0;
class Watcher{
constructor(vm, node, exp, callback){
this.uid = uid++;
this.$vm = vm;
// 每个watch监听的节点
this.node = node;
// 每个监听点关联的data的属性的表达式
this.exp = exp;
this.callback = callback;
// 每个监听点的依赖列表
this.depList = {};
// 设置初始值
this.value = this.getVal()
}
// 添加依赖
addDep(dep){
if(!this.depList.hasOwnProperty(dep.uid)){
dep.addSub(this)
}
}
update(newVal){
this.callback.call(this.$vm, newVal, this.value)
}
getVal(){
// 获取当前的watch指向dep的依赖,方便数据在劫持get函数里建立依赖关系
Dep.target = this;
// 获取值,触发get函数,获取节点的位置
let val = this.$vm[this.exp]
// 获取完之后对Dep.target进行复原
Dep.target = null;
return val;
}
}
// 建立观察类
class Observer{
constructor(data, vm){
this.data = data;
this.$vm = vm;
this.walk();
}
walk(){
Object.keys(this.data).forEach(key => {
this.defineReactive(key, this.data[key])
})
}
defineReactive(key, val){
// 每个属性都要生成dep依赖对象
let dep = new Dep();
// 重新定义data属性,以便添加get和set方法
Object.defineProperty(this.data, key, {
enumerable: true,
get: ()=>{
if(Dep.target){
dep.depend()
}
return val;
},
set: (newVal)=>{
if(val !== newVal){
dep.notify(newVal)
val = newVal
}
}
})
}
}