Skip to content

jackchoumine/vue2-core-reactivity

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

make my own vue2 core reactivity

特性

  • 实现 {{}}v-model@这三个指令;

  • 计算属性;

  • methods;

  • watch;

usage

<script src="https://unpkg.com/[email protected]/index.js"

dont install it by npm.

const vue = new MyVue({
  el: '#app',
  data: {
    firstName: 'Jack',
    lastName: 'Chou',
    age: 20,
    a: { b: 'b' },
  },
  computed: {
    name() {
      return this.firstName + this.lastName
    },
    yourAge: {
      get() {
        return this.age + 10
      },
      set(value) {
        this.age = value - 10
      },
    },
  },
  methods: {
    addAge(event) {
      console.log(event)
      this.age += 10
      console.log(this)
    },
    onClick(num, name, age, str) {
      console.log('点击了')
      console.log(num, name, age, str)
      console.log(this.name)
    },
  },
})

vue2-core-reactivity

vue 的核心功能就是实现了数据到模板的响应式系统----修改数据,vue 自动执行副作用(更新 DOM、执行监听器等),从而让开发者从手动处理 DOM 更新的繁琐中解脱出来。

今天就来实现一个响应式系统核心,完全实现 vue 的响应式系统,还是一个很复杂的一项工程,本文只实现核心部分和三个指令:{{}}v-model@click

vue 2 中,是利用 Object.defineProperty 来重新定义 vue 实例上的属性,从而实现的响应式系统的。

主要涉及属性:

  • enumerable,属性是否可枚举,默认 false。

  • configurable,属性是否可以被修改或者删除,默认 false。

  • get,获取属性的方法。

  • set,设置属性的方法。

响应式基本原理:在 Vue 的构造函数中,对 vue 对象的 options 进行二次定义,即在初始化 vue 实例的时候,对 data、props、methods 等对象的每一个属性都通过 Object.defineProperty 定义一次,在数据被修改时,可在 set 中执行某些操作,比如更新视图、执行一个监听器等。

myVue 实现

function MyVue(options = {}) {
  // vue 组件选项赋值给 $options
  this.$options = options
  // options 的 data 给 this_data
  const data = (this._data = options.data ?? {})
  //监听 data
  observe(data)

  Object.keys(data).forEach(key => {
    //NOTE 重新定义 this,实现 this 代理 this._data
    // 即 this.a 获取的值是  this._data.a
    Object.defineProperty(this, key, {
      enumerable: true,
      get() {
        return this._data[key]
      },
      set(newValue) {
        this._data[key] = newValue
      },
    })
  })

  // 初始化计算属性
  initComputed.call(this)
  // 初始化实例方法
  initMethods().call(this)
  // 编译模板即使得 vue 对象和 dom 模板产生关联,更新 vue 实例的属性,模板才会更新
  new Compile(options.el, this)
}

看 initMethods 和 initComputed

function initComputed() {
  const vm = this
  const { computed } = vm.$options ?? {}
  Object.keys(computed).forEach(key => {
    Object.defineProperty(vm, key, {
      get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
      set: computed[key] === 'function' ? computed[key] : computed[key].set,
    })
  })
}

function initMethods() {
  const vm = this
  const { methods = {} } = vm.$options
  Object.keys(methods).forEach(key => {
    vm[key] = methods[key]
  })
}

在 myVue 中,调用 initMethods 和 initComputed 传递 this 特别重要:需要将方法和计算属性代理到实例上。

// 初始化计算属性
initComputed.call(this)
// 初始化实例方法
initMethods().call(this)

compile 是模板编译函数

function Compile(el, vm) {
  vm.$el = document.querySelector(el)
  const compileElement = compileTemplate(vm)
  vm.$el.appendChild(compileElement)
}

这里使用 DOM 查询 代替模板编译。

compileTemplate是很关键的函数,稍后再看。

如何观察 data 的变化?

observe的作用是监听 data 的变化,然后执行某些操作。

// NOTE 要求 data 必须是一个对象
function observe(dataObj) {
  if (typeof dataObj !== 'object') {
    // NOTE 监听对象上的属性
    return //dataObj
  }
  return new Observe(dataObj)
}

function Observe(data) {
  const dep = new Dep()
  // NOTE 新增的属性,不存在 get 和 set,故不能新增
  Object.keys(data).forEach(key => {
    let value = data[key]
    observe(value)
    Object.defineProperty(data, key, {
      enumerable: true,
      get() {
        // NOTE 订阅
        Dep.target && dep.addSub(Dep.target) // [watcher]
        return value
      },
      set(newValue) {
        // 新值和老值相同,啥都不做
        if (newValue === value) {
          return
        }
        value = newValue
        // NOTE 这样写爆栈
        // data[key] = newValue

        // NOTE 监听 data.key = { key:'value'}
        // 实现深度监听
        observe(newValue)
        // 发布
        dep.notify()
      },
    })
  })
}

