Vue2.x原理浅析

一说到vue的实现原理,我们首先想到是的观察者模式在数据劫持(Object.defineProperty (opens new window))中的使用。那么,什么数据劫持?何谓观察者模式?

Object.defineProperty

简单来说,数据劫持↓

const obj = {
  a: 1
}
Object.defineProperty(obj, "a", {
  // value : 37,   // 值,与get、set方法不能同时存在
  writable : true,    // 可否被修改
  enumerable : true,   // 可否被枚举  for in
  configurable : true,  // 可否被删除 delete
  get() {
    return 3
  },
  set(val) {
    console.log(val)
  }
});

通过这个api,我们可以捕捉到劫持对象键值的获取和改变。并可以在其中Do what we want do.

但是😰有个弊端 => 数据下标也是数组的键,对于数组对象下标赋值不会生效

const arr = [1]
Object.defineProperty(arr, 0, {
  get() {
    return 3
  },
  set(val) {
    console.log(val)
  }
});

那么,自定义一个数组自身变化的是要怎么去监听呢,说好的用Object.defineProperty改变世界呢?
细想一下,数组原型上有自己有以下方法可以改变自己,能想到的都在下面了

[ 'push','pop','shift','unshift','splice','sort','reverse' ]

我们可以劫持这些key,当get访问方法时,我们就能捕获到数组改变的动作了。光说不练假把式↓

const arrayProto = Array.prototype   // 拷贝原型防止污染Array
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(item => { //对数组中改变自身的方法劫持
  Object.defineProperty(arrayMethods, item, {
    value: function () {
      arrayProto[item].call(this, ...arguments) //执行方法
      console.log(123)
    }
  })
})
const arr = [1]
arr.__proto__ = arrayMethods
arr.push(2)  

然后,我们再来简单地看看所谓观察者模式↓

vue中观察者

Subject是要干嘛?Subject即是要添加删除通知Observer更新。所以vue中的dep我们可以简单的这样来写↓
dep.js

export default class Dep {
  constructor() {
    this.subs = [] 
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  notify() {
    //通知订阅者更新
    console.log('dep notify')
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}

有了Subject,我们还差Observer和ConcreteSubject,Observer即是提供一个接口在Subject派发notify的时候通知ConcreteSubject更新。那么,既然要用Object.defineProperty,该怎样设计才能让他们结合起来呢?先不考虑ConcreteSubject,Observer一个对象obj = {a: 1},要与defineProperty结合起来,我们可以在obj键值改变执行set的时候让Subject通知更新 ↓
observer.js

import Dep from './dep'
class Observer {
  constructor(data = {}) {
    this.data = data
    this.walk(this.data)
  }
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      this.defineReactive(obj, keys[i])  // 重新包装
    }
  }
  defineReactive(data, key) {
    const dep = new Dep()
    let val = data[key]
    Object.defineProperty(data, key, {
      enumerabel: true, 
      configurabel: true,
      get() {
        return val
      },
      set(newVal) {
        if (newVal === val) return
        val = newVal
        console.log('in set key =>' + key)
        dep.notify() //通知更新
      }
    })
  }
}
const obj = { a: 1 }
new Observer(obj)
obj.a = 2

至此,,结合了的Object.defineProperty,obj改变通知Subject执行notify已是可行。但是dep的subs还是空的,应该什么时候让Subject添加Observer呢?且notify里sub的update接口也还未实现。我们是否应该给Observer一个update接口?不了,这个时候有个Watcher该登场了,让Watcher去代替Observer提供一个update的接口,或者说Watcher便是ConcreteSubject,省去了observer而直接update了,这是vue中的实现方案。
首先,完善Subject ↓

Class Dep

dep.js

