手撸Vue框架

自己实现一个简单的MVVM框架,可以帮助我们更好的理解Vue框架,接下来我们将一步一步去实现自己的框架。
我们不妨叫它Wue,之前有一篇文章介绍过响应式原理,这里就不作赘述。

代码链接

思路

首先我们要明白,我们的Wue 构造函数需要做什么。

  • 接收初始化参数
  • 将data中的属性注册到Wue实例上
  • 监听data属性值的变化
  • 解析指令和插值表达式
    明确直到要做的事情之后,我们一步一步实现

接受初始化参数

这个很简单,只需要保存传入的对象,并做校验

class Wue {
  constructor (options) {
    //保存配置项
    this.$options = options || {}
    //保存dom
    this.$el =
      typeof options.el === 'string'
        ? document.querySelector(options.el)
        : options.el
    //保存data
    this.$data = options.data
  }
}

注册data的属性

为了能通过Wue实例来操作data对象的属性,我们需要将其注册到Wue实例上。
怎么注册呢?
其实也是通过Object.defineProperty实现的,我们只需要在操作Wue属性的时候,映射到data属性上即可。

 //将data属性的值注册到options上
  _proxyData () {
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get () {
          return this.$data[key]
        },
        set (newVal) {
          if (this.$data[key] !== newVal) {
            this.$data[key] = newVal
          }
        }
      })
    })
  }

测试一下,我们会发现,可以直接通过Wue实例来操作data属性了。

创建Obserer

通过Observer来监听data属性。
这里需要注意,监听的是data属性,而非Wue的属性。

//监听data属性值的变化
class Observer {
  constructor (data) {
    this.walk(data)
  }

  //遍历data,进行监听
  walk (data) {
    if (!data || typeof data != 'object') {
      return
    }
    Object.keys(data).forEach(key => {
      let currentVal = data[key]
      this.defineReactive(data, key, currentVal)
    })
  }

  defineReactive (data, key, currentVal) {
    const context = this
    //假如属性的值是对象,需要添加响应式
    this.walk(currentVal)
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get () {
        return currentVal
      },
      set (newVal) {
        if (newVal !== currentVal) {
          currentVal = newVal
          //假如新赋值给属性的值式对象,需要添加响应式
          context.walk(newVal)
        }
      }
    })
  }
}

这里需要额外注意两种情况

  • 初始化的时候,属性值是对象的情况
  • 重新给属性赋值为对象的情况

这两种情况的处理方法,都在上面的注释中给出,就是单独将这个对象属性做响应式处理。

解析指令和插值表达式

通过字符串匹配,将dom元素里的指令和插值表达式用相应的响应式数据代替

//解析器

const NodeTypeElement = 1
const NodeTypeText = 3

class Complier {
  //需要传入 Wue实例
  constructor (wm) {
    this.$wm = wm
    this.$el = wm.$el
    this.complie(wm.$el)
  }

  complie (el) {
    Array.from(el.childNodes).forEach(node => {
      if (node.childNodes && node.childNodes.lenth != 0) {
        //有子节点的情况下遍历子节点
        this.complie(node)
      }
      //处理插值表达式
      if (this.isTextNode(node)) {
        this.complieText(node)
      }
      //处理指令
      if (this.isElementNode(node)) {
        this.complieElement(node)
      }
    })
  }

  //判断是否为文本节点
  isTextNode (node) {
    return node.nodeType === NodeTypeText
  }

  //判断是否为元素节点
  isElementNode (node) {
    return node.nodeType === NodeTypeElement
  }

  //判断是否属性是指令
  isDirective (attr) {
    return attr.startsWith('v-')
  }

  //解析插值表达式,将{{msg}} 替换为 wm.msg
  complieText (node) {
    let reg = /\{\{(.+)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.$wm[key])
    }
  }

