《深入浅出Vue.js》读书笔记

49 阅读13分钟

Object的变化侦测总结

变化侦测就是侦测数据的变化,当数据发生变化时,要能侦测到并发出通知。

object可以通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter。

收集依赖需要为依赖找一个存储依赖的地方,为此创建Dep,它用来收集依赖、删除依赖和向依赖发送消息。

依赖就是watcher,只有当它触发的getter才会收集依赖,哪个watcher触发了getter,就会把它收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

Watcher的原理是先把自己设置到全局唯一的指定位置,然后读取数据。因为读取了数据所以会触发这个数据的getter。在getter中就会从全局唯一的那个位置读取当前正在读取的watcher,并把这个watcher收集到Dep中。这样watcher就可以主动订阅任意一个数据的变化。

创建Observer类:是把一个object的所有数据都转换成响应式的,也就是它会侦测object中所有数据的变化。

ES6之前的js没有提供元变成能力,所以在对象上新增属性和删除属性都无法被追踪到。

过程解读:

Data通过Observer转换成了getter/setter的形式来追踪变化。

当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。

当数据发生了变化时,会触发setter,从而向Dep中的依赖发送通知

Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的么讴歌回调函数。

Array的变化侦测总结

通过方法来改变内容,我们通过创建拦截器去覆盖数组原型的方法来追踪变化。

为了不污染全局Array.prototype,在Observer中只针对那些需要侦测变化的数组使用__proto__来覆盖原型方法,但__proto__不是所有浏览器都支持。对不支持的浏览器直接循环拦截器,把拦截器中的方法直接设置到数组身上来拦截Array.prototype上的原生方法。

在Observer中,我们对每个侦测了变化的数据都标记上__ob__,并把Observer实例保存在__ob__上。作用有两个:一方面是为了标记数据是否被侦测了变化(保证数组只能被侦测一次),另一方面可以很方便地通过数据取到__ob__,从而拿到Observer实例上保存的依赖。当拦截到数组发生变化时,向依赖发送通知。

除了侦测数组自身变化外,数组中元素发生变化也要侦测。在Observer中判断如果当前被侦测的数据是数组,则调用observeArray方法将数组中的每一个元素都转换成响应式的并侦测变化。

除了侦测已有数据外,当用户使用push等方法向数组中新增数据时,新增的数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是push、unshift和splice方法,则从参数中奖新增数据提取出来,然后使用observeArray对新增数据进行变化侦测。

虚拟DOM简介

应用状态:任何应用都会有状态,并不是使用现在的框架才会有状态。我们的关注点聚焦在状态维护上,而DOM操作其实可以省略。如JQ的变量都是状态。

1、渲染过程

最简单的方式就是不关心状态发生了什么变化,也不关心哪些DOM更新了,只要把所有的DOM删除,然后使用状态重新生成一份DOM,输出出来。这样会造成很多的性能浪费,访问DOM也是需要性能。

  • Angular:脏数据检查(不知道了哪些状态发生改变)
  • React:使用是虚拟DOM(不知道了哪些状态发生改变)
  • Vue.js1:通过细粒度绑定

虚拟DOM: 通过状态生成一个虚拟节点树,使用虚拟节点树进行渲染。渲染之前会用新的虚拟节点树和上一次生成的虚拟节点树进进行对比,只渲染不同的部分。虚拟节点树是由组件树简历其阿里的整个虚拟节点(vnode)。

Vue.js1每一个绑定都会有个对应的watcher来观察状态的变化,状态被越多的节点使用,开销就越大。

Vue.js2.0引入虚拟DOM,组件是一个watcher实例,例如一个组件内有10个节点使用了某个状态,其实就是一个watcher在观察这个状态的变化。然后去组件内部通过虚拟DOM去进行对比与渲染。

2、Vue.js中的虚拟DOM

模板来描述状态与DOM之间的映射关系,vue通过编译将模板转换成渲染函数,执行渲染函数就可以得到一个虚拟节点树,使用虚拟节点树可以渲染页面。

3、模板转换成视图过程

直接使用虚拟节点覆盖旧节点,会有很多不必要的DOM操作。如ul中的li发生变化,会替换ul。DOM操作比较慢,就会有性能上的浪费。

为避免这些性能浪费,将虚拟节点与上一次渲染视图所使用的旧节点做对比,找出真正需要更新的节点来进行DOM操作,避免其无需改动的DOM。

4、虚拟DOM的执行流程