export default class Dep {
  //订阅器  ==>  收集订阅者
  static target = null
  constructor() {
    this.subs = [] //收集watcher数组
  }
  addSub(sub) {
    //添加watcher的方法
    this.subs.push(sub)
  }
  removeSub(sub) {  // 移除watcher
    if (this.subs.length) {
      const index = this.subs.indexOf(sub)
      if (index > -1) {
        return this.subs.splice(index, 1)
      }
    }
  }
  depend() {
    //传入订阅器,然后添加订阅者
    if (Dep.target) Dep.target.addDep(this)
  }
  notify() {
    // console.log('notify', this.subs)
    //通知订阅者更新
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}
const targetStack = [] // 存放target的数组
export const pushTarget = (target) => {
  targetStack.push(target)
  Dep.target = target
}
export const popTarget = () => {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

又到了思考的时候,怎样让Watcher作为ConcreteSubject却又和Observer关联起来呢?也许是那被Observer了的那个obj?什么时候该让dep添加watcher呢?什么时候watcher又应该去执行dep的notify呢?带着疑惑,我们走进源码。

搞Watcher之前,先把Observer稍微完善一下吧 ↓
observer.js

export default class Observer {
  constructor(data = {}) {
    this.data = data
    this.dep = new Dep() // 以备不时之需
    this.walk(this.data)
  }
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      this.defineReactive(obj, keys[i])
    }
  }
   // 数据劫持/包装
  defineReactive(obj, key) {
    const _this = this
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key) //不包括Symbol值的所有属性的描述,即enumerabel、configurabel等
    if (property?.configurable === false) return // 处理不可修改属性
    const getter = property?.get // get方法是否被修改
    const setter = property?.set // set方法是否被修改
    let val = obj[key]
    let childObj = this.observe(val) // 处理值是对象的情况
    Object.defineProperty(obj, key, {
      enumerabel: true,
      configurabel: true,
      get() {
        const value = getter?.call(obj) ?? val
        dep.depend()
        if (childObj) childObj.dep.depend()  // 这里的dep就是从【以备不时之需】来的
        return value
      },
      set(newVal) {
        const value = getter?.call(obj) ?? val
        if (newVal === value) return
        if (setter) {
          setter.call(obj, newVal)
        } else {
          val = newVal
        }
        childObj = _this.observe(newVal) // 处理新值是对象的情况
        dep.notify()
      }
    })
  }
  observe(value) {
    if (!value || typeof value !== 'object') return
    return new Observer(value) //只对object进行劫持
  }
}

从半成品的Observer,我们能看到在get的时候走了dep.depend,也就是说在这个是Subject才能去添加watcher,从Dep的方法可以看出,当前Dep的target存在且是watcher,由watcher调用Dep的方法添加自身,而且添加过后得清除Dep的target,不然每次obj取值的时候都会添加一次watcher。 作为ConcreteSubject的watcher要在notify的时候干嘛呢?简单点,如果我们只想知道新旧值呢?

import Observer from '../../util/vue/observer'
import Watcher from '../../util/vue/watcher'

const data = {
  a: 1,
  b: {
    c: 2
  },
  d: []
}
const observer = new Observer(data)
const watcher1 = new Watcher(data, 'a', (val, oldVal) => {
  console.log(val, oldVal)
})
const watcher2 = new Watcher(data, 'b.c', (val, oldVal) => {
  console.log(val, oldVal)
})
const watcher3 = new Watcher(data, 'd', (val, oldVal) => {
  console.log(val, oldVal)
})
data.a = 2
data.b.c = 1
data.d.push(3)

该怎样去实现这个Watcher呢?

  • 先有Observer后有Watcher,这样才能depend嘛
  • Watcher初始化的时候得过一遍depend
  • Watcher有个update

Class Watcher

vue/watcher.js

import _ from 'lodash'
import Dep, {
  pushTarget,
  popTarget
} from './dep'
export default class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this.exp = expOrFn  
    this.cb = cb
    this.initGetter(expOrFn)  
    this.value = this.get()
  }
  initGetter(expOrFn) {
    Object.prototype.toString.call(expOrFn) === '[object Function]' && (this.getter = expOrFn)
    if (typeof expOrFn === 'string') this.getter = this.parsePath(expOrFn)  // 处理a.b等情况
  }
  get() {
    pushTarget(this)
    const vm = this.vm
    let value
    try {
      value = this.getter.call(vm, vm) // call,apply,bind能改变箭头函数this指向吗?
    } catch (error) {} finally {
      if(typeof value === 'object') {
        value = _.cloneDeep( value ) // 简单替代源码中的traverse解决引用类型问题
      }
    }
    popTarget()
    return value
  }
  addDep(dep) {
    dep.addSub(this)  // 添加到dep
  }
  update() {   // dep的update
    // 什么
    this.run()
  }
  parsePath(path) {
    const segments = path.split('.')
    return function(obj) {
      for (let i = 0; i < segments.length; i++) {
        if (!obj) return
        obj = obj[segments[i]]
      }
      return obj
    }
  }
  run() {
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    typeof value === 'object' && (value = _.cloneDeep( value ))
    if (value !== this.value) {
      const oldValue = this.value
      this.value = value
      this.cb.call(this.vm, value, oldValue)  // 回调
    }
    const fn = this.vm?.updated?? null  
    // console.log(this.vm)
    if(typeof fn === 'function') fn() // vue的updated生命周期钩子
  }
}

Wathcer有了,Vue的实现有了一点点眉目了。我们大概可以猜到Vue中的MVVM大概就是Vue所管理的data被Observer了,view和data之间又建立的很多个Watcher,view的更新也就是执行了Watcher的回调函数。
那么,我们先完善一下Observer,搞了对象,还有数组的情况没处理呢

  • 数组也该depend
  • 数组也该notify
  • 不能嫌弃数组下标不能Object.defineProperty

Class Observer

vue/observer.js

// @ts-nocheck
import Dep from './dep'
const def = (obj, key, val, enumerable = false) => {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

const arrayProto = Array.prototype  // 拷贝原型防止污染Array
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach((method) => {
  def(arrayMethods, method, function(...args) {
    // console.log(this)   
    const ob = this.__ob__
    let inserted
    switch (
      method // 也许添加了数组或者对象,需要重新Observer
    ) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2) 
        break
    }
    if (inserted) ob.observeArray(inserted)
    const result = arrayProto[method].apply(this, args)
    // notify change
    ob.dep.notify()   // 数组变化通知更新
    return result
  })
})
//  数组修复
const arrayKeys = Object.getOwnPropertyNames(arrayMethods) //获取数组劫持的方法

export default class Observer {
  constructor(data = {}) {
    this.data = data
    this.dep = new Dep() // 以备不时之需
    def(data, '__ob__', this)   // 上面数组要用
    if (Array.isArray(data)) {
      const augment = data.__proto__ ? this.protoAugment : this.copyAugment
      // 获取替换原型的方法
      //此处的 arrayMethods 就是上面使用Object.defineProperty处理过
      augment(data, arrayMethods, arrayKeys) //将传进来的数组替换原型,以后当前data使用push什么的,就是arrayMethods的劫持了
      this.observeArray(data) //数组的订阅器添加
    }
    this.walk(this.data)
  }
  walk(obj) {  // 为所有可枚举的key走包装
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      this.defineReactive(obj, keys[i])
    }
  }
  observeArray(arr) {  // 数组的包装
    for (let i = 0, l = arr.length; i < l; i++) {
      this.observe(arr[i])
    }
  }
  // 数据劫持/包装
  defineReactive(obj, key, shallow = true) {   // 默认浅包装
    const _this = this
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key) //不包括Symbol值的所有属性的描述,即enumerabel、configurabel等
    if (property ?.configurable === false) return // 处理不可修改属性
    const getter = property ?.get // get方法是否被修改
    const setter = property ?.set // set方法是否被修改
    let val = obj[key]
    let childObj = !shallow && this.observe(val) // 处理值是对象的情况
    Object.defineProperty(obj, key, {
      enumerabel: true,
      configurabel: true,
      get() {
        const value = getter ?.call(obj) ?? val
        dep.depend()
        if (childObj) {
          childObj.dep.depend() // 这里的dep就是从【以为不时之需】来的
          if (Array.isArray(value)) {
              _this.dependArray(value)   // 新设置的是数据,走数组包装
          }  
        }
        return value
      },
      set(newVal) {
        const value = getter ?.call(obj) ?? val  // 从被修改的get拿值, computed中的get?
        if (newVal === value) return
        if (setter) {
          setter.call(obj, newVal)
        } else {
          val = newVal
        }
        childObj = !shallow && _this.observe(newVal) // 处理新值是对象的情况
        dep.notify()   // 数据变化通知更新
      }
    })
  }
  observe(value) {
    if (!value || typeof value !== 'object') return
    let ob
    if ( Object.prototype.hasOwnProperty.call(value, '__ob__') && 
    value.__ob__ instanceof Observer ) {
      ob = value.__ob__
    } else {
      ob = new Observer(value)
    }
    return ob //只对object进行劫持
  }
  protoAugment(target, src) {
    target.__proto__ = src //支持隐式原型直接赋值
  }
  copyAugment(target, src, keys) { //不支持的set key
    for (let i = 0, l = keys.length; i < l; i++) {
      const key = keys[i]
      this.def(target, key, src[key]) //将key值设置到target来源对象上去
    }
  }
  dependArray(value) {  // 对数组的每个下标都处理
    for (let e, i = 0, l = value.length; i < l; i++) {
      e = value[i]
      e ?.__ob__ ?.__ob__.dep.depend()
      if (Array.isArray(e)) {
        dependArray(e)
      }
    }
  }
}

