精华内容
下载资源
问答
  • sunplus8202V双无线游戏手柄设计

    千次阅读 2011-10-14 16:05:59
    原创文章,如有转载,请注明出处:http://blog.csdn.net/desert2009sz/article/details/6873834 一、系统分析 DVD的游戏手柄通讯是由SYNC, CLK, DATA1,DATA2组成,DATA1和DATA2分别代表从主
    
    
    
    

     原创文章,如有转载,请注明出处:http://blog.csdn.net/desert2009sz/article/details/6873834

    一、系统分析

    DVD的双游戏手柄通讯是由SYNC, CLK, DATA1,DATA2组成,DATA1和DATA2分别代表从主、副手柄上收到的按键数据。

    工作原理:DVD每隔80ms会在SYNC脚上产生一个高脉冲,用于唤醒游戏手柄进入收数据状态;然后会在CLK上产生低、高、低、高的脉冲信号(9个,第一个低脉冲较长一点,便于手柄同步);在CLK变低后等到一段时间,DVD检测DATA1和DATA2的数据,将保存到IOP与RISC交换的内存区。

    时序图如下:

    

    二、系统设计

    无线游戏手柄实现的方式分2种:

    ①专用MCU收码后,模拟有线手柄协仪

    无线游戏手柄发码→MCU接收→解码→转码→模拟有线手柄协仪→DVD接收

    ②屏驱MCU收码后,转发给DVD,DVD通过IOP接收后,将数据存放到RISC交换区

    屏驱MCU收码→转发给DVD→DVD接收→解码→转码→存入IOP与RISC交换区

    双手柄控制方式如下图所示:


    优点:

    ①灵活性

    单游戏手柄:用屏驱MCU方式实现,不需要增加任何硬件成本;(如果想保护自己的利益,也可以增加使用专用MCU方案,成本也较低)

    双游戏手柄:在单游戏手柄方式下,增加一个专用MCU就可以实现。

    用户可以自由选择,软件上可以做的兼容,PCB预留专用MCU空位,根据客户需要选择单打,还是双打,非常方便。

    ②性价比

    由于架构合理,操作体验和有线时速度一样;无线方式省掉了有线那块线材,降低了成本 ;

    ③稳定性

    省掉线材可以提高手柄的寿命,减少故障率

    三、需要注意的问题

    ①专用MCU方式实现时,需要调整IOP中SYNC,CLOCK的时序,特别需要注意DVD读取数据的前的延时时间

    ②专用MCU方式实现时,还需要注意处理好手柄去抖动,不然有时候同步时钟出问题时,就会漏掉了一次放数据的机会,这样就会出现有时候按键释放的现象。举例:如在玩8位魂斗罗时,长按下时,游戏角色是应该趴下的,但如果没有做去抖动,就会出现有时候会站起来。这种时候就会出被NPC的流弹打死。

    ③需要注意将各种组合键做进去,这里有个小技巧,按位判断比逐渐比较每组键的速度要节省很多执行时间和代码空间

    ④屏驱MCU方式实现时,需要将IOP相关的汇编看懂才行,解码那块会做不好,后面的就更不用说了,而且还会影响游戏的操作体验(速度)

    四、补充

    专用MCU方式已经实现,帮现有公司节约了70%的成本;

    屏驱MCU方式还在构思中,如能实现,单无线手柄可以相比以前要节约100%成本,双手柄的话也节约70%的成本


    展开全文
  • 双手

    2019-09-10 19:01:05
    老W下棋时觉得无聊,便决定加强马所行走的方式,更具体地,他有两双手,其中一双手能让马从(u,v)(u,v)(u,v) 移动到(u+Bx,v+By)(u+Bx,v+By)(u+Bx,v+By) 而另一双手能让 马从(u,v)(u,v)(u,v)移动到(u+Ax,v+Ay)(u+Ax,v+...

    两双手

        老W是个棋艺高超的棋手,他最喜欢的棋子是马,更具体地,他更加喜欢马所行走的方式。
        老W下棋时觉得无聊,便决定加强马所行走的方式,更具体地,他有两双手,其中一双手能让马从(u,v)(u,v) 移动到(u+Bx,v+By)(u+Bx,v+By) 而另一双手能让 马从(u,v)(u,v)移动到(u+Ax,v+Ay)(u+Ax,v+Ay)
        小W看见老W的下棋方式,觉得非常有趣,他开始思考一个问题:假设棋盘是个无限大的二维平面,一开始马在原点(0,0)(0,0)上,若用老W的两种方式进行移动,他有多少种不同的移动方法到达点(Ex,Ey)(Ex,Ey)呢?两种移动方法不同当且仅当移动步数不同或某一步所到达的点不同。
        老W听了这个问题,觉得还不够有趣,他在平面上又设立了n个禁止点,表示马不能走到这些点上,现在他们想知道,这种情况下马有多少种不同的移动方法呢?答案数可能很大,你只要告诉他们答案模(109+7)(10^9+7)的值就行。

    大佬博客传送门(这个讲的比较清楚)
    虽然上述的做法跟我的是不太一样的,但是如果你能看懂一半,再回来看我的您就看得懂了。
        分析思路:
        如果我们不考虑禁止点,那么我们可以得到一个二元一次方程,就u+aAx+bBx=Exu+a*Ax+b*Bx=Exv+aAy+bBy=Eyv+a*Ay+b*By=Ey。因为出发点是(0,0)(0,0)。所以我们可以得到一个唯一解
        a=(ExByEyBx)/(ByAxBxAy)a=(Ex*By-Ey*Bx)/(By*Ax-Bx*Ay)
        b=(EyAxExAy)/(ByAxBxAy)b=(Ey*Ax-Ex*Ay)/(By*Ax-Bx*Ay)
        如果a,b其中有一个<0或者是小数都不能算为能到达的点呢
        那么方案数是多少呢?这里运用下组合数,就是(aa+b)\tbinom{a}{a+b}。相当于我们总共有a+b次操作,选择Ax,Ay作为首次移动方式的情况有a种。
        那么我们现在来考虑禁止点。如果路上只有一个禁止点,那么我们只需要减去经过那个禁止点到达终点的路径数就可以了。设数组c[i][j]c[i][j]表示从i号节点到j号节点的方案数。那么我们只有一个禁止点的情况就是c[0][v]c[v][n]c[0][v]*c[v][n]。如果是两个点呢?我同样也可以用这样的方法表示,但是我们这样会减多了(简单的容斥原理)。我们不妨先以a 为关键字,b为第二关键字,由小到大排序。重新转化问题。我们将a看成向右移动,b看成向上移动。以这样的方式我们会得到新的图,新的点坐标。计算方式依旧不变,但是这就方便了我们容斥。排在后面的点,一般来说是能通过前面转移得到的(有特殊情况,比如a很大,b很小的那种)。我们设f[i]f[i]表示从起点到i号点路上不经过任何一个禁止点的路径数。初值赋值为c[ai+bi][ai]c[a_i+b_i][a_i]容斥一下:f[i]=c[aiaj+bibj][aiaj]f[i]-=c[a_i-a_j+b_i-b_j][a_i-a_j]注意这里需要保证bi>=bjb_i>=b_j
    最后输出答案f[n]f[n]即可。
        需要注意的是,组合数的处理要用阶层处理,阶层预处理的时候要处理到1000000才行。
        代码的话····A了再说

    展开全文
  • //(v-bind, v-model, v-click)等,并在这个过程中对view与model进行绑定 this._complie(this.$el) }, // 4.接下来实现_observe函数,对data进行处理,重写set和get函数,并改造_init函数 _observe: function...

        所需构造函数:myVue和Watcher
        myVue中有 _init

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <form>
          <input type="text" v-model="number">
          <button type="button" v-click="increment">增加</button>
        </form>
        <h3 v-bind="number"></h3>
      </div>
    </body>
    <script>
      //  2.需要一个myVue的构造函数
      function myVue(options){
        this._init(options)
      }
      myVue.prototype = {
        //  3.为了初始化这个构造函数,为其添加一个_init属性
        _init: function(options) {
          this.$options = options //  options为上面使用时传入的结构体,包括el,data,methods
          this.$el = document.querySelector(options.el) //  el是#app,this.$el是id为app的element元素
          this.$data = options.data //  this.$data = {number:0}
          this.$methods = options.methods //  this.$methods = {increment: function(){}}
            
           // 6._binding保存着model与view的映射关系,也就是我们前面定义的
          //Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
           this._binding = {};   
    
          this._observe(this.$data)
          //  7.那么如何将view与model进行绑定呢?接下来我们定义一个_complie函数,来解析我们的指令
          //(v-bind, v-model, v-click)等,并在这个过程中对view与model进行绑定
          this._complie(this.$el) 
        },
        //  4.接下来实现_observe函数,对data进行处理,重写set和get函数,并改造_init函数
        _observe: function(obj){  //  {number:0}
          var value;
          for(var key in obj){
            if(obj.hasOwnProperty(key)){  //  key为obj的实例属性
              // 6.
              this._binding[key] = {  // 按照前面的数据,
                _binding = {number: _directives: []} 
                _directives: []
              }
              var binding = this._binding[key]
              
              value = obj[key];
              //  如果value 仍为引用类型,则需要重新遍历
              if(typeof value === 'object'){
                this._observe(value)
              }
              Object.defineProperty(this.$data, key, {  //  关键
                enumerable: true, //  可枚举
                configurable: true, //  可修改
                get(){
                  console.log(`获取${value}`)
                  return value
                },
                set(newValue){
                  console.log(`更新${newValue}`)
                  if(value !== newValue) {
                    value = newValue
                  }
                  // 6.
                  // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新
                  binding._directives.forEach(function (item) {  
                  console.log(item)
                    item.update();
                  })
                }
              })
            }
          }
        },
        _complie: function(root) {  //root为id为app的element元素,也就是根元素
        var _this = this
        var nodes = root.children
        for(var i = 0; i< nodes.length; i++) { 
          var node = nodes[i]
          if(node.children.length) {// 对所有元素进行遍历,并进行处理
            this._complie(node)
          }
    
          //  如果有v-click属性,要触发onclick事件,出发increment事件,即 number++
          if(node.hasAttribute('v-click')){  
            node.onclick = (function() {
              var attrVal = node.getAttribute('v-click')
              //bind是使data的作用域与method函数的作用域保持一致
              return _this.$methods[attrVal].bind(_this.$data)  
            })()
          }
    
          // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
          if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { 
            node.addEventListener('input', (function(key) {  
              console.log('触发input')
              var attrVal = node.getAttribute('v-model');
               //_this._binding['number']._directives = [一个Watcher实例]
               // 其中Watcher.prototype.update = function () {
               // node['vaule'] = _this.$data['number'];  这就将node的值保持与number一致
               // }
              _this._binding[attrVal]._directives.push(new Watcher(  
                'input',
                node,
                _this,
                attrVal,
                'value'
              ))
    
              return function() {
                // 使number 的值与 node的value保持一致,已经实现了双向绑定
                _this.$data[attrVal] =  nodes[key].value; 
              }
            })(i));
          } 
    
          // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可
          if (node.hasAttribute('v-bind')) { 
            var attrVal = node.getAttribute('v-bind');
            console.log(attrVal)
            _this._binding[attrVal]._directives.push(new Watcher(
              'text',
              node,
              _this,
              attrVal,
              'innerHTML'
            ))
          }
        }
    
        }
      }
    
      //  5.接下来要写一个指令类Watcher,用来绑定更新函数,实现对dom元素的更新
        function Watcher(name, el, vm, exp, attr) {
          this.name = name  //  指令名称,例如文本节点设为text
          this.el = el  //  指令对应的DOM元素
          this.vm = vm  //  指令所属的myVue实例
          this.exp = exp  //  指令对应的值, 如'number'
          this.attr = attr  //  绑定的属性值,如innerHTML
    
          this.update()
        }
        Watcher.prototype = {
          update() {
    //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。
            this.el[this.attr] = this.vm.$data[this.exp]                       
            input.value = this.data.number
          }
        }
    
      //  1.模范vue中双向数据绑定的实现方式
      window.onload = function() {
        var app = new myVue({
        el: '#app',
        data: {
          number: 0
        },
        methods: {
          increment() {
            this.number ++
          }
        }
      })
      }
      
    </script>
    </html>

     

    展开全文
  • console.log(`name:${name} --- value:${value}`) }) } 接下来要对v-开头的元素进行处理,除了v-开头之外,v-bind的简写:和v-on的简写@也要进行处理,这里写一个函数来判断一个标签是否为要处理的标签 isVueElement...

    首先写一下要用于测试的html代码

    <div id="app">
        <h1>{{person.name}} --- {{person.age}}</h1>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <p>{{}}</p>
        <p v-text="msg"></p>
        <input type="text" v-model="msg">
    </div>
    
    <script src="./vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                person: {
                    name: 'zem',
                    age: 18
                },
                msg: 'text'
            }
        })
    </script>
    

    在引入vue源文件后,我们在浏览器中的显示就会如图
    在这里插入图片描述
    修改输入框的内容,msg的值会相应发生变化,这就是视图(view)的改变导致数据的改变

    接下来我们要做的就是来实现这些功能,解析节点中的{{}},替换其中的内容,当数据发生变化的时候改变这些内容,对v-model绑定的数据,当视图发生改变的时候对数据也进行改变

    下面会是详细的实现步骤,实现的最终代码我放在github上,也可直接看实现代码学习,有相应的注释

    初始化一个Vue类


    首先,我们新建一个vue.js,替换掉原来的vue.js,然后初始化一个Vue类

    class Vue{
        constructor(){
    
        }
    }
    

    在构造实例的时候,我们要传入的内容,就是上面在script标签中new Vue()括号内的内容,即是

    {
        el:"#app",
        data:{
            person:{
                name:"qzm",
                age:18
            },
            msg:"text"
        }
    }
    

    所以我们要将这些内容放到实例里面对应的属性里

    class Vue{
        constructor(options){
            // 将传入的值放到实例中对应的属性
            this.$el = options.el;
            this.$data = options.data;
            this.$options = options;
        }
    }
    

    接下来,我们要对传入的元素进行解析,解析前首先要判断是否存在这个属性值

    class Vue{
        constructor(options){
            // 将传入的值放到实例中对应的属性
            this.$el = options.el;
            this.$data = options.data;
            this.$options = options;
            if(this.$el){ // 判断是否有传入el属性
                // 解析入口节点元素
            }
        }
    }
    

    实现编译类


    完成了Vue类的初始化,拿到了对应的入口节点元素,接下来就是对这个入口节点元素进行编译了
    我们在Vue中判断存在入口节点元素后,将其传入编译类中,因为在编译中,我们可能还需要用到传入vue实例的data属性的内容,所以将vue实例也传入编译类实例中

    class Vue{
        constructor(options){
            // 将传入的值放到实例中对应的属性
            this.$el = options.el;
            this.$data = options.data;
            this.$options = options;
            if(this.$el){ // 判断是否有传入el属性
                // 解析入口节点元素
                new Compile(this.$el,this);
            }
        }
    }
    

    接下来就是实现Compile类了

    class Compile{
        constructor(el,vm){
    
        }
    }
    

    实现Compile类第一步,对传入的节点进行判断,因为el属性是可以传入字符串,也可以直接传入节点的,如果是传入字符串的话,我们要使用document.querySelector()来获取这个节点,如果传入的本身就是一个节点的话,我们就直接使用这个节点。这里我通过实现一个方法来,用nodeType对节点类型进行判断

    关于nodeType常见的值

    • 1:代表节点元素
    • 2:代表属性
    • 3:代表元素或属性中的文本内容
    • 8:代表注释

    完整的看W3C中关于nodeType的介绍

    接下来继续写代码,实现如下

    class Compile{
        constructor(el,vm){
            this.el = this.isElement(el) ? el : document.querySelector(el);
        }
        isElement(node){ // 判断是否为一个节点对象
            return node.nodeType === 1;
        }
    }
    

    将this.el打印出来,可以看到完整的入口节点元素
    在这里插入图片描述

    拿到了入口节点元素后,接下来就是要对这个元素进行解析,将里面的{{}}的内容替换为相应的值,但是因为每次的替换都会造成整个页面的回流和重绘,如果直接遍历节点替换的话,会造成很多的回流重绘,对整个性能会有很大的损耗,为了在理论上减少损耗,这里将传入的节点元素转换城文档碎片

    关于文档碎片的相关知识及为什么说是理论上的–>javascript文档碎片的使用

    这里写个方法来将获取的节点的子孙节点放入文档碎片,

    class Compile{
        constructor(el,vm){
            this.el = this.isElement(el) ? el : document.querySelector(el);
            this.vm = vm;
            // 将获取入口节点元素的子孙节点放入到文档碎片中
            const frag = this.transformToFrag(this.el)
        }
        isElement(node){ // 判断是否为一个节点对象
            return node.nodeType === 1;
        }
        transformToFrag(node){
            // 创建文档碎片
            const frag = document.createDocumentFragment();
            // 将节点依次放入到文档碎片中
            let firstChild = node.firstChild; // 取出开头的节点
            while(firstChild){ // 判断是否还有子孙节点
                frag.appendChild(firstChild);
                firstChild = node.firstChild;
            }
            return frag;
        }
    }
    

    此时打开页面发现所有内容都被放入到文档碎片中了,入口节点元素已经没有子节点了
    在这里插入图片描述
    将frag文档碎片打印出来,可以看到

    const frag = this.transformToFrag(this.el)
    console.log(frag)
    

    在这里插入图片描述

    放入文档碎片后,要对文档碎片中的节点进行遍历,替换相应内容,这个过程就是编译,在编译之后将编译好的子孙节点重新插入到入口节点元素中
    遍历时要判断是为文本还是为节点,如果是节点的话,要继续看该节点是否有子孙节点,对该节点使用同样的方法操作

    class Compile{
        constructor(el,vm){
            this.el = this.isElement(el) ? el : document.querySelector(el);
            this.vm = vm;
            // 将获取入口节点元素的子孙节点放入到文档碎片中
            const frag = this.transformToFrag(this.el);
            // 编译文档碎片中的子孙节点
            this.compile(frag)
            // 将编译好的文档碎片插入到入口元素节点
            this.el.appendChild(frag);
        }
        compile(frag){
            // 获取文档碎片的子元素
            const childNodes = frag.childNodes;
            // 这里的childNodes就是一个NodeList数组NodeList(9) [text, h1, text, ul, text, p, text, input, text]
            // 使用...运算符来处理childNodes将其变为一个数组
            [...childNodes].forEach(node=>{
                if(this.isElement(node)){ // 判断为元素节点
                    // 对元素节点的编译操作
                    this.compileElement(node);
                }else{ // 文本节点
                    // 对文本节点的编译操作
                    this.compileText(node);
                }
                if(node.childNodes){
                    this.compile(node);
                }
            })
        }
        compileElement(node){
    
        }
        compileText(node){
    
        }
        // ...
    }
    

    这里写了compileElement方法和compileText方法分别用来解析元素节点和文本节点

    对元素节点的解析

    首先是对元素节点的解析,一个元素节点是否需要进行处理,就看元素中是否有v-开头的属性,如v-text,v-html,v-model,这三个值分别表示元素对应的文本内容,html内容,和表单的value值。

    首先,我们获取元素所有的属性值,打印出所有的属性和值

    compileElement(node){
        const attrs = node.attributes;
        [...attrs].forEach(attr=>{ // attr是object类型
            const {name,value} = attr;
            console.log(`name:${name} --- value:${value}`)
        })
    }
    

    在这里插入图片描述
    接下来要对v-开头的元素进行处理,除了v-开头之外,v-bind的简写:和v-on的简写@也要进行处理,这里写一个函数来判断一个标签是否为要处理的标签

    isVueElement(name){ // 判断一个属性是否为要处理的属性
        return name.startsWith("v-")||name.startsWith(":")||name.startsWith("@")
    }
    

    接下来就是要调用这个方法来判断是否为要处理的标签,对要处理的标签进行分割字符串,判断是哪种类型,然后执行相应的处理

    这里先在文件根目录下创建一个对象来存储对不同指令的操作

    const compileHandle = {
        text:function(node,expr,vm){
    
        },
        html:function(node,expr,vm){
    
        },
        model:function(node,expr,vm){
    
        },
        if:function(node,expr,vm){
    
        },
        show:function(node,expr,vm){
    
        },
        for:function(node,expr,vm){
    
        },
        key:function(node,expr,vm){
    
        },
        bind:function(node,expr,vm,bindType){
    
        },
        on:function(node,expr,vm,bindType){
    
        }
    }
    

    接下来对指令进行分割,将分割后对应的内容传到上面创造的对象里面相应的方法,在调用方法后将指令删掉

    compileElement(node){
        const attrs = node.attributes;
        [...attrs].forEach(attr=>{ // attr是object类型
            let {name,value} = attr;
            if(this.isVueElement(name)){
                if(name.startsWith(":"))
                    name = "v-bind"+name;
                else if(name.startsWith("@"))
                    name = "v-on:"+name.slice(1);
                const [,instructions] = name.split("-"); // 将指令如text,bind:type,on:click赋值给instructions
                const [type,bindType] = instructions.split(":"); // 将text,on,bind之类的值给type,bind,on绑定的值给bindType
                compileHandle[type](node,value,this.vm,bindType);
                node.removeAttribute(name)
            }
        })
    }
    

    接下来实现上面对象中的对应方法
    首先是v-text,其实也就是在vue实例的data属性中找到当前处理指令的值相应名字的属性,因为传入的字符串可能是"msg"这种字符串,也可能是"person.name"这种字符串,所以不能直接使用vm.data[expr]来处理,这里使用reduce函数来处理,用textContent来给元素的文本赋值

    const compileHandle = {
        text:function(node,expr,vm){
            node.textContent = expr.split(".").reduce((data,attr)=>{
                return data[attr];
            },vm.$data)
        },
        // ...
    }
    

    同理实现v-html和v-model,这里先不实现v-model的双向绑定

    const compileHandle = {
        text:function(node,expr,vm){
            node.textContent = expr.split(".").reduce((data,attr)=>{
                return data[attr];
            },vm.$data)
        },
        html:function(node,expr,vm){
            node.innerHTML = expr.split(".").reduce((data,attr)=>{
                return data[attr];
            },vm.$data)
        },
        model:function(node,expr,vm){
            node.value = expr.split(".").reduce((data,attr)=>{
                return data[attr];
            },vm.$data)
        },
        // ...
    }
    

    这里三种获取值的操作都是一样的,写一个方法将其分离出来,因为后面的视图改变也可能影响数据,所以将数据的修改放到该对象的另一个属性里

    const compileHandle = {
        text(node,expr,vm){
            const val = this.getVal(expr,vm)
            this.updater.textUpdate(node,val)
        },
        html(node,expr,vm){
            const val = this.getVal(expr,vm)
            this.updater.htmlUpdate(node,val)
        },
        model(node,expr,vm){
            const val = this.getVal(expr,vm)
            this.updater.modelUpdate(node,val)
        },
        // ...
        updater:{
            textUpdate(node,value){
                node.textContent = value
            },
            htmlUpdate(node,value){
                node.innerHTML = value
            },
            modelUpdate(node,value){
                node.value = value
            }
        },
        getVal(expr,vm){
            return expr.split(".").reduce((data,attr)=>{
                return data[attr];
            },vm.$data)
        }
    }
    

    打开页面,发现text已经被渲染出来了
    在这里插入图片描述
    在index.html中加上下面的标签,测试是否有效

    <p v-text="person.name"></p>
    

    在这里插入图片描述
    实践有效,zem渲染出来了,接下来我们来处理一下方法,其他的指令我会在之后写到代码中,这里就不一一叙述了

    先写两个按钮,测试两种绑定方法,在vue实例中添加一个方法

    <button v-on:click="test">v-on test</button>
    <button @click="test">@ test</button>
    
    let vm = new Vue({
        el: "#app",
        data: {
            person: {
                name: 'zem',
                age: 18
            },
            msg: 'text'
        },
        methods: {
            test() {
                console.log(this);
            }
        }
    })
    

    回到vue.js里面的compileHandle对象来编写相应的方法

    on(node,expr,vm,bindType){
        let fn = vm.$options.methods&&vm.$options.methods[expr]; // 将函数赋值给fn
        node.addEventListener(bindType,fn.bind(vm),false); // 使用bind将this绑定到vue实例中
    },
    

    对文本节点的解析

    接下来对文本节点进行解析
    对文本节点,我们只需要解析放在{{}}中的内容就可以了,所以首先写一个正则表达式来匹配这些内容

    compileText(node){
        const text = node.textContent;
        if(/\{\{.+?\}\}/g.test(text)){
            console.log(text);
        }
    }
    

    这里的中间的.+?就是我们后面要用来替换的依据,即要从data里面获取的相应属性名,这里的.+?和.+的区别是
    .+:贪婪匹配,贪婪模式是先看整个是否匹配,如果不匹配则去掉最后一个再匹配,不符合继续去掉最后一个,知道匹配或者字符串数为0
    .+?:惰性匹配,惰性模式是先匹配第一个字符,不符合加入下一个字符,直到匹配或者到最后一位
    打印出来的内容
    在这里插入图片描述
    因为{{}}和v-text一样都是对textContent进行操作,所以我们可以使用compileHandle对象中的text方法来完成替换操作,但是,我们需要对方法进行一定的修改

    const compileHandle = {
        text(node,expr,vm){
            let val
            if(expr.includes("{")){ // 判断是否为文本节点的修改
                val = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ // 找到所有的{{}},将{{}}内的内容做为一个分组来替换
                    return this.getVal(args[1],vm); // 这里的args[1]就是我们要的每个{{}}内的值
                })
            }else{ 
                val = this.getVal(expr,vm)
            }
            this.updater.textUpdate(node,val)
        },
        // ...
    }
    

    然后调用该方法

    compileText(node){
        const text = node.textContent;
        if(/\{\{.+?\}\}/g.test(text)){
            compileHandle.text(node,text,this.vm)
        }
    }
    

    打开页面发现已经渲染成功了
    在这里插入图片描述

    到这一步之后,文本节点的解析基本完成,数据已经可以正常渲染了,接下来我们需要对数据进行监听

    实现监听


    数据渲染虽然完成了,但是每当数据发生改变的时候,我们要监听数据的变化,将相应的变化的值渲染到页面上,在vue文件里面实现一个监听类,监听vue实例中data的数据变化,在vue实例化的时候就使用这个监听,将vue实例的data传入观察实例中

    class Observer{
        constructor(data){
    
        }
    }
    class Vue{
        constructor(options){
            this.$el = options.el;
            this.$data = options.data;
            this.$options = options;
            if(this.$el){ // 判断el是否存在
                // 实现一个观察者
                new Observer(this.$data);
                // 实现一个指令解析器
                new Compile(this.$el,this);
            }
        }
    }
    

    在观察者中,我们要对data中的每个数据进行监听,如果数据是对象的话,还要进行递归遍历,这里使用object.defineProperty来实现数据监听

    class Observer{
        constructor(data,vm){
            vm.$data = this.observe(data);
        }
        observe(data){
            if(data && typeof data === "object"){
                Object.keys(data).forEach(key=>{ // 使用Object.keys获取当前一层的属性名
                    this.defineReactive(data,key,data[key]); // 对data的key属性进行监听
                })
            }
            return data;
        }
        defineReactive(data,key,val){
            this.observe(val); // 递归遍历
            Object.defineProperty(data,key,{
                get:()=>{
                    return val
                },
                set:(newVal)=>{
                    if(newVal!==val){
                        this.observe(newVal); // 对传入的新值进行监听
                        val = newVal
                    }
                }
            })
        }
    }
    

    实现观察者和依赖收集


    通过上面实现observer后,我们已经可以监听数据的变化,那么接下来就是要在数据变化的时候,调用相应的函数修改视图
    初始化观察者类Watcher和依赖收集类Dep,观察者要做到能获取旧值,当传入的新值与旧值不同的时候,要触发更新方法调用相应的更新函数,使得所有是要该数据的视图都重新渲染,而获取旧值需要有指令相应的字符串表述(就如{{}}里面的内容),所以初始化Watch要传入三个值

    class Watcher{
        constructor(vm,expr,cb){
            this.vm = vm;
            this.expr = expr;
            this.cb = cb;
            this.oldVal = this.getOldVal()
        }
        update(){
            const newVal = compileHandle.getVal(this.expr,this.vm);
            if(newVal!==this.oldVal){ // 如果新值与旧值不同
                this.cb(newVal) // 调用回调函数
            }
        }
        getOldVal(){
            return compileHandle.getVal(this.expr,this.vm)
        }
    }
    

    对于依赖收集类,我们要在初始化的时候即在对数据进行劫持时就创建一个列表来放置watcher,此外要有添加watcher到这个列表的方法,以及通知这个列表中的watcher触发更新,这也就是发布订阅模式中的发布者

    class Dep{
        constructor(){
            this.subs = []; // 初始化依赖收集列表
        }
        addSub(watcher){ // 添加观察者
            this.subs.push(watcher);
        }
        notify(){ // 通知列表中的所有观察者触发更新
            this.subs.forEach(w=>w.update());
        }
    }
    

    完成观察者类和依赖收集的声明后,接下来要做的就是将Dep和Observer关联起来,一个数据要在什么时候被监听,或者说什么数据应该被监听,如果在data中有的数据一直没被用到,那我们有什么必要去更新这个数据呢?
    所以应该在监听的时候,在get方法中将watcher放到Dep的sub中,而Watcher是在我们初次对数据进行解析的时候就new的,所以也不会在Observer中new新的watcher,所以我们在Watcher初始化获取旧值的时候,先将实例本身挂载到Dep的target属性上,然后在获取旧值完后将Dep的target属性置为null
    在将watcher放到sub之后,当数据发生变化,也就是触发set操作的时候,就要触发dep的notify方法,通知各个watcher去更新视图

    class Watcher{
        // ...
        getOldVal(){
            Dep.target = this; // 挂载到Dep.target上
            const oldVal = compileHandle.getVal(this.expr,this.vm);
            Dep.target = null; // 将target.target置为null
            return oldVal
        }
    }
    
    class Observer{
        // ...
        defineReactive(data,key,val){
            this.observe(val); // 递归遍历
            const dep = new Dep();
            Object.defineProperty(data,key,{
                get:()=>{
                    // 判断Dep.target是否有值
                    // 若有,将挂载在Dep上的watcher添加到dep的依赖列表中
                    Dep.target && dep.addSub(Dep.target); 
                    return val
                },
                set:(newVal)=>{
                    if(newVal!==val){
                        this.observe(newVal); // 对传入的新值进行监听
                        val = newVal
                    }
                    dep.notify(); // 通知dep的依赖列表中的watcher触发更新
                }
            })
        }
    }
    

    将Observer和Watcher关联起来后,接下来就是要在解析的时候创建watcher了,watcher对应的回调函数就是updater对应的方法
    以html为例,我们只要加上一个new Watcher就可以了

    new Watcher(vm,expr,(newVal=>{
        this.updater.htmlUpdate(node,newVal)
    }))
    

    但是文本内容有所不同,我们可能在处理的时候遇到{{person.name}}—{{person.age}}这样的情况,而其中的person.name或者person.age的改变都会使得整个发生改变,变为其中一个的值
    比如我们这样写

    const compileHandle = {
        text(node,expr,vm){
            let val
            if(expr.includes("{")){ // 判断是否为文本节点的修改
                val = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ // 找到所有的{{}},将{{}}内的内容做为一个分组来替换
                    new Watcher(vm,args[1],(newVal=>{
                        this.updater.textUpdate(node,newVal) // 错误写法
                    }))
                    return this.getVal(args[1],vm); // 这里的args[1]就是我们要的每个{{}}内的值
                })
            }else{ 
                val = this.getVal(expr,vm)
                new Watcher(vm,expr,(newVal=>{
                    this.updater.textUpdate(node,newVal)
                }))
            }
            this.updater.textUpdate(node,val)
        },
        // ...
    }
    

    在控制台写上

    vm.$data.person.name = "1"
    

    发现
    在这里插入图片描述
    变成了
    在这里插入图片描述
    这显然不是我们要的结果,所以要再写一个方法来找到文本内容中的每个{{}}内的内容,对每个内容的值返回相应的值
    修改后的compileHandle对象如下

    const compileHandle = {
        text(node,expr,vm){
            let val
            if(expr.includes("{")){ // 判断是否为文本节点的修改
                val = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ // 找到所有的{{}},将{{}}内的内容做为一个分组来替换
                    new Watcher(vm,args[1],(newVal=>{
                        this.updater.textUpdate(node,this.getContentVal(expr,vm))
                    }))
                    return this.getVal(args[1],vm); // 这里的args[1]就是我们要的每个{{}}内的值
                })
            }else{ 
                val = this.getVal(expr,vm)
                new Watcher(vm,expr,(newVal=>{
                    this.updater.textUpdate(node,newVal)
                }))
            }
            this.updater.textUpdate(node,val)
        },
        html(node,expr,vm){
            const val = this.getVal(expr,vm)
            new Watcher(vm,expr,(newVal=>{
                this.updater.htmlUpdate(node,newVal)
            }))
            this.updater.htmlUpdate(node,val)
        },
        model(node,expr,vm){
            const val = this.getVal(expr,vm)
            new Watcher(vm,expr,(newVal=>{
                this.updater.modelUpdate(node,newVal)
            }))
            this.updater.modelUpdate(node,val)
        }
        getContentVal(expr,vm){
            return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ // 找到所有的{{}},将{{}}内的内容做为一个分组来替换
                return this.getVal(args[1],vm); // 这里的args[1]就是我们要的每个{{}}内的值
            })
        },
        // ...
    }
    

    到这里,我们完成了监听数据变化修改视图,在这里,依赖收集Dep的实例对象就是data中数据的数量,而watcher就是使用到数据的视图的数量,上面的data对象如下,就会有5个Dep的实例,person,person.name,person.age,msg,judge

    data: {
        person: {
            name: 'zem',
            age: 18
        },
        msg: 'text',
        judge: true
    }
    

    接下里就是要做视图的变化来影响数据了

    实现视图驱动数据


    实际上会通过视图驱动数据的,也就是v-model,所以我们直接修改compileHandle对象中的model方法,在创建watcher后,给有v-model属性的元素绑定input方法,将表单元素的新值赋给vm.$data中相应的属性,写一个setVal方法来设置值

    const compileHandle = {
        model(node,expr,vm){
            const val = this.getVal(expr,vm)
            new Watcher(vm,expr,(newVal=>{
                this.updater.modelUpdate(node,newVal)
            }))
            node.addEventListener("input",e=>{ // 监听表单元素
                this.setVal(expr,vm,e.target.value) // 设置新值
            })
            this.updater.modelUpdate(node,val)
        },
        setVal(expr,vm,newVal){
            expr.split(".").reduce((data,attr)=>{
                data[attr] = newVal;
            },vm.$data)
        },
        // ...
    }
    
    展开全文
  • 实现一个双向绑定 一、响应式原理 我们都用过 Vue 中的 v-model 实现输入框和数据的双向绑定,其实就是 MVVM框架的核心原理实现。 如果刚接触 MVVM,可以看小鹿之前在公众号分享的一篇文章: 动画:浅谈后台 MVC ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 729
精华内容 291
关键字:

双v手