Observe的作用就是重新定义data的每一个属性,嵌套的对象属性也得到了处理。

每监听一个对象,都需要进行依赖收集,const dep = new Dep()

依赖收集的实现采用了发布于订阅模式。

function Dep() {
  this.watchers = []
}

// 订阅
Dep.prototype.addSub = function (watcher) {
  this.watchers.push(watcher)
}
// 发布
Dep.prototype.notify = function () {
  this.watchers.forEach(watcher => {
    watcher.update()
  })
}

get收集依赖(订阅),在set检测到依赖变化,进行发布。

get() {
  // NOTE 订阅
  Dep.target && dep.addSub(Dep.target) // [watcher]
  return value
},
set(newValue) {
  // 新值和老值相同,啥都不做
  if (newValue === value) {
    return
  }
  value = newValue
  // NOTE 这样写爆栈
  // data[key] = newValue
  // NOTE 监听 data.key = { key:'value'}
  // 实现深度监听
  observe(newValue)
  // 发布
  dep.notify()
},

Watcher用于监听 vue 实例属性,当属性有变化时,会执行 fn。

function Watcher(vm, propAttrs, fn) {
  // fn(newValue)
  this.fn = fn
  this.vm = vm
  this.propAttrs = propAttrs
  // 使用一个全局对象,表明当前存在需要收集的依赖
  Dep.target = this
  let val = vm
  propAttrs.forEach(key => {
    val = val[key]
  })
  Dep.target = null
}

Watcher.prototype.getUpdatedValue = function () {
  let value = this.vm
  this.propAttrs.forEach(key => {
    value = value[key]
  })
  return value
}

Watcher.prototype.update = function () {
  this.fn(this.getUpdatedValue())
}

fn 是在模板编译阶段传入的更新函数,该函数将模板和 Watcher 连接起来,比如模板里使用v-model指令,在 fn 中就要更新node的 value,检测到@指令,就要去处理事件。

本教程只实现{{}}v-model@这三个指令,指令的解析在模板编译阶段进行:

function compileTemplate(vm) {
  const fragment = document.createDocumentFragment()
  while ((child = vm.$el.firstElementChild)) {
    fragment.appendChild(child)
  }
  bindValueToTemplate(fragment, vm)

  function bindValueToTemplate(fragment, vm) {
    if (!vm) {
      throw new Error('bindValueToTemplate 缺少 vm')
    }
    Array.from(fragment.childNodes).forEach(node => {
      const text = node.textContent

      const reg = /\{\{(.*)\}\}/g
      if (node.nodeType === 1) {
        // 元素节点
        const nodeAttrs = Array.from(node.attributes)
        nodeAttrs.forEach(attr => {
          const { name, value: prop } = attr
          if (name.indexOf('@') === 0) {
            const eventName = name.substring(1)
            let handleName = prop.substring(0, prop.indexOf('('))
            let params = prop.substring(prop.indexOf('(') + 1, prop.indexOf(')')).split(',')
            if (!prop.includes('(')) {
              handleName = prop
              params = []
            }
            // 处理箭头函数绑定
            if (prop.includes('=>')) {
              if (prop.includes('{')) {
                const body = prop.substring(prop.indexOf('{') + 1, prop.indexOf('}'))
                node.addEventListener(eventName, event => {
                  const handler = new Function('event', body)
                  handler(event)
                })
                return
              } else {
                const body = prop.split('=>')[1]
                node.addEventListener(eventName, event => {
                  const handler = new Function('event', body)
                  handler(event)
                })
                return
              }
            }
            // 不能直接绑定函数,需要处理 this
            // node.addEventListener(eventName, vm[prop])
            node.addEventListener(eventName, event => {
              const _params = params.map(item => {
                const { data, computed } = vm.$options
                const value = isDataKey(data, item)
                  ? vm[item]
                  : isComputed(computed, item)
                  ? typeof computed[item] === 'function'
                    ? computed[item].call(vm) // 处理 this
                    : computed[item].get.call(vm) // 处理 this
                  : !Number.isNaN(+item)
                  ? +item
                  : item
                return value
              })
              // NOTE 拿不到 arguments
              // console.log(vm[handleName].arguments)
              if (_params.length) {
                vm[handleName](..._params)
              } else {
                // console.log(handleName)
                vm[handleName](event)
              }
            })
          }
        })
        if (reg.test(text)) {
          let val = vm
          // NOTE 关键 处理了 a.b
          const propAttrs = RegExp.$1.split('.')
          propAttrs.forEach(key => {
            val = val[key]
          })
          // NOTE 技巧
          new Watcher(vm, propAttrs, updatedValue => {
            console.log('fn', updatedValue)
            node.textContent = text.replace(reg, updatedValue)
          })
          node.textContent = text.replace(reg, val)
        } else if (!text) {
          // 处理 v-model
          const nodeAttrs = Array.from(node.attributes)
          nodeAttrs.forEach(attr => {
            const { name, value: prop } = attr
            if (name.indexOf('v-') === 0) {
              // NOTE 处理 v-mode="a.b"
              let val = vm
              const propAttrs = prop.split('.')
              propAttrs.forEach(key => {
                val = val[key]
              })
              node.value = val
              // 监听属性更改
              new Watcher(vm, propAttrs, updatedValue => {
                // NOTE 修改属性时自动更新 input 的 value
                node.value = updatedValue
              })

              node.addEventListener('input', function (event) {
                const value = event.target.value
                // NOTE 处理 v-mode="a.b"
                let currentValue = vm
                let lastProp = propAttrs[0]
                propAttrs.forEach((key, index) => {
                  if (index <= propAttrs.length - 1) {
                    lastProp = key
                    if (index <= propAttrs.length - 2) {
                      currentValue = currentValue[key]
                    }
                  }
                })
                currentValue[lastProp] = value
              })
            }
          })
        }
      }
      if (node.childNodes) {
        bindValueToTemplate(node, vm)
      }
    })
  }
  return fragment
}