至此,vue的观察者告一段落。 然后,该步入正题去简单的实现vue了。要简单实现vue的一些什么功能呢?

Example

index.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Vue原理浅析</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <div id='app'>
    <p>Vue原理解析</p>
    <!-- 表达式解析,数据绑定 -->
    <p>表达式解析,数据绑定:</p>
    <p>JSON.stringify/parse:{{JSON.stringify(child)}}</p>
    <p>eval:{{eval("text + ' word ' +  child.child.text")}}</p>
    <!-- 计算属性 -->
    <p>计算属性:{{computeText}}</p>
    <p>计算属性get方法:{{getText}}</p>
    <!-- 双向数据绑定 -->
    <p>双向数据绑定【text】:<input type="text" v-model="text"></p>
    <!-- style -->
    <p :style="style">style:{{JSON.stringify(style)}}</p>
    <!-- 事件绑定 -->
    <p @click='numAdd(2,3,$event)'>
      事件:<span :style='{cursor:"pointer",color: blueColor}' v-on:click="text = '' ">清空text</span>
      <span>{{num}}</span>
    </p>
    <!-- class -->
    <p class="pointer no-select" :class="[isRed?'red': 'blue', 'bigFont']" @click="isRed = !isRed">class: {{text}}</p>
    <!-- 指令 -->
    <!-- v-html -->
    <p>v-html:<span v-html='strHtml'></span></p>
    <!-- v-for -->
    v-for:【list】 <span @click='addList'>向list数组添加数据</span>
    <ul>
      <li v-for='(item,index) in list'>{{list}}</li>
    </ul>
  </div>
</body>

</html>
<style type="text/css">
  .red{
      color: red;
  }
  .blue{
      color: blue;
  }
  .pointer{
      cursor: pointer;
  }
  .bigFont {
    font-size: 30px;
  }
  .active{

  }
  .no-select{
    user-select:none;
  }
</style>
  • 数据绑定,模板引擎解析
  • v-model双向数据绑定
  • 事件
  • class、style
  • 常用指令:v-html、v-for等

有了想要实现的,想太多太累,先写一个vue
index.js

import Vue from '../../util/vue'
window.vm = new Vue({
  el: '#app',
  data() {
    return {
      num: 1,
      text: 'hello',
      child: {
        text: 'world',
        child: {
          text: '2018'
        }
      },
      isRed: true,
      list: [1, 2, 3],
      strHtml: '<p class="red">我是来自data的html</p>',
      style: {
        color: '#00bbcc'
      },
      blueColor: 'blue'
    }
  },
  computed: {
    computeText: function() {
      return this.text + ' ' + this.child.text + ' form computed'
    },
    getText: {
      get() {
        return this.text + ' form get '
      }
    }
  },
  methods: {
    numAdd(...args) {
      // console.log(args)
      this.num ++
    },
    addList() {
      this.list.push(this.text)
      // console.log(this.list)
    }
  },
  beforeCreated() {
    // console.log('beforeCreated')
  },
  created() {
    // console.log('created')
  },
  beforeMount() {
    // console.log('beforeMount')
  },
  mounted() {
    // console.log('mounted')
  },
  updated() {
    // console.log('updated')
  },
  watch: {
    // text: function(newVal, old) {
    //   console.log(newVal,old)
    // },
    // 'child.text': function(newVal, old) {
    //   console.log('child.text => ' + newVal)
    // },
    // list: function(newVal,oldVal) {
    //     console.log(newVal,oldVal)
    // }
  }
})

Class Vue

vue/index.js

