浏览器工作原理及web 性能优化(下)

469 阅读6分钟

(前端)主流框架主流做法案例

Diff算法 github.com/NervJS/nerv…

虚拟DOM的理论基础,React/Vue算法层优越之处

React基于两个假设:

  • 两个相同的组件产生类似的DOM结构,不同组件产生不同DOM结构
  • 对于同一层次的一组子节点,它们可以通过唯一的id区分

发明了一种叫Diff的算法来比较两棵DOM tree,它极大的优化了这个比较的过程,将算法复杂度从O(n^3)降低到O(n)。

同时,基于第一点假设,我们可以推论出,Diff算法只会对同层的节点进行比较。如图:

具体看下Diff算法是怎么做的,这里分为三种情况考虑

  • 节点类型不同:直接删去旧的节点,新建一个新的节点。

  • 节点类型相同:

    • DOM元素类型:react会对比它们的属性,只改变需要改变的属性
    • 自定义组件:由于React此时并不知道如何去更新DOM树,因为这些逻辑都在React组件里面,所以它能做的就是根据新节点的props去更新原来根节点的组件实例,触发一个更新的过程,最后在对所有的child节点在进行diff的递归比较更新。
  • 子节点比较:因为React在没有key的情况下对比节点的时候,是一个一个按着顺序对比的。 当节点很多的时候,这样做是非常低效的。有两种方法可以解决这个问题:

    • 保持DOM结构的稳定性,
    • 配置key shouldComponentUpdate PureComponent(immutable.js) 分离组件,只传入关心的值

Vue(2.5.17-beta.0)

响应式原理

数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

如何追踪变化

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。 每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

异步更新队列(性能关键)

可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

  • 为什么要异步?

修改 data 中的数据后修改视图。即“setter -> Dep -> Watcher -> patch -> 视图”的过程。

Vue.js在默认情况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick 的时候将这个队列 queue 全部拿出来 run( Watcher 对象的一个方法,用来触发 patch 操作) 一遍。

template 的 Compile 过程

compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。

  • parse:用正则等解析 template 模板中进行字符串,得到v-if、v-show、v-on、class、style等数据,形成 AST。
  • optimize:这个涉及到patch 过程——将 VNode 节点进行一层一层的比对,然后将「差异Diff」更新到视图上。

如果为静态的节点做上一些「标记」,在 patch 的时候只比对有必要比对的节点,从而达到「优化」的目的。

  • generate: 将 AST 转化成 render funtion 字符串,最终得到 render 的字符串以及 staticRenderFns 字符串

patch 的核心 diff 算法

实现方式

import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'

初始化props,methods,data,computed和watch;

初始化props,methods,data,computed和watch

/**
 * Define a reactive property on an Object.
 */
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 () {
      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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

做了两件事,一是将_data上面的数据代理到vm上,二是通过执行 observe(data, true / asRootData /)将所有data变成可观察的,即对data定义的每个属性进行getter/setter操作,这里就是Vue实现响应式的基础

Observer类是将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。

Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例回调cb进行视图的更新。

被Observer的data在触发 getter 时,Dep 就会收集依赖的 Watcher ,其实 Dep 就像刚才说的是一个书店,可以接受多个订阅者的订阅,当有新书时即在data变动时,就会通过 Dep 给 Watcher 发通知进行更新。

React

JSX如何生成element

当我们写下一段JSX代码的时候,react是如何根据我们的JSX代码来生成虚拟DOM的组成元素element的。

//jsx
return(
  <div className="cn">
  <Header> Hello, This is React </Header>
  <div>Start to learn right now!</div>
Right Reserve.
  </div>
)
//render
return(
React.createElement(
	'div',
{ className: 'cn' },
React.createElement(
Header,
null,
'Hello, This is React'
),
React.createElement(
'div',
null,
'Start to learn right now!'
),
'Right Reserve'
))
// VNODE
{
  type: 'div',
  props: {
    className: 'cn',
    children: [
      {
        type: function Header,
        props: {
          children: 'Hello, This is React'
        }
      },
      {
        type: 'div',
        props: {
          children: 'start to learn right now!'
        }
      },
      'Right Reserve'
    ]
  }
}

  • element如何生成真实DOM节点 再生成elment之后,react又如何将其转成浏览器的真实节点。这里会通过介绍首次渲染以及更新渲染的流程来帮助大家理解这个渲染流程

mountComponent(container): 会将element转成真实DOM节点,并且插入到相应的container里,然后返回markup(real DOM)。

mountComponent(container) {
const domElement = document.createElement(this._currentElement.type);
const textNode = document.createTextNode(this._currentElement.props.children);
domElement.appendChild(textNode);
container.appendChild(domElement);
return domElement;
}

如何性能优化, 参考:github.com/Pines-Cheng…

Angular

参考这篇帖子即可

webpack 分片加载原理

相关链接: