美文网首页
Vue2源码阅读(一):响应式原理

Vue2源码阅读(一):响应式原理

作者: 前端艾希 | 来源:发表于2021-12-26 14:07 被阅读0次

    一、阅读准备

    阅读Vue.js代码前,需要准备:

    • 仓库代码,方便加注释和多段关键代码可以同时阅读
    • 打包后未压缩的代码,方便打断点,看代码执行情况

    做好以上准备后,我们写一个demo

    <!DOCTYPE html>
    <html>
    
    <head>
        <title>learn Vue</title>
    </head>
    
    <body>
        <div id="app">
            <div class="title">hello vue.js</div>
            <div class="content">{{count}}</div>
            <input type="text" v-model="count">
            <div @click="handleClick" style="cursor: pointer;">点击我</div>
    
            <div v-for="item in array" :key="item">{{item}}</div>
        </div>
        <script src="vue.js" type="text/javascript"></script>
        <!-- <script src="https://unpkg.com/vue@next"></script> -->
        <script>
            // debugger
            var app = new Vue({
                el: '#app',
                data: {
                    count: 1,
                    array: [1, 2, 3]
                },
                methods: {
                    handleClick() {
                        this.count = +this.count + 1;
                    }
                },
            });
        </script>
    </body>
    
    </html>
    

    用 Chrome 打开这个demo后,我们在new Vue前打一个断点,就可以看到Vue是如何执行的了。

    二、响应式原理

    2.1 代码阅读

    根据第一个断点找到了Vue的入口function Vue(),然后从断点进入Vue.prototype._init,代码如下:

    Vue.prototype._init = function (options?: Object) {
        // 这里删去了无关代码
        
        vm._self = vm
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        // 执行 beforeCreate 钩子
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        // 执行 created 钩子
        callHook(vm, 'created')
    
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          vm._name = formatComponentName(vm, false)
          mark(endTag)
          measure(`vue ${vm._name} init`, startTag, endTag)
        }
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    

    通过Vue生命周期图得知Vue是在breforeCreatecreated这两个钩子之间完成reactivity,于是我们进入initState(vm)方法,代码如下:

    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) {
        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)
      }
    }
    

    通过上述代码不难得知VueinitData中完成了对options.data的改造,使其具有响应式功能,于是我们进入initData

    function initData (vm: Component) {
    
      /**** begin: 从vm中取出data对象 ****/
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      /**** end ****/
      
      // 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--) {
        // 判断 data 中是否有和 props 和 methods 重名的属性
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        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)) {
          // 使用 vm 代理 vm._data
          // 通过 vm[prop] 访问 vm._data[prop]
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      // 在这里使用观察者模式对 data 进行封装
      observe(data, true /* asRootData */)
    }
    

    上述代码完成了(1)把datavm.data中取出来转为对象;(2)对props, methods属性进行检查,看是否和data属性冲突;(3)调用observedata进行响应式封装。下面我们找到observe的代码:

    /**
     * Attempt to create an observer instance for a value,
     * returns the new observer if successfully observed,
     * or the existing observer if the value already has one.
     */
    function observe (value: any, asRootData: ?boolean): Observer | void {
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob: Observer | void
      // 如果存在 __ob__ ,则返回已存在的观察者
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }
    

    上面代码的注释已经很清楚的讲述了这个函数的用处:为传入的value(这里传入的是vm.data)创建一个observer对象,如果value已经有observer了,那就返回已有的observer实例。这里又调用了Observer构造函数,代码如下:

    class Observer {
      value: any;
      dep: Dep;
      vmCount: number; // number of vms that have this object as root $data
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        // 把 observer 挂载到 value.__ob__ 上
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
          // 数组方法重写,支持响应式
          if (hasProto) {
            protoAugment(value, arrayMethods)
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          // 递归 observe 数组, 所以直接修改数组的元素值是具有响应性质的
          this.observeArray(value)
        } else {
          // 让 vm._data上的每个属性都支持响应式
          this.walk(value)
        }
      }
    
      /**
       * Walk through all properties and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
    
      /**
       * Observe a list of Array items.
       */
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    

    生成一个observer挂载到value.__ob__上,如果value为数组,那么重写该数组的原型上的方法。最后,遍历value上的属性,使其具有响应性。具体实现通过defineReactive,代码如下:

    function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // 属性的依赖也是保存在闭包中
      const dep = new Dep()
    
      // 获取 obj[key] 的属性描述对象,如果该属性不允许配置,那么直接返回
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
    
      // 如果是深度reactive,还要 observe obj[key]
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            // 这里不知道是干什么的。。。先往下看吧
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            // 如果要 set 的新的值和原值相等,那么不需要 reactive
            // 第二个条件是为了避免 NaN !== NaN
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          if (setter) {
            setter.call(obj, newVal)
          } else {
            // 注意这里,新的值是保存在闭包内的。
            val = newVal
          }
          // 修改了val后,需要修改深度监听的的 observer
          childOb = !shallow && observe(newVal)
          // 通知所有的 watcher update
          dep.notify()
        }
      })
    }
    

    这里是响应式的关键,修改对象属性的settergetter,在setter执行时,先调用对象属性原本的setter,然后通知该属性的所有的watcher做更新。

    2.2 流程梳理

    看了上面的代码后,脑子里就一个字:!所以我决定,这里在对实现响应式的流程做一个梳理。这里用文字对流程进行描述:

    1. reactive的入口是Vue.prototype._init里调用的initState(vm)
    2. initState中调用initData。主要把data对象从data函数中取出来挂载到vm._data上,并使用vm代理vm._data;检查propsmethods中是否有属性名和data属性名冲突;调用observe(data)
    3. observe调用new Observer(value)(这里的value是被观察对象)生成一个Observer对象挂载到value.__ob__上。
    4. Observer构造函数中重写了数组原型链的方法使其支持reactive,然后调用walk遍历vm._data执行defineReactive,另外,使用new Dep生成一个依赖对象挂载到value.__ob__.dep上用来保存依赖。
    5. defineReactive里声明了一个闭包变量dep这个变量是真正保存属性watcher的,在调用属性getter的时候,如果当时存在Dep.target,就会让Dep.target把自身加入到前面申明的闭包变量dep.sub中。
    6. 当属性值发生改变时,会调用dep.notify通知sub中的所有watcher让其更新。

    总结

    1. 真正实现观察者模式的是DepWatcher,如果能改下名字,那就更好了。。。
    2. 因为调用defineReactive时默都是深响应,data中每一个属性都会递归转为settergetter,着实有点占用性能。
    3. Observer开始实现响应式。
    4. created后,vm._data初始化完成,适合请求数据。
    5. 模板渲染的时候,会通过调用属性的getter添加依赖。

    相关文章

      网友评论

          本文标题:Vue2源码阅读(一):响应式原理

          本文链接:https://www.haomeiwen.com/subject/wgjsqrtx.html