Vue官方文档和源码研读

2021.08.31开始 运行时的源码在vue/dist/vue.runtime.esm.js里面,不过有些非函数的定义,打印不出来(看看vue源码断点怎么打比较合适)。看官方文档时候,直接百度中文的那段文字,经常搜不出来,可以先根据官方文档或自己定位到对应的代码段,然后搜索代码段的变量名,就能搜到了 

1.官方文档 模板语法-插值-使用javascript表达式 最后,有这么一句话:模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不应该在模板表达式中试图访问用户定义的全局变量。

开始我以为是vue.prototype.$aa = 1,不能访问这个$aa。看了源码之后,才明白。全局对象指的是Array、Object这类的js关键字,当然也可以自己定义。比如我写个js文件 export 1,然后在vue文件中import aaa from ./js文件,在模板语法中使用{{aaa}},就会报错。因为模板表达式的沙盒里没有aaa这个变量。想使用的话可以把它挂载到vueprototype上或者直接赋值给当前vue实例的data中的一个属性

function makeMap (
  str,
  expectsLowerCase
) {
  var map = Object.create(null);
  var list = str.split(',');
  for (var i = 0; i < list.length; i++) {
    map[list[i]] = true;
  }
  return expectsLowerCase
    ? function (val) { return map[val.toLowerCase()]; }
    : function (val) { return map[val]; }
}
var allowedGlobals = makeMap(  //
    'Infinity,undefined,NaN,isFinite,isNaN,' +
    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,' +
    'require' // for Webpack/Browserify
  );
