miniVue v1

11/13/2020 vuemini

# Vue 原理


  • 利用 Object.defineProperty/Proxy 对 js 对象进行拦截
  • 在取值操作中进行依赖收集
  • 在赋值操作中进行更新通知
  • 通过 Vdom 进行高性能 diff 变化,达到最小更新 dom 提升性能

# 过程


  • 响应式解析
  • 编译 把非 render 转换成 render 函数,比如 template dom 节点等
  • 渲染 进行 render 计算 Vdom
  • 挂载 把 Vdom 转换成真实节点并进行挂载
  • 更新 监听数据变化,diff 计算,更新页面

# mini 版本实现


# mini 代码

index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>miniVue</title>
  <style>
    .num {
      width: 100px;
      margin: 10px;
      box-shadow: 0 0 3px rgba(0, 0, 0, .3);
    }

    .red {
      color: red;
    }

    .green {
      color: green;
    }
  </style>
</head>

<body>
  <div id="app">
    <h2 a='1' :class='titleClass'>{{title}}</h2>
    <div class='green'>{{desc}}</div>
    <div class="num">{{num}}</div>
    <button @click='addNum'>add</button>
    <button v-on:click='delNum'>minus</button>
    <hr>
    <input type="text" v-model='val'>
    <div v-text='val'></div>
  </div>
</body>
<script src='./utils.js'></script>
<script src='./watch.js'></script>
<script src='./observe.js'></script>
<script src='./vue.js'></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      title: "这是一个demo",
      desc: '一个mini版本的vue简单实现',
      num: 1,
      val: "双向绑定",
      titleClass: "red"
    },
    methods: {
      addNum() {
        console.log('add')
        this.num++
      },
      delNum() {
        this.num--
      }
    }

  })
</script>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
utils.js
function isString(str) {
  return typeof str === 'string'
}
function isObj(obj) {
  return typeof obj === 'object' && obj !== null
}

function parseEl(el) {
  if (isString(el)) {
    if (el.startsWith('#')) {
      return document.getElementById(el.substr(1))
    }
    return document.querySelector(el)
  }
  return el
}

function isMulti(str) {
  return /\{\{(.*)\}\}/.test(str)
}

function isDir(name) {
  if (name.startsWith('v-')) return name.slice(2).split(':')
  if (name.startsWith('@')) return ['on', name.slice(1)]
  if (name.startsWith(':')) return ['bind', name.slice(1)]
}

function isVModel(name) {
  return name === 'v-model'
}
function addEvent(node, eventName, handle) {
  node.addEventListener(eventName, handle, false)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
watch.js
// Dep 就是依赖收集的媒介,存储着一个key所对应的所有依赖项
class Dep {
  constructor() {
    this.deps = []
  }
  add(dep) {
    this.deps.push(dep)
  }
  notice(val) {
    this.deps.forEach(d => d.update(val))
  }
}


// 监听对象,每一处依赖对应一个watcher  
class Watch {
  constructor(vm, key, fn) {
    this.$vm = vm
    // 生成一个依赖,这样在get的时候就会进行收集
    Dep.target = this
    // 这一步是重点,在此处就自动获取一次,触发 get  从而进行收集
    vm[key]
    Dep.target = null
    this.fn = fn
  }
  update(val) {
    this.fn(val)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
observe.js

// 给定一个对象 key value  建立 get set监听函数,
// 在get里面设置依赖收集
// 在set 中进行赋值,并触发相应的依赖项进行跟新 
function observe(obj, key, val) {
  if (!isObj(obj)) return
  let dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      // 这一步比较关键,当存在依赖,需要收集时 进行必要的收集
      if (Dep.target) { dep.add(Dep.target) }
      return val
    },
    set(newVal) {
      val = newVal
      // 触发依赖通知
      dep.notice(newVal)
    }
  })
}

// 把一个对象的属性代理到另一个对象
// 从而实现 便捷访问  比如 可以通过 this.a  直接访问到 this.data.a
function proxy(obj, sourceKey) {
  if (!isObj(obj)) return
  let source = obj[sourceKey]
  if (!isObj(source)) return
  Object.keys(source).forEach(key => {
    Object.defineProperty(obj, key, {
      get() {
        return source[key]
      },
      set(val) {
        source[key] = val
      }
    })
  })
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
vue.js
class Vue {
  constructor(options) {
    this.$options = options
    const { el, data, methods } = options
    // 解析 挂载点
    this.$el = parseEl(el)
    this.$data = data
    this.$methods = methods
    // 建立 双向绑定
    this.ob(data)
    // 代理  data methods 到 this上
    proxy(this, '$data')
    proxy(this, '$methods')
    // 尝试挂载
    this.mount()
  }
  ob(obj) {
    // 此处 值做了对象的处理, 完善的应该增加对数组的处理,以及递归处理
    if (!isObj(obj)) return
    Object.keys(obj).forEach(key => {
      observe(obj, key, obj[key])
    })
  }
  mount(el) {
    // 挂载并进行编译 渲染
    el = el || this.$el
    if (el) this.compile(el)
  }

  compile(node) {
    if (!node) return
    // 获取所有的子节点,进行相对应的解析,并递归
    let childNodes = [...node.childNodes]
    if (childNodes && childNodes.length) {
      childNodes.forEach(childNode => {
        let nodeType = childNode.nodeType
        switch (nodeType) {
          case 1:// 元素
            this.compileEl(childNode)
            this.compile(childNode)
            break;
          case 3: // 文本
            this.compileText(childNode)
            break;
        }
      })
    }
  }
  // 解析元素节点,主要是对属性进行设置,以及添加事件等
  compileEl(node) {
    let attrs = [...node.attributes]
    attrs.forEach(at => {
      const { name, value } = at
      // 判断是否是指令,如果是指令形式,并且已经存在对应的解析器,则调用解析器进行解析
      const dirParsed = isDir(name) // v-on v-model v-text v-html
      if (dirParsed) {
        const [dir, exp] = dirParsed
        this[dir] && this[dir](node, exp, value)
        return
      }
      node[name] = this[value]

    })

  }
  // 事件解析器
  on(node, exp, name) {
    addEvent(node, exp.toLocaleLowerCase(), e => this[name](e, this))
  }
  // v-text解析器
  text(node, exp, val) {
    this.update(node, 'InnerText', val)
  }
  // bind  : 解析器
  bind(node, exp, val) {
    if (exp === 'class') this.update(node, 'Class', val)
  }
  // v-model解析器  Vue中会根据元素类型 做不同的解析,此处比较简单。我们认为就是input
  model(node, exp, value) {
    addEvent(node, 'input', e => {
      const val = e.target.value
      this[value] = val
    })
    const updater = (e) => node.value = e
    new Watch(this, value, updater)
    updater(this[value])
  }
  // 对文本节点进行处理 如果是 {{attr}}形式,则需要进行处理,替换成data中的数据
  compileText(node) {
    if (isMulti(node.nodeValue)) {
      this.update(node, 'Text', RegExp.$1)
    }
  }
  // 建立更新 并进行监听
  update(node, dir, val) {
    let updateFun = this['update' + dir]
    if (updateFun) {
      let updater = (v) => updateFun(node, v)
      new Watch(this, val, updater)
      updater(this[val])
    }
  }
  // 更新器  有text html  等等
  updateText(node, val) {
    node.nodeValue = val
  }
  updateClass(node, val) {
    node.className = val
  }
  updateInnerText(node, val) {
    node.innerText = val
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
Last Updated: 12/11/2020, 11:27:59 AM