vue响应式和依赖收集

2021-01-08  本文已影响0人  一蓑烟雨任平生_cui

看了vue源码后实现的一个很简单很简单的vue😂

目的主要是串一下new Vue()之后到组件挂载的流程,及数据更新时视图的更新流程。

源码主要流程如下:

  1. new Vue()
  2. this._init()
    1. initLifecycle(vm)
    2. initEvents(vm)
    3. initRender(vm)
    4. callHook(vm, 'beforeCreate')
    5. initInjections(vm)
    6. initState(vm)
      1. initProps(vm)
      2. initMethods(vm)
        1. vm.XXX = this.methods[XXX]
      3. initData(vm)
        1. observe(value) // 开启响应式
      4. initComputed(vm)
      5. initWatch(vm)
        1. vm.$watch(expOrFn, cb, option)
    7. initProvide(vm)
    8. callHook(vm, 'created')
    9. vm.options.el ? vm.mount(vm.options.el) : vm.mount(el)
  3. vm.$mount(el)
    1. mountComponent(vm)
      1. callHook(vm, 'beforeMount')
      2. new Watcher()
      3. callHook(vm, 'mounted')

具体包括对象和数组的响应式原理、发布订阅、观察者、单例模式、依赖收集、模版编译

// index.js
import { initState, initMethods } from './init.js'
import initLifecycle from './initLifecycle.js'
import mounted from './mounted.js'
import Compiler from './compiler.js'

class Vue {
  constructor(options) {
    this.vm = this
    this.$options = options
    this.init(this)
  }

  // 初始化操作
  init(vm) {
    initState(vm)
    initMethods(vm)
    initLifecycle(vm)
    mounted(vm)
  }

  $mount(el) {
    Compiler.getInstance(this, el)
  }
}

export default Vue
// init.js
import { def, observe, proxy } from './utils.js'

function initData(vm) {
  let data = vm.$options.data

  // 保存_data 主要是因为data可能是函数
  data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {}

  Object.keys(data).forEach(key => {
    proxy(vm, '_data', key)
  })

  observe(data)
}

function initProps() {}
function initComputed() {}
function initWatch() {}

export function initState(vm) {
  initData(vm)
  initProps(vm)
  initComputed(vm)
  initWatch(vm)
}

export function initMethods(vm) {
  const methods = vm.$options.methods

  if (methods) {
    for (const key in methods) {
      if (methods.hasOwnProperty(key)) {
        def(vm, key, methods[key])
      }
    }
  }
}
// initLifecycle.js
export default function initLifecycle(vm) {
  const created = vm.$options.created

  if (created) {
    created.call(vm)
  }
}
// mounted.js
import Compiler from './compiler.js'

// 挂载
export default function mounted(vm) {
  const el = vm.$options.el

  // 是否提供 el 选项,如果没有则调用实例的 $mount() 方法
  if (el) {
    Compiler.getInstance(vm, el)
  }
}
// utils.js
import Observer from './observer.js'

export function def(target, key, value, enumerable = false) {
  Object.defineProperty(target, key, {
    value,
    enumerable,
    writable: true,
    configurable: true
  })
}

// 将属性代理到实例上 以便可以通过 this.XXX 方式访问
export function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    get() {
      return target[sourceKey][key]
    },
    set(newValue) {
      target[sourceKey][key] = newValue
    }
  })
}

export function observe(data) {
  if (typeof data !== 'object' || data === null) return

  return new Observer(data)
}
// array.js
const arrayPrototype = Array.prototype
const arrayMethods = Object.create(arrayPrototype)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 重写数组方法
methodsToPatch.forEach(method => {
  // 缓存原方法
  const originalMethod = arrayPrototype[method]

  arrayMethods[method] = function (...args) {
    const result = originalMethod.apply(this, args)
    const ob = this.__ob__
    let inserted

    switch (method) {
      // 首尾追加元素
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
      default:
        break
    }

    if (inserted) {
      // this 为修改后的数组
      // 在一开始初始化数组时已经给它添加了属性 __ob__ 指向 Observer 的实例
      ob.observeArray(this)
    }

    return result
  }
})

export default arrayMethods
// defineReactive.js
import Dep from './dep.js'
import { observe } from './utils.js'

// 响应式
export default function defineReactive(target, key, value) {
  observe(value)

  const dep = new Dep()

  Object.defineProperty(target, key, {
    get() {
      Dep.target && dep.addSub(Dep.target)

      return value
    },
    set(newValue) {
      if (value !== newValue) {
        value = newValue
        observe(newValue)

        // 通知依赖 更新视图
        dep.notify()
      }
    },
    enumerable: true,
    configurable: true
  })
}
// dep.js

// 订阅发布
export default class Dep {
  constructor() {
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}
// watcher.js
import Dep from './dep.js'

// 观察者
export default class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb

    Dep.target = this

    // 取值
    this.get()

    Dep.target = null // 避免重复添加Watcher
  }

  get() {
    this.value = this.vm[this.exp]
    this.cb(this.value)
  }

  update() {
    const value = this.vm[this.exp]

    this.cb.call(this.vm, value, this.value)
    this.value = value
  }
}
// observer.js
import arrayMethods from './array.js'
import defineReactive from './defineReactive.js'
import { observe, def } from './utils.js'

