lsdyna如何设置set中的node_list_vue中MVVM实现原理

60c04b9518c838a504c341e5d7dee3ac.png

此文实现一个mvvm模式,并且讲解一下vue的实现原理

MVVM

定义:双向数据绑定
特点:数据影响视图,视图影响数据
angular主要靠的是脏值检测实现双向数据绑定,vue靠的是数据劫持+发布订阅模式
vue不兼容ie8以下的版本,因为vue核心实现的mvvm模式用的是 Object.defineProperty

· Object.defineProperty

它可以给对象定义属性

首先,我们定义一个空对象,然后在该对象中定义一个name属性,代码如下:

let obj={}
Object.defineProperty(obj,'name',{
 value:'zhangsan'    
})

3893230187c95f264df8cdc6f7355d75.png

结果,可知,name属性已经定义完成,现在我想删除这个属性。

delete obj.name;

但是查看结果的时候,发现它并未删掉

2481e897943ee0f94931db0638976ce9.png

以上可知,通过Object.defineProperty的方式定义对象属性,并不能直接用delete的方式删除,而是需要配置:configurable(默认false,是否可删除)。

let obj={}
Object.defineProperty(obj,'name',{
 configurable:true,//默认false,是否可删除
 value:'zhangsan'    
})

dffcaf47ade97fe9f7a23b01f4c1ae43.png

这样就可以删除了,同样的,可修改,可枚举(可遍历)也需要配置特有属性。

let obj={}
Object.defineProperty(obj,'name',{
  configurable:true,//默认false,是否可删除
  writable:true,//默认false,是否可修改
  enumerable:true,//默认false,是否可枚举(是否可遍历)
  value:'zhangsan',
 })

在实际中,value一般会被分成两个部分:get、set,这也就是我们常说的getter和setter。

注意: 有了get和set,就不能用writable和value,否则会报错。

8ba154c4a1e5f4b2febf09c338659419.png
let obj = {}
Object.defineProperty(obj, 'name', {
 configurable: true,//默认false,是否可删除
 enumerable: true,//默认false,是否可枚举(是否可遍历)
 get() {
   return 'zhangsan'
 },
 set(val) {
    console.log(val)
 }
})

3861a7772c8b227d1037d3d70e0a4ab8.png

从结果中,我们可知,当获取obj中的name属性值的时候,调用get方法;当改变name属性值的时候,调用set方法。

· 数据劫持

定义:通俗的讲,就是用Object.defineProperty来定义我们的所有属性
let vue =new Vue({
  el:'#app',
  data:{a:1}
})

这段代码,大家应该都熟悉,其中的data对象中的每一个属性,我们需要用Object.defineProperty重新定义。

现在呢,我们自己实现一下这个功能。

首先我们先创建一个mvvm.js文件,专门写这些功能。

html部分

<div id="app">
    {{a}}
</div>
 
<script src="mvvm.js"></script>
<script>
 let vueDemo = new VueDemo({
    el: '#app',
    data: { a: 1 }
  })
 </script>

mvvm.js部分

function VueDemo(options = {}) {
  this.$options = options;//将所有属性挂载到$options(仿vue)
  var data = this._data = this.$options.data;
  observer(data)
}

// 观察对象给对象增加Object.defineProperty
function observer(data) {
  if(typeof data !=='object') return;
  return new Observer(data)
}

// Observer构造,这里我们写主要逻辑
function Observer(data) {
  // 通过Object.defineProperty给data定义属性
 for (let key in data) {//遍历
  let val = data[key];
  Object.defineProperty(data, key, {
   enumerable: true,//可枚举
   get() {
     return val
   },
   set(newVal) { //更改值的时候
    if (newVal === val) { //如果设置的值跟以前一样,我们就忽视它
      return;
    }
    val = newVal; //如果以后再获取值的时候,将设置的新值再丢回去
   }
  })
 }
}

现在看一下结果:

5d99ae5dc9df8d7681230739c7f00992.png

结果可知,data中的a属性已经被Object.defineProperty成功定义,也可以修改和获取它,但是在实际项目中,属性a很有可能是这种形式:a:{b:2,{c:3}}。

比如,我给属性a赋值{b:1}

88b800c9f0b02ddf138d42d03f531eaa.png

