Vue数据响应式和编译原理分析 和 模拟实战

1,104 阅读5分钟

第一步,分析

需要文件

  1. 一个自己写的Vue.js文件 称为MyVue
  2. 一个模板编译文件,把插值表达式( {{xxx}}称之为插值表达式 )替换成对应的值

思路分析

​ 引入MyVue文件,New一个 Vue对象,挂载元素,处理数据。响应化设置,模板编译。

所有类的功能

Vue文件

MyVue

这个不多说,主文件

Dep

管理/控制者,管理/控制数据的更新

Watcher

观察者,观察数据的变化,写数据更新的方法

Compil文件

只有这一个,负责编译

第二步,框架搭建

MyVue

class Myvue { // 核心文件
  
}

// 管理 若干watcher 的实例,和key 是一对一
class Dep {
  
}

// 保存依赖(和key的依赖)  实现update更新
class Watcher {
  
}

compile

// 遍历模板 将里面的插值表达式做处理
// 如果发现 k-bind 等指令 特殊处理

class Compile {
  
}

第三步,具体实现

MyVue文件中

myvue类

全部代码展示

class myvue {
  constructor (options) {
    // 初始化 加$作用 区分开
    this.$options = options
    this.$data = options.data
    // 响应式
    this.observe(this.$data)
      
    new Compile(options.el, this)
    options.created && options.created.call(this)
  }

  // 递归遍历,是数据相应化
  observe (value) {
    if (!value || typeof value !== 'object') {
      return
    }

    // 遍历
    Object.keys(value).forEach(key => {
      // 定义响应式
      this.defineReactive(value, key, value[key])
      this.proxyData(key)
    })
  }

  // 座一层代理
  proxyData (key) {
    // 这里的this 指的是app 实例
    Object.defineProperty(this, key, {
      get () {
        return this.$data[key]
      },
      set (newValue) {
        this.$data[key] = newValue
      }
    })
  }

  // 函数外面访问了内部遍历 形成了闭包  定义响应式
  defineReactive (obj, key, val) {
    // 递归 遍历深对象
    this.observe(val)

    // 创建Dep Dep和key 一一对应
    const dep = new Dep() // Vue依赖收集的代码
    Object.defineProperty(obj, key, {
      get () {
        // 将Dep 指向的watcher 放到Deo中
        Dep.target && dep.addDep(Dep.target) // Vue依赖收集的代码
        return val
      },
      set (newValue) {
        if (newValue !== val) {
          val = newValue
          // console.log(`${key}属性更新了`) // Vue数据响应的代码
          dep.notify()
        }
      }
    })
  }
}

构造函数

constructor (options) {
    // 初始化 加$作用 区分开
    this.$options = options
    this.$data = options.data
    // 响应式
    this.observe(this.$data)
    
    // 编译相关 后面会再说
    new Compile(options.el, this)
    options.created && options.created.call(this)
  }

我们首先要接收一个 配置项options,然后保存一下并拿出来数据。

拿出data数据后要进行 数据的响应式 也就是observe,并把data传入。最后编译模板

注,$符号仅仅作为区分,并无他用

observe函数

// 递归遍历,是数据相应化
  observe (value) {
    if (!value || typeof value !== 'object') {
      return
    }

    Object.keys(value).forEach(key => {
      // 定义响应式
      this.defineReactive(value, key, value[key])
      this.proxyData(key)
    })
  }

众所周知,我们需要的data是一个函数或者对象。这里我们 只考虑对象,如果不是对象直接返回。然后遍历数据,拿到key。给value对象里面对应的key设置响应式。最后 设置一层代理

代理的作用app是myvue实例

不设置代理的化,我们需要这样访问数据

app.$data.test = '123456'

设置代理之后

可以直接访问app.test = '123456'

defineReactive函数

  defineReactive (obj, key, val) {
    // 递归 遍历深对象
    this.observe(val)
    // 创建Dep Dep和key 一一对应
    const dep = new Dep() // Vue依赖收集的代码
    Object.defineProperty(obj, key, { // 给这个对象添加 访问器属性
      get () {
        // 将Dep 指向的watcher 放到Deo中
        Dep.target && dep.addDep(Dep.target) // Vue依赖收集的代码
        return val
      },
      set (newValue) {
        if (newValue !== val) {
          val = newValue
          // console.log(`${key}属性更新了`) // Vue数据响应的代码
          dep.notify()
        }
      }
    })
  }

