Prechádzať zdrojové kódy

proxy渲染代理讲解

HcySunYang 7 rokov pred
rodič
commit
73cc79a383
2 zmenil súbory, kde vykonal 314 pridanie a 3 odobranie
  1. 1 1
      README.md
  2. 313 2
      note/6Vue的初始化.md

+ 1 - 1
README.md

@@ -14,7 +14,7 @@
 ##### [3Vue的思路之以一个例子为线索](/note/3Vue的思路之以一个例子为线索)
 ##### [4Vue的思路之选项的规范化](/note/4Vue的思路之选项的规范化)
 ##### [5Vue的思路之选项的合并](/note/5Vue的思路之选项的合并)
-##### [6Vue的初始化](/note/6Vue的思路之选项的合并)
+##### [6Vue的初始化](/note/6Vue的初始化)
 
 ### 附录
 

+ 313 - 2
note/6Vue的初始化.md

@@ -1,4 +1,6 @@
-## 待定
+## Vue 的初始化
+
+#### 用于初始化的最终选项 $options
 
 在 [Vue的思路之以一个例子为线索](/note/Vue的思路之以一个例子为线索) 一节中,我们写了一个很简单的例子,这个例子如下:
 
@@ -97,6 +99,8 @@ var vm = new Vue({
 const normalized = options.inject = {}
 ```
 
+#### 渲染函数的作用域代理
+
 ok,现在我们已经足够了解 `vm.$options` 这个属性了,它才是用来做一系列初始化工作的最终选项,那么接下来我们就继续看 `_init` 方法中的代码,继续了解 `Vue` 的初始化工作。
 
 `_init` 方法中,在经过 `mergeOptions` 合并处理选项之后,要执行的是下面这段代码:
@@ -118,5 +122,312 @@ if (process.env.NODE_ENV !== 'production') {
 vm._renderProxy = vm
 ```
 
-那么可想而知,在非生产环境下也应该执行这句代码,但实际上却调用了 `initProxy` 函数,所以 `initProxy` 函数的作用之一必然也是在实例对象 `vm` 上添加 `_renderProxy` 属性,那么接下来我们就看看 `initProxy` 的内容,验证一下我们的判断,打开 `core/instance/proxy.js` 文件
+那么可想而知,在非生产环境下也应该执行这句代码,但实际上却调用了 `initProxy` 函数,所以 `initProxy` 函数的作用之一必然也是在实例对象 `vm` 上添加 `_renderProxy` 属性,那么接下来我们就看看 `initProxy` 的内容,验证一下我们的判断,打开 `core/instance/proxy.js` 文件:
+
+```js
+/* not type checking this file because flow doesn't play well with Proxy */
+
+import config from 'core/config'
+import { warn, makeMap } from '../util/index'
+
+// 声明 initProxy 变量
+let initProxy
+
+if (process.env.NODE_ENV !== 'production') {
+  // ... 其他代码
+  
+  // 在这里初始化 initProxy
+  initProxy = function initProxy (vm) {
+    if (hasProxy) {
+      // determine which proxy handler to use
+      const options = vm.$options
+      const handlers = options.render && options.render._withStripped
+        ? getHandler
+        : hasHandler
+      vm._renderProxy = new Proxy(vm, handlers)
+    } else {
+      vm._renderProxy = vm
+    }
+  }
+}
+
+// 导出
+export { initProxy }
+```
+
+上面的代码是简化后的,可以发现在文件的开头声明了 `initProxy` 变量,但并未初始化,所以目前 `initProxy` 还是 `undefined`,随后,在文件的结尾将 `initProxy` 导出,那么 `initProxy` 到底是什么呢?实际上变量 `initProxy` 的初始化赋值是在 `if` 语句块内进行的,这个 `if` 语句块进行环境判断,如果是非生产环境的话,那么才会对 `initProxy` 变量赋值,也就是说在生产环境下我们导出的 `initProxy` 实际上就是 `undefined`。只有在非生产环境下导出的 `initProxy` 才会有值,其值就是这个函数:
+
+```js
+initProxy = function initProxy (vm) {
+    if (hasProxy) {
+        // determine which proxy handler to use
+        const options = vm.$options
+        const handlers = options.render && options.render._withStripped
+        ? getHandler
+        : hasHandler
+        vm._renderProxy = new Proxy(vm, handlers)
+    } else {
+        vm._renderProxy = vm
+    }
+}
+```
+
+这个函数接收一个参数,实际就是 `Vue` 实例对象,我们先从宏观角度来看一下这个函数的作用是什么,可以发现,这个函数由 `if...else` 语句块组成,但无论走 `if` 还是 `else`,其最终的效果都是在 `vm` 对象上添加了 `_renderProxy` 属性。如果 `hasProxy` 为真则走 `if` 分支,对于 `hasProxy` 顾名思义,这是用来判断宿主环境是否支持 `js` 原生的 `Proxy` 特性的,如果发现 `Proxy` 存在,则执行:
+
+```js
+vm._renderProxy = new Proxy(vm, handlers)
+```
+
+如果不存在,那么和生产环境一样,直接赋值就可以了:
+
+```js
+vm._renderProxy = vm
+```
+
+所以我们发现 `initProxy` 的作用实际上就是对实例对象 `vm` 的代理,通过原生的 `Proxy` 实现。
+
+另外 `hasProxy` 变量的定义也在当前文件中,代码如下:
+
+```js
+const hasProxy =
+    typeof Proxy !== 'undefined' &&
+    Proxy.toString().match(/native code/)
+```
+
+上面的代码相信大家都能看得懂,所以就不做过多解释,接下来我们就看看它是如何做代理的,并且有什么作用。
+
+查看 `initProxy` 函数的 `if` 语句块,内容如下:
+
+```js
+initProxy = function initProxy (vm) {
+    if (hasProxy) {
+        // determine which proxy handler to use
+        // options 就是 vm.$options 的引用
+        const options = vm.$options
+        // handlers 可能是 getHandler 也可能是 hasHandler
+        const handlers = options.render && options.render._withStripped
+            ? getHandler
+            : hasHandler
+        // 代理 vm 对象
+        vm._renderProxy = new Proxy(vm, handlers)
+    } else {
+        // ...
+    }
+}
+```
+
+可以发现,如果 `Proxy` 存在,那么将会使用 `Proxy` 对 `vm` 做一层代理,代理对象赋值给 `vm._renderProxy`,所以今后对 `vm._renderProxy` 的访问,如果有代理那么就会被拦截。代理对象配置参数是 `handlers`,可以发现 `handlers` 即可能是 `getHandler` 又可能是 `hasHandler`,至于到底使用哪个,是由判断条件决定的:
+
+```js
+options.render && options.render._withStripped
+```
+
+如果上面的条件为真,则使用 `getHandler`,否则使用 `hasHandler`,判断条件要求 `options.render` 和 `options.render._withStripped` 必须都为真才行,我现在明确告诉大家,这个是用来写测试用的,所以一般情况下这个条件都会为假,也就是使用 `hasHandler` 作为代理配置。
+
+`hasHandler` 这个变量就定义在当前文件,如下:
+
+```js
+const hasHandler = {
+    has (target, key) {
+        // has 变量是真实经过 in 运算符得来的结果
+        const has = key in target
+        // 如果 key 在 allowedGlobals 之内,或者 key 以下划线 _ 开头,则为真
+        const isAllowed = allowedGlobals(key) || key.charAt(0) === '_'
+        // 如果 has 和 isAllowed 都为假,使用 warnNonPresent 函数打印错误
+        if (!has && !isAllowed) {
+            warnNonPresent(target, key)
+        }
+        return has || !isAllowed
+    }
+}
+```
+
+这里我假设大家都对 `Proxy` 的使用已经没有任何问题了,我们知道 `has` 可以拦截一下操作:
+
+> * 属性查询: foo in proxy
+* 继承属性查询: foo in Object.create(proxy)
+* with 检查: with(proxy) { (foo); }
+* Reflect.has()
+
+其中关键在就在可以拦截 `with` 语句块里对变量的访问,后面我们会讲到。`has` 函数内出现了两个函数,分别是 `allowedGlobals` 以及 `warnNonPresent`,这两个函数也是定义在当前文件中,首先我们看一下 `allowedGlobals`:
+
+```js
+const 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,' +
+    'require' // for Webpack/Browserify
+)
+```
+
+可以看到 `allowedGlobals` 实际上是通过 `makeMap` 生成的函数,所以 `allowedGlobals` 函数的作用是判断给定的 `key` 是否出现在上面字符串中定义的关键字中的。这些关键字都是在 `js` 中可以全局访问的。
+
+`warnNonPresent` 函数如下:
+
+```js
+const warnNonPresent = (target, key) => {
+    warn(
+        `Property or method "${key}" is not defined on the instance but ` +
+        'referenced during render. Make sure that this property is reactive, ' +
+        'either in the data option, or for class-based components, by ' +
+        'initializing the property. ' +
+        'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
+        target
+    )
+}
+```
+
+这个函数就是通过 `warn` 打印一段警告信息,警告信息提示你“在渲染的时候引用了 `key`,但是在实例上并没有定义 `key` 这个属性或方法”。其实我们很容易就可以看到这个信息,比如下面的代码:
+
+```js
+const vm = new Vue({
+    el: '#app',
+    template: '<div>{{a}}</div>',
+    data: {
+        test: 1
+    }
+})
+```
+
+大家注意,在模板中我们使用 `a`,但是在 `data` 属性中并没有定义这个属性,这个时候我们就能够得到以上报错信息:
+
+![](http://ovjvjtt4l.bkt.clouddn.com/2017-11-03-073757.jpg)
+
+大家可能比较疑惑的是为什么会这样,其实我们后面讲到渲染函数的时候你自然就知道了,不过现在大家可以先看一下,打开 `core/instance/render.js` 文件,找到 `Vue.prototype._render` 方法,里面有这样的代码:
+
+```js
+vnode = render.call(vm._renderProxy, vm.$createElement)
+```
+
+可以发现,调用 `render` 函数的时候,使用 `call` 方法指定了函数的执行环境为 `vm._renderProxy`,渲染函数长成什么样呢?还是以上面的例子为例,我们可以通过打印 `vm.$options.render` 查看,所以它张成这样:
+
+```js
+vm.$options.render = function () {
+    // render 函数的 this 指向实例的 _renderProxy
+    with(this){
+        return _c('div', [_v(_s(a))])   // 在这里访问 a,相当于访问 vm._renderProxy.a
+    }
+}
+```
+
+从上面的代码可以发现,显然函数使用 `with` 语句块指定了内部代码的执行环境为 `this`,由于 `render` 函数调用的时候使用 `call` 指定了其 `this` 指向为 `vm._renderProxy`,所以 `with` 语句块内代码的执行环境就是 `vm._renderProxy`,所以在 `with` 语句块内访问 `a` 就相当于访问 `vm._renderProxy` 的 `a` 属性,前面我们提到过 `with` 语句块内访问变量将会被 `Proxy` 的 `has` 代理所拦截,所以自然就执行了 `has` 函数内的代码。最终通过 `warnNonPresent` 打印警告信息给我们,所以这个代理的作用就是为了给在开发阶段给我们一个友好而准确的提示。
+
+我们理解了 `hasHandler`,但是还有一个 `getHandler`,这个代理将会在判断条件:
+
+```js
+options.render && options.render._withStripped
+```
+
+为真的情况下被使用,那这个条件什么时候成立呢?其实 `_withStripped` 只在 `test/unit/features/instance/render-proxy.spec.js` 文件中出现过,该文件有这样一段代码:
+
+```js
+it('should warn missing property in render fns without `with`', () => {
+    const render = function (h) {
+        // 这里访问了 a
+        return h('div', [this.a])
+    }
+    // 在这里将 render._withStripped 设置为 true
+    render._withStripped = true
+    new Vue({
+        render
+    }).$mount()
+    // 应该得到警告
+    expect(`Property or method "a" is not defined`).toHaveBeenWarned()
+})
+```
+
+这个时候就会触发 `getHandler` 设置的 `get` 拦截,`getHandler` 代码如下:
+
+```js
+const getHandler = {
+    get (target, key) {
+        if (typeof key === 'string' && !(key in target)) {
+            warnNonPresent(target, key)
+        }
+        return target[key]
+    }
+}
+```
+ 
+其最终实现的效果无非就是检测到访问的属性不存在就给你一个警告。但我们也提到了,只有当 `render` 函数的 `_withStripped` 为真的时候,才会给出警告,但是 `render._withStripped` 又只有写测试的时候出现过,也就是说需要我们手动设置其为 `true` 才会得到提示,否则是得不到的,比如:
+
+```js
+const render = function (h) {
+    return h('div', [this.a])
+}
+
+var vm = new Vue({
+    el: '#app',
+    render,
+    data: {
+        test: 1
+    }
+})
+```
+
+上面的代码由于 `render` 函数时我们手动书写的,所以 `render` 函数并不会被包裹在 `with` 语句块内,当然也就触发不了 `has` 拦截,但是由于 `render._withStripped` 也未定义,所以也不会被 `get` 拦截,那这个时候我们虽然访问了不存在的 `this.a`,但是却得不到警告,想要得到警告我们需要手动设置 `render._withStripped` 为 `true`:
+
+```js
+const render = function (h) {
+    return h('div', [this.a])
+}
+render._withStripped = true
+
+var vm = new Vue({
+    el: '#app',
+    render,
+    data: {
+        test: 1
+    }
+})
+```
+
+为什么会这么设计呢?这也许是 `Vue` 留的一个后门吧。
+
+现在,我们基本知道了 `initProxy` 的目的,就是设置渲染函数的作用域代理,其目的是为我们提供更好的提示信息。不过对于 `proxy.js` 文件内的代码,还有一段使我们没有讲过的,就是下面这段:
+
+```js
+if (hasProxy) {
+    // isBuiltInModifier 函数用来检测是否是内置的修饰符
+    const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
+    // 为 config.keyCodes 设置 set 代理,防止内置修饰符被覆盖
+    config.keyCodes = new Proxy(config.keyCodes, {
+        set (target, key, value) {
+            if (isBuiltInModifier(key)) {
+                warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
+                return false
+            } else {
+                target[key] = value
+                return true
+            }
+        }
+    })
+}
+```
+
+上面的代码首先检测宿主环境是否支持 `Proxy`,如果支持的话才会执行里面的代码,内部的代码首先使用 `makeMap` 函数生成一个 `isBuiltInModifier` 函数,该函数用来检测给定的值是否是内置的时间修饰符,我们知道在 `Vue` 中我们可以使用事件修饰符很方便的做一些工作,比如阻止默认事件等。
+
+然后为 `config.keyCodes` 设置了 `set` 代理,其目的是防止开发者在自定义键位别名的时候,覆盖了内置的修饰符,比如:
+
+```js
+Vue.config.keyCodes.shift = 16
+```
+
+由于 `shift` 是内置的修饰符,所以上面的代码将会得到警告。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+