Vue实例简单实现

简单实现vue框架实例,实现的目的主要看下几个知识点如何进行的:

  • Vue工作机制
  • Vue响应式的原理
  • 依赖收集与追踪
  • 编译compile

以及一些相关操作,代码如下




  
  mvue-test-html


  

{{name}}

{{age}}

如上, 我们需要实现几点:

  • 根组件初始化,el挂载
  • data实现数据双向绑定,视图层响应更新, 如 this.name = '刘翔' 赋值后视图层自动更新
  • created生命周期简单实现
  • 指令v-text、表达式{{name}}、@click、v-model双向数据绑定的实现
  • data、方法等挂载到this上,可以直接调用

这里分两块去处理这些东西,一部分是我们vue实例的处理,还一部分是编译到html的处理。我这里写了两个文件,先实现vue实例,然后又写了个compile的js文件。
MVue
这个里面首先包含一个vue实例,在constructor中我们做一些初始化的事情,然后执行响应式处理,将data中的值都做好拦截及监听处理,最后调用compile渲染出指定的el

observe 这个方法主要做响应式处理,遍历data中的所有键名一一调用defineReactive进行数据响应式处理,然后代理到this实例上。

defineReactive 这个方法主要是给每个属性的get、set定义拦截,做一些拦截处理。同时生成dep和key一一对应,对所有的依赖进行管理。

proxyData 顾名思义就是把data中的值代理到实例上面,方便this.name这样去调用。

这里面还有一个Dep和Watcher两个类,他们主要做依赖收集及管理,Dep里面会管理所有的watcher

// 定义KVue构造函数
class MVue {
  constructor(options) {
    // 保存传入的选项
    this.$options = options;

    // 传入data
    this.$data = options.data;
    // 响应式处理
    this.observe(this.$data);

    this.$methods = options.methods;


    new Compile(options.el, this)

    if (options.created) {
      options.created.call(this)
    }
  }

  // 响应式处理
  observe(data) {
    if (!data || typeof data !== "object") {
      return;
    }

    // 遍历data
    Object.keys(data).forEach(key => {
      // 响应式处理
      this.defineReactive(data, key, data[key]);
      // 代理data中的属性
      this.proxyData(key);
    });
  }

  defineReactive(data, key, val) {
    this.observe(val);

    // 定义一个Dep
    const dep = new Dep(); // 每个dep实例都与key值一一对应
    // 给obj的每个key定义拦截
    Object.defineProperty(data, key, {
      get() {
        // 依赖收集
        Dep.target && dep.addDep(Dep.target);
        return val;
      },
      set(v) {
        if (v !== val) {
          val = v;
          dep.notify();
        }
      }
    });
  }

  // 讲$data中的属性代理到实例上
  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key];
      },
      set(v) {
        this.$data[key] = v;
      }
    });
  }
}

// 创建dep:管理所有的watcher
class Dep {
  constructor() {
    // 存储所有的依赖
    this.deps = [];
  }

  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知更新
  notify() {
    this.deps.forEach(dep => dep.update());
  }
}

// 创建watcher: 保存data中的数值和页面中的挂钩关系
class Watcher {
  constructor(vm, key, cb) {
    // 创建实例时立刻将该实例指向Dep.target便于依赖收集
    this.vm = vm;
    this.cb = cb;
    this.key = key;

    // 触发依赖收集
    Dep.target = this;
    this.vm[this.key]; // 触发依赖收集
    Dep.target = null;
  }

  // 更新
  update() {
    console.log(this.key + "更新了");
    this.cb.call(this.vm, this.vm[this.key])
  }
}

compile 主要做一些指令等一系列操作的处理,包括实例中的el元素经过处理后挂载到dom上等操作

这里主要使用了正则去匹配相应的表达式、指令等,然后做出相关操作处理。具体看代码操作即可。

// 遍历dom,解析指令和插值表达式
class Compile {
  // el 待编译的模板, vm-MVue实例
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);

    // 把模版中的内容移到片段操作
    this.$fragment = this.node2Fragment(this.$el);
    // 执行编译
    this.compile(this.$fragment)
    // 放回至el中
    this.$el.appendChild(this.$fragment)

  }

  node2Fragment(el) {
    // 创建片段
    const fragment = document.createDocumentFragment();

    let child;
    while(child = el.firstChild) {
      fragment.appendChild(child)
    }
    return fragment;
  }

  compile(el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (node.nodeType == 1) {
        // 元素
        // console.log('编译元素' + node.nodeName)
        this.compileEle(node)
      } else if (this.isInter(node)) {
        // 只关心{{XXX}}
        // console.log('编译插值文本' + node.textContent)
        this.compileText(node)
      }
      // 递归子节点
      if (node.children && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  isInter(node) {
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  compileEle(node) {
    const nodeAttr = node.attributes;
    // 匹配 m-xxx
    Array.from(nodeAttr).forEach(attr => {
      // 规定 m-xxx="yyyy"
      const attrName = attr.name;
      const exp = attr.value;
      if (attrName.indexOf('m-') == 0) {
        // 指令
        const dir = attrName.substring(2);
        // 执行
        this[dir] && this[dir](node, this.$vm, exp)
      } else if (attrName.indexOf('@') == 0) {
        // 事件
        const method = attrName.substring(1)
        this.addEvent(node, this.$vm, exp, method)
      }
    })
  }

  // 文本替换
  compileText(node) {
    // console.log(RegExp.$1);
    // console.log(this.$vm[RegExp.$1])
    // 表达式
    const exp = RegExp.$1
    this.update(node, this.$vm, exp, 'text')
  }

  update(node, vm, exp, type) {
    const updater = this[type + 'Updater']
    updater && updater(node, vm[exp])
    new Watcher(vm, exp, function(val) {
      updater && updater(node, val)
    })
  }

  textUpdater(node, val) {
    node.textContent = val
  }

  htmlUpdater(node, val) {
    node.innerHTML = val
  }

  modelUpdater(node, val) {
    node.value = val
  }

  text(node, vm, exp) {
    this.update(node, vm, exp, 'text')
  }

  html(node, vm, exp) {
    this.update(node, vm, exp, 'html')
  }

  model(node, vm, exp) {
    this.update(node, vm, exp, 'model')
    node.addEventListener('input', (e) => {
      vm[exp] = e.target.value
    })
  }

  addEvent(node, vm, exp, method) {
    const fn = vm.$options.methods && vm.$options.methods[exp]
    node.addEventListener(method, fn.bind(vm))
  }
}

Vue实例简单实现_第1张图片
image.png

这只是一个非常简单的vue模仿,距离框架真正处理差了十万八千里,不过里面的一些思路还是比较温和的,仅供vue框架源码初探,后面会具体分析vue的源码。

你可能感兴趣的:(Vue实例简单实现)