Dep类

作用:管理 若干watcher 的实例

class Dep {
  constructor () {
    this.deps = []
  }

  addDep (Watcher) {
    this.deps.push(Watcher)
  }

  // 通知 即执行watcher 里面的 update 函数
  notify () {
    this.deps.forEach(dep => dep.update())
  }
}

这个类比较简单。就不详细说了

Watcher类

// 保存依赖(和key的依赖)  实现update更新
class Watcher {
  constructor (vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb // 后来加上的  动态改变的时候加上的

    // 把当前实例指定给Dep.target
    Dep.target = this
    this.vm[this.key] // 触发get 动态改变的时候加上的
    Dep.target = null
  }

  update () {
    this.cb.call(this.vm, this.vm[this.key]) // 发生嵌套得不到值  保证this指向正确
  }
}

这个引用是再 编译代码的时候创建的,感觉最主要的是this.vm[this.key]这句可能不太理解,就是这样访问这个属性,这个就触发了get函数,使得 添加到Dep.deps 里面,这样就可以监听到他的变化

compile文件

全部代码展示

class Compile {
  constructor (el, vm) { // 接收一个vue 实例 和绑定元素
    this.$vm = vm 
    this.$el = document.querySelector(el) // 获得元素

    if (this.$el) {
      // 把 el里面的内容放到另一个fragment里面去,也就是另一个空白DOM树,提高操作效率
      this.$fragment = this.node2Fragment(this.$el)
      console.log(this.$fragment)
      // 编译 fragment
      this.compile(this.$fragment)

      // 将编译结果追加到宿主中 有则删除重新添加
      this.$el.appendChild(this.$fragment)
    }
  }

  // 遍历el 将里面的内容 搬到新创建的fragment
  node2Fragment (el) {
    const fragment = document.createDocumentFragment() // 创建空白DOM树
    let child
    while ((child = el.firstChild)) { // 每次取出一个
      // console.log(el.firstChild.nodeName)  // 文本也算一个节点
      // appendChild移动操作 即所有孩子全到了 fragment下
      fragment.appendChild(child)
    }
    // console.log(el.firstChild) 输出结果为空
    return fragment
  }

  // 编译  把指令和事件做处理
  compile (el) {
    // 遍历el
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isElement(node)) {
        // console.log(`编译元素:${node.nodeName}`)

        // 如果是元素节点,就要处理指令等
        this.compileElement(node)
      } else if (this.isInterpolation(node)) { // 是不是插值表达式
        // console.log(`编译文本:${node.textContent}`)
        // 处理文本
        this.compileText(node)
      }

      // 递归子元素
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 是不是 元素节点  参考网址 https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
  isElement (node) {
    return node.nodeType === 1
  }

  // 插值表达式的判断   需要满足 {{xx}}
  isInterpolation (node) {
    // test 测试方法 参考网址如下
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent) // textContent返回一个节点后代和文本内容
  }

  // 编译文本
  compileText (node) {
    // console.log(RegExp.$1)  // 这个就是匹配出来的值 {{xxx}}  这个就是xxx
    const exp = RegExp.$1
    this.update(node, this.$vm, exp, 'text')
    // node.textContent = this.$vm[exp]
  }

  // update函数 可复用 exp表达式 dir具体操作
  update (node, vm, exp, dir) {
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    new Watcher(vm, exp, function () { // 添加响应式
      fn && fn(node, vm[exp])
    })
    // 创建watcher
  }

  textUpdate (node, exp) {
    node.textContent = exp
  }

  modelUpdate (node, value) {
    node.value = value
  }

  htmlUpdate (node, value) {
    node.innerHTML = value
  }

  // 编译元素节点
  compileElement (node) {
    // 查看 node特性中 是否有 k-xx这样的指令
    const nodeAttrs = node.attributes // attribute属性返回该元素所有属性节点的一个实时集合
    // console.log(nodeAttrs)
    Array.from(nodeAttrs).forEach(attr => {
      const attrName = attr.name // k-xxx
      const exp = attr.value // k-xxx = 'abc'  这是abc
      if (attrName.indexOf('k-') === 0) {
        const dir = attrName.substring(2) // 拿到xxx
        // console.log(this)
        this[dir] && this[dir](node, this.$vm, exp)
      } else if (attrName.indexOf('@') === 0) { // 这就是事件
        const eventName = attrName.substring(1)
        this.eventHandle(node, this.$vm, exp, eventName)
      }
    })
  }

  // text指令实现
  text (node, vm, exp) {
    this.update(node, vm, exp, 'text')
  }

  // 双向绑定实现
  model (node, vm, exp) {
    this.update(node, vm, exp, 'model')
    node.addEventListener('input', e => {
      vm[exp] = e.target.value
    })
  }

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

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