import _ from 'lodash'
import Observer from './observer'
import Watcher from './watcher'
import Compiler from './compiler'
class Vue {
  constructor(options) {
    this.$options = options
    this.props = options.props
    this.data = options.data || {}
    this.watch = options.watch || {}
    this.computed = options.computed || {}
    this.methods = options.methods || {}
    this.beforeCreated = options.beforeCreated || {}
    this.created = options.created || function() {}
    this.beforeMount = options.beforeMount || function() {}
    this.beforeUpdate = options.beforeUpdate || function() {}
    this.mounted = options.mounted || function() {}
    this.updated = options.updated || function() {}
    this.$el = options.el
    this._init()
  }
  _init(options) {
    const vm = this
    this.initProxy(vm) // 代理vue实例,_renderProxy 
    this.initLifecycle(vm) // 初始化一些生命周期相关的属性,以及为parent,child(非抽象组件)等属性赋值
    this.initEvents(vm) // 初始化存放事件对象,父组件:hook钩子处理
    this.initRender(vm) // vNode、$slots、¥scopedSlots、$createElement函数等初始化,$attrs、$listeners代理
    this.initInjections(vm) // resolve injections before data/props
    this.beforeCreated() // 第一个钩子
    this.initState(vm)
    this.initProvide(vm) // resolve provide after data/props
    this.created()
    if (vm.$el) vm.$mount(vm.$el)
  }
  expectKey(key) { //判断this上的key是否有重复了
    const f = Object.keys(this).includes(key)
    if (f) {
      throw Error(`The property or method {key} already define`)
    }
  }
  initProxy(vm) {
    // vm._renderProxy = vm
  }
  initLifecycle(vm) {
    // vm._watcher = null
    // vm._inactive = null
    // vm._directInactive = false
  }
  initEvents(vm) {}
  initRender(vm) {
    vm.$vnode = null
    vm._isMounted = false
    // vm.$createElement = createElement
  }
  initInjections(vm) {}
  initState(vm) {
    if (this.props) this.initProps(vm, this.props)
    this.initMethods(vm, this.methods)
    this.initData(vm)
    this.initComputed(vm, this.computed)
    this.initWatch(vm, this.watch)
  }
  initProvide(vm) {}
  initProps(vm, props) {}
  initMethods(vm, methods) {
    Object.keys(methods).forEach(key => {
      this.expectKey(key)
      Object.defineProperty(vm, key, {
        get: () => methods[key]
      })
    })
  }
  initData(vm) { //代理data
    let data = vm.data
    data = vm._data = typeof data === 'function' ?
      vm.data() :
      data || {}
    Object.keys(data).forEach(key => { //将data上的key代理到this身上,即 this._data[key] === this[key]
      this.expectKey(key)
      Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        get: () => data[key],
        set: (newVal) => {
          data[key] = newVal
        }
      })
    })
    new Observer(vm)
  }
  initComputed(vm, computed) {
    Object.keys(computed).forEach(key => {
      this.expectKey(key)
      const get = typeof computed[key] === 'function' ? computed[key] : computed[key].get
      Object.defineProperty(vm, key, {
        get
      })
    })
  }
  initWatch(vm, watch = {}) {
    for (const key in watch) {
      const handler = watch[key]
      this.createWatcher(vm, key, handler)
    }
  }
  createWatcher(vm, expOrFn, handler) {
    if (typeof handler === 'string') {
      handler = vm[handler]
    }
    if (typeof handler !== 'function') console.error(`{key} of watch should be function`)
    if (typeof handler === 'function') {
      new Watcher(vm, expOrFn, handler)
    }
  }
  $mount(el) {
    // if (!this.$options.render) this.$options.render = createEmptyVNode
    this.beforeMount()
    // let updateComponent = function() {
    //   vm._update(vm._render(), hydrating);
    // };
    // new Watcher(vm, updateComponent, noop, {
    //   before: function before() {
    //     if (vm._isMounted && !vm._isDestroyed) {
    //       callHook(vm, 'beforeUpdate');
    //     }
    //   }
    // }, true /* isRenderWatcher */ );
    if (this.$vnode == null) {
      this._isMounted = true
      new Compiler(el, this)
      this.mounted()
    }
    return this
  }
  _update(vnode) {
    const vm = this
    // const prevVnode = vm._vnode
    // vm._vnode = _.cloneDeep(vnode)
    // if (!prevVnode) {
    //   vm.$el = vm.__patch__(vm.$el, vnode)
    // } else {
    //   vm.$el = vm.__patch__(prevVnode, vnode)
    // }
  }
  _render() {
    const vm = this
    // const { render } = vm.$options
    // let vnode
    // try {
    //   vnode = render.call(vm._renderProxy, vm.$createElement)
    // } catch (e) {
    //   vnode = vm._vnode
    // }
    // if (Array.isArray(vnode) && vnode.length === 1) {
    //   vnode = vnode[0]
    // }
    // if (!(vnode instanceof VNode)) {
    //   vnode = createEmptyVNode()
    // }
    // return vnode
  }
}

