Vue 的一些笔记

Vue 实例化过程关于数据方面的处理

var vm = new Vue({ data: ..., ... })
  1. 使用构造函数的数据初始化实例的 _data属性(即vm._data)

  2. _data属性,使用getter、setter代理到vue实例vm上 即访问、修改vue的属性的时候,只是简单地通过getter、setter访问vm._data属性。

  3. 观察vm._data属性(这部分功能交由Observer类进行)

  • 生成一个Observer对象obs,该对象持有vm._data(obs.value === vm._data), 通过递归,将vm._data的数据属性,全部用Object.defineProperty转换成存取器属性
  • 针对每个转换后的存取器属性,会实例化一个依赖管理器dep(Dep类型)对象来维护依赖该属性的观察者(Watcher类型)列表dep.subs
  • 当某个属性的值被修改时,obs的setter中,会递归地观察新的属性值,并最终让该属性的依赖管理器dep去通知所有watcher(订阅者)执行其update方法(dep.notify(),内部遍历watcher逐个执行watcher.update())
  • 每当属性值被读取,会在getter中,检测Dep.target(Dep类的静态属性)是否有值,有值则是一个Watcher对值的读取(Watcher类的getter中,读取属性的时候,会临时设置Dep.target为watcher实例自身),这时候把该watcher加入当前属性的依赖管理器dep里面,这样该属性值下次改变,该watcher就能获得通知并update
  1. vue 实例中,会暴露一个$watch(expOrFn, cb)接口,主要在指令中使用订阅者,调用该接口,会创建一个Watcher类型的对象,watcher对象创建过程,会读取vm._data的对应属性,从而触发上述的obs的getter,进而自动将本watcher加入对应属性的依赖管理器dep里,以便在属性下次变化时获得通知有机会执行update方法。

总的来讲:

Observer类

将数据属性转换成存取器属性,并:

  • 在每个属性的getter里面,提供值之前,分析是否是新的watcher,若是,则将其加入对应属性的依赖管理器
  • 在每个属性的setter里面,修改值之后,让依赖管理器去调用所有watcher的update方法

Dep类

用于管理某个data的属性的所有订阅者,拥有notify方法,该方法在关联的data属性发生变化时被调用,内部会执行所有订阅者的update方法

Watcher类

用于订阅某个data属性,当属性变化时,执行实例化时构造器中传入的回调函数

Vue类

  • 构造时,自动使用Observer类来转换、维护数据
  • 暴露一个$watch方法,该方法可以生成一个watcher用于订阅data数据

模版编译

编译元素 compileElement(el, options)

  • 检查el的attributes,
  • 将attributes转成数组 attrs,
  • 调用compileDirectives(attrs, options)进行指令编译,返回一个nodeLinkFn,这是一个链接函数,作用是绑定指令和Vue实例
  • nodeLinkFn作为最终结果返回

编译指令 compileDirectives(attrs, options)

  • 解析每个attribute,提取修改器(modifier)、过渡效果(transiton)、动态绑定(v-bind、:),普通指令(v-text、v-ref:xxx、v-el:xxx)等等信息
  • 每个成功解析出来的结果,就是一个指令描述(directive descriptor),将所有解析结果推入一个数组待用
    // 指令描述数据结构
    {
    name: 'text', // 指令名称
    attr:'v-text', // 指令原始名
    raw:'值内容', // 指令原始值
    def: { // 指令定义
    bind(){},
    update() {}
    },
    arg: arg, // 参数,`v-ref:xxx`中`xxx`部分
    modifier: modifier, // 修改器
    expression: parsed && parsed.expression, // 指令原始值中的表达式部分
    filters: parsed && parsed.filters, // 指令原始值中的过滤器部分
    interp: interpTokens,
    hasOneTime: hasOneTimeToken
    }
  • 指令描述数组作为参数,调用makeNodeLinkFn(directiveDiscriptors),该函数返回一个新函数nodeLinkFn(vm, el, host, scope, frag) nodeLinkFn作为闭包可以访问makeNodeLinkFn的参数directiveDiscriptors,nodeLinkFn执行时为每个directive discriptor逐一调用vm._bindDir方法
  • 将nodeLinkFn作为结果返回

Vue.prototype._bindDir(descriptor, node, host, scope, frag)

  • 使用 new Directive(descriptor, this, node, host, scope, frag) 得到一个Directive实例。
  • 将Directive实例push入Vue实例的_directive数组属性里
READ MORE...

JavaScript 变量赋值的问题

最近看到有人在讨论一个关于 JavaScript 赋值的问题,代码如下:

var a = {n: 1}
var b = a
a.x = a = {n: 2}
console.log(a.x) // undefined
console.log(b.x) // {n: 2}

代码看着有点绕,这个结果很多人表示不能理解。 为了说明这个问题,下面便逐步详细分析。

为了化简式子,下面行文的一些约定:

  1. 声明环境记录用 ENV 表示
  2. {n: 1}N1 表示
  3. {n: 2}N2 表示

分析开始:

执行完第一行,声明环境记录 ENV 里面存在绑定: ENV.a —指向—> {n: 1}(后续用 N1 指代)

执行完第二行,声明环境记录 ENV 里面存在绑定: ENV.a —指向—> N1 ENV.b —指向—> N1

