Vue响应式原理
前言#
响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。
本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行:
- 分析主要成员,了解它们有助于理解流程
- 将流程拆分,理解其中的作用
- 结合以上的点,理解整体流程
文章稍长,但部分是代码,还请耐心观看。为了方便理解原理,文中的代码会进行简化,如果可以请对照源码学习。
主要成员#
在响应式原理中,Observe、Dep、Watcher 这三个类是构成完整原理的主要成员。
- Observe,响应式原理的入口,根据数据类型处理观测逻辑
- Dep,依赖收集器,属性都会有一个- Dep,方便发生变化时能够找到对应的依赖触发更新
- Watcher,用于执行更新渲染,组件会拥有一个渲染- Watcher,我们常说的收集依赖,就是收集- Watcher
下面来看看这些类的实现,包含哪些主要属性和方法。
Observe:我会对数据进行观测#
温馨提示:代码里的序号对应代码块下面序号的讲解
// 源码位置:/src/core/observer/index.js
class Observe {
  constructor(data) {
    this.dep = new Dep()
    // 1
    def(data, '__ob__', this)
    if (Array.isArray(data)) {
      // 2
      protoAugment(data, arrayMethods)
      // 3
      this.observeArray(data)
    } else {
      // 4
      this.walk(data)
    }
  }
  walk(data) {
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key])
    })
  }
  observeArray(data) {
    data.forEach(item => {
      observe(item)
    })
  }
}
- 为观测的属性添加 __ob__属性,它的值等于this,即当前Observe的实例
- 为数组添加重写的数组方法,比如:push、unshift、splice等方法,重写目的是在调用这些方法时,进行更新渲染
- 观测数组内的数据,observe内部会调用new Observe,形成递归观测
- 观测对象数据,defineReactive为数据定义get和set,即数据劫持
Dep:我会为数据收集依赖#
// 源码位置:/src/core/observer/dep.js
let id = 0
class Dep{
  constructor() {
    this.id = ++id // dep 唯一标识
    this.subs = [] // 存储 Watcher
  }
  // 1
  depend() {
    Dep.target.addDep(this)
  }
  // 2
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 3
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}
// 4
Dep.target = null
export function pushTarget(watcher) {
  Dep.target = watcher
} 
export function popTarget(){
  Dep.target = null
}
export default Dep
- 数据收集依赖的主要方法,Dep.target是一个watcher实例
- 添加 watcher到数组中,也就是添加依赖
- 属性在变化时会调用 notify方法,通知每一个依赖进行更新
- Dep.target用来记录- watcher实例,是全局唯一的,主要作用是为了在收集依赖的过程中找到相应的- watcher
pushTarget 和 popTarget 这两个方法显而易见是用来设置 Dep.target的。Dep.target 也是一个关键点,这个概念可能初次查看源码会有些难以理解,在后面的流程中,会详细讲解它的作用,需要注意这部分的内容。
Watcher:我会触发视图更新#
// 源码位置:/src/core/observer/watcher.js
let id = 0
export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.id = ++id  // watcher 唯一标识
    this.vm = vm
    this.cb = cb
    this.options = options
    // 1
    this.getter = exprOrFn
    this.deps = []
    this.depIds = new Set()
    this.get()
  }
  run() {
    this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget(this)
  }
  // 2
  addDep(dep) {
    // 防止重复添加 dep
    if (!this.depIds.has(dep.id)) {
      this.depIds.add(dep.id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
  // 3
  update() {
    queueWatcher(this)
  }
}
- this.getter存储的是更新视图的函数
- watcher存储- dep,同时- dep也存储- watcher,进行双向记录
- 触发更新,queueWatcher是为了进行异步更新,异步更新会调用run方法进行更新页面
响应式原理流程#
对于以上这些成员具有的功能,我们都有大概的了解。下面结合它们,来看看这些功能是如何在响应式原理流程中工作的。
数据观测#
数据在初始化时会通过 observe 方法来调用 Observe
// 源码位置:/src/core/observer/index.js
export function observe(data) {
  // 1
  if (!isObject(data)) {
    return
  }
  let ob;
  // 2
  if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {
    ob = data.__ob__
  } else {
    // 3
    ob = new Observe(data)
  }
  return ob
}
在初始化时,observe 拿到的 data 就是我们在 data 函数内返回的对象。
- observe函数只对- object类型数据进行观测
- 观测过的数据都会被添加上 __ob__属性,通过判断该属性是否存在,防止重复观测
- 创建 Observe实例,开始处理观测逻辑
对象观测
进入 Observe 内部,由于初始化的数据是一个对象,所以会调用 walk 方法:
walk(data) {
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}
defineReactive 方法内部使用 Object.defineProperty 对数据进行劫持,是实现响应式原理最核心的地方。
function defineReactive(obj, key, value) {
  // 1
  let childOb = observe(value)
  // 2
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        // 3
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      // 4
      childOb = observe(newVal)
      // 5
      dep.notify()
      return value
    }
  })
}
- 由于值可能是对象类型,这里需要调用 observe进行递归观测
- 这里的 dep就是上面讲到的每一个属性都会有一个dep,它是作为一个闭包的存在,负责收集依赖和通知更新
- 在初始化时,Dep.target是组件的渲染watcher,这里dep.depend收集的依赖就是这个watcher,childOb.dep.depend主要是为数组收集依赖
- 设置的新值可能是对象类型,需要对新值进行观测
- 值发生改变,dep.notify通知watcher更新,这是我们改变数据后能够实时更新页面的触发点
通过 Object.defineProperty 对属性定义后,属性的获取触发 get 回调,属性的设置触发 set 回调,实现响应式更新。
通过上面的逻辑,也能得出为什么 Vue3.0 要使用 Proxy 代替 Object.defineProperty 了。Object.defineProperty 只能对单个属性进行定义,如果属性是对象类型,还需要递归去观测,会很消耗性能。而 Proxy 是代理整个对象,只要属性发生变化就会触发回调。
数组观测
对于数组类型观测,会调用 observeArray 方法:
observeArray(data) {
  data.forEach(item => {
    observe(item)
  })
}
与对象不同,它执行 observe 对数组内的对象类型进行观测,并没有对数组的每一项进行 Object.defineProperty 的定义,也就是说数组内的项是没有 dep 的。
所以,我们通过数组索引对项进行修改时,是不会触发更新的。但可以通过 this.$set 来修改触发更新。那么问题来了,为什么 Vue要这样设计?
结合实际场景,数组中通常会存放多项数据,比如列表数据。这样观测起来会消耗性能。还有一点原因,一般修改数组元素很少会直接通过索引将整个元素替换掉。例如:
export default {
    data() {
        return {
            list: [
                {id: 1, name: 'Jack'},
                {id: 2, name: 'Mike'}
            ]
        }
    },
    cretaed() {
        // 如果想要修改 name 的值,一般是这样使用
        this.list[0].name = 'JOJO'
        // 而不是以下这样
        // this.list[0] = {id:1, name: 'JOJO'}
        // 当然你可以这样更新
        // this.$set(this.list, '0', {id:1, name: 'JOJO'})
    }
}
数组方法重写#
当数组元素新增或删除,视图会随之更新。这并不是理所当然的,而是 Vue 内部重写了数组的方法,调用这些方法时,数组会更新检测,触发视图更新。这些方法包括:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
回到 Observe 的类中,当观测的数据类型为数组时,会调用 protoAugment 方法。
if (Array.isArray(data)) {
  protoAugment(data, arrayMethods)
  // 观察数组
  this.observeArray(data)
} else {
  // 观察对象
  this.walk(data)
}
这个方法里把数组原型替换为 arrayMethods ,当调用改变数组的方法时,优先使用重写后的方法。
function protoAugment(data, arrayMethods) {
  data.__proto__ = arrayMethods
}
接下来看看 arrayMethods 是如何实现的:
// 源码位置:/src/core/observer/array.js
// 1
let arrayProto = Array.prototype
// 2
export let arrayMethods = Object.create(arrayProto)
let methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'reverse',
  'sort',
  'splice'
]
methods.forEach(method => {
  arrayMethods[method] = function(...args) {
    // 3
    let res = arrayProto[method].apply(this, args)
    let ob = this.__ob__
    let inserted = ''
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    // 4
    inserted && ob.observeArray(inserted)
    // 5
    ob.dep.notify()
    return res
  }
})
- 将数组的原型保存起来,因为重写的数组方法里,还是需要调用原生数组方法的
- arrayMethods是一个对象,用于保存重写的方法,这里使用- Object.create(arrayProto)创建对象是为了使用者在调用非重写方法时,能够继承使用原生的方法
- 调用原生方法,存储返回值,用于设置重写函数的返回值
- inserted存储新增的值,若- inserted存在,对新值进行观测
- ob.dep.notify触发视图更新
依赖收集#
依赖收集是视图更新的前提,也是响应式原理中至关重要的环节。
伪代码流程
为了方便理解,这里写一段伪代码,大概了解依赖收集的流程:
// data 数据
let data = {
    name: 'joe'
}
// 渲染watcher
let watcher = {
    run() {
        dep.tagret = watcher
        document.write(data.name)
    }
}
// dep
let dep = [] // 存储依赖 
dep.tagret = null // 记录 watcher
// 数据劫持
Object.defineProperty(data, 'name', {
   get(){
       // 收集依赖
       dep.push(dep.tagret)
   },
   set(newVal){
       data.name = newVal
       dep.forEach(watcher => {
           watcher.run()
       })
   }
})
初始化:
- 首先会对 name属性定义get和set
- 然后初始化会执行一次 watcher.run渲染页面
- 这时候获取 data.name,触发get函数收集依赖。
更新:
修改 data.name,触发 set 函数,调用 run 更新视图。
真正流程
下面来看看真正的依赖收集流程是如何进行的。
function defineReactive(obj, key, value) {
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend() // 收集依赖
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      childOb = observe(newVal)
      dep.notify()
      return value
    }
  })
}
首先初始化数据,调用 defineReactive 函数对数据进行劫持。
export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.getter = exprOrFn
    this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget(this)
  }
}
初始化将 watcher 挂载到 Dep.target,this.getter 开始渲染页面。渲染页面需要对数据取值,触发 get 回调,dep.depend 收集依赖。
class Dep{
  constructor() {
    this.id = id++
    this.subs = []
  }
  depend() {
    Dep.target.addDep(this)
  }
}
Dep.target 为 watcher,调用 addDep 方法,并传入 dep 实例。
export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.deps = []
    this.depIds = new Set()
  }
  addDep(dep) {
    if (!this.depIds.has(dep.id)) {
      this.depIds.add(dep.id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
}
addDep 中添加完 dep 后,调用 dep.addSub 并传入当前 watcher 实例。
class Dep{
  constructor() {
    this.id = id++
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
}
将传入的 watcher 收集起来,至此依赖收集流程完毕。
补充一点,通常页面上会绑定很多属性变量,渲染会对属性取值,此时每个属性收集的依赖都是同一个 watcher,即组件的渲染 watcher。
数组的依赖收集#
methods.forEach(method => {
  arrayMethods[method] = function(...args) {
    let res = arrayProto[method].apply(this, args)
    let ob = this.__ob__
    let inserted = ''
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    // 对新增的值观测
    inserted && ob.observeArray(inserted)
    // 更新视图
    ob.dep.notify()
    return res
  }
})
还记得重写的方法里,会调用 ob.dep.notify 更新视图,__ob__ 是我们在 Observe 为观测数据定义的标识,值为 Observe 实例。那么 ob.dep 的依赖是在哪里收集的?
function defineReactive(obj, key, value) {
  // 1
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend()
        // 2
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      childOb = observe(newVal)
      dep.notify()
      return value
    }
  })
}
- observe函数返回值为- Observe实例
- childOb.dep.depend执行,为- Observe实例的- dep添加依赖
所以在数组更新时,ob.dep 内已经收集到依赖了。
整体流程#
下面捋一遍初始化流程和更新流程,如果你是初次看源码,不知道从哪里看起,也可以参照以下的顺序。由于源码实现比较多,下面展示的源码会稍微删减一些代码
初始化流程#
入口文件:
// 源码位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
  this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