export default Vue

vue中的$mount是new了一个watcher,通过改变_isMounted执行updateComponent函数,先_render返回vNode,update接收vNode作__patch_(diff算法),__patch__内部实现了节点的更新替换。vNode和diff算法暂不做深入了,直接以new Compiler方式去实现节点挂载和更新

Class Compiler

vue/compiler.js

import {CompileUtil} from './compileHelper'
export default class Compiler {
  constructor(el, vm) {
    this.$el = el instanceof HTMLDivElement ? el : document.querySelector(el)   // 拿dom
    this.$vm = vm  // 拿vue实例
    this._init()
  }
  _init() {
    if (!this.$el) return
    this.$fragment = this.createFragment(this.$el)   // 创建碎片
    // console.log(this.$fragment)
    this.compileElement(this.$fragment)     //---解析碎片
    this.$el.appendChild(this.$fragment)   //挂载
  }
  createFragment(el) {
    let fragment = document.createDocumentFragment(),
      childNode
    while (childNode = el.firstChild) { //循环拿到所有节点
      // console.log(childNode)
      fragment.appendChild(childNode)
    }
    return fragment
  }
  compileElement(el) {    // 节点编译函数
    // console.log(el.childNodes)
    Array.from(el.childNodes, node => {
			const text = node.textContent
			const	reg = /\{\{((?:.|\r?\n)+?)\}\}/g
      // console.log(node,text)
      if (node.childNodes && node.childNodes.length) {
				this.compileElement(node)      //递归编译所有节点
			}
      if (this.isTextNode(node) && reg.test(text)) {
				this.compileText(node)       //编译文本节点中的模板
			}
		})
  }
  isElementNode(node) {   // 判断元素节点
		return node.nodeType === 1
	}
  isTextNode(node) {   // 判断文本节点
		return node.nodeType === 3
	}
  compileText(node) {  // 文本节点编译
    CompileUtil.text(node,this.$vm, node.textContent)   // 渲染到节点
  }
}

目前的complier我们拿到了文本节点及其内容,我们需要将一个工具函数将双花括号内的表达式与vm结合起来,然后编译到文本节点中去 ↓
vue/compilerHelper.js

import Watcher from './watcher'
import _ from 'lodash'
export const CompileUtil = {
	text(node, vm, exp) {   //编译文本
		this._bind(node, vm, exp, 'text')
	},
	_bind(node, vm, exp, dir) {
		const updaterFn = Updater[`${dir}Updater`]     //根据指令来的是什么的更新?
		updaterFn && updaterFn(node, vm, exp)   //存在并更新到fragment上去
    // new Watcher(vm, 'data', (val, oldVal) => {
    //   updaterFn(node, vm, exp)
    // })
	}
}

export const Updater = {
	textUpdater: (node, vm, exp) => {  
    _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;  // 设置自定义模版
    const text = _.template(exp)(vm)
		node['textContent'] = text
	}
}

上面几个简单的函数帮助我们将模版中的内容编译成功,但是尚且不能做到vm中的值改变后重新编译。
注释中直接对data作Watcher,data改变就去执行updaterFn明显不可取。所以我们还需要拿到表达式中对应在data中的key,对这些key作Watcher,相应的key的值变化,执行相对于的updaterFn ↓
vue/compilerHelper.js

