利用Vue2响应式原理中的数据劫持自制一个响应式小demo

为什么要用小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。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值