简版vue实现(数据响应化,双向绑定,指令解析,事件绑定,编译器实现)

目的

实现类似vue的简版vue包含(数据响应化,双向绑定,指令解析,事件绑定,编译器实现)

思路

首先,先看一下vue的工作机制,如下图所示。

初始化Vue实例。Observer劫持监听所有属性并且遍历每个属性的getter,setter。Compile编译模板解析模板中的指令(m-text,m-html,m-model等),插值文本{{key}},@click事件等。然后通过创建Watcher 收集依赖 (添加订阅者)并且 通过update更新视图。最后当页面中数据发生改。会触发属性的setter 。然后通知订阅者的更新函数实现试图更新。
在这里插入图片描述

其次我们知道Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM的系统,
所以我们重点围绕这一特点展开来实现相应的功能

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

具体实现

1.创建类MyVue

通过Object.defineProperty()实现依赖收集,通知更新。
首先,封装MyVue类,

类的用法 new MyVue({ data:{} })。

在constructor中接收一个options参数。

然后通过observe() 方法遍历实例中的data属性,
注意,这里会有个错误判断 data属性必须存在并且data值是一个对象。
否则:提示console.error(‘data not is Object’);
通过defineReactive方法遍历data每一个属性
并且将data对象代理到vue实例。方便在实例中用this访问。
defineReactive方法内部是 Object.defineProperty(obj, key, { })
在get()方法中对每一个属性进行依赖收集。
Dep.target && dep.addDep(Dep.target)
并且返回属性值。
在set()方法中监听属性值得变化。并且通知更新。

class MyVue {
  constructor(options) {
    // 缓存options
    this.$options = options;

    // 数据响应化
    this.$data = options.data;

    // 观察监听$data
    this.observe(this.$data);

    // 模拟watcher
    // new Watcher();
    // this.$data.test;
    // new Watcher();
    // this.$data.foo.bar;
    // console.log(options.el);
    new Compile(options.el, this);

    //判断created
    if (options.created) {
      // 执行created,并且将this指向created
      options.created.call(this);
    }

  }

  observe(val) {
    // console.log(val)
    // 判断val是否存在 已经类型是否是Object
    if (!val || typeof val !== 'object') {
      return console.error('data not is Object');
    }

    // 遍历对象
    Object.keys(val).forEach(key => {
      this.defineReactive(val, key, val[key]);
      // 代理data中的属性到vue实例
      this.proxyData(key);
    });
  }

  // 数据响应化
  defineReactive(obj, key, val) {

    // 判断对象指为对象时,进行递归操作
    if (typeof val === 'object') {
      this.observe(val);
    }
    // this.observe(val);

    const dep = new Dep();

    Object.defineProperty(obj, key, {
      get() {
        Dep.target && dep.addDep(Dep.target)
        return val;
      },
      set(newValue) {
        if (newValue === val) {
          return;
        }
        val = newValue;
        // console.log(`${key}属性发生改变:${val}`)
        dep.notify();
      }
    })
  }

  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        // 将当前实例中data属性值代理到实例下面直接访问
        return this.$data[key];
      },
      set(newVal) {
        // 通过改变实例中 的值来更新data中的值
        this.$data[key] = newVal;
      }
    })
  }
}

2.创建Dep类(订阅者)

Dep类用来将页面模板中的数据进行依赖收集。

// dep:用来管理watcher 依赖收集
class Dep {
  constructor() {
    // 存放若干依赖
    this.deps = [];
  }
  // 添加依赖
  addDep(dep) {
    this.deps.push(dep)
  }
  // 通知依赖更新
  notify() {
    this.deps.forEach(dep => dep.update())
  }
}

3.创建Watcher类(观察者)

在创建依赖的时候将当前依赖的实例指定到Dep的静态属性target 。以此来实现 在属性getter时 依赖收集的目的。

//watcher 观察者
class Watcher {
  // console.log(this);
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // 将当前watcher实例指定到Dep静态属性target
    Dep.target = this;
    // 出发相对应的getter
    this.vm[this.key];
    Dep.target = null;
    // console.log(this);
  }
  // 依赖更新函数
  update() {
    console.log('属性更新了')
    // callback
    this.cb.call(this.vm, this.vm[this.key]);
  }
}

4.编译器实现,创建类Compile

1.我们为什么编译?

因为我们的vue模板里面的插值绑定,指令,事件等浏览器是不能直接识别的。所以我们需要将模板编译成浏览器可识别的dom。

2.编译流程