虚拟DOM主要是:1、提供与真实DOM节点所对应的虚拟节点vnode。2、将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图。

VNode

VNode是VNode类实例化的对象,可以认为是节点描述对象,描述怎么去创建真实的DOM节点。

VNode表示一个真实的DOM元素,所有真实的DOM节点都使用vnode创建并插入到页面中。

vnode和视图一一对应,可以理解为vnode是JS对象版的DOM元素。渲染视图的过程是先创建vnode,再使用vnode去生成真实DOM元素,插入到页面渲染视图。

1、VNode作用

根据vnode渲染视图的过程,我们可以缓存上一次渲染视图所创建的vnode,每当要重新渲染的时候在新的vnode和缓存的vnode做对比,找出不一样的去修改。

Vue.js目前对状态的侦测策略采用了中等粒度。当状态发生变化时,只通知到组件级别,然后组件内使用虚拟DOM来渲染视图。

对比vnode缓存进行比较,只更新发生变化的节点,可以节省性能浪费。

2、VNode类型

注释节点、文本节点、元素节点、组件节点、函数式组件、克隆节点

1、注释节点

<!-- 注释节点-->

对应的vnode只有两个有效属性 text、isComment其余属性都是undefined或者false

01  {
02    text: "注释节点",
03    isComment: true
04  }

2、文本节点

01  export function createTextVNode (val) {
02    return new VNode(undefined, undefined, undefined, String(val))
03  }

创建时只有一个有效的属性text

01  {
02    text: "Hello Berwin"
03  }

3、克隆节点

将现有的节点的属性赋值到新节点中,让新创建的节点和被克隆节点的属性保持一致,作用是优化静态节点和插槽节点。

静态节点:当组件某个状态发生变化后,当前组件会通过虚拟DOM重新渲染,静态节点内容不会改变,除了首次渲染需要执行渲染函数获取vnode之外,后续不会执行渲染函数重新生成vnode,这样就会使用创建克隆节点的方法将vnode克隆一份,使用克隆节点进行渲染就不要重新执行渲染函数生成新的静态节点的vnode,提升性能。

01  export function cloneVNode (vnode, deep) {
02    const cloned = new VNode(
03      vnode.tag,
04      vnode.data,
05      vnode.children,
06      vnode.text,
07      vnode.elm,
08      vnode.context,
09      vnode.componentOptions,
10      vnode.asyncFactory
11    )
12    cloned.ns = vnode.ns
13    cloned.isStatic = vnode.isStatic
14    cloned.key = vnode.key
15    cloned.isComment = vnode.isComment
16    cloned.isCloned = true
17    if (deep && vnode.children) {
18      cloned.children = cloneVNodes(vnode.children)
19    }
20    return cloned
21  }

克隆节点和被克隆节点之间的唯一区别是isCloned 属性,克隆节点的isCloned 为true ,被克隆的原始节点的isCloned 为false 。

4、元素节点

四种有效属性

  • tag:节点名称。如:p、ul、li。
  • data:包含节点上的数据。如:attrs、class、style。
  • children:当前节点的子节点列表。
  • context:当前组件的Vue.js实例。
01  <p><span>Hello</span><span>Berwin</span></p>

对应的vnode

01  {
02    children: [VNode, VNode],
03    context: {...},
04    data: {...}
05    tag: "p",
06    ……
07  }

5、组件节点

组件节与元素节点类似,区别在于:

  • componentOptions:组件节点的选项参数,包含propsData、tag和children。
  • componentInstance:组件的实例,也是Vue.js的实例。
01  <child></child>

对应的vnode

01  {
02    componentInstance: {...},
03    componentOptions: {...},
04    context: {...},
05    data: {...}
06    tag: "vue-component-1-child",
07    ……
08  }

6、函数式组件

对比组件节点有两个独有属性functionalContext和functionalOptions。

01  {
02    functionalContext: {...},
03    functionalOptions: {...},
04    context: {...},
05    data: {...}
06    tag: "div"
07  }

总结:

VNode类可以生成不同类型的vnode实例,不同类型的vnode标识不同类型的真实DOM元素。

虚拟DOM更新视图,根据新旧vnode节点进行对比,只要更新对应变化DOM可以提升性能。

vnode类型是从VNode类实例化出的对象区别是属性不同。

七、patch

可以将vnode渲染成真实DOM,对比两个vnode之间的差异,对现有的DOM进行修改:1、创建新增的节点,2、删除已经废弃的节点,3、修改需要更新的节点。