var hasHandler = {
    has: function has (target, key) { //target就是当前vue实例,key就是要解析的值
      var has = key in target; //对象的in方法,只要key在target的原型链上,就会返回true,所以设置vue.prototype.$aa = 1之后,使用$aa是没问题的
var isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); //这里判断的是全局变量或者以_开头的属性,这么做是由于渲染函数中会包含很多以 _开头的内部方法,如渲染函数里遇到的 _c_v 等等 if (!has && !isAllowed) { if (key in target.$data) { warnReservedPrefix(target, key); } else { warnNonPresent(target, key); } } return has || !isAllowed } };

最后的判断!has我们可以理解为你访问了一个没有定义在实例对象上(或原型链上)的属性,所以这个时候提示错误信息是合理,但是即便!has成立也不一定要提示错误信息,因为必须要满足!isAllowed,也就是说当你访问了一个虽然不在实例对象上(或原型链上)的属性,但如果你访问的是全局对象那么也是被允许的。这样我们就可以在模板中使用全局对象了

 

2.在谷歌浏览器的控制台,打印一些数据时。发现同样的数据,显示的格式经常是不一样的,有时是data:{...},有时是data:{a:1},有时是data:{__ob__:Observer}。

经过我的验证,发现没被挂载到vue实例上的(被浅拷贝的也算),会直接显示数据data:{a:1},被挂载到vue实例上并且对象长度小于等于4的,会显示data:{__ob__:Observer},其余显示的都是data:{...}

拓展:给vue data属性的对象添加属性时,有以下几种情况。

const a = {content:{a:1,b:2,c:3,d:4,e:5}}
情况一 
this.form = a;
这样相当于将a的地址赋值给this.form,这时不管是打印a 还是this.form都是一样的,不会去判断对象长度是多少,点开查看(调用getter)之前都是显示{...}
情况二
this.form = {...a}或者this.form = Object.assign({},this.form,a).
这时this.form===a是false,他俩这时的引用类型属性是共用一个地址,但是基本类型的值是互不干扰的。
打印a,会判断长度,小于等于4的显示{__ob__:Observer},大于4的显示{...};
打印this.form,不会判断长度,统一显示{...}
情况三
this.form = JSON.parse(JSON.stringify(a)) //深拷贝
这时打印a,会直接显示所有的值,不会有{...}和{__ob__:Observer},因为它没有被vue影响
打印this.form,不会判断长度,统一显示{...}
但是,以上三种情况,当我直接打印this.form.a时,都会显示一个{__ob__:Observer}。

 具体原因要等以后看源码再了解了(2021.09.07)

  

3.当你设置 vm.someData = 'new value',该组件不会立即重新渲染(这里指的是html里的dom元素 不会重新渲染)。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

  当我使用vue检测不到的方法来增减对象和数组时,可能是数据驱动不了视图,也可能是数据驱动了视图(原因参考7)。但通过this.$refs.textContent是获取不到更新后的dom的,这时候就要用$nextTick了。

 

4.VUE事件修饰符 https://cn.vuejs.org/v2/guide/events.html#%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6

注意: 特殊的系统修饰键有ctrl、alt 、shift、 meta(window键),例如 @key.alt.67  //alt+c   @click.ctrl   //ctrl+click

修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl 

的情况下释放其它按键,才能触发 keyup.ctrl。而单单释放 ctrl 也不会触发事件。如果你想要这样的行为,请为 ctrl 换用 keyCodekeyup.17

2.5.0新增的exact修饰符 允许你控制由精确的系统修饰符组合触发的事件。

<!-- 即使 Alt 或 Shift 被一同按下时也会触发。只监听ctrl,不管同时有其他几个按键在按 -->  

<button v-on:click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->

<button v-on:click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->

<button v-on:click.exact="onClick">A</button>

鼠标修饰符有 .left .right .middle 

 

5.复选框<input type="checkbox" v-model="toggle" true-value="yes" false-value="no">

// 当选中时
vm.toggle === 'yes'
// 当没有选中时
vm.toggle === 'no'

这里的 true-value 和 false-value attribute 并不会影响输入控件的 value attribute,因为浏览器在提交表单时并不会包含未被选中的复选框。如果要确保表单中这两个值中的一个能够被提交,(即“yes”或“no”),请换用单选按钮。

翻译:比如有这样的页面

<input type="checkbox" v-model="picked" :true-value="value1" :false-value="value2">
        <label> 复选框 </label>
        <p> {{picked}} </p>
        <p>{{value1}} </p>
        <p>{{value2}} </p>
data: {
              picked:false,
              value1:123,
              value2:345
            }

 那么刚进页面时,picked的值就是false,这个value1和value2的值是不会影响picked的值的。

 但是一旦对这个chekcbox做了勾选或取消勾选的操作,这个picked的值就会变成123或者456。

拓展:如果换成radio的话,刚进页面是一样的,但是一旦做了操作,picked的值就会变成null。

 

6.阅读vuex官方文档时,对于rootState一直无法理解,官方文档对于它的定义是 根节点状态。但是有rootState.count这种用法,而我打印的时候,rootState底下就是几个module对象,没有.count这一层级。

原因:参考了一些文档,发现.count确实是根节点的状态,并且想获取这个层级的值,就要在new Vuex.Store()时给根节点赋一个state对象。像这样

const store = new Vuex.Store({
  state: {
    count: 1,
  },
  modules: {
    a: moduleA,
    b: moduleB,
  },
})

  这时候在moduleA的action或者getters里面打印rootState,里面就有count和a、b两个modules里面的state对象。

 

7.Vue 深入响应式原理说明:Vue 不能检测数组和对象的一些变化,也就是说数据更新时视图不会更新。但是实际使用中发现还是更新了。

原因: 查了一些文档,没发现回答。自己又测试了一下,发现官方文档写的是没有问题的。操作的时候,如果只有arr.length=1或者arr[0] = 1这种操作时,不会触发视图更新。但是如果有其他的能被vue检测到的数据更新(this.count='xx),就会顺带把vue检测不到的数据变动也一起更新了,这样就实现了类似arr.length = 1也被vue检测到的效果。但是这样只更新了视图,并没有给这个新数据加上getter和setter,在控制台打印可以看到,有getter和setter的会默认不显示,没有的会直接显示出来。

坏处:目前能想到的坏处,就是虽然视图更新了,但是vue的watch是监听不到数据变化的。因为这个数据没有setter,watch是靠监听setter来实现的。

    export default {
      data() {
        return {
          items: ['a', 'b', 'c'],
   count:1 } }, methods: { hah() { this.items.length = 2 // 不是响应性的,没有被vue检测到
      //this.count = 'xx' //是响应性的,会触发vue更新 setTimeout(() => { this.items.shift() },3000) }, }, watch: { items(new,old) { console.log(new,old) // 1,3 ,vue只能监听到一开始的长度3和shift之后的长度1 } } }

源码解析:简单打断点看了一下,应该是this.count触发了它自己的set和watch.然后又触发了vue的$nextTick和render方法,vue就顺带把当前实例的数据都更新到视图上了。但是并没有给新属性加上setter和getter,也没有触发其他属性的watch。

经验:以后对于vue检测不到的变动,还是都用this.$set来操作吧。因为不可能每次都正好有别的属性在更新,万一一开始有,后面又拿掉了那个更新的属性操作,vue就检测不到更新了,这是在埋bug.

 

8.vue官方文档说到父组件引用子组件时的标签名和props在标签上的使用,是这样的

使用 kebab-case    Vue.component('my-component-name', { /* ... */ })

当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>

使用PascalCase     Vue.component('MyComponentName', { /* ... */ })

当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name> 和 <MyComponentName> 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的

重点在最后一句,我在.vue文件中直接使用<MyComponent userName="a"/>是可用的,这是为啥?

原因:vue官方文档经常提到的字符串模板和非字符串模板,其实是这样定义的

1.字符串模板就是写在vue中的template中定义的模板,如.vue的单文件组件模板和定义组件时template属性值的模板。字符串模板不会在页面初始化参与页面的渲染,会被vue进行解析编译之后再被浏览器渲染,所以不受限于html结构和标签的命名。

Vue.component('MyComponentA', {
    template: '<div MyId="123"><MyComponentB>hello, world</MyComponentB></div>'
})

<div id="app">
    <MyComponentA></MyComponentA>
</div>

2.dom模板(或非字符串模板、Html模板)就是写在html文件中,一打开就会被浏览器进行解析渲染的,所以要遵循html结构和标签的命名,否则浏览器不解析也就不能获取内容了。

下面的例子不会被正确渲染, 会被解析成mycomponent,但是注册的vue的组件是MyComponent,因此无法渲染。

<!DOCTYPE <html>
    <head>
        <meta charset="utf-8">
        <title>Vue Component</title>
    </head>
    <body>
        <div id="app"> 
            Hello Vue
            <MyComponent></MyComponent>
        </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <script >
        //全局注册
        Vue.component('MyComponent', {
            template: '<div>组件类容</div>'
        });
        new Vue ({
            el: '#app'
        });
    </script>
    </body>
</html>

  

所以,下面的例子就可以正常显示了:

<!DOCTYPE <html>
    <head>
        <meta charset="utf-8">
        <title>Vue Component</title>
    </head>
    <body>
        <div id="app"> 
            Hello Vue
            <my-component></my-component>
        </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <script >
        //全局注册
        Vue.component('my-component', {
            template: '<div>组件类容</div>'
        });
        new Vue ({
            el: '#app'
        });
    </script>
    </body>
</html>

因为html对大小写不敏感,所以在DOM模板中使用组件必须使用kebab-case命名法(短横线命名)。
因此,对于组件名称的命名,可参考如下实现:

/*-- 在单文件组件、JSX和字符串模板中 --*/
<MyComponent/>
/*-- 在 DOM 模板中 --*/
<my-component></my-component>
或者
/*-- 在所有地方 --*/
<my-component></my-component>

  

9,vuex官方文档说只能通过mutation来修改state,而实际使用中,发现this.$store.state.a = 132是可以生效的,并且其他组件也都可以访问,只不过它没有被设置set和get,想要这两个用this.$set就可以了。不过在vuex的严格模式下会报错。

开启严格模式,仅需在创建 store 的时候传入 strict: true
const store = new Vuex.Store({ // ... strict: true 或者
 strict: process.env.NODE_ENV !== 'production'
})

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

总结:为了防止vuex的数据改变来源难以追踪,使用中统一用mutation来改变state。也可以在开发过程中开启vuex严格模式(生产环境中必须关闭)。

 

10.vue实例中的data、computed和methods中,访问this指向的为什么是当前vue实例?

原因:源码是使用了bind和call,改变了computed、methods()和data()中的this指向。代码在src/core/instance/init.js的initData()和initMethods()中。官方文档有说明,computed和data可以使用箭头函数,但是函数内部的this就不是vue实例了,这时候可以把vm作为函数的第一个参数进行传递,data: vm => ({ a: vm.myProp })。但是对于methods和watch,是不可以使用箭头函数的,在参数里也获取不到vm。

 

11.在阅读src/core/instance/state.js时,发现js初始化的定义了一个sharedPropertyDefinition。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};

  在后续的代码中,每次给不同的属性加set和get时,都是直接

 sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);

  每次都是这样,但是更改后的sharedPropertyDefinition.get和set并不会影响到之前的属性

原因:给第一个属性a赋值时,在内存中开辟了一处空间A来存放get和set。后续改变sharedPropertyDefinition时,由于是直接将函数赋值给get和set的,相当于是在内存中又开辟了一处空间B,来存放新的get和set。而之前的a属性,对应的地址还是A,这时再继续给其他属性b赋值的话,对应的地址就是B了。所以每次更改sharedPropertyDefinition时,不会影响之前的属性。

拓展:像这种改变之后不会影响之前赋值的情况,除了引用类型的直接赋值,还有一种就是值类型的赋值。因为值类型的赋值只拷贝了值。可以看下面的例子

① 不会影响(值类型只拷贝了值)
let aa = 1;
let bb = aa;
aa = 2;
console.log(bb)  // 1   

②不会影响 (引用类型直接赋值,指向新地址,开辟新空间)
let aa = {a:1};
let bb = aa;
aa = {a:2};
console.log(bb)  // {a:1}

③会影响 (引用类型共用一个地址)
let aa = {a:1};
let bb = aa;
aa.a =2;
console.log(bb) // {a:2}

  

12.在阅读src/core/instance/state.js时的createComputedGetter()方法时,发现函数中直接使用了this,指向的是vue实例。我打断点看了一下,这个方法是vue的某个computed属性被读取时触发,也就是某个computed属性的get方法。这里可以理解,因为触发条件是this.b,这个this就是vue实例。自己写了个小demo,测试属性的get和set方法中的this指向;

const aa = {b:2};
Object.defineProperty(aa,'a',{get:function() {
    console.log(this)  // {b:2} // 为什么这里不是{b:2,a:1},因为return 1的语句在这里还没有执行
   return 1;
}});
console.log(aa.a) //1 

  

13.vue源码的initData方法中,最后执行了observe(data, true)方法。

function observe(value, asRootData) {
  // 非对象和 VNode 实例不做响应式处理。因为get和set是加在非对象外面的对象上的,比如data里有个a,那么这个a的get和set是加在data这个对象上
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  .....
}

  但是打印一下vue实例,会发现vm实例上没有__Ob__属性,是因为没有做响应式处理吗?显示不是的,我们再继续找,原来__Ob__是被加到了_data和$data对象上。vue实例上能直接看到data里的各个属性,只是因为使用了proxy()方法,其实真正的属性是在_data和$data上

function initData (vm: Component) {
  // vm.$options 是访问自定属性,此处就是vue实例中的 this.$data
  let data = vm.$options.data;
  // 先执行三元表达式,然后赋值给 vm._data 和 data,这样子这两个值都是同时变化的,就是 this.$data.xxx 和 this._data 同时变化
  data = vm._data = typeof data === 'function' 
    ? getData(data, vm)
    : data || {}
  ………
}

  看看proxy是怎么实现的

//调用:proxy(vm, "_props", key)  和  proxy(vm, "_data", key);

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
//比如_data有个a:1,就是Object.defineProperty(vm,'a', sharedPropertyDefinition);sharedPropertyDefinition return 的都是this._data.a;
这样就实现了代理,可以通过this.a访问和修改this._data_a了

 

14.computed属性的生命周期

initComputed函数

① 在这里为每一个computed[key]添加了一个lazy=true的watcher,并且让每一个watcher[key]的dirty属性 = this.lazy(true)。

②执行defineComputed,为computed[key]设置getter和setter,注意这里只是设置,并不会立即执行。接着,通过 Object.defineProperty(target, key, sharedPropertyDefinition) 将computed[key]对应的getter和setter设置在vue根实例上 (props属性也是如此。但是不知道为什么不设置在当前vue实例上,并且computed[key]的值貌似不是通过proxy设置在当前vue实例上的)。

当computed[key]被读取时,会执行getter函数,第一次会执行watcher.evaluate(),然后将dirty置为false,下次再获取当前的computed[key],就会不进行计算,直接返回watcher.value,这就是computed的缓存机制。当页面刷新,或者computed[key]依赖的data值更新时,会触发watcher.update()来将dirty 重置为true。但是,这里不是全量重置。比如有两个computed的key,aa(依赖a),bb(依赖b),当b的值更新时,aa的watcher的dirty依旧是false,还是取缓存。

当computed[key]被修改时,会执行setter函数,这个函数是用户自己定义的,只能用来监听setter并做出一些操作,并不能修改自己的值,所以没啥好说的。

当computed[key]被读取时,还会判断是否有Dep.target,如果有,就执行watcher.depend(依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep)

 

15.Dep、Watcher的关系

<div>{{name}}</div>

data() {
        return {
            name: '林三心'
        }
    },
    computed: {
        info () {
            return this.name
        }
    },
    watch: {
        name(newVal) {
            console.log(newVal)
        }
    }

上方代码可知,name变量被三处地方所依赖,分别是html里,computed里,watch里。只要name一改变,html里就会重新渲染,computed里就会重新计算,watch里就会重新执行。那么是谁去通知这三个地方name修改了呢?那就是Watcher了。

上面所说的三处地方就刚刚好代表了三种Watcher,分别是:

  • 渲染Watcher:变量修改时,负责通知HTML里的重新渲染
  • computed Watcher:变量修改时,负责通知computed里依赖此变量的computed属性变量的修改
  • user Watcher:变量修改时,负责通知watch属性里所对应的变量函数的执行
那Dep又是什么呢?举个例子,还是之前的例子代码:
这里name变量被三个地方所依赖,三个地方代表了三种Watcher,那么name会直接自己管这三个Watcher吗?答案是不会的,name会实例一个Dep,来帮自己管这几个Wacther,类似于管家,当name更改的时候,会通知dep,而dep则会带着主人的命令去通知这些Wacther去完成自己该做的事。
讲道理,dep里设置Watcher可以理解,Watcher为啥要反过来收集dep呢?

这是因为computed属性里的变量没有自己的dep,也就是他没有自己的管家,看以下例子:

这里先说一个知识点:如果html里不依赖name这个变量,那么无论name再怎么变,他都不会主动去刷新视图,因为html没引用他(说专业点就是:namedep里没有渲染Watcher),注意,这里说的是不会主动,但这并不代表他不会被动去更新。什么情况下他会被动去更新呢?那就是computed有依赖他的属性变量。

<div>{{person}}</div>

computed: {
    person {
        return `名称:${this.name}`
        }
    }
这里的person事依赖于name的,但是person是没有自己的dep的(因为他是computed属性变量),而name是有的。好了,继续看,请注意,此例子html里只有person的引用没有name的引用,所以name一改变,按理说虽然person跟着变了,但是html不会重新渲染,因为name虽然有dep,有更新视图的能力,但是奈何人家html不引用他啊!person想要自己去更新视图,但他却没这个能力啊,毕竟他没有dep这个管家!这个时候computed Watcher里收集的namedep就派上用场了,可以借助这些dep去更新视图,达到更新html里的person的效果。具体会在下面computed里实现。
posted @ 2021-09-01 00:15  太阳锅锅  阅读(672)  评论(0编辑  收藏  举报