结果看出,它是没有get和set方法的,所以我们需要在赋值的时候再执行observe方法,让里面的属性再次用Object.defineProperty定义,同样,获取的时候也需要调用。

283172e44cfdaaed8b9cf909edde052f.png

569d9edb7627dd66576821de825d33ca.png

在实际vue操作中,我们获取data的属性,只需要this.属性名即可,实现这个功能,同样,我们只要在this中用Object.defineProperty定义data中所有的属性即可。

代码如下:

for (let key in data) {
 Object.defineProperty(this, key, {
  enumerable: true,
  get() {
   return this._data[key]
  },
  set(newVal) {
    this._data[key] = newVal
  }
 })
}

984e6abc4d6c6feef780de14fd1fc9c1.png

这样我们就实现了。

· 编译

现在,数据我们已经拿到了,那如何显示在页面中呢?

先将html变复杂一点

<div id="app">
   <p>a中a的值:{{a.a}}</p>
   <div>b的值{{b}}</div>
 </div>
 <script src="mvvm.js"></script>
 <script>
   let vueDemo = new VueDemo({
     el: '#app',
     data: {
         a: {
           a:1
         },
         b:2
       }
    })
 </script>

思路:

1.先获取div#app下的所有子节点,比如<p/>、<div/>
2.然后,获取<p/>、<div/>的子节点,如果子节点是文本节点而且里面有{{}},那么,通过textContent获取里面的内容,然后通过正则将{{}}替换成响应的数据;如果不是元素节点,就判断是否有子节点,如果有,再获取其内的子节点进行遍历(递归),直到是文本节点为止。
// 编译
function Compile(el, vm) {//el:替换的范围
 vm.$el = document.querySelector(el);//获取el
 let reg = /{{(.*)}}/;//用于后面检测{{}}
 //文档碎片,在内存,不占dom
 let fragment = document.createDocumentFragment();
 //然后将每一个dom节点塞到文档碎片中(内存中),fragment.append具有移动节点的作用
  var child;
  while (child = vm.$el.firstChild) {
    fragment.append(child);
  }
 replace(fragment)
 function replace(fragment) {
  // 需要遍历fragment中的子节点,注意:childNodes是类数组,需要Array.from转换一下
  Array.from(fragment.childNodes).forEach(node => {
   // 获取里面的内容
  var text = node.textContent;
  // 如果是元素节点而且有{{}}
  if (node.nodeType == 3 && reg.test(text)) {
   //console.log(RegExp.$1) //取到了vm.a.a / vm.b
   let arr = RegExp.$1.split('.');
   let val = vm;
   arr.forEach(k=>{
     val = val[k];
    })
    node.textContent = text.replace(/{{(.*)}}/,val);
   }
   if (node.childNodes) {
      replace(node)
   }

  })
 }
 vm.$el.appendChild(fragment);
}

编译完成以后,数据渲染到了页面当中。

d6c3675a37606d0a7fca3f6d7ae8a087.png

但是当我们修改数据的时候,修改成功后,数据并未更新到页面上,这需要我们将observe(数据劫持)和compile(编译)结合起来,需要用发布-订阅模式。

· 发布-订阅模式

先有订阅 再有发布
思路:现在有一个方法(watcher)可以帮我们订阅一些事件(是函数),这些事件放在一个数组里面([fn1,fn2,fn3]),当我们发布的时候,只需要遍历这些数组,依次执行。
订阅:就是往数组里面放函数
发布:就是讲数组中的函数依次执行

现在我们实现一个发布-订阅方法

先定义一个函数Dep,让订阅的函数都存在Dep的数组中,并定义两个方法:addSub是往数组中添加函数,notify是依次执行函数。

function Dep() {
    this.subs = [];
}

Dep.prototype.addSub = function (sub) {//订阅,sub是个函数(事件)
    this.subs.push(sub)
}

// 调用的时候,事件依次执行,notify:通知
Dep.prototype.notify = function () {
    this.subs.forEach(sub => sub.update());
}

然后,写一个订阅的方法:watcher,它的作用是给每个传入进来的函数,添加一个属性方法(update),而这个属性方法可以执行传进来的函数。

特定的,给每一个函数设定一个update属性方法。

