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

第一步,分析

须要文件

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

思路分析

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

全部类的功能

Vue文件

MyVue

这个很少说,主文件html

Dep

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

Watcher

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

Compil文件

只有这一个,负责编译node

第二步,框架搭建

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,而后保存一下并拿出来数据。app

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

注,$符号仅仅做为区分,并没有他用dom

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>

复制代码

相关文章
相关标签/搜索