_init:
// 源码位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions 对 mixin 选项和传入的 options 选项进行合并
      // 这里的 $options 可以理解为 new Vue 时传入的对象
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化数据
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    if (vm.$options.el) {
      // 初始化渲染页面 挂载组件
      vm.$mount(vm.$options.el)
    }
  }
}
上面主要关注两个函数,initState 初始化数据,vm.$mount(vm.$options.el) 初始化渲染页面。
先进入 initState:
// 源码位置:/src/core/instance/state.js 
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // data 初始化
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
function initData (vm: Component) {
  let data = vm.$options.data
  // data 为函数时,执行 data 函数,取出返回值
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 这里就开始走观测数据的逻辑了
  observe(data, true /* asRootData */)
}
observe 内部流程在上面已经讲过,这里再简单过一遍:
- new Observe观测数据
- defineReactive对数据进行劫持
initState 逻辑执行完毕,回到开头,接下来执行 vm.$mount(vm.$options.el) 渲染页面:
$mount:
// 源码位置:/src/platforms/web/runtime/index.js 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
mountComponent:
// 源码位置:/src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`
      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)
      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    // 数据改变时  会调用此方法
    updateComponent = () => {
      // vm._render() 返回 vnode,这里面会就对 data 数据进行取值
      // vm._update 将 vnode 转为真实dom,渲染到页面上
      vm._update(vm._render(), hydrating)
    }
  }
  
  // 执行 Watcher,这个就是上面所说的渲染wacther 
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
Watcher:
// 源码位置:/src/core/observer/watcher.js 
let uid = 0
export default class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.id = ++id
    this.vm = vm
    this.cb = cb
    this.options = options
    // exprOrFn 就是上面传入的 updateComponent
    this.getter = exprOrFn
    this.deps = []
    this.depIds = new Set()
    this.get()
  }
  get() {
    // 1. pushTarget 将当前 watcher 记录到 Dep.target,Dep.target 是全局唯一的
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    // 2. 调用 this.getter 相当于会执行 vm._render 函数,对实例上的属性取值,
    //由此触发 Object.defineProperty 的 get 方法,在 get 方法内进行依赖收集(dep.depend),这里依赖收集就需要用到 Dep.target
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // 3. popTarget 将 Dep.target 置空
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}
至此初始化流程完毕,初始化流程的主要工作是数据劫持、渲染页面和收集依赖。
更新流程#
数据发生变化,触发 set ,执行 dep.notify
// 源码位置:/src/core/observer/dep.js 
let uid = 0
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 执行 watcher 的 update 方法
      subs[i].update()
    }
  }
}
wathcer.update:
// 源码位置:/src/core/observer/watcher.js 
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {  // 计算属性更新
    this.dirty = true
  } else if (this.sync) {  // 同步更新
    this.run()
  } else {
    // 一般的数据都会进行异步更新
    queueWatcher(this)
  }
}
queueWatcher:
// 源码位置:/src/core/observer/scheduler.js
// 用于存储 watcher
const queue: Array<Watcher> = []
// 用于 watcher 去重
let has: { [key: number]: ?true } = {}
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  let watcher, id
  // 对 watcher 排序
  queue.sort((a, b) => a.id - b.id)
  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    // run方法更新视图
    watcher.run()
  }
}
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    // watcher 加入数组
    queue.push(watcher)
    // 异步更新
    nextTick(flushSchedulerQueue)
  }
}
nextTick:
// 源码位置:/src/core/util/next-tick.js
const callbacks = []
let pending = false
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 遍历回调函数执行
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
}
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将回调函数加入数组
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 遍历回调函数执行
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
这一步是为了使用微任务将回调函数异步执行,也就是上面的p.then。最终,会调用 watcher.run 更新页面。
至此更新流程完毕。
写在最后#
如果没有接触过源码的同学,我相信看完可能还是会有点懵的,这很正常。建议对照源码再自己多看几遍就能知道流程了。对于有基础的同学就当做是复习了。
想要变强,学会看源码是必经之路。在这过程中,不仅能学习框架的设计思想,还能培养自己的逻辑思维。万事开头难,迟早都要迈出这一步,不如就从今天开始。
简化后的代码我已放在 github,有需要的可以看看。
 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号