关于前端:VUE-9大核心原理总结

5次阅读

共计 6184 个字符,预计需要花费 16 分钟才能阅读完成。

小程序框架有很多,都是反对前端 JavaScript 语言和 vue.js 框架的。FinClip 小程序是兼容各家平台的。所以在学习了框架应用之后的进阶就要相熟框架的底层原理。明天给大家分享一下 VUE 外围原理。

1、数据响应式

  1. 首先判断数据的类型,如果是根底数据类型,间接返回,如果曾经有 ob 属性,示意曾经是响应式的数据了,间接返回该数据。如果是对象就走第 2 步,如果是数组就走第 3 步
  2. 对象是通过 Object.defineProperty,在 getter 里收集依赖,在 setter 里触发更新
  3. 数组是首先拷贝数组的原型,而后基于拷贝的原型改写 (push,pop,unshift,shift,sort,reverse,splice) 七个能够扭转数组长度的办法,而后将改写后的原型赋给数组的隐式原型
  4. 对数组的隐式原型赋值后,还要观测数组的每一项,反复第一步
  5. 如果 Object.defineProperty 的 setter 里赋值,如果新赋的值是对象,也要进行观测
  6. 如果对数组的操作是有数据新增 (push,unshift,splice),还须要观测数组新增的每一项,同第 4 步(这里 Vue 源码的实现是给每个响应式数据[对象和数组] 新增了一个不可枚举的属性 ob,它的作用有三,其一是用来判断数据是否曾经是响应式的数据,如果是就不需再次观测,其二是属性 ob 是 Observer 类的一个实例,实例上有对数组每一项进行响应式解决的办法),其三是 $set 办法中,ob 用来判断要设置属性的对象是不是响应式的对象,如果它自身就不是响应式对象,则该属性无需定义为响应式的属性

对象是在 Object.defineProperty 的 getter 里进行依赖的收集,在 setter 里触发更新。具体是通过观察者模式,每一个属性都有一个 Dep 类的实例,Dep.target 有值即指向 watcher 的时候,在 dep 内收集 watcher,并且在 watcher 内收集 dep,dep 和 watcher 是多对多的关系,因为一个组件会有多个属性,而 watcher 是组件级的,所以 一个 watcher 可能对应多个 dep,dep 可能对应多个组件,组件外部的 computed 和 watch 都是 watcher。

不论是根组件还是非根组件(函数),它们的 data 最终的值都是对象,所以只会在 data 最外层对象的某些属性值是数组,所以在 Object.defineProperty 的 getter 里对数组进行依赖收集,咱们晓得依赖的收集是调用 dep 类上收集依赖的办法,Vue 的做法是在创立 Observer 类的实例的时候,定义了一个属性 dep,dep 是 Dep 类的实例。

对于多维数组和数组新增的数据,Vue 的做法是,在创立 Observer 类的实例的时候,设置了一个不可枚举的属性 ob,它的值是 Observer 类的实例,所以咱们在对多维数组进行依赖收集的时候,能够调用 ob 的 dep 的办法,对于数组新增的数据,调用 ob 上的办法对数组的每一项做数据响应式,并且调用 ob.dep 上的 notify 办法触发更新。

1.1、数据初始化的程序:props -> methods -> data -> computed -> watch

  • 如果 data 的层级过深会影响性能
  • 对象有新增和删除属性没方法做数据的响应式解决(通过 $set 解决)
  • 如果给对象的属性赋值为对象,也会对赋值后的对象进行响应式解决

1.2、data 中数组的响应式解决是通过改写数组原型上的七个办法(push/pop/shift/unshift/sort/reverse/splice)

  • 在重写数组原型之前,Vue 给每个响应式数据新增了一个不可枚举的 ob 属性,这个属性指向了 Observer 实例,能够用来避免曾经被响应式解决的数据重复被响应式解决,其次,响应式的数据能够通过 ob 获取到 Observer 实例的相干办法
  • 对于数组的新增操作(push/unshift/splice),会对新增的数据也做响应式解决
  • 通过索引批改数组内容和间接批改数组长度是观测不到的

2、Vue 如何进行依赖收集的?

  1. 每个属性都有 dep 实例,dep 实例用来收集它所依赖的 watcher
  2. 在模板编译的时候,会取值触发依赖的收集
  3. 当属性发生变化时会触发 watcher 更新

3、Vue 的更新粒度是组件级?

首先渲染 watcher 是组件级的。在初始化的时候,会调用 _init 办法,_init 外部会调用 $mount 办法,$mount 办法会调用 mountComponent 办法,mountComponent 办法外部定义了 updateComponent 办法,updateComponent 办法外部就是调用 _update 办法将 vnode 渲染成实在 DOM,mountComponent 办法会 new 一个渲染 watcher,并把 updateComponent 传给渲染 watcher,所以渲染 watcher 能够从新渲染 DOM(试想一下,如果咱们没有把更新 DOM 渲染的办法传递给 watcher,更改数据后,咱们须要手动去调用 DOM 渲染的办法;传递给 watcher 后,数据变动后,能够让 watcher 主动的去调用更新 DOM 渲染的办法)

