0年前端的Vue响应式原理学习总结4:最终章

1,615 阅读3分钟

终于到了本系列的最后一篇文章了,这篇文章将会做一个简单的模板编译器,并结合上一篇文章的渲染watcher来实现一个小demo

由于observe,Observer,defineReactive三个文件会互相引用,因此我把他们整合到了一个文件中,便于使用,并且也符合了vue源码的结构

项目地址:gitee

系列文章地址:

  1. 基本原理
  2. 数组的处理
  3. 渲染watcher

MVVM类

我们会像vue一样,建立一个类,叫做MVVM,接收一个配置参数:

class MVVM {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
    // 将数据变为响应式
    observe(this.$data)
    // 模板编译
    if (this.$el) new Compiler(this.$el, this)
  }
}

Vue中,我们可以直接用vue实例访问数据,所以,我们也可以实现这样的功能,定义一个方法:

proxyData(data) {
  Object.keys(data).forEach((key) => {
    Object.defineProperty(this, key, {
      get() {
        return data[key]
      },
      set(newVal) {
        if (data[key] === newVal) return
        data[key] = newVal
      }
    })
  })
}

实例化时执行该方法即可

constructor(options) {
  this.$el = options.el
  this.$data = options.data
  // 先代理数据,这样之后需要数据时就不需要用this.$data.prop了,直接使用this.prop即可
  this.proxyData(this.$data)
  observe(this.$data)
  if (this.$el) new Compiler(this.$el, this)
}

模板编译器

将数据设置为响应式后,开始模板编译。模板编译思路如下:先将el从页面上取出来放到fragment中,编译完成后再放回页面中。

class Compiler {
  constructor(el, vm) {
    // el可以是选择器或元素节点
    this.el = isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    let fragment = node2Fragment(this.el)
    this.compile(fragment)
    this.el.appendChild(fragment)
  }
}

function isElementNode(node) {
  return node.nodeType === 1
}

function node2Fragment(node) {
  let fragment = document.createDocumentFragment()
  let firstChild = node.firstChild
  while (firstChild) {
    fragment.appendChild(firstChild)
    firstChild = node.firstChild
  }
  return fragment
}

compile方法就是主编译方法

compile(node) {
  let childNodes = Array.from(node.childNodes)
  childNodes.forEach((c) => {
    if (isElementNode(c)) {
      this.compileElementNode(c)
    } else {
      this.compileTextNode(c)
    }
  })
}

元素节点编译方法如下

compileElementNode(node) {
  // 获取元素节点的属性来找出指令
  Array.from(node.attributes).forEach(({ name, value: expression }) => {
    if (isDirective(name)) {
      // 指令以v-开头
      const directive = name.split('-')[1]
      // 渲染watcher
      new Watcher(
        this.vm,
        // 相当于解析指令的渲染函数
        directiveCompiler[directive](node, expression, this.vm)
      )
    }
  })
  // 如果该节点时元素节点,应该递归编译该节点内部的节点
  this.compile(node)
}

function isDirective(str) {
  return str.startsWith('v-')
}

指令解析器如下:

function setValue(vm, expression, value) {
  const keys = expression.split('.')
  let obj = vm.$data
  for (let i = 0; i < keys.length - 1; i++) {
    obj = obj[keys[i]]
  }
  obj[keys.slice(-1)] = value
}

// 只处理了v-model指令
export default {
  model(node, expression, vm) {
    // 监听输入框的input方法,实现双向绑定
    node.addEventListener('input', (e) =>
      setValue(vm, expression, e.target.value)
    )
    const value = parsePath(expression).call(vm, vm)
    return function () {
      node.value = value
    }
  }
}

文本解析方法

compileTextNode(node) {
  if (/\{\{(.+?)\}\}/g.test(node.textContent)) {
    // 渲染watcher
    new Watcher(this.vm, textCompiler(node, this.vm))
  }
}

textCompiler方法如下

function textCompiler(node, vm) {
  const text = node.textContent
  return function () {
    const content = text.replace(/\{\{(.+?)\}\}/g, (...args) => {
      const path = args[1].trim()
      const val = parsePath(path).call(vm, vm)
      if (isObject(val)) {
        // 如果模板内是对象,使用JSON.stringify来显示
        // JSON.stringify也会访问对象内部的属性
        // 这样就完成了对该对象所有属性的依赖收集
        return JSON.stringify(val, null, 1) // 第三个参数:空格数量
      }
      return val
    })
    node.textContent = content
  }
}

这样,我们就实现了一个简单模板编译器,实例化MVVM后,就有一个响应式的应用了!!

// index.js
import MVVM from './MVVM'

window.vm = new MVVM({
  el: '.app',
  data: {
    obj: {
      a: 1,
      b: 2
    },
    a: 1,
    arr: [
      {
        a: 1
      }
    ]
  }
})

<div class="app">
  {{ obj }}
  <br />
  {{ arr }}
  <br />
  <input type="text" v-model="obj.a" />
</div>

两个全局方法

不知道大家是否还记得,前面的文章中提到过Vue.$set方法就使用了__ob__属性来添加响应式数据,我们来看一下

$set(target, key, value) {
  // 对于数组利用splice实现添加元素
  if (Array.isArray(target)) {
    // 如果splice索引超过数组长度会报错
    target.length = Math.max(target.length, key)
    target.splice(key, 1, value)
    return value
  }
  // 对于对象,如果该属性已经存在,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = value
    return value
  }
  const ob = target.__ob__
  // 如果目标对象不是响应式对象,直接赋值
  if (!ob) {
    target[key] = value
    return value
  }
  // 设置响应式属性
  defineReactive(target, key, value)
  // 派发更新
  ob.dep.notify()
  return value
}

此外,Vue.$delete方法也是如此

$delete(target, key) {
  // 对于数组用splice方法删除元素
  if (Array.isArray(target)) {
    target.splice(key, 1)
    return
  }
  const ob = target.__ob__
  // 如果对象没有该属性,直接返回
  if (!target.hasOwnProperty(key)) return
  delete target[key]
  // 如果不是响应式对象,则不需要派发更新
  if (!ob) return
  // 对于响应式对象,删除属性后要派发更新
  ob.dep.notify()
}

因此,我们也可以总结出下面的结论

  1. gettersetter闭包中保存的dep用来存储依赖纯对象的属性watcher,只有这个闭包能够访问到这个变量
  2. gettersetter闭包中保存的childOb,就是与这个属性同级的__ob__属性,这个属性存储一个Observer实例,实例上也有一个dep属性,这个属性可以保存依赖数组的watcher,并且外部的方法也可以通过__ob__属性来派发更新

总结

这个系列文章的到此就告一段落了,其实,还有很多内容没有涉及到,比如计算属性watcher等等(主要是我不会)。如果大家意见或者建议,欢迎评论区留言,如果大家看过其他好的文章,也可以留言分享。

之后还会更新vue项目总结的系列文章,也欢迎大家关注,谢谢!!!