将dom节点通过node2Fragment()方法转化为虚拟dom,然后执行编译。
我们之所以选择将dom 转化为虚拟dom 。
是因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(reflow)(对元素位置和几何上的计算)。
因此,使用文档片段documentfragments 通常会起到优化性能的作用(better performance)。
执行编译时先foreach 元素所有子节点。
然后依次判断节点类型。
当节点有子节点就递归。
然后依据元素类型进行不同的编译。
在编译的过程中要通过update函数创建Watcher进行依赖收集。
最后将编译好的文档appendChild到dom中

在这里插入图片描述

// 编译模板 用法 new Compile(el, vm)
//
class Compile {
  constructor(el, vm) {
    // 需要遍历宿主节点
    this.$el = document.querySelector(el);
    this.$vm = vm;

    //编译
    console.log(this.$el)
    if (this.$el) {
      // 转化内容为片段
      this.$fragment = this.node2Fragment(this.$el);
      // 执行编译
      this.compile(this.$fragment);
      //将编译完的HTML结果追加至$el
      this.$el.appendChild(this.$fragment);
    }
  }

  // 将宿主元素中代码片段拿出来遍历。
  node2Fragment(el) {
    const frag = document.createDocumentFragment();
    // 将el中所以子元素搬家到frag
    let child;
    while (child = el.firstChild) {
      frag.appendChild(child)
    }
    return frag;
  }
  // 编译过程
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // node(DOM节点)
      // 类型判断
      if (this.isElement(node)) {
        // 元素
        console.log('编译元素' + node.nodeName)
        // 查找m- @指令和事件,
        const nodeAttrs = node.attributes;
        // 拿出元素所有的元素
        Array.from(nodeAttrs).forEach(attr => {
          // console.log(attr)
          const attrName = attr.name; //属性名
          const exp = attr.value; //属性值
          // 指令
          if (this.isDirective(attrName)) {
            // k-text
            // console.log(attrName)
            const dir = attrName.substring(2);
            // 执行指令
            this[dir] && this[dir](node, this.$vm, exp);
          }
          // 事件
          if (this.isEvent(attrName)) {
            const dir = attrName.substring(1);
            // 执行事件
            this.eventHandler(node, this.$vm, exp, dir);
          }
        })
      } else if (this.isInterpolation(node)) {
        //文本
        console.log('编译文本' + node.textContent)
        // 编译插值文本
        this.compileText(node);
      }

      // 递归子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    })
  }

  //事件处理器
  eventHandler(node, vm, exp, dir) {
    // 判断实例methods是否存在,并且methods 中事件方法是否存在
    let fn = vm.$options.methods && vm.$options.methods[exp]
    if (dir && fn) {
      //dom 监听事件,并且执行实例中methods 方法。将this指向方法
      node.addEventListener(dir, fn.bind(vm));
    }
  }
  // 更新函数
  update(node, vm, exp, dir) {
    // 从当前类组合一个更新方法
    const updaterFn = this[dir + 'Updater']
    // 初始化
    updaterFn && updaterFn(node, vm[exp]) && vm[exp];
    // 依赖收集
    new Watcher(vm, exp, function (val) {
      updaterFn && updaterFn(node, val);
    })
  }

  // 更新文本
  textUpdater(node, val) {
    // 将值传给dom节点
    node.textContent = val;
  }
  // 
  modelUpdater(node, val) {
    // 将值传给dom节点
    node.value = val;
  }
  htmlUpdater(node, val) {
    // 将值传给dom节点
    node.innerHTML = val;
  }
  //m-text 指令
  text(node, vm, exp) {
    // console.log('text--', node, vm, exp)
    this.update(node, vm, exp, 'text');
  }

  html(node, vm, exp) {
    // console.log('text--', node, vm, exp)
    this.update(node, vm, exp, 'html');
  }
  //m-model 指令
  model(node, vm, exp) {
    // 指定input的value属性
    this.update(node, vm, exp, 'model')
    // 视图对模型的响应
    node.addEventListener('input', e => {
      vm[exp] = e.target.value;
    })

  }
  isElement(node) {
    return node.nodeType == 1;
  }
  // 判断插值文本
  isInterpolation(node) {
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
  // 编译文本
  compileText(node) {
    this.update(node, this.$vm, RegExp.$1, 'text');
  }

  // 判断属性是否为指令
  isDirective(attr) {
    return attr.indexOf('m-') == 0;
  }

  // 判断是否为事件
  isEvent(attr) {
    return attr.indexOf('@') == 0;
  }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值