在 render 函数生成 vnode 时,会判断是否是原生的 HTML 标签,如果不是原生 HEML 标签即是 组件,会创立组件的 vnode,子组件实质是 VueComponent 函数,VueComponent 外部会调用 _init 办法,所以创立子组件 vnode 的时候,也会 new 一个渲染 watcher,所以说渲染 watcher 是组件级的,也就是说 Vue 的更新粒度是组件级的

4、模板编译原理

留神一:咱们平时开发中应用的是不带编译的 Vue 版本(runtime-only),所以在传入选项的时候是不能应用 template 的。

留神二:咱们 .vue 文件中的 template 是通过 vue-loader 解决的,vue-loader 其实也是应用 vue-template-compiler 解决的

  1. 如果选项 options 里有 render 间接应用 render,如果没有 render 看选项里有没有 tempalte,如果有就用 template,如果没有就看选项里有没有 el,如果有 template = document.querySelector(el),最初用 compileToFunctions(tempalte) 生成 render

    最终都是生成 render 函数,优先级是 render > tempalte > el

  2. 模板编译的整体逻辑次要分为三个局部:\
  3. 第一步:将模板字符串转换成 element ASTs (解析器)\
  4. 第二步:对 AST 进行动态节点标记,次要用来做虚构 DOM 的渲染优化 (优化器)(进行新旧 vnode 比照的时候能够跳过动态节点)\
  5. 第三步:应用 elements ASTs 生成 render 函数代码字符串 (代码生成器)

4.1、生成 AST 的过程

其实就是 while 循环里一直的通过正则匹配字符串,如果是匹配到是开始标签,就触发 start 钩子解决开始标签和属性,如果匹配到文本,就触发 chars 钩子解决文本,如果匹配到完结标签,就调用 end 钩子解决完结标签。解决完后就把模板中曾经匹配到子串截取进去,始终这样循环操作,直到模板的字符串被截取成空串跳出 while 循环。

在匹配到开始标签后,就把开始标签压入栈中,匹配到完结标签就把栈顶元素出栈。第一个进栈的元素就是根节点,除了第一根元素外,其余元素在进栈之前,栈顶的元素就是该元素的父亲节点,所以能够保护元素之间的父子关系(入栈元素的 parent 是栈顶元素,该入栈元素是栈顶元素的儿子),当栈被清空之后,根节点就是生成的 AST 匹配到文本内容是没有子节点的,所以它间接作为栈顶元素的儿子即可。

4.2、解析器运行过程

AST 是用 JS 中的对象来形容节点,一个对象代表一个节点,对象的属性用来保留节点所需的各种数据。

解析器内局部了好几个子解析器,比方 HTML 解析器,文本解析器,过滤器解析器。其中最次要的是 HTML 解析器,HTML 解析器的作用就是解析 HTML,它在解析的过程中会一直的触发各种钩子函数。这些钩子函数包含,开始标签钩子函数 (start)、完结标签钩子函数(end),文本钩子函数(chars) 和正文钩子函数(comment)。

实际上,模板解析的过程就是一直的调用钩子函数的过程,读取 template,应用不同的正则表达式匹配到不同的内容,而后触发对应的钩子函数解决匹配到的字符串截取片段。比方比配到开始标签,触发 start 钩子函数,start 钩子函数解决匹配到开始标签片段,生成一个标签节点增加到形象语法树上。

HTML 解析器解析 HTML 的过程就是循环 (while 循环) 的过程,简略来说就是利用 HTML 模板字符串来循环,每轮循环都从 HTML 字符串中截取一小段字符串,反复以上过程,始终到 HTML 字符串被截取成一个空串完结循环,解析结束。

在解析开始标签和完结标签是用栈来保护的,解析到开始标签就压入栈中,解析到完结标签,就从栈顶取出对应的开始标签的 AST,栈顶的前一个开始标签就是该标签的父元素,而后就能够建设父子元素之间的关系。

文本解析器是对 HTML 解析器解析进去的文本进行二次加工。文本分为两种类型,一种是纯文本,一种是带变量的文本。HTML 解析器在解析文本的时候,并不会辨别是纯文本还是带变量的文本,如果是纯文本,不须要进行任何解决,带变量的文本须要文本解析器的进一步解析,因为带变量的文本在应用虚构 DOM 进行渲染时,须要将变量替换成变量中的值。

本解析器通过正则匹配出变量,把变量改写成 _s(x)的模式增加到数组中

