zoukankan      html  css  js  c++  java
  • Vue.js 是如何实现 MVVM 的?

    框架到底为我们做了什么?

    • 数据和视图分离,解耦(开放封闭原则)
      • 所有数据和视图不分离的,都会命中开放封闭原则
      • Vue 数据独立在 data 里面,视图在 template
    • 以数据驱动视图,只关心数据变化,dom 操作被封装
      • 使用原生js是直接通过操作dom来修改视图,例如 ducument.getElementById('xx').innerHTML="xxx"
      • 以数据驱动视图就是,我们只管修改数据,视图的部分由框架去帮我们修改,符合开放封闭模式

    如何理解 MVVM ?

    • MVC
      • Model 数据 → View 视图 → Controller 控制器
    • MVVM
      • MVVM不算是一种创新
      • 但是其中的 ViewModel 是一种创新
      • ViewModel 是真正结合前端应用场景的实现
    • 如何理解MVVM
      • MVVM - Model View ViewModel,数据,视图,视图模型
      • 三者与 Vue 的对应:view 对应 templatevm 对应 new Vue({…})model 对应 data
      • 三者的关系:view 可以通过事件绑定的方式影响 modelmodel 可以通过数据绑定的形式影响到viewviewModel是把 modelview 连起来的连接器

    如何实现 MVVM - 以 Vue.js 为例

    MVVM 框架的三大要素

    • 响应式:Vue 如何监听到 data 的每个属性变化
    • 模板引擎:Vue 的模板如何被解析,指令如何处理
    • 渲染:Vue 的模板如何被渲染成 html,渲染过程是怎样的

    Vue 如何实现响应式

    • 什么是响应式
      • 修改 data 属性之后,Vue 立刻监听到,立刻渲染页面
      • data 属性被代理到 vm
    • Object.defineProperty
      • 将对象属性的值的设置和访问 (get,set) 都变成函数,可以在当中加入我们自己的逻辑(进行监听)
      • 普通的 JavaScript 对象,做属性修改,我们监听不到,所以需要用到 Object.defineProperty
      • 既能get,又能set,才是双向数据绑定

    Vue 如何解析模板

    • 模板是什么
      • 本质:模板就是字符串
      • 与html格式很像,但是模板中是有逻辑的,可以嵌入JS变量,如v-if, v-for等
      • 视图最终还是需要由模板生成 html 来显示
      • 模板必须先要转换成JS代码
        • 有逻辑(v-if, v-for),必须用JS才能实现(图灵完备)
        • 转换为html渲染页面,必须用JS才能实现
        • 因此,模板要转换成render函数
    • render函数
      • render函数包含了模板中所有的信息,返回 vnode,解决了模板中的逻辑(v-if, v-for)问题
      • 如何找到最终生成的render函数
        • 找到vue源码,搜索code.render,将code打印出来,就是生成的render函数
    • render函数与vdom
      • 模板生成 htmlvm._c
      • vm._csnabbdom 中的 h 函数的实现很像,都是传入标签,属性,子元素作为参数
      • Vue.jsvdom 实现借鉴了 snabbdom
      • updateComponent 中实现了 vdompatch
      • 页面首次渲染执行 updateComponent
      • data 中每次修改属性,都会执行 updateComponent

    Vue.js 运行机制

    • 第一步:解析模板成 render 函数
      • 因为在打包的时候就已经生成了render函数,所以编译是第一步;响应式监听是在代码执行的时候才开始监听。
      • 模板中的所有信息都被render函数包含
      • 模板中用到的data中的属性,都变成了js变量
      • 模板中的 v-model v-for v-on都变成了js逻辑
      • render函数返回vnode
    • 第二步:响应式开始监听
      • 通过Object.definedProperty监听到对象属性的get和set
      • 将data的属性代理到vm上
    • 第三步:首次渲染,显示页面,且绑定依赖
      • 初次渲染,执行 updateComponent,执行 vm._render()
      • 执行 render 函数,会访问到 data 中的值,访问时会被响应式的 get 方法监听到
      • 执行 updateComponent,会走到 vdompatch 方法
      • patchvnode 渲染成 dom,初次渲染完成
      • 疑问:为何要监听 get,而不是直接监听 set
        • 因为 data 中有很多属性,有些被用到,有些可能不被用到
        • 只有被用到的才会走 get
        • 没有走到 get 中的属性,set 的时候我们也无需关心
        • 避免不必要的重新渲染
    • 第四步:data 属性变化,触发 re-render
      • 修改属性,被响应式的 set 监听到
      • set 中执行 updateComponent
      • updateComponent 重新执行 vm._render()
      • 生成的 vnodeprevVnode,通过 patch 进行对比
      • 渲染到 html

    手写一个 Vue.js

    index.html

    这是最终的测试代码,我们自己实现的 Vue 在 XVue.jscompile.js两个文件中,加起来大概200行代码左右,主要包括功能如下:

    • 数据响应式:页面中能直接引用data中的变量 test,我们给data.test重新赋值时,页面能随test值改变
    • 双向数据绑定:v-model
    • 模板解析,处理指令和事件绑定:v-text v-model @click
    • 渲染页面:将模板转化为 html
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title></title>
      </head>
      <body>
        <div id="app">
          {{test}}
          <div v-text="test"></div>
          <p>
            <input type="text" v-model="test" />
          </p>
          <p v-html="html"></p>
          <p>
            <button @click="onClick">按钮</button>
          </p>
        </div>
        <script src="./compile.js"></script>
        <script src="./XVue.js"></script>
        <script>
          const o = new XVue({
            el: '#app',
            data: {
              test: '123',
              foo: { bar: 'bar' },
              html: '<button>html test</button>'
            },
            methods: {
              onClick() {
                alert('按钮点击了')
              }
            }
          })
          console.log(o.$data.test) //123
          o.$data.test = 'hello, Xvue!'
          console.log(o.$data.test) //hello, Xvue!
        </script>
      </body>
    </html>
    

    Mini Vue 的组成部分:

    • 监听器 observe :数据劫持,实现响应式;属性代理
    • 依赖管理器 Dep :负责将视图中所有依赖收集管理,包括依赖添加和通知更新
    • 监听器 Watcher :具体更新的执行者
    • 编译器 Compile :扫描模板中所有依赖(指令、插值、绑定、事件等),创建更新函数和监听器( Watcher )

    XVue.js

    class XVue {
      constructor(options) {
        this.$data = options.data;
        this.observe(this.$data);
        // 执行编译
        new Compile(options.el, this);
      }
    
      observe(value) {
        if (!value || typeof value !== 'object') {
          return;
        }
        Object.keys(value).forEach(key => {
          this.defineReactive(value, key, value[key]);
          // 为vue的data做属性代理
          this.proxyData(key);
        });
      }
    
      defineReactive(obj, key, val) {
        // 递归查找嵌套属性
        this.observe(val);
    
        // 创建Dep
        const dep = new Dep();
    
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // 收集依赖
            Dep.target && dep.addDep(Dep.target);
            // console.log(dep.deps);
            return val;
          },
          set(newVal) {
            if (newVal === val) {
              return;
            }
            val = newVal;
            dep.notify();
          },
        });
      }
    
      proxyData(key) {
        Object.defineProperty(this, key, {
          get() {
            return this.$data[key];
          },
          set(newVal) {
            this.$data[key] = newVal;
          },
        });
      }
    }
    
    // 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知
    class Dep {
      constructor() {
        // deps里面存放的是Watcher的实例
        this.deps = [];
      }
      addDep(dep) {
        this.deps.push(dep);
      }
      // 通知所有watcher执行更新
      notify() {
        this.deps.forEach(dep => {
          dep.update();
        });
      }
    }
    
    // Watcher: 具体的更新执行者
    class Watcher {
      constructor(vm, key, cb) {
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        // 将来 new 一个监听器时,将当前 Watcher 实例附加到 Dep.target
        // 将来通过 Dep.target 就能拿到当时创建的 Watcher 实例
        Dep.target = this;
        // 读取操作,主动触发 get,当前 Watcher 实例被添加到依赖管理器中 
        this.vm[this.key];
        // 清空操作,避免不必要的重复添加(再次触发 get 就不需要再添加 watcher 了)
        Dep.target = null;
      }
      update() {
        // console.log('from Watcher update: 视图更新啦!!!');
        // 通知页面做更新
        this.cb.call(this.vm, this.vm[this.key]);
      }
    }
    

    compile.js

    // 扫描模板中所有依赖(指令、插值、绑定、事件等)创建更新函数和watcher
    class Compile {
      // el是宿主元素或其选择器
      // vm当前Vue实例
      constructor(el, vm) {
        this.$el = document.querySelector(el);
        this.$vm = vm;
        if (this.$el) {
          // 将dom节点转换为Fragment提高执行效率
          this.$fragment = this.node2Fragment(this.$el);
          // 执行编译,编译完成以后所有的依赖已经替换成真正的值
          this.compile(this.$fragment);
          // 将生成的结果追加至宿主元素
          this.$el.appendChild(this.$fragment);
        }
      }
      node2Fragment(el) {
        // 创建一个新的Fragment
        const fragment = document.createDocumentFragment();
        let child;
        // 将原生节点移动至fragment
        while ((child = el.firstChild)) {
          // appendChild 是移动操作,移动一个节点,child 就会少一个,最终结束循环
          fragment.appendChild(child);
        }
        return fragment;
      }
      // 编译指定片段
      compile(el) {
        let childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
          // 判断node类型,做相应处理
          if (this.isElementNode(node)) {
            // 元素节点要识别v-xx或@xx
            this.compileElement(node);
          } else if (
            this.isTextNode(node) &&
            /{{(.*)}}/.test(node.textContent)
          ) {
            // 文本节点,只关心{{msg}}格式
            this.compileText(node, RegExp.$1); // RegExp.$1匹配{{}}之中的内容
          }
          // 遍历可能存在的子节点
          if (node.childNodes && node.childNodes.length) {
            this.compile(node);
          }
        });
      }
    
      compileElement(node) {
        // console.log('编译元素节点');
        // <div v-text="test" @click="onClick"></div>
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          const attrName = attr.name; // 获取属性名 v-text
          const exp = attr.value; // 获取属性值 test
          if (this.isDirective(attrName)) {
            // 指令
            const dir = attrName.substr(2); // text
            this[dir] && this[dir](node, this.$vm, exp);
          } else if (this.isEventDirective(attrName)) {
            // 事件
            const dir = attrName.substr(1); // click
            this.eventHandler(node, this.$vm, exp, dir);
          }
        });
      }
    
      compileText(node, exp) {
        // console.log('编译文本节点');
        this.text(node, this.$vm, exp);
      }
    
      isElementNode(node) {
        return node.nodeType == 1; //元素节点
      }
    
      isTextNode(node) {
        return node.nodeType == 3; //元素节点
      }
    
      isDirective(attr) {
        return attr.indexOf('v-') == 0;
      }
    
      isEventDirective(dir) {
        return dir.indexOf('@') == 0;
      }
    
      // 文本更新
      text(node, vm, exp) {
        this.update(node, vm, exp, 'text');
      }
    
      // 处理html
      html(node, vm, exp) {
        this.update(node, vm, exp, 'html');
      }
    
      // 双向绑定
      model(node, vm, exp) {
        this.update(node, vm, exp, 'model');
    
        let val = vm.exp;
        // 双绑还要处理视图对模型的更新
        node.addEventListener('input', e => {
          vm[exp] = e.target.value; // 这里相当于执行了 set
        });
      }
    
      // 更新
      // 能够触发这个 update 方法的时机有两个:1-编译器初始化视图时触发;2-Watcher更新视图时触发
      update(node, vm, exp, dir) {
        let updaterFn = this[dir + 'Updater'];
        updaterFn && updaterFn(node, vm[exp]); // 立即执行更新;这里的 vm[exp] 相当于执行了 get
        new Watcher(vm, exp, function (value) {
          // 每次创建 Watcher 实例,都会传入一个回调函数,使函数和 Watcher 实例之间形成一对一的挂钩关系
          // 将来数据发生变化时, Watcher 就能知道它更新的时候要执行哪个函数
          updaterFn && updaterFn(node, value);
        });
      }
    
      textUpdater(node, value) {
        node.textContent = value;
      }
    
      htmlUpdater(node, value) {
        node.innerHTML = value;
      }
    
      modelUpdater(node, value) {
        node.value = value;
      }
    
      eventHandler(node, vm, exp, dir) {
        let fn = vm.$options.methods && vm.$options.methods[exp];
        if (dir && fn) {
          node.addEventListener(dir, fn.bind(vm), false);
        }
      }
    }
    
  • 相关阅读:
    ORACLE锁表问题
    迅雷极速版解决版本太旧提示问题
    迅雷极速版任务出错的解决办法(亲测可用)
    百度文库免积分免费下载工具的原理与实现
    C#启动另一个应用程序并传参数
    C#设置WebBrowser IE浏览器版本
    JS强制刷新页面、清除缓存刷新
    JS播放声音
    Chrome插件(扩展)开发全攻略
    IIS7.5全站301跳转,内页+带参数url,这才是真正的全站跳转
  • 原文地址:https://www.cnblogs.com/dora-zc/p/11111813.html
Copyright © 2011-2022 走看看