_bind(node, vm, exp, dir) => {
const updaterFn = Updater[`${dir}Updater`]     //根据指令来的是什么的更新?
updaterFn && updaterFn(node, vm, exp)   //存在并更新到fragment上去
  // new Watcher(vm, '_data', (val, oldVal) => {
  //   updaterFn(node, vm, exp)
  // })
const temp = dir === 'text'? true: false
const variable = this._keyOfExp(exp, temp)    //获取表达式里的变量,添加订阅者,变化后自动更新
if(!variable || !variable in vm) return
  // console.log(variable)
variable.forEach(key => {
    new Watcher(vm, key, () => {
      updaterFn(node, vm, exp)
    })
  })
},
_keyOfExp(exp, temp = false) {  //从一个表达式(string)里拿变量(vm的属性)
 const variaArr = [exp].reduce((result,item) => {
    let variaStr = ''
    if(temp) {  // 来自模板引擎,也许多个{{}}情况
      item.match(/\{\{((?:.|\r?\n)+?)\}\}/g).forEach(v => {
        variaStr += /^(?:\{\{)((?:\s|\S)*)(?:\}\})$/.exec(v)[1]
      })
    } else {
      variaStr = item
    }
    if( /JSON\.(?:stringify|parse)\(([a-zA-Z_\$]\w*)\)/g.test(variaStr) ) {
      result.push(RegExp.$1)
    }
    if( /eval\((['"])([\s\S]*)\1\)/g.test(variaStr) ) {
      variaStr = RegExp.$2
    }
    variaStr.replace(/\s/g,'').split(/[\+\-\*\/\|\&]/).forEach(str => {
      const exp = str.match(/^\w+(?:\.\w*)*$/g)
      if(exp) result.push(...exp)
    })
    return result
  },[])
  return [...new Set(variaArr)]
}

处理了数据变化视图响应之后,我们来处理其他
完善 vue/compiler.js

import {CompileUtil} from './compileHelper'
export default class Compiler {
  constructor(el, vm) {
    this.$el = el instanceof HTMLDivElement ? el : document.querySelector(el)   // 拿dom
    this.$vm = vm  // 拿vue实例
    this._init()
  }
  _init() {
    if (!this.$el) return
    this.$fragment = this.createFragment(this.$el)   // 创建碎片
    // console.log(this.$fragment)
    this.compileElement(this.$fragment)     //---解析碎片
    this.$el.appendChild(this.$fragment)   //挂载
  }
  createFragment(el) {
    let fragment = document.createDocumentFragment(),
      childNode
    while (childNode = el.firstChild) { //循环拿到所有节点
      // console.log(childNode)
      fragment.appendChild(childNode)
    }
    return fragment
  }
  compileElement(el) {    // 节点编译函数
    // console.log(el.childNodes)
    Array.from(el.childNodes, node => {
			const text = node.textContent
			const	reg = /\{\{((?:.|\r?\n)+?)\}\}/g
      // console.log(node,text)
      if (node.childNodes && node.childNodes.length) {
				this.compileElement(node)      //递归编译所有节点
			}
      if (this.isTextNode(node) && reg.test(text)) {
				this.compileText(node)       //编译文本节点中的模板
			}
      if (this.isElementNode(node)) {     //编译节点
				this.compileNode(node)
			}
		})
  }
  compileText(node) {  // 文本节点编译
    CompileUtil.text(node,this.$vm, node.textContent)   // 渲染到节点
  }
  compileNode(node) {
		Array.from(node.attributes, attr => {
			// console.dir(attr)
			if (this.isDirective(attr.name)) {     //指令的编译
        // console.dir(attr)
				const exp = attr.value,			  //获取节点的属性              
					[dirFont,dirValue] = [RegExp.$1, RegExp.$2]   //获取指令前缀和值
          // console.log([dirFont,dirValue])
				if ( /(v-on:)|@/.test(dirFont) ) {     //是否是事件指令
					// console.log(dirFont,dirValue,exp)
					CompileUtil.eventHandler(node, this.$vm, dirValue, exp)   //处理绑定事件
				} else {
          // console.log(dirValue,exp)
					CompileUtil[dirValue] && CompileUtil[dirValue](node, this.$vm, exp)   //根据获取的指令,执行指令编译
				}
			}
		})
	}
  isElementNode(node) {   // 判断元素节点
		return node.nodeType === 1
	}
  isTextNode(node) {   // 判断文本节点
		return node.nodeType === 3
	}
  isDirective(attr) {  // 判断指令
		return/^((?:v-(?:\w+:)?)|:|@)(\w+)/.test(attr)
	}
}

以上,我们完成了:

  • 对节点和文本的complie
  • 对指令的判断
  • 对事件的处理

完善 vue/compilerHelper.js

import Watcher from './watcher'
import _ from 'lodash'
export const CompileUtil = {
  text(node, vm, exp) { //编译文本
    this._bind(node, vm, exp, 'text')
  },
  eventHandler(node, vm, eventType, exp) {
    node.addEventListener(eventType, function(e) {
      // with(vm) {
      // 	eval(exp)        //执行事件里的函数
      // }
      _with(exp, vm, /^\$event$/, () => {
        const o = {}
        for (let i in e) {
          try {
            o[i] = JSON.stringify(e[i])
          } catch (err) {}
        }
        return JSON.stringify(o)
      }, (expOrFn, $1) => {
        if (/^this\.(\w+)$/.test(expOrFn) && $1 in vm.methods) {
          expOrFn += '()'
        }
        return expOrFn
      })
    })
  },
  model(node, vm, exp) {
    if (node.nodeName == 'INPUT') {
      this._bind(node, vm, exp, 'model')
      node.addEventListener('input', (e) => {
        const val = e.target.value
        vm[exp] = val
      })
    }
  },
  class(node, vm, exp) {
    this._bind(node, vm, exp, 'class')
  },
  style(node, vm, exp) {
    this._bind(node, vm, exp, 'style')
  },
  _bind(node, vm, exp, dir) {
    const updaterFn = Updater[`${dir}Updater`] //根据指令来的是什么的更新?
    updaterFn && updaterFn(node, vm, exp) //存在并更新到fragment上去
    // new Watcher(vm, '_data', (val, oldVal) => {
    //   updaterFn(node, vm, exp)
    // })
    const variable = this._keyOfExp(exp, dir) //获取表达式里的变量,添加订阅者,变化后自动更新
    if (!variable || !variable in vm) return
    // console.log(variable)
    variable.forEach(key => {
      new Watcher(vm, key, () => {
        updaterFn(node, vm, exp)
      })
    })
  },
  _keyOfExp(exp, temp = 'text') { //从一个表达式(string)里拿变量(vm的属性) 
    const variaArr = [exp].reduce((result, item) => {
      let variaStr = ''
      if (temp === 'text') { // 来自模板引擎,也许多个{{}}情况
        item.match(/\{\{((?:.|\r?\n)+?)\}\}/g).forEach(v => {
          const mv = /^(?:\{\{)((?:\s|\S)*)(?:\}\})$/.exec(v)[1] + '+'
          variaStr += mv
        })
      } else if (temp === 'class') {
        variaStr = item.replace(/^\[([\s\S]+?)\]$/, '$1')
      } else {
        variaStr = item
      }
      if (/JSON\.(?:stringify|parse)\(([a-zA-Z_\$]\w*)\)/g.test(variaStr)) {
        result.push(RegExp.$1)
      }
      if (/eval\((['"])([\s\S]*)\1\)/g.test(variaStr)) {
        variaStr = RegExp.$2
      }
      variaStr.replace(/[\s\r\n]/g, '').split(/[\+\-\*\/\|\&,\?]/).forEach(str => {
        const nExp = str.match(/^\w+(?:\.\w*)*$/g)
        if (nExp) result.push(...nExp)
      })
      return result
    }, [])
    return [...new Set(variaArr)]
  }
}

let classI = 0,
  proClass

export const Updater = {
  textUpdater(node, vm, exp) {
    _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
    const text = _.template(exp)(vm)
    node['textContent'] = text
  },
  modelUpdater(node, vm, exp) {
    const value = vm[exp]
    node.value = typeof value == 'undefined' ? '' : value;
  },
  classUpdater(node, vm, exp) {
    _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
    const nExp = `{{${exp}}}`
    const classNames = _.template(nExp)(vm).split(',') 
    !classI && (proClass = [...node.classList])
    node.className = [...proClass, ...classNames].join(' ')
    classI++
  },
  styleUpdater(node, vm, exp) {
    const value = _with(exp,vm)
    node.style = Object.keys(value).reduce((sty,key) => {
      const style = `${key}:${value[key]};`
      sty += style
      return sty
    },'')
  }
}

function _with(exp, vm, extraRule, extraFn, otherFn) {
  let expOrFn, rule = /([a-zA-Z\$]+\w*)/g
  if(/^{[\s\S]+?}$/.test(exp)) {
    rule = /\:\s*([a-zA-Z\$]+\w*)/g
    exp = exp.replace(/\s/g,'')
  }
  expOrFn =  exp.replace(rule, (a, b) => {
    let nb, i = a.indexOf(b)
    const f = extraRule instanceof RegExp && extraRule.test(b)
    if (!f) nb = `this.${b}`
    else {
      nb = typeof extraFn === 'function' && extraFn(b)
    }
    a = a.split(b)
    a.splice(i,0,nb)
    return a.join('')
  })
  if(typeof otherFn === 'function') expOrFn = otherFn(expOrFn, RegExp.$1)
  return new Function('return ' + expOrFn).call(vm)
}

到此为止,实现了↓

  • 模板解析
  • 事件绑定
  • v-model
  • :style
  • :class

~gayhub (opens new window)