手写一个简单的vue


一、观察者模式

先上一个图来描述一下
在这里插入图片描述
vue的响应式就是基于观察者模式进行的。其实不难理解:比如你是一个作者(发布者),很多粉丝(观察者)关注了你(订阅事件),你一旦写好了一本书发布了(触发了事件),然后粉丝就会阅读书籍(观察者根据触发的事件做一些事情)。那么在vue里面,数据改变了视图就更新,其实道理很简单,数据改变了执行一些函数(dom操作)让页面更新。用一个图来表示数据和这些函数之间的关系。那么这里,每个数据就是发布者,函数就是观察者。
在这里插入图片描述
在上面可以看到,依赖(dom操作的函数)收集发生在数据被访问的时候,其实准确来说应该是第一次被访问的时候。一个数据(发布者)可以对应多个函数(观察者),当这个数据被修改的时候,就会触发依赖(dom操作的函数),执行对应的函数,然后达到视图更新的效果。

二、对Vue的整体分析

在这里插入图片描述
整个流程按顺序大概就是:
1.初始化Vue,完成对应的参数配置(Vue类)
2.把每个数据都进行数据劫持(Observer类)
3.解析指令(v-model等),渲染页面(Compiler类)
4.在初次渲染页面中,初始化观察者(Watcher类)
5.把数据对应的每一个watcher添加到Dep中(Dep类)
那么整个过程下来以后,我们就能实现响应式。
下面就按顺序实现每一个类。

三.Vue类

在Vue类中,有一下事情需要做:
1.初始化一些参数
2.把data中的成员转换成getter和setter,注入到vue实例中(简单理解为用户可以用过this.xx直接拿到数据)
3.调用Observer类监听数据的变化
4.解析模板,渲染页面

class Vue {
  constructor(options) {
  	// 1.初始化一些参数
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2.把data中的成员转换成getter和setter,注入到vue实例中
    this._proxyData(this.$data)
    // 3.调用Observer类监听数据的变化
    new Observer(this.$data)
    // 4.解析模板,渲染页面
    new Compiler(this)
  }

  _proxyData (data) {
    Object.keys(data).forEach(key => {
    	// 注意这里劫持的是this,也就是Vue的实例。
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) return
          data[key] = newValue
        }
      })
    })
  }
}

四.Observer类

这个类的作用就是把传入的data的每一个属性都要劫持,包括嵌套对象的属性。

class Observer {
  constructor(data) {
    this.walk(data)
  }
  // 如果当前的data为对象才可进行遍历每个属性,监听每个属性。
  walk (data) {
    if (!data || typeof data !== 'object') {
      return
    }
    Object.keys(data).forEach(key => {
      this.defineProperty(data, key, data[key])
    })
  }

  defineProperty (data, key, value) {
    const that = this
    // 为每一个属性定义一个发布者
    let dep = new Dep()
    // 若当前的值还是对象,则继续使用walk方法,递归执行。
    this.walk(value)
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get () {
        // 依赖收集,在数据仅在第一次被访问的时候才会执行下面这一行代码
        Dep.target && dep.addWatch(Dep.target)
        return value
      },
      set (newValue) {
        if (value === newValue) {
          return
        }
        value = newValue
        // 如果newValue也是个对象,则使用walk方法,劫持它每一个属性
        that.walk(newValue)

        // 数据当前已经发生了改变,触发依赖,告诉对应的函数(观察者)该执行了
        dep.notify()
      }
    })
  }
}

五.Compiler类

这里只是简单的实现了插值表达式的渲染,以及v-text,v-model指令的渲染。
明确一点就是:每一个指令对应的渲染函数只会执行一次(比如compilerText),在后面修改数据的时候,修改页面是使用watcher去修改的,其实watcher中的函数,和原本的渲染函数是一样的。在Vue源码中,一般正在执行的渲染函数用一个全局变量记载,被称为被激活的渲染函数,那么添加依赖的时候,直接把这个全局变量添加即可。

class Compiler {
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    // 立刻执行compiler进行解析指令,渲染页面
    this.compiler(this.el)
  }
  compiler (el) {
    // 先把当前元素的子节点拿出来,然后逐个分析,解析其中的指令。
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isTextNode(node)) {
        this.compilerText(node)
      } else if (this.isElementNode(node)) {
        this.compilerElement(node)
      }
      // <h3>123123</h3> 元素节点中如果有内容,它的childNodes中就会有一个文本节点
      if (node.childNodes && node.childNodes.length) {
        this.compiler(node)
      }
    })
  }
  // 文本节点渲染方法
  compilerText (node) {
    // 匹配{{}}的形式,这种形式就是使用插值表达式
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
  // 元素节点渲染方法
  compilerElement (node) {
    let attributes = node.attributes
    Array.from(attributes).forEach(attr => {
      if (attr.name.startsWith('v-')) {
        // 因为vue的指令一般以v-开头,所以这里截取了v-之后的字符串,对应指令的渲染函数以 v-之后的字符串+'Update'组成
        let command = attr.name.substr(2)
        let key = attr.value.trim()
        let fn = this[command + 'Update']
        fn && fn.call(this, node, key)
      }
    })
  }
  // v-text的渲染函数
  textUpdate (node, key) {
    node.textContent = this.vm[key]
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // v-model的渲染函数
  modelUpdate (node, key) {
    console.log('modelUpdate');
    node.value = this.vm[key]
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // v-model的形式还要监听文本框的变化,如果文本框的内容一旦变化了,把值重新赋给对应的数据,就会触发依赖,其它用到该数据的视图就会发生变化
    node.addEventListener('input', (value) => {
      console.log(value);
      this.vm[key] = node.value
    })
  }
  // 判断是否为元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
  // 判断是否为文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }
}

六.Watcher类

watcher只有在数据第一次执行对应的渲染函数时候创建,我们在watcher内部进行依赖添加。我们可以先看到Observer类中,添加依赖的条件就是

Dep.target && dep.addWatch(Dep.target)

也就是说,Dep这个类下,有个target的属性才会去添加依赖。这个target其实指的是准备要添加的watcher。
所以第一步我们先把Dep下的target指向当前的watcher。

Dep.target = this

第二步,访问这个数据,触发这个数据的拦截器,来进行依赖收集,其实就是收集这个watcher。

this.oldValue = vm[key]

第三步,把Dep.target置为null,防止下一个下个数据被访问的时候,添加了当前的watcher。

Dep.target = null
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb

    // 触发依赖收集
    Dep.target = this
    this.oldValue = vm[key] //主要这里访问了vm.[key]触发了对应的get方法进行依赖收集
    Dep.target = null
  }
  update (newValue) {
    let newValue = this.vm[this.key]
    if (this.oldValue == newValue) return
    this.oldValue = newValue
    this.cb(newValue)
  }
}

七.Dep类

class Dep {
  constructor() {
    this.subs = []
  }
  addWatch (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 数据修改的时候,告诉收集回来的所有依赖该执行对应的方法了
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值