1

现象:

在Vue开发的时候,data初始化一个对象没有定义任何属性,经过变量赋值的之后,不需要$set方法,该对象下面的属性就也能变成响应式属性

提问:分析下面代码,页面首先先显示什么?先点击changeTest1显示什么?然后点击chageTest2显示什么?

<body>
  <div id="app">
    <button @click="changeTest1">changeTest1</button>
    <button @click="changeTest2">changeTest2</button>
    <div>
      {{ form.test1 }}-{{ form.test2 }}
    </div>
  </div>
  <script>
    let vm = new Vue({
      el: "#app",
      data: {
        form: {},
      },
      mounted() {
        this.form = { test1: 1};
        this.form.test2 = 2;
      },
      methods: {
        changeTest2() {
          this.form.test2 = 'change2';
        },
        changeTest1() {
          this.form.test1 = 'change1';
        },
      }
    });
  </script>
</body>
  • 公布答案:
首先显示:1-2, 点击changeTest1显示:change1-2,点击changeTest2显示:change1-2

1592670725(1).jpg

疑问:在data中的form对象没有定义test1和test2属性,test1可以改变视图,而test2却不能改变视图?

1): 打印一下form对象,可以看到test1有set,get修饰符,而test2没有get,set修饰符

2): 原来test1能够改变视图是因为被Vue用Object.defineProperties()处理,有get,set修饰符,收集到渲染watch。

3): 而test2没有get,set修饰符,没有收集到渲染watch。

4): 从这个表现就能知道test1可以更新视图,而test2不可以更新视图

1592670725(1).jpg
1592671231(1).jpg

疑问:为什么test1有get,set修饰符

1): 上面代码可以注意到在mounted生命周期那里,对this.form进行了赋值,所以触发了form的set修饰符,它会执行以下函数中的set函数

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  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]
  }
  
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      console.log(obj, key)
      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)) {
        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
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

2):可以看到newVal是{test1: 1}, 抛掉所有if-else的判断,会执行一行代码 childOb = !shallow && observe(newVal),shallow是undefined,然后执行observe函数,参数为{test1: 1}

看一下observe函数的功能
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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
}

3): 会发现最终会执行ob = new Observer(value), 再看Observer函数

export 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
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      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])
    }
  }
}

4): 最终发现ob = new Observer(value),就是把对象定义响应式的入口函数

5): 原来值{ test1: 1 },就这样被隐式添加了set,get修饰符

总结:对this.form = { test1: 1 } 进行赋值的时候,就会触发到form的set函数。在set函数里面,会对newVal也就是{test1: 1}
走一次new Obsever(value)来添加set,get的修饰符,因此test1就变成了响应式属性

疑问:为什么test2没有set,get修饰符

1): 应为对象无法监听新增和删除,对this.form.test2 = 2的时候,没有触发form的set函数,因而没有添加get,set修饰符

流程总结:

1): Vue在初始化阶段对data定义的对象form添加set,get修饰符

2): 执行render函数(生成vnode的过程),form取值一次,触发get函数,收集渲染watch。form.test1取值一次为1,没有get修饰符。form.test2取值一次为2,没有get修饰符

3): 把vnode(虚拟dom)变成真实dom,页面显示: 1-2

4): 在mounted生命周期中对this.form = { test1: 1 }进行了赋值,触发了form的set函数,然后对{ test1: 1 }添加get,set修饰符,test1变成了响应式属性

5): 然后执行dep.notify(),触发渲染watch,执行render函数

6): 在生成vnode的时候(模板中的{{ form.test1 }} {{ form.test2 }}),需要form.test1,form.test2取值一次,这时候就触发了test1的get修饰符,收集该渲染watch, 使得test1有了更新视图的能力,而test2没有get修饰符,无法收集改渲染watch,没有更新视图的能力

7): 点击chageTest1的时候,修改了test1的值,就会执行set修饰符,执行dep.notify()去更新视图,页面显示: change1-2

8): 点击changeTest2的时候,修改了test2的值,由于test2没有get,set修饰符,所有无法更新视图, 页面显示: change1-2

疑问:数组是怎么监听自身的改变的呢

1): 原来Vue对数组的push,pop,shift,unshfit,sort,reverse方法额外处理,例如:当数组新增的时候,能够给新增的项添加get,set修饰符,使得它们变成响应式属性,这就是对象和数组的区别

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.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
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

最后:

给Vue中data的值赋值对象或者数组,Vue内部会自动帮我们给该对象或数组添加get,set修饰符,并且为每一个属性收集渲染watch,使得它们有更新视图的能力

结束语:

文章大概说了Vue如何添加响应式属性,如何触发视图的更新,忽略了很多细节,比如渲染watch,AST抽象语法树,render函数,异步更新,虚拟dom变成真实dom等等。文章的目的是想说:什么情况属性会变成响应式属性,什么情况下没有响应式属性(当然手动调用$set可以变成响应式属性)

aConfuseBoy
9 声望0 粉丝