当前位置:   article > 正文

Vue源码解析:mustache模板引擎、数据响应式原理_vue mustache 干嘛的

vue mustache 干嘛的


一、mustache模板引擎(为vue所借鉴)

   Vue.js内部使用了Mustache模板引擎来实现数据绑定和渲染

 - 背景:什么是模板引擎?是将数据要变为视图最优雅的解决方案。
  <li v-for="item in arr"></li>这实际上就是一种模板引擎


 - 曾经出现的数据变为视图的方法:
  • 纯DOM法:即DOM操作添加数据到DOM节点,节点再挂载到容器上.
  • 数组join法: 为省略繁琐的DOM操作,而字符串又不能换行,于是使用数组字符串元素连接.
  • ES6的反引号法 :省略数组字符串元素连接, 但当循环内再有循环, 仍热会复杂.
  • 模板引擎

 - mustache的基本使用

   * mustache是“胡子”的意思,因为它的嵌入标记{{ }}非常像胡子.
   * mustache是最早的模板引擎库,比Vue诞生的早多了,它的底层实现机理在当时是非常有创造性的、轰动性的.

  1.必须要引入mustache库,可以在bootcdn.com上找到.
  2.mustache的模板语法: Mustache.render(templateStr, data), 调用后生成可以渲染的页面DOM结构,再挂载到容器即可.
  (类似vue,准备好模板、容器、数据)

 - mustache的底层核心机理

   过程:模板字符串-(编译)-> tokens数组-(结合数据+解析)->dom字符串

  tokens特点:
  1.tokens是一个JS的嵌套数组,就是模板字符串的JS表示;它是“抽象语法树”、“虚拟节点”等等的开山鼻祖

  2.形态:
    a.text存其余html和文本,name存变量。
      [
        ["text", "<h1>我买了一个"],       (一个token)
        ["name", "thing"]         
      ]

    b.当模板字符串中有循环存在时,它将被编译为嵌套更深的tokens
      <div>
        <ul>
          {{#arr}}
          <li>{{.}}</li>
          {{/arr}}
        </ul>
      </div>

      [
          ["text", "<div><ul>"],
          ["#", "arr", [
              ["text", "<li>"],
              ["name", "."],
              ["text", "</li>"]
          ]],
          ["text", "</ul></div>"]
      ]


  -手写实现mustache库

  1.mustache官方库使用rollup进行模块化打包,而我们使用webpack(webpack-dev-server)进行模块化打包,
  这是因为它能让我们更方便地在浏览器中(而不是nodejs环境中)实时调试程序,相比nodejs控制台,
  浏览器控制台更好用,比如能够点击展开数组的每项。

  2.生成库是UMD的,这意味着它可以同时在nodejs环境中使用,也可以在浏览器环境中使用。实现UMD不难,只需要一个“通用头”即可(不懂)


  手写过程:
  -js模块化,利用webpack模块化打包工具识别

  -npm i -D webpack@4 webpack-dev-server@3  webpack-cli@3(4版本兼容性好)

  -配置热更新,使用没有生成文件的方式,打包后的文件在内存里

    webpack.config.js文件:

      const path = require('path');
      module.exports = { 
        mode: 'development', 
        entry: './index.js', 
        output: {
            filename: 'bundle.js'
        },
        devServer: {
            contentBase: path.join( dirname, "www"),    
            compress: false,
            port: 8080,
            // 虚拟打包的路径,bundle.js文件没有真正的生成
            publicPath: "/xuni/"
        }
      };

  -怎么运行?启动服务,打开端口,修改文件后保存就有热更新。

  -代码思路:
  1.全局暴露一个对象,拥有render方法(传入模板字符串和数据),方法先从模板字符串获取token数组,再将tokens变成真正的DOM字符串。
  2.模板字符串获取token数组(重点):
    设置扫描器(类),功能是从0索引开始扫描字符串、决定返回/跳过字符,其拥有:(实例化时传入要转化的模板字符串)
         一个游标指针变量,从0开始
         一个尾巴,存剩余长度的字符串(初始值等于传入的模板字符串)
         scan方法,跳过{{ (或者}}),一次调用跳一次{{ (或者}}),此时游标和尾巴对应变化要跳的tag长度。
         scanUntil方法,收集token的值,返回到text/name中。

    扫描器执行思路:
         1.先找{{     (特殊情况:找不到便全部返回/指标到头 不执行)
         2.找到则停止收集,本次收集完成
         3.跳过{{     (特殊情况:如果指标到头,不执行)
         4.再找}}
         5.找到则停止收集,本次收集完成
         6.跳过}}
         反复循环(使用扫描器跳出循环的条件:指针刚好溢出。 扫描器定义时也能处理循环条件出问题时不溢出)(类似安全使用规范,使用者、设计者的分开)

  3.将扫描器通过{{、}}收集到的tokens数组,进一步处理含#和/的折叠情况,返回最终tokens。

  4.再将tokens处理成最终返回DOM字符串,即变量替换成传入数据,并解析有数组循环情况。


 二、数据响应式原理(vue2更新原理)

 
  侵入式:通过明显的api设置,修改数据和视图 
  非侵入式:改变属性,视图自动变化


  1.Object.defineProperty()方法:给指定的对象定义属性
    优点:配置对象可以有额外隐藏属性及get/set方法

    Object.defineProperty(obj, 'a', { 
      value: 3,
      // 是否可写
      writable: false
    });
    Object.defineProperty(obj, 'b', {
      value: 5,
      // 是否可以被枚举,在for..in遍历对象的情况下
      enumerable: false
    })


    Object.defineProperty(obj, 'a', {
      // getter
      get() {
       console.log('你试图访问obj的a属性');
      },
      // setter 
      set() {
        console.log('你试图改变obj的a属性');
      }
    });

    注意:value和get属性不能同时设置


  2.defineReactive函数

    背景:
    -getter/setter需要中转变量才能工作
      
      var obj={};
      var temp;
      Object.defineProperty(obj, 'a', {
        get() {
          console.log('你试图访问obj的a属性'); 
          return temp;
        },
        set(newValue) {
          console.log('你试图改变obj的a属性', newValue); 
          temp = newValue;
        }
      })

    -使用defineReactive函数不需要设置临时变量了,而是用闭包
    (包括之前vue学习的,是一个对象代理到另一个空对象,也是有中间变量。这里解决了中间变量问题,为铺垫了对象往自己属性追加get/set的需求)

      export default function defineReactive(data, key, val) {      
        if (arguments.length == 2) {   //未赋值时
            val = data[key];
        }

        Object.defineProperty(data, key, {
            // 可枚举
            enumerable: true,
            // 可以被配置,比如可以被delete
            configurable: true,
            // getter
            get() {
                console.log('你试图访问' + key + '属性');
                return val;
            },
            // setter
            set(newValue) {
                console.log('你试图改变' + key + '属性', newValue);
                if (val === newValue) {
                    return;
                }
                val = newValue;
            }
        });
        };


  3.处理不设置值的情况、嵌套属性无法检测get/set的情况。
  (首先明白,defineReactive函数是相当于为对象添加 有get/set的属性、或者给已有属性添加get/set)

    var obj = {
        a: {
            m: {
                n: 5
            }
        }
      }

    defineReactive(obj, 'a'); //不赋值,给obj的a属性增加get/set。
    console.log(obj.a.m.n);  //只能输出 读取了obj的a属性,没有具体读取了哪些

    export default function defineReactive(data, key, val) {      
        if (arguments.length == 2) {   //追加旧属性时,使用该对象的属性原来的默认值,没有则为undefined。(顺便解决了新属性也不赋值的问题)
            val = data[key];
        }
        
        Object.defineProperty(data, key, {
            ...
        });
     };

    
    接下来,引入需求:监测层级深的属性,给对象的所有属性直接添加监听。
    observe类:任何一个对象,使得其所有层级属性都能添加检测到。


 -深层次监测:
    上面定义的defineProperty()函数,不能监听到对象嵌套的形式。

  实现过程:(调用一个方法,给对象全部属性添加深层监听)

    observe方法:传入的可能是对象或者其它值,如果是对象,且没有__ob__属性(说明没有添加过监听),则new Observer继续监测其下的属性。
    Observer类:传入对象进行实例化后,1.给传入对象添加__ob__属性(值是当前Observer类的实例,不可枚举)。
                                    2.遍历对象属性的每个key,分别执行defineReactive(对象,key), 逐个添加监测。
    __ob__属性:区分该对象有没有添加监听过。
    defineReactive方法:传入对象、属性、[属性值],为该对象的属性添加监测。1.如果有传入属性值,执行observe方法,因为可能值是一个对象 (这个情况只有在单独使用defineReactive且传入了新值才会有效。)
                                                                     2.set函数的newValue,也要执行observe方法,因为可能赋值一个对象。   
                                                                    (上面两点其实就是,无论是新增/修改属性,其值是对象都要进行监测
                                                                      注意:1.即便传入空对象也会添加__ob__属性,只是没有属性可以检测 
                                                                            2.解释了vue中直接对象.属性添加,不会有监听效果。 )

         ---------------------<--------------------------
        |                                               |        
        |                                               |
    observe(obj)   -->    Observer.js    -->     defineReactive.js
                              |        
                              |
                            def.js  (添加__ob__属性作为标记)

 -考虑数组类型检测:

    背景:
        Vue是通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理
        这些方法分别是:push、pop、shift、unshift、splice、sort、reverse
        这七个方法都是定义在Array.prototype上,要保留方法的功能,同时增加数据劫持的代码

  
    前置知识:
        Object.setPrototypeOf(goalObj, newPrototype)   (其实就是改变对象的原型,该方法还会返回新的对象)
            参数1:要设置其原型的对象
            参数2:新原型对象

        Object.create(proto, [propertiesObject])    创建一个新对象,带着指定的__proto__属性(即原型对象)和属性。(es6新增)
            参数proto: 即将新创建对象的原型对象。
            参数propertiesObject: 可选。传入一个对象,将其属性作为新对象的属性
        
    思路:
        思路就是 以Array.prototype为原型,创建一个新对象arrayMthods,
        然后在新对象arrayMthods上添加操作数组的方法(属性),需仍保留原生Array的原型方法去操作数组。
        (展开:1.新对象arrayMthods的方法返回值还是原生Array的原型去处理
                2.如果是插入新元素,需对新元素添加监听,新元素为一个数组,无论里面有多少项;
                3.数组一定不是最高层,已经监听过外层了,有__ob__的属性存放实例,该实例有observeArray方法检测新元素。
        )
        最后,让数组的原型指向arrayMthods,之后调用7个数组方法便会触发自定义的原型上的方法(在原数组和数组原型之间,多一层自定义的原型)


 -收集依赖

    前面之所以要劫持数据,目的是当数据的属性发生变化时,可以通知那些曾经用到的该数据的地方(页面DOM/组件)。
    而将这些用到数据的地方先收集起来,等属性改变,再触发一遍就好了

        *收集方式:在getter中收集依赖
        *触发方式 (一行代码)
        对象是在setter中触发依赖
        数组是在拦截器(重写后的Array原型对象)中触发依赖

    Dep类(depend):负责管理依赖,可以收集依赖、删除依赖、向依赖发送通知 (每个Observer的实例(__ob__),成员中都有一个Dep的实例,即检测过的每一层属性都有dep依赖关系)
                    两次new Dep,一次是Observer实例中,一次是defineReactive闭包中,为什么?
    Watcher类:依赖,即需要用到数据的地方(中介的角色,由它交互DOM、组件) 


  实现过程:代码不是很懂

    1.哪个Watcher实例读数据触发了getter,就把哪个Watcher实例 收集到Dep 中. 
    2.当数据发生变化时触发setter/数组执行方法,Dep会循环依赖列表,把所有的Watcher 都通知一遍。


                                             Component
                页面 ---<--- Render ---<---  Render Function <-------
                                                |                   |
                                                |                   |(通知组件更新)
                                                |                   |
                                                 ------------->     |
                                                (读数据)     |     |
    Data -->-- Observer -->-- Data: 1.getter  -------<-------- Watcher 
                                              ----->            |
                                    2.setter        |           |
                                        |           | (收集依赖) |
                                        |           |           |(通知watcher)
                             (通知依赖)  |           |           |
                                         --------> Dep--------->


观察者模式: 也可以简略理解为订阅-发布模式,一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/636438
推荐阅读
相关标签
  

闽ICP备14008679号