关键是bindValueToTemplate的实现,这里只处理元素节点类型,比较简单。

{{}}的处理:

const reg = /\{\{(.*)\}\}/g
if (reg.test(text)) {
  let val = vm
  // NOTE 关键  处理类似  <p>{{a.b}}</p>
  // 获取到内层属性值
  const propAttrs = RegExp.$1.split('.')
  propAttrs.forEach(key => {
    val = val[key]
  })
  // NOTE 为何需要这个语句?
  node.textContent = text.replace(reg, val)

  // NOTE 技巧
  new Watcher(vm, propAttrs, updatedValue => {
    node.textContent = text.replace(reg, updatedValue)
  })
}

为何需要执行node.textContent = text.replace(reg, val)?

首次挂载组件时需要将 vue 实例中的属性绑定到页面上,否则会看到这样的情况:

{{}}中的属性值没有被替换。

new Watcher(vm, propAttrs, updatedValue => {
  node.textContent = text.replace(reg, updatedValue)
})

Watcher的第三个参数,就是 vue 实例属性更新时,需要执行的函数。

我们可将其提取成一个函数:

const reg = /\{\{(.*)\}\}/g
if (reg.test(text)) {
  let val = vm
  // NOTE 关键  处理类似  <p>{{a.b}}</p>
  // 获取到内层属性值
  const propAttrs = RegExp.$1.split('.')
  propAttrs.forEach(key => {
    val = val[key]
  })
  const updateText = val => {
    node.textContent = text.replace(reg, val)
  }
  updateText(val) // 第一次挂载组件时,执行这里
  new Watcher(vm, propAttrs, updateText) // 监听属性,执行 updateText
}

v-model的处理,是input事件和node.value = newValue的结合。

// 处理 v-model
const nodeAttrs = Array.from(node.attributes)
nodeAttrs.forEach(attr => {
  const { name, value: prop } = attr
  if (name.indexOf('v-') === 0) {
    // NOTE 处理 v-mode="a.b"
    let val = vm
    const propAttrs = prop.split('.')
    propAttrs.forEach(key => {
      val = val[key]
    })
    node.value = val
    // 监听属性更改
    new Watcher(vm, propAttrs, updatedValue => {
      // NOTE 修改属性时自动更新 input 的 value
      node.value = updatedValue
    })

    node.addEventListener('input', function (event) {
      const value = event.target.value
      // NOTE 处理 v-mode="a.b"
      let currentValue = vm
      let lastProp = propAttrs[0]
      propAttrs.forEach((key, index) => {
        if (index <= propAttrs.length - 1) {
          lastProp = key
          if (index <= propAttrs.length - 2) {
            currentValue = currentValue[key]
          }
        }
      })
      currentValue[lastProp] = value
    })
  }
})

@的处理直接看代码即可。

总结

vue2 响应式原理使用Object.defineProperty重新定义属性,在 getters 中收集依赖,在 setters 检查依赖更新,然后在通知 watcher 执行 render 更新模板。

demo 演示

参考

Vue 2.x 相关原理

Vue2 原理浅谈

About

vue2 响应式系统原理实现

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published