4.3、初始渲染原理

  1. 首先是生成 render 函数
  2. vm._render 函数生成虚构 DOM render 函数次要返回了这样的代码 _c(‘div'{id:”app”},_c(‘div’,undefined,_v(“hello”+_s(name)),_c(‘span’,undefined,_v(“world”)))),所以须要定义 _c, _v, _s 这样的函数能力真正转换成虚构 DOM
  3. vm._update 办法将生成的虚构 DOM 进行实例挂载 update 办法的外围是利用 patch 办法来渲染和更新视图,这里是首次渲染,patch 办法的第一个参数是实在 DOM,更新阶段第一个参数是 oldVnode

5、Vue.mixin 的应用场景和原理

  • Vue.mixin 的作用就是抽离公共的业务逻辑,原理相似“对象的继承”,当组件初始化的时候会调用 mergeOptions 办法进行合并,对于不同的 key(data,hooks,components…)有不同的合并策略。如果混入的数据和组件自身的数据有抵触,会采纳“就近准则”,以组件自身的为准。
  • mixin 有很多的缺点:命名抵触,起源不清晰,依赖问题

6、nextTick 在哪里应用?原理是什么?

  • nextTick 可用于获取更新后的 DOM
  • Vue 的数据更新是异步的,会把所有的数据更新操作都放入工作队列中,而后在 nextTick 中去顺次执行这些工作,nextTick 是一个异步工作,采纳的是优雅降级(Promise -> MutationObserver -> setImmediate -> setTimeout)

7、watch 原理

  1. watch 的应用形式,能够是对象,能够是函数,也能够是数组
  2. 不论是哪种应用形式,watch 的每一个属性对应的函数 (数组的应用形式,数组中的每一项(函数)) 都是一个 用户 watcher,其实现都是调用的 $watch(vm, handler)
  3. $watch 办法的实现都是 new Watcher(),只不过是 options 参数里标记了是用户自定义的 watcher(options.user = true)
  4. watch 的属性对应的函数里有新值和旧值,咱们是如何返回新值和旧值的呢?
  5. new Watcher() 的时候传递的是属性的 key,咱们要把它包装成一个函数(函数外部就是依据 key 取值),赋值给 Watcher 类的 getter 属性,在 Watcher 类实例化的时候,会调用一次 get 办法,咱们就能够拿到它的值(取值同时会进行依赖收集)
  6. 在值更新后,会再次调用 Watcher 类的 get 办法取得新值
  7. 而后判断 watcher 的类型,如果是用户 watcher,执行 callback,把新值旧值传递给 callback

watch api 不论是哪种应用形式,最终都是一个 key, 一个函数,对应一个 user watcher,每一个 watcher 都有一个 getter 办法,watch api 对应的 getter 办法是依据 key 来封装的,getter 办法就是取 key 对应的数据,因为 watcher 在初始化的时候默认会调用一次 getter,所以就拿到 key 对应的旧值了,取值也就进行了依赖收集,当 key 对应的数据扭转了,watcher 的 getter 办法会再次执行,这时就拿到了新值,而后调用 key 对应的回调函数,将新值和旧值传给它

8、computed 原理

  1. 每个计算属性实质上也是一个用户 watcher,在它取值的时候进行依赖收集,computed 依赖的值扭转后触发更新
  2. 计算属性的 watcher 在初始化的时候会有两个属性 lazy 和 dirty
  3. watcher 在初始化的时候,会默认调用一次 get 办法,然而 computed 默认是不执行的,所以用 lazy 属性来标记是 computed watcher
  4. computed 是有缓存的,即依赖的值没有产生扭转,屡次获取,是不会屡次调用 watcher 的 get 办法获取值的,所以用 dirty 属性来标记是否须要从新计算值,如果不须要计算,间接返回 watcher 的 value,如果须要计算,再来调用 get 办法获取新的值,再返回 watcher 的 value\
    补充:什么时候 dirty 的值是 true 呢?
  • computed watcher 初始化的时候
  • computed watcher 依赖的值扭转时(调用了 computed watcher 的 update 办法,即可示意依赖的值扭转了)

9、diff 算法

Vue 的 diff 算法是平级比拟,不思考跨级比拟的状况。外部采纳深度递归和双指针的形式

  1. 首先比对是否是雷同的节点,如果不是删除旧的 DOM,生成新的 DOM 插入
  2. 如果是雷同的节点,比对更新属性
  3. 判断是否是文本节点,如果是,判断文本内容是否雷同,不同更新文本内容
  4. 比对新旧子节点,如果只有新的有子节点,新增子节点插入;如果只有旧的有子节点,将元素的 innerHTML 置为空
  5. 如果新旧都有子节点,比对新旧子节点(采纳双指针)
  6. 顺次是头头、尾尾、头尾、尾头比拟,没有匹配到,就乱序比对
  7. 乱序比对:建设旧的节点的映射表(key->index)
  8. 新的起始节点是否能在旧的映射表中找到,不能找到间接在旧的后面插入,如果找到,将映射表找到的旧的节点,挪动到后面,并将该地位置为 null
  9. 因为在乱序比对中,有将旧节点置为 null 的状况,所以在进行子节点比对前,先判断该节点是否为 null,为 null 顺移
  10. 比对完之后如果新的节点还有,插入新的节点(插入的地位要判断是否在哪里插入),如果旧的节点还有,删除旧的节点(null 的地位跳过)

学习框架技术是为了更好的开发,学习底层原理是为了让产品更好用,更好的让 FinClip 小程序在各家平台上更好的兼容和晦涩应用。

正文完
 0