重要代码分析

node2Fragment

// 遍历el 将里面的内容 搬到新创建的fragment
  node2Fragment (el) {
    const fragment = document.createDocumentFragment() // 创建空白DOM树
    let child
    while ((child = el.firstChild)) { // 每次取出一个
      // console.log(el.firstChild.nodeName)  // 文本也算一个节点
      // appendChild移动操作 即所有孩子全到了 fragment下
      fragment.appendChild(child)
    }
    // console.log(el.firstChild) 输出结果为空
    return fragment
  }

document.createDocumentFragment() 这个方法是创建了新的 空dom树(一般来说,不直接修改数据),然后进行遍历.el.firstChild,属于 移动操作,会自动向下走

compileElement

// 编译元素节点
  compileElement (node) {
    // 查看 node特性中 是否有 k-xx这样的指令
    const nodeAttrs = node.attributes // attribute属性返回该元素所有属性节点的一个实时集合
    // console.log(nodeAttrs)
    Array.from(nodeAttrs).forEach(attr => { // 获取到每一个属性 进行判断
      const attrName = attr.name // k-xxx
      const exp = attr.value // k-xxx = 'abc'  这是abc
      if (attrName.indexOf('k-') === 0) {
        const dir = attrName.substring(2) // 拿到xxx
        // console.log(this)
        this[dir] && this[dir](node, this.$vm, exp) // 如果存在就执行这个函数
      } else if (attrName.indexOf('@') === 0) { // 这是事件
        const eventName = attrName.substring(1)
        this.eventHandle(node, this.$vm, exp, eventName) // 执行事件函数
      }
    })
  }

compile

// 编译  把指令和事件做处理
  compile (el) {
    // 遍历el
    const childNodes = el.childNodes // 返回所有节点的集合
    Array.from(childNodes).forEach(node => {
      if (this.isElement(node)) {
        // console.log(`编译元素:${node.nodeName}`)
        // 如果是元素节点,就要处理指令等
        this.compileElement(node) // 处理执行之类的操作
      } else if (this.isInterpolation(node)) { // 是不是插值表达式
        // console.log(`编译文本:${node.textContent}`)
        // 处理文本
        this.compileText(node)
      }

      // 递归子元素
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

第四步,全部代码和效果展示

<!DOCTYPE html>
<html lang="en" xmlns="">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <p>{{name}}</p>
    <p k-text="name"></p>
    <p>{{age}}</p>
<!--    <p>{{doubleAge}}</p>-->
    <input type="text" k-model="name">
    <button @click="changeName">呵呵</button>
    <div k-html="html"></div>
</div>
<script src='./Myvue.js'></script>
<script src='./Compile.js'></script>

<script>
const app = new Kvue({
  el: '#app',
  data: {
    name: 'I am test.',
    age: 12,
    html: '<button>这是一个按钮</button>'
  },
  created () {
    console.log('开始啦')
    setTimeout(() => {
      this.name = '我是测试'
    }, 1500)
  },
  methods: {
    changeName () {
      this.name = '哈喽,我是xxx'
      this.age = 1
    }
  }
})
</script>
</body>
</html>