// 订阅,通过这个类Watcher ,通过这个类创建的实例都拥有update方法
function Watcher(fn) {
    this.fn = fn;
}
Watcher.prototype.update = function () {
    this.fn()
}

let watcher = new Watcher(function () {
    console.log(1)
});

实例化Dep,实现发布--订阅

let dep = new Dep();
dep.addSub(watcher) //订阅
dep.addSub(watcher) //订阅
dep.addSub(watcher) //订阅
dep.notify(); // 发布 ,  结果是 1 1 1

以上,通过dep.addSub实现将函数保存到数组subs中(订阅),通过notify依次执行subs中的函数(发布),实现了发布--订阅

现在,我们就看一下如何用发布--订阅模式,来实现数据的关联。

思路:当数据改变了,我们需要刷新视图,所以我们要找到编译方法中,将数据替换到节点中的部分。

代码如下:

...
<!-- 这个地方添加逻辑-->
node.textContent = text.replace(/{{(.*)}}/,val);
...

在替换内容之前,我们需要订阅一下,数据一变,我们直接再执行替换内容这个操作就行了。

 // watcher
 new Watcher(vm, RegExp.$1, function(newVal){ //数据一变,我们需要接受一个新值
    node.textContent = text.replace(/{{(.*)}}/,newVal);
 }

我们需要重新构建一下watcher

// 订阅,通过这个类Watcher ,通过这个类创建的实例都拥有update方法
function Watcher(vm,exp,fn) {
    this.vm=vm;
    this.exp=exp;
    this.fn = fn;  //我们要把watcher添加到订阅中
    Dep.target = this;
    let val =vm;
    let arr = exp.split('.');
    arr.forEach(k=>{ //这个操作就是取值:this.a.a,会调用get方法
        val = val[k]
    })
    Dep.target=null;
}
Watcher.prototype.update = function () {
    // 我们在这获取值
    let val =this.vm;
    let arr = this.exp.split('.');
    arr.forEach(k=>{ 
//这个操作就是取值:this.a.a,会调用get方法,如果改变了数据,那就会到得到最新值
        val = val[k]
    })
    this.fn(val);将最新值当做参数,去执行new Watcher中的函数,然后刷新视图
}

当执行get获取值的时候,把this订阅到Dep方法的数组中,然后改变数据的时候,执行dep.notify()执行发布。

代码如图:

b22c2fbd466d1126c9fb6aee7c2d66ad.png

· 实现双向绑定(v-model)

html部分

<div id="app">
  <p>a中a的值:{{a.a}}</p>
  <div>b的值{{b}}</div>
  <input type="text" v-model="b">
</div>
思路:首先判断,元素节点中如果带有v-model属性,我就将相对应的值赋给它。

上面我们只判断是否为文本节点,现在我们需要判断元素节点。

代码如下:

// v-model
if(node.nodeType==1){
  let nodeAttrs = node.attributes;//获取dom节点中的属性
  Array.from(nodeAttrs).forEach(attr=>{
   let name = attr.name;
   let value = attr.value;
   if(name.indexOf('v-model') !== -1){
     node.value = vm[value]; //将值显然在input中
   }
   // 订阅一下
    new Watcher(vm,value,function(newVal){
       node.value = newVal; //当watcher触发时会自动将内容放到输入框中
    })
    node.addEventListener('input',e=>{
      let newVal = e.target.value;
       vm[value] = newVal;
     })
   })
 }

· 实现computed

html部分

computed:{
  hello(){
   return Number(this.b) + Number(this.c)
  }
}
//在data中定义一个属性c:20
//页面中添加{{hello}}

思路:

首先获取computed中的key,也就是方法名,然后将它通过Object.defineProperty定义到实例上。
注意:computed中也有get和set属性,所以需要判断。
function initComputed(){
 let vm = this;
 let computed = this.$options.computed; 
 console.log(Object.keys(computed))
 Object.keys(computed).forEach(key=>{
   Object.defineProperty(vm,key,{
     get:typeof computed[key] === 'function' ? computed[key] : computed[key].get,
     set:{}
   })
 })
}

然后将initComputed方法加载到VueDemo构造函数中。

initComputed.call(this);

到此,功能完全实现,完整代码我发布到了github上,大家可以查看。

https://github.com/JinGuiMin/vue__mvvm__demo​github.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值