vue基本原理

vue原理学习和解读

Posted by SkioFox on July 4, 2018

vue的基本原理解读

vue底层原理关系图

avatar

vue运行流程

avatar

  • 在new Vue() 之后,vue会调用进行初始化, 初始化生命周期、事件、props、methods、data、computed、watch等等。核心是通过Object.defineProperty设置getter、setter, 用来实现响应式和依赖收集。
  • $mount用于挂载组件
  • 编译模块分为三个阶段
    • parse(解析)
      • 使用正则解析template中的vue指令和变量等等, 形成树AST
    • optimize(优化)
      • 标记静态节点,用作后面优化,做diff的时候直接省略
    • generate(生成)
      • 把第一步生成的AST转化为渲染函数render function
  • 响应式
    • 初始化的时候通过defineProperty进行绑定,设置通知的机制。当编译生成的渲染函数被实际渲染时,会触发getter进行依赖收集,在数据变化时会触发setter进行更新(见下面代码)。
  • 什么是虚拟DOM
    • 简单来说虚拟DOM就是用JS对象来模拟DOM结构,数据修改时,我们先修改虚拟DOM中的数据,然后用数组做diff,最后汇总所有的diff,然后再更新真实的DOM。js执行比dom快很多,当diff算法后,实现最少的真实dom更新,因此提高了速率。
        // vdom
        {
        tag: 'div',
        props: {
            name: 'test',
            style: 'color:red',
            onClick: 'try'
        }
        children:[
            {
                tag: 'a',
                text: 'click me'
            }
        ]
        }
      

      ```

    <div name=”test” style=”color:red” @click=”try”> click me </div> ```

  • 更新视图
    • 数据修改触发setter,然后监听器会通知所有修改,对比两个dom树, 得到改变的地方,就是patch。然后修改这部分差异。

实现vue.js响应式(Observe vue2.0)

// kvue.js=>Object.defineProperty
class KVue {
    constructor(options) {
        this.$data = options.data; // 保存data选项
        this.observe(this.$data); // 执行响应式

        this.$options = options; // 保存options

        // test
        // new Watcher();
        // console.log('模拟compile', this.$data.test);
        // 解析(编译)并保存编译 
        this.$compile = new Compile(options.el, this);  
    }

    observe(value){
        if (!value || typeof value !== 'object' ) { // 中午只是简单判断,还可能是函数等等
            return;
        }
        
        // 遍历data选项
        Object.keys(value).forEach(key => {
            // 为每一个key定义响应式=>将属性定义响应式到data对象上
            this.defineReactive(value, key, value[key]);
            // 为vue的data做属性代理
            this.proxyData(key);
        })
    }

    defineReactive(obj, key, val) {
        this.observe(val); // 递归查找嵌套属性

        // 创建Dep
        const dep = new Dep();

        // 为data对象定义属性:三个参数(data对象,属性,描述符/配置)
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚举
            configurable: true, // 可修改或删除
            get() {
              // 触发get时收集依赖项 Dep.target,即监听器实例watcher
                Dep.target && dep.addDep(Dep.target);
                console.log(dep.deps);
                
                return val;
            },
            set(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // console.log('数据发生变化!');
                // 通知所有watcher更新视图
                dep.notify();
            }
        })
    }
    // 将$data里面的数据拓宽到vue实例的根上vm
    proxyData(key) {
        Object.defineProperty(this, key, {
            get(){
                return this.$data[key];
            },
            set(newVal){
                this.$data[key] = newVal;
            },
        });
    }

}

// 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知
class Dep {
    constructor() {
        this.deps = []; // deps里面存放的是Watcher的实例
    }
    addDep(dep) {
        this.deps.push(dep);// 添加的是每一个watcher
    }
    // 通知所有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 = this;
        this.vm[this.key];
        Dep.target = null;
    }

    // 更新
    update() {
        // console.log('视图更新啦!');
        this.cb.call(this.vm, this.vm[this.key]);
    }
}

实现编译解析过程(Compile)

// compile.js