之所以用算法来对比两个节点的差异,并针对不同的节点进行更新,主要是为了性能考虑。

1、新增节点

场景:

1、当oldVnode不存在而vnode存在时,需要使用vnode生成真实DOM元素并将其插入视图中。通常发生在首次渲染的时候。

2、当vnode和oldVnode完全不是同一个节点时,需要使用vnode生成真实的DOM元素将其插入视图中。

2、删除节点

当一个节点只在oldVnode中存在的时,需要把它从DOM中删除。渲染视图shiyivnode为标准,vnode中不存在的节点都属于被废弃的节点需要从DOM删除。替换的过程是将新创建的DOM节点插入到旧节点旁边,再删除旧节点。

3、更新节点

更精确在于新旧节点是同一个节点,需要对这两个节点进行细致别对,然后对oldVnode在视图中所对应的真实节点进行更新。

总结

patch过程分为:

1、当oldVnode不存在直接使用vnode渲染视图

2、当oldVnode和vnode都存在但并不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM

3、党oldVnode和vnode是同一个节点时,更详细的对比操作对真实的DOM节点进行更新

7.1创建节点

只有元素节点、注释节点和文本节点会被创建并插入DOM中。

元素节点:是否具有tag属性,调用当前环境下createElement方法创建真实的元素节点,创建完成后将他插入指定的父节点中。元素节点通常会有子节点,党一个元素节点被创建后也要把它的自己点也创建出来并插入到这个刚创建出的节点下面。

创建节点的过程是一个递归的过程,vnode中的children属性保存了当前节点的所有子虚拟节点,只要循环子虚拟节点执行一遍创建元素的逻辑。

创建子节点,子节点的父节点就是当前刚创建出来的节点,子节点被创建后会被插入当前节点下面。所有子节点创建完成后把当前节点插入到指定父节点下面。指定父节点已经渲染到视图,那么插入节点之后,当前节点渲染到视图中。

元素节点从创建到渲染的过程:

如果vnode中不存在tag属性,它可能是注释节点或者文本节点

注释节点属性标识isComment是true,使用createComment创建真实注释节点并插入到父元素中

7.2删除节点

使用的removeVnode函数从指定位置到指定位置的删除,使用到了nodeOps把parent.removeChild(child)删除节点封装到里面避免跨平台渲染中出现问题。

7.3更新节点

1、静态节点:值那些一旦渲染到界面上之后,无论日后状态不如何变化,都不会发生变化的节点。

在更新节点时,首先需要判断新旧两个虚拟节点是否是静态节点,如果是,就不需要进行更新操作,可以直接跳过更新节点的过程。

2、新虚拟节点有文本属性

当心虚拟节点有文本属性,并且和旧虚拟节点文本属性不一样时,直接把视图中的真实DOM节点内容改成新虚拟节点文本。

3、新虚拟节点无文本属性

如果虚拟节点没有text属性,它就是一个元素节点。可能会有子节点(children)或者没有。

  • 有children:这里还分两种情况,1、旧虚拟节点上有children就要与它进行更详细对比并更新。2、旧虚拟节点没有children属性,要么是文本节点:先清空然后再创建新children真实DOM插入视图DOM节点下面。
  • 无children:有什么删什么,最后达到视图中是空标签的目的。

八、模板编译

模板编译成渲染函数:先将模板解析成AST,然后遍历AST标记静态节点,最后使用AST生成代码字符串。这三部分内容分别对应三个模块:解析器、优化器和代码生成器。

AST:用javaScript中的对象来描述一个节点,一个对象标识一个节点,对象中的属性用来保存节点所需要的各种数据。其中有很多属性:parent保存父节点描述对象,children属性是一个数组,保存一些子节点描述对象。多个独立节点通过children和parent连在一起就变成一个树。

九、解析器

作用:将模板解析成AST

AST

以树状结构表示程序的语法,其中树中的每个节点表示代码中的特定元素或结构,例如表达式、语句或函数。以树状结构表示程序的语法,其中树中的每个节点表示代码中的特定元素或结构,例如表达式、语句或函数

解析器内部运行原理

HTML解析器、文本解析器以及过滤器解析器

如HTML解析器:大致理解就是遍历HTML触发相应的钩子函数,如:标签开始、结束、文本、注释。当解析器不再触发钩子函数,说明模板解析完成,所有节点都在钩子函数中构建完成,AST树生成完毕。