为什么要用小demo的字眼,因为实力不允许做出像样的东西。---小天才阿阔
可直接拉到最后,将完整版代码直接复制粘贴到一个html文件中运行即可,方便打印观察某些变量什么样子。
实现思路:所谓的响应式也就是说当变量的值发生变化时,页面上也要做出相应的变化,其实大概流程就是数据变化的时候,我们通过某种方式监测到了数据发生了变化,然后生成新的虚拟dom,将虚拟dom与真实dom相比较,将发生变动的部分渲染到页面上。
1.这里我们在一个html文件中,写好一个div标签,id为app;另外,我们也定义了一个变量data,它里面有一个name属性。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
</style>
<body>
<div id="app"></div>
<script>
let data = {
name: '李狗娃'
}
</script>
</body>
</html>
2.下面我们要做的是,在页面中创建虚拟dom,并将其放入到真实dom中,这里用到了一个库,叫做snabbdom,主要就是用来生成虚拟dom的,百度翻译了下这个snabb单词是快速的意思,后面加上dom您自行脑补咋翻译好吧。
所谓的虚拟dom官方的说法是,在内存中以javascript对象的形式表示的真实dom的抽象,它是一种轻量级的数据结构,用于描述页面的结构和状态。
算了,咱们还是一会直接打印下看看长啥样吧。
这里我们使用script标签引入的方式,引入snabbdom相关内容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
</style>
<body>
<div id="app"></div>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
let data = {
name: '李狗娃'
}
// 获取snabbdom对象
var snabbdom = window.snabbdom;
//初始化patch函数,这个函数具备将虚拟dom挂载到真实dom中的功能,也就说把虚拟dom渲染到页面上
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
// 定义h函数,h函数主要是用来创建虚拟dom的
var h = snabbdom.h;
// 找到页面上id为app的元素,作为挂载点
var container = document.getElementById('app');
// 使用h函数创建虚拟dom
// 创建一个ul标签,id为list,其下有两个子元素,均为li标签,且class均为item;
// 标签的文本内容,一个为水浒少年,一个则为我们定义的data对象中的name属性
let vnode = h('ul#list', {}, [
h('li.item', {}, '水浒少年'),
h('li.item', {}, data.name),
])
// 这里我们打印下虚拟dom,看看什么样
console.log('虚拟dom为',vnode)
// 将虚拟dom挂载到页面中,第一个参数是我们前面的挂载点,第二个参数为我们创建的虚拟dom
patch(container, vnode)
</script>
</body>
</html>
此时我们页面上的内容变为了这样:
虚拟dom长这样,是一个对象,里面有很多属性,比如children,data,elm,key等等……
我们现在的想法就是通过修改data中name的属性,从而实现页面的更新,比如执行data.name="王大瓜"后,页面上的文本内容李狗娃也自动变成王大瓜。
根据我们在文章一开头提到的实现思路,下面我们要做的就是在data的name属性发生变化时我们要监测到才行,这样才能进行下一步的操作,这里我们创建一个Observer函数,并利用Object.defineProperty()方法劫持(或称为监听)对象的属性,从而在属性被访问或修改时能够进行拦截和响应。
Vue2中的数据劫持,便是利用Object.defineProperty()方法来实现的。
这样我们便可得出一个结论,所谓的数据劫持,就是利用Object.defineProperty()方法劫持(或称为监听)对象的属性,从而在属性被访问或修改时能够进行拦截和响应。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
</style>
<body>
<div id="app"></div>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
let data = {
name: '李狗娃'
}
// 获取snabbdom对象
var snabbdom = window.snabbdom;
//初始化patch函数,这个函数具备将虚拟dom挂载到真实dom中的功能,也就说把虚拟dom渲染到页面上
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
// 定义h函数,h函数主要是用来创建虚拟dom的
var h = snabbdom.h;
// 找到页面上id为app的元素,作为挂载点
var container = document.getElementById('app');
// 使用h函数创建虚拟dom
// 创建一个ul标签,id为list,其下有两个子元素,均为li标签,且class均为item;
// 标签的文本内容,一个为水浒少年,一个则为我们定义的data对象中的name属性
let vnode = h('ul#list', {}, [
h('li.item', {}, '水浒少年'),
h('li.item', {}, data.name),
])
// 这里我们打印下虚拟dom,看看什么样
console.log('虚拟dom为',vnode)
// 将虚拟dom挂载到页面中,第一个参数是我们前面的挂载点,第二个参数为我们创建的虚拟dom
patch(container, vnode)
// 创建一个Observer函数,传入一个对象作为参数
function Observer(obj) {
// 将传入对象中的所有key,也就是属性,取出并形成一个数组
const keys = Object.keys(obj);
// 对取出来的,形成数组的key进行遍历
keys.forEach((k) => {
// 利用Object.defineProperty()方法进行数据劫持,这里的this为Observe创建的对象
Object.defineProperty(this, k, {
get() {
return obj[k]
},
set(val) {
obj[k] = val;
}
})
})
}
const obs = new Observer(data)
data = obs;
</script>
</body>
</html>
有人或许对Object.defineProperty()方法不熟悉,这里大概说一下,以上图的代码为例:
几个参数的意义是这样的:Object.defineProperty(一个里面的属性要被监测的对象,对象中被监测的属性,为被监测属性添加get/set实现监听)
如果对象中没有被监测的属性,像我们上面的代码中,this是一个空对象,k为name,那么会自动为前面的对象添加上这个要被监测的属性。
首先是第一个参数this,有人不清楚知道这个this是什么,其实你观察下this的位置就能得出结论,this是存在于(k)=》{}这个函数体中的,因为是箭头函数,所以this要继续往上找,从而得知,这里的this指向就是Observer函数的实例对象。
在下面我们执行了const obs = new Observer(data),this指向这里的obs实例对象。
第二个参数k也就是我们上面从传入对象中取出的key,在这里也就是属性name,因为我们的data对象中只有一个属性name。
第三个参数是一个配置对象,里面有两个属性get,set,且都为函数,所以这里采用了简写方式,例如aa:function(){}简写为aa() {}。
完成该步操作后,输出obs,会发现obs为{name:'李狗娃'},当我们想要输出obs.name的值时,第三个参数中的get函数体中的内容就会响应执行,从而返回obj[k],而我们想obs.name = xxx重新赋值时,则会触发set函数体中的内容,在这一步,我们做到了对obs的监听,你体会一下,我们访问或修改obs.name时,它里面的get,set都响应的执行了,这不就是相当于监听到了其数值的变化吗?如果有后续的逻辑,我们不就可以写在get或set的方法体中了吗?
我们将obs打印下,会发现其身上带有get,set两个方法,也正是这两个方法让我们可以监测到它数据的变化:
不过这有什么用呢,我们不是要做当data.name的值发生改变时我们能监听到嘛,在这里搞出个obs是什么鬼?
其实就是为了下面这一步 data=obs,这样data就和obs一样,我们在执行data.name时会触发get,而在data.name=xxx时则会触发set,这样我们在改变data.name的值时,就可以在set中重新去生成虚拟dom,并将其渲染到页面中。
最后我们添加一个按钮,点击后将会执行data.name = '王大瓜',结果发现页面上的内容果然也变成了王大瓜,这样我们便实现了响应化,在改变数据的情况下,视图也发生了改变。
完整代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
</style>
<body>
<div id="app"></div>
<button id="btn">改变data中的name属性</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
let data = {
name: '李狗娃'
}
console.log('初始时候的data',data)
// 监听按钮
document.getElementById('btn').addEventListener('click', () => {
data.name = '王大瓜'
})
// 获取snabbdom对象
var snabbdom = window.snabbdom;
//初始化patch函数,这个函数具备将虚拟dom挂载到真实dom中的功能,也就说把虚拟dom渲染到页面上
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
// 定义h函数,h函数主要是用来创建虚拟dom的
var h = snabbdom.h;
// 找到页面上id为app的元素,作为挂载点
var container = document.getElementById('app');
// 使用h函数创建虚拟dom
// 创建一个ul标签,id为list,其下有两个子元素,均为li标签,且class均为item;
// 标签的文本内容,一个为水浒少年,一个则为我们定义的data对象中的name属性
let vnode = h('ul#list', {}, [
h('li.item', {}, '水浒少年'),
h('li.item', {}, data.name),
])
// 这里我们打印下虚拟dom,看看什么样
console.log('虚拟dom为',vnode)
// 将虚拟dom挂载到页面中,第一个参数是我们前面的挂载点,第二个参数为我们创建的虚拟dom
patch(container, vnode)
// 创建一个函数,名称随意,传入一个对象作为参数,我们一般将我们想要监听的对象放进去,这里的话就是data
function Observer(obj) {
// 将传入对象中的所有key,也就是属性,取出并形成一个数组
const keys = Object.keys(obj);
// 对取出来的,形成数组的key进行遍历
keys.forEach((k) => {
// 利用Object.defineProperty()方法进行数据劫持,这里的this为Observe创建的对象
Object.defineProperty(this, k, {
get() {
return obj[k]
},
set(val) {
obj[k] = val;
// 监听到data中的数据发生改变,所以set被触发,重新生成虚拟dom
const newVnode = h('ul#list', {}, [
h('li.item', {}, '水浒少年'),
h('li.item', {}, data.name),
]);
// 渲染新的dom节点,第一个参数为原来的节点,第二个为要作比较的新节点
patch(vnode, newVnode)
// 对vnode进行赋值,这样下次比较的时候vnode就相当于当前的newnode,其实就是让vnode一直保持是最新的状态
vnode = newVnode
}
})
})
}
// 创建Observer对象,Observer中的this指向的就是这个obs实例对象
const obs = new Observer(data)
// 将我们之前自定义的data重新赋值,这样其身上便也具备了obs上的get,set,从而能让我们监听到data数据的变化
// 注意此时的data已经与我们最初定义的data不一样了,它现在身上已经多出了get,set
data = obs;
console.log('经过重新赋值后的data',data)
</script>
</body>
</html>
我们在这看下data的前后有哪些不一样,可以发现经过赋值后的data上多了一个get,set,也正是这两个方法,让我们实现了对属性的监测。
点击按钮后你会发现,页面上的内容发生了变化,只有第二个li标签发生了闪烁,这表明我们只重新渲染了文本内容发生变化的dom,这跟vue中的渲染是一样的,通过虚拟dom与真实dom的比对,只重新渲染发生变化的部分。
以上便是一个简单的demo。