// 扫描模板中所有依赖创建更新函数和watcher
class Compile {
  // el是宿主元素或其选择器
  // vm当前Vue实例
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el); // 默认选择器

    if (this.$el) {
      // 将dom节点转换为Fragment提高执行效率
      this.$fragment = this.node2Fragment(this.$el);
      // 执行编译
      this.compile(this.$fragment);
      // 将生成的结果追加至宿主元素
      this.$el.appendChild(this.$fragment);
    }
  }
  // 将当前的html dom节点转化为代码块(fragment)
  node2Fragment(el) {
    // 创建一个新的Fragment
    const fragment = document.createDocumentFragment();
    let child;
    // 将原生节点拷贝至fragment
    while ((child = el.firstChild)) {
      // appendChild是移动操作
      fragment.appendChild(child);
    }
    return fragment;
  }

  // 编译指定片段
  compile(el) {
    let childNodes = el.childNodes;
    // 将类数组转化为数组
    Array.from(childNodes).forEach(node => {
      // 判断node类型,做相应处理
      if (this.isElementNode(node)) {
        // 元素节点要识别k-xx或@xx
        this.compileElement(node);
      } else if (
        this.isTextNode(node) &&
        /\{\{(.*)\}\}/.test(node.textContent)
      ) {
        // 文本节点,只关心格式
        this.compileText(node, RegExp.$1); // RegExp.$1匹配内容
      }
      // 遍历可能存在的子节点
      if (node.childNodes && node.childNodes.length) {
        // 递归
        this.compile(node);
      }
    });
  }

  // 编译元素节点
  compileElement(node) {
    // console.log("编译元素节点");

    // <div k-text="test" @click="onClick">
    const attrs = node.attributes;
    Array.from(attrs).forEach(attr => {
      // 规定指令 k-text="test" @click="onClick"
      const attrName = attr.name; // 属性名k-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("k-") == 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");
    // 正常是取不到的, 使用proxyData才能取到
    let val = vm.exp;
    // 双绑还要处理视图对模型的更新
    node.addEventListener("input", e => {
      vm[exp] = e.target.value;
      // val = e.target.value;
    });
  }

  // 更新
  update(node, vm, exp, dir) {
    let updaterFn = this[dir + "Updater"];
    updaterFn && updaterFn(node, vm[exp]); // 执行更新,get
    new Watcher(vm, exp, function(value) {
      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);
      }
  }
}
<!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">
        
        <p k-text="test"></p>
        <p k-html="html"></p>
        <p>
            <input type="text" k-model="test">
        </p>
        <p>
            <button @click="onClick">按钮</button>
        </p>
    </div>

    <script src="kvue.js"></script>
    <script src="compile.js"></script>
    <script>
      const o = new KVue({
        el: "#app",
        data: {
          test: "allala",
          foo: { bar: "bar" },
          html: '<button>adfadsf</button>'
        },
        methods: {
            onClick() {
                alert('blabla')
            }
        },
      });
      console.log(o.$data.test);
      o.$data.test = "hello,kvue!";
      console.log(o.$data.test);

      console.log(o.$data.foo.bar);
      o.$data.foo.bar = "hello,kvue!";
      console.log(o.$data.foo.bar);
    </script>

    <!-- <div id="app">
        <p>你好,<span id="name"></span></p>
    </div>
    <script>
        var obj = {};

        Object.defineProperty(obj, 'name', {
            get: function () {
                return document.getElementById('name').innerHTML;
            },
            set: function (inner) {
                document.getElementById('name').innerHTML = inner;
            }
        })

        console.log(obj.name);
        obj.name = '乔峰';
        console.log(obj.name);
        
    </script> -->
  </body>
</html>

总结

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。利用了 Object.defineProperty() 这个方法重新定义了对象获取属性值(get)和设置属性值(set)。

vue react angularjs jquery的区别

JQuery与另外几者最大的区别是,JQuery是事件驱动,其他三者是数据驱动。

JQuery业务逻辑和UI更改该混在一起, UI里面还参杂这交互逻辑,让本来混乱的逻辑更加混乱。

Angular使用双向数据绑定,React用于单数据流,Vue支持两者(双向数据绑定和单向数据流)。React是一种开发理念,组件化,分治的管理,数据与view的一体化。它只有一个中心,发出状态,渲染view,对于虚拟dom它并没有提高渲染页面的性能,它提供更多的是利用jsx便捷生成dom元素,利用组件概念进行分治管理页面每个部分(例如 header section footer slider)。

avatar