export default class Observer {
  constructor(data) {
    this.value = data
    // 便于在其他处操作data时,可以找到该data对应的Observer实例
    def(data, '__ob__', this) // 必须定义成不可枚举的,否则会陷入死循环。仔细品一下为啥😄

    if (Array.isArray(data)) {
      // 数组
      data.__proto__ = arrayMethods

      this.observeArray(data)
    } else {
      // 对象
      this.walk(data)
    }
  }

  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }

  observeArray(array) {
    for (let i = 0; i < array.length; i++) {
      observe(array[i])
    }
  }
}
// compiler.js

import Watcher from './watcher.js'

// 插值正则
const REG = /\{\{(.*)\}\}/

class Compiler {
  constructor(vm, el) {
      this.$el = document.querySelector(el)
      this.$vm = vm

    if (this.$el) {
      // 将节点转成文档片段
      this.nodeToFragment(this.$el)

      // 编译
      this.compile(this.$fragment)

      // 将编译后的片段插入html
      this.$el.appendChild(this.$fragment)
    }
  }

  // 采用单例模式 避免同时存在传入 el 选项 和 调用了 $mount(el)
  static getInstance(vm, el) {
    if (!this.instance) {
      this.instance = new Compiler(vm, el)
    }
    return this.instance
  }

  nodeToFragment(el) {
    const fragment = document.createDocumentFragment()
    let child

    while ((child = el.firstChild)) {
      fragment.appendChild(child)
    }
    this.$fragment = fragment
  }

  compile(fragment) {
    const childNodes = fragment.childNodes

    Array.from(childNodes).forEach(node => {
      if (this.isElement(node)) {
        // console.log('编译元素', node.nodeName)

        const attrs = node.attributes

        Array.from(attrs).forEach(attr => {
          const attrName = attr.name
          const attrValue = attr.value

          if (this.isDirective(attrName)) {
            // 指令
            const dirName = attrName.slice(2) + 'Dir'

            console.log(dirName)

            this[dirName] && this[dirName](node, this.$vm, attrValue)
          }

          // 事件
          const eventName = this.isEvent(attrName)

          if (eventName) {
            this.eventHandler(node, eventName, attrValue)
          }
        })
      } else if (this.isInterpolation(node)) {
        // console.log('编译插值表达式', node.nodeValue)
        this.update(node, this.$vm, RegExp.$1, 'text')
      }

      if (node.childNodes?.length) {
        this.compile(node)
      }
    })
  }

  isElement({ nodeType }) {
    return nodeType === 1
  }

  // 含有插值表达式的文本节点
  isInterpolation({ nodeType, nodeValue }) {
    return nodeType === 3 && REG.test(nodeValue)
  }

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

  // 更新函数
  update(node, vm, exp, dir) {
    const updateFn = this[`${dir}Update`]

    new Watcher(vm, exp, function (newValue) {
      updateFn && updateFn(node, newValue)
    })
  }

  isDirective(name) {
    return /^v-(.*)/.test(name)
  }

  isEvent(name) {
    return /^v-on:|@(.*)/.test(name) && RegExp.$1
  }

  textDir(node, vm, exp) {
    this.commonWatcher(node, vm, 'textContent', exp)
  }

  modelDir(node, vm, exp) {
    this.commonWatcher(node, vm, 'value', exp)

    node.addEventListener('input', e => {
      vm[exp] = e.target.value
    })
  }

  htmlDir(node, vm, exp) {
    this.commonWatcher(node, vm, 'innerHTML', exp)
  }

  commonWatcher(node, vm, prop, exp) {
    new Watcher(vm, exp, function (value) {
      node[prop] = value
    })
  }

  eventHandler(node, eventName, exp) {
    node.addEventListener(eventName, this.$vm[exp].bind(this.$vm))
  }
}

export default Compiler
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <h1>{{title}}</h1>
    <h2>{{age}}</h2>
    <h3>
      <span>{{age}}</span>
    </h3>
    <hr>
    <input type="text" v-model="tel">
    <h3>{{tel}}</h3>
    <hr>
    <div v-text="desc"></div>
    <br>
    <div>这是静态文本</div>
    <hr>
    <button @click="add">add</button>
    <h2>{{count}}</h2>
    <div v-html="html"></div>
  </div>
</body>
</html>

<script type="module">
  import Vue from './index.js'

  new Vue({
    el: '#app',
    data() {
      return {
        title: 'hello',
        tel: 1571121,
        desc: '这是v-text文本',
        count: 2,
        age: 12,
        html: '<p>这是html片段</p>',
        info: {
          name: 'mike',
          age: 23
        },
        skill: ['eat', 'song', {
          foo: 'bar'
        }]
      }
    },
    methods: {
      add(e) {
        this.count = this.count + 1
        this.html = '<h2>修改html片段</h2>'
        this.tel = 565744
        this.desc = '3453534'
      }
    },
    created() {
      setTimeout(() => {
        this.age = 34
      }, 2000)
    }
  }).$mount('#app')   // 如果没有传el选项,则调用$mount方法实现挂载

  // OR
  // const vm = new Vue()
  // vm.$mount('#app')
</script>
上一篇下一篇

猜你喜欢

热点阅读