执行第三行,将三部分分别来看:

  1. 第一部分 a.xENV.a.x,即 N1.x
  2. 第二部分 aENV.a,即 N1
  3. 第三部分为 {n: 2}(后续用 N2 指代)

赋值右结合,相当于 N1.x = (ENV.a = N2)

先看第一个赋值动作的执行: ENV.a = N2

执行后:

  1. ENV.a —指向—> N2
  2. ENV.b —指向—> N1
  3. 表达式返回值为 N2

继续第二个赋值动作,将表达式的返回值 N2 赋值给第一部分: N1.x = N2

此时:

  1. ENV.a —指向—> N2N1 现在为 {n: 2, x: N2}
  2. ENV.b —指向—> N1
  3. 表达式返回值为 N2

执行 console.log(a.x): 此时 ENV 中的 aN2,相当于: console.log(N2.x)N2 中没有 x 属性,所以结果: undefined

执行 console.log(b.x): 此时 ENV 中的 bN1,相当于: console.log(N1.x) 所以结果为 N2,即 {n: 2}

READ MORE...

arguments 对象与形参联动关系

strict mode 中,函数的 arguments 对象的属性值,就是传入该函数的实参的简单拷贝,它们与形参之间的值不存在动态的联动关系

非严格模式下的函数,函数的 arguments 对象的属性与绑定在该函数执行环境中的形参共享值。 换句话说,改变该 arguments 对象的属性将改变对应的形参值,反之亦然。 不过,如果其中一个属性被删除然后再重设值,则会打破这种联动关系。

下面用一个简单的例子来说明这些现象。

代码

// strict mode
;(function test(a) {
'use strict';
console.log(arguments[0]) // 1
console.log(a) // 1
a = 2
console.log(arguments[0]) // 1
console.log(a) // 2
})(1)
// 非strict mode
;(function test2(a) {
// 初始一一对应
console.log(arguments[0]) // 1
console.log(a) // 1
// 修改形参,参数对象联动变化
a = 2
arguments[1] = 2
console.log(arguments[0]) // 2
console.log(a) // 2
// delete 属性再重新设值,则不在联动
delete arguments[0]
arguments[0] = 3
console.log(arguments[0]) // 3
console.log(a) // 2
})(1)
READ MORE...

构造函数的返回值

构造函数不需要显式 return,如果指定了返回值,则根据返回值的类型有不同的结果。

代码:

function PrimitiveUndefined() { }
function PrimitiveNull() { return null }
function PrimitiveBoolean() { return true }
function PrimitiveNumber() { return 123 }
function PrimitiveString() { return 'string' }
function PrimitiveSymbol() { return Symbol() }
function BooleanObject() { return new Boolean(true) }
function NumberObject() { return new Number(123) }
function StringObject() { return new String('string') }
function DateObject() { return new Date() }
function RegExpObject() { return /test/ }
function FunctionObject() { return function(){} }
function IteralObject() { return {} }
function IteralArray() { return [] }
const FUNCTIONS = [
'PrimitiveUndefined',
'PrimitiveNull',
'PrimitiveBoolean',
'PrimitiveNumber',
'PrimitiveString',
'PrimitiveSymbol',
'BooleanObject',
'NumberObject',
'StringObject',
'DateObject',
'RegExpObject',
'FunctionObject',
'IteralObject',
'IteralArray'
]
FUNCTIONS.forEach(func => {
let obj = eval('new ' + func)
let type = obj.constructor.name
console.log('%c%s %c: %c%s',
'color:#607ed8', func,
'color:#888',
'color:#d87860', type)
})

测试结果打印:

PrimitiveUndefined : PrimitiveUndefined
PrimitiveNull : PrimitiveNull
PrimitiveBoolean : PrimitiveBoolean
PrimitiveNumber : PrimitiveNumber
PrimitiveString : PrimitiveString
PrimitiveSymbol : PrimitiveSymbol
BooleanObject : Boolean
NumberObject : Number
StringObject : String
DateObject : Date
RegExpObject : RegExp
FunctionObject : Function
IteralObject : Object
IteralArray : Array

总结

  • 若手动 return 一个原始类型的值 (primitive value),则 return 语句不会起作用,仍然返回构造函数隐式创建的对象。
  • 若手动 return 一个对象类型的值,则会丢弃构造函数隐式创建的对象,返回 return 语句指定的对象。
READ MORE...

离线应用与 manifest.appcache

manifest 文件是一个文本文件,用来告诉浏览器缓存资源的规则,可以用来构建一个离线可用的应用。

manifest 文件的 MIME 类型是 text/cache-manifest

manifest 文件使用时只需简单地在 html 文件中引入。具体方法是,在 <html> 标签中,增加一个 manifest 属性,该属性引用一个 manifest.appcache 文件来描述资源的缓存情况。

代码如下:

<!DOCTYPE html>
<html lang="en" manifest="manifest.appcache">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
</body>
</html>

浏览器兼容性

在使用前,我们都关心的兼容性…

  • IE 10+
  • Chrome 4+
  • Safari 4+
  • Firefox 3.5+
  • Opera 11.5+
  • Safari (iOS)
  • Android Browser
  • Edge

现代浏览器都有支持,尽管实现上据说都有坑。

manifest 已从标准中移除,将来浏览器随时可能会枪毙该特性…
然而,后继者 service workers 还未获得广泛的支持。

READ MORE...