  //解析元素节点
  complieElement (node) {
    let attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      if (this.isDirective(attr.name)) {
        let type = attr.name.slice(2) + 'Updater'
        this[type].call(this, attr)
      }
    })
  }

  // v-text处理  将属性值替换为data属性值
  textUpdater (attr) {
    attr.value = this.$wm[attr.value]
  }

  // v-model处理
  modelUpdater (attr) {
    attr.value = this.$wm[attr.value]
  }
}

到此我们完成了模板解析,接下来是需要把数据监听和模板解析联系起来,这样才能保证,当我们数据模型改变的时候,视图会自动更新。
我们要观察者模式,响应式对象的每个属性都会创建自己的发布者,负责收集依赖。而观察者负责适时的去更新dom。

发布者

发布者有两个核心功能,依赖收集和触发观察者

//发布者,依赖收集,通知观察者
class Dep {
  constructor () {
    this.sub = []
  }

  addSub (watcher) {
    this.sub.push(watcher)
  }

  notify () {
    this.sub.forEach(item => {
      item.update && item.update()
    })
  }
}

观察者

某个属性更新之后,发布者会通知观察者,触发dom更新

//观察者
class Watcher {
  constructor (wm, key, cb) {
    //传入wm实例和key,是为了创建watcher之后马上调用get()来然dep收集依赖
    this.$wm = wm
    this.$key = key
    //dep当前的watcher
    Dep.target = this
    //会调用get方法,让dep收集当前watcher
    let oldVal = wm[key]
    //收集完之后销毁,防止重复收集
    Dep.target = undefined

    this.cb = cb
  }

  update () {
    this.cb(this.$wm[this.$key])
  }
}

联系数据模型和视图

接下来我们需要用观察者模式将数据模型和视图联系起来,真正实现数据驱动视图。

创建dep

每个对象都要对应一个dep,所以在做响应化处理的时候来创建dep。

Observer.js

defineReactive (data, key, currentVal) {
    const context = this
    //假如属性的值是对象,需要添加响应式
    this.walk(currentVal)
    //数据响应式处理的时候创建一个新的发布者
    let dep = new Dep()
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get () {
        //依赖收集
        Dep.target && dep.addSub(Dep.target)
        return currentVal
      },
      set (newVal) {
        if (newVal !== currentVal) {
          currentVal = newVal
          //假如新赋值给属性的值式对象,需要添加响应式
          context.walk(newVal)
          //通知观察者
          dep.notify()
        }
      }
    })
  }
创建watcher

watcher负责视图的更新,自然要在解析模板的时候创建。
我们拿v-text指令举例

compiler.js

// v-text处理  将属性值替换为data属性值
  textUpdater (node,attr) {
    node.textContent = this.$wm[attr.value]
    //创建watcher,进行视图更新
    new Watcher(this.$wm,attr.value,function(newValue){
      node.textContent = newValue
    })
  }
dep和watcher对应

现在有了depwatcher,怎么把他们对应起来呢?某个属性的dep应该被跟它相关的watcher观察到。

使用Dep.target静态属性来保存watcher 实例。创建watcher实例的时候,把当前watcher实例赋值给Dep.target,然后马上访问data.key,
get里,Dep实例从Dep.target里取到watcher实例收集起来,然后马上将Dep.targer清空。

这里不可谓不精妙,我当时想了很久。
代码上面已经给出过了,这里不再重复贴了。

双向数据绑定

我们已经实现了数据驱动视图,接下来是先双向数据绑定,视图更新修改数据模型。
道理很简单就是在相应的dom上添加事件,我们以input为例,只需要在模板解析时加上相应代码。

// v-model处理
  modelUpdater (node,attr) {
    node.value = this.$wm[attr.value]
    new Watcher(this.$wm,attr.value,function(newValue){
      node.value = newValue
    })
    //双向数据绑定
    window.addEventListener('input',(event)=>{
      this.$wm[attr.value] = node.value
    })
  }
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csw_coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值