热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Vuejs源码剖析响应式原理、虚拟DOM、模板编译和组件化(23)

1.请简述Vue首次渲染过程1.实例创建完成后,调用$mount()方法完整版中会先调用srcplatformswebentry-runtime-with-compiler.js中




1.请简述Vue首次渲染过程


1.实例创建完成后,调用$mount()方法

完整版中会先调用src/platforms/web/entry-runtime-with-compiler.js中重写的$mount()(即进行模板编译),其中:


  • 先判断options中是否有render,如果没有传递render,调用compileToFunctions(),生成render()函数
  • 然后设置options.render = render

然后调用原来的$mount()(在src/platform/web/runtime/index.js中定义),其中会调用mountComponent(this, el, hydrating)


2.mountComponent(this, el, hydrating)


  • 触发beforeMount:callHook(vm, ‘beforeMount’)`
  • 定义函数updateCompOnent= () => { vm._update(vm._render(), hydrating) }
    • vm._render()生成虚拟DOM(vm._render()定义在src/core/instance/render.js中)
    • vm._update()将虚拟DOM转换成真是DOM(vm._update()定义在src/core/instance/lifecycle.js中)
  • 创建Watcher实例,把updateComponent传递进去,updateComponent是在Watcher中通过Watcherget()实例方法执行的
  • 触发mounted;返回实例return vm

3.watcher.get()

首次渲染创建的是渲染Watcher,创建完Watcher实例后会调用一个get()方法,get()中会调用updateComponent()updateComponent()中会调用vm._update(VNode. hydrating),而其中的VNode是调用vm._render()创建VNode

vm._render()的执行过程:


  • 获取创建实例时存放在options中的render函数:const { render, _parentVnode } = vm.$options
  • 调用render.call(vm.renderProxy, vm.$createElement)(这个render是创建实例new Vue()时传入的render(),或者是编译template生成的render()),最后返回VNode

然后执行vm._update(VNode, hydrating),其中:


  • 调用vm.__patch__(vm.$el, vnode)挂载真实DOM
  • vm.__patch__的返回值记录在vm.$el



2.请简述 Vue 响应式原理

Vue使用观察者模式来对其数据进行响应式处理,过程如下:


创建观察者


  • 在创建 Vue 实例时,调用的 this._init(options) 中会执行 initInjections(vm)initState(vm),这两个方法中分别会对 inject 的成员、本实例的 propsdata 进行响应式处理

  • initInjections(vm) 中会遍历 inject 的成员,通过 defineReactive(vm, key, result[key]) 将每个成员转换成响应式属性(即劫持 getter/setter

  • initState(vm) 中调用 initProps(vm, opts.props),其中编辑 props 属性,通过 defineReactive(props, key, value) 将属性转换成gettersetter,然后存入 props(也是 vm._props)中

  • initState(vm) 中调用 initData(vm, opts.props),其中调用 observe(data, true /* asRootData */)data 进行响应式处理

  • defineReactive(obj, key, val, customSetter?, shallow?)


    • 会将传入的 key 换成响应式属性,即其劫持 getter/setterObject.defineProperty( obj, key, { ..., get(){...}, set(){...} } )
    • 为每个属性 key 生成一个 Dep 对象 depconst dep = new Dep());dep 会在 getter 中收集依赖(即相应属性的 Watcher 对象),在 setter 中调用 dep.notify() 派发更新;
    • 在需要递归观察子对象时,会调用 observe(val)let childOb = !shallow && observe(val)),若 val 是个对象,则会为这个对象创建一个 Observer 对象,并返回。
  • observe (value, asRootData?)


    • 判断 value 是否是对象,不是对象就返回
    • 是对象,则这个对象可称之为 观测对象 ,然后为这个对象创建一个 Observer 对象 :ob = new Observer(value),并返回 ob(在 defineReactive 中,这个返回的 ob 会在 getter 中收集依赖相应的依赖)
    • Observer 构造函数中,会新建一个 Dep 对象 dep,这里的 dep 是为传入的 观测对象(进行响应式处理的对象)收集依赖(Watcher);与 defineReactive 中的 dep 不一样
    • Observer 实例挂载到 观测对象 的 __ob__ 属性:def(value, '__ob__', this)
    • value 不是数组,则执行 this.walk(value) 方法,遍历 value 中的每一个属性,然后调用 defineReactive(obj, keys[i])
    • value 是数组
      • Vue 并没有对数组对象的索引调用 defineReactive 来生成 getter/setter,而是重写了原生数组中会更改原数组的方法,调用这些新方法后,数组对象对应的 dep 对象会调用 dep.notify 方法来驱动视图的更新
      • 重写的数组方法:'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
      • 重写数组方法后,调用 this.observeArray(value),作用是当数组中的元素存在对象时,为数组中的每一个对象创建一个 observer 实例

至此,创建观察者结束


依赖的收集


  • 在进行挂载时( 调用 $mount() ),会执行 mountComponent 方法,其中会创建一个渲染 Watcher 对象
  • 渲染 Watcher 对象构造函数的最后会执行 get() 方法,get() 中会先执行 pushTarget(this)pushTarget 中则会将 Dep.target 设置为该 watcherDep.target = target
  • 然后调用 this.getter.call(vm, vm),即执行了 vm._update(vm._render(), hydrating),而 vm._render() 中执行 render.call(vm._renderProxy, vm.$createElement) 以生成 vnode,这个生成 VNode 的过程中,会触发 相应的响应式数据的 getter ,然后其中的 dep.depend() 则会收集当前实例 watcher

当生成完 VNode 后,就完成了响应式数据的的依赖收集


通知的发送

但修改某个响应式数据时,会触发该数据的 setter


  • 如果新值是对象,且需要递归观察子对象时执行 childOb = !shallow && observe(newVal),将新增也进行响应式处理
  • 调用 dep.notify() 派发更新,notify 会调用每个订阅者(watcher)的 update 方法实现更新
  • watcherupdate 中使用 queueWatcher() 判断 watcher 是否被处理,若没有,则把 watcher 添加进 queue 队列中,并调用 flushSchedulerQueue()
  • flushSchedulerQueue() 中先触发 beforeUpdate 钩子函数,然后调用 watcher.run()
  • watcher.run() 中会调用 get() 方法,get 中执行 getter,而 getter 就是传入的 updateComponent 方法,updateComponent 中执行 vm._update(vm._render(), hydrating),如此就完成了视图的更新
  • 然后 flushSchedulerQueue() 后续代码中 还原更新步骤的初始状态、触发 actived 钩子函数、触发 updated 钩子函数



3.请简述虚拟 DOM 中 Key 的作用和好处

Key 是用来优化 Diff 算法的。Diff算法核心在于同层次节点比较,Key 就是用于在比较同层次新、旧节点时,判断其是否相同。

Key 一般用于生成一列同类型节点时使用,这种情况下,当修改这些同类型节点的某个内容、变更位置、删除、添加等时,此时界面需要更新视图,Vue 会调用 patch 方法通过对比新、旧节点的变化来更新视图。其从根节点开始若新、旧 VNode 相同,则调用 patchVnode

patchVnode 中若新节点没有文本,且新节点和旧节点都有有子节点,则需对子节点进行 Diff 操作,即调用 updateChildrenKey 就在 updateChildren 起了大作用

updateChildren 中会遍历对比上步中的新、旧节点的子节点,并按 Diff 算法通过 sameVnode 来判断要对比的节点是否相同


  • 若这里的子节点未设置 Key,则此时的每个新、旧子节点在执行 sameVnode 时会判定相同,然后再次执行一次 patchVnode 来对比这些子节点的子节点
  • 若设置了 Key,当执行 sameVnode
    • Key 不同 sameVnode 返回 false,然后执行后续判断;
    • Key 相同 sameVnode 返回 true,然后再执行 patchVnode 来对比这些子节点的子节点

即,使用了 Key 后,可以优化新、旧节点的对比判断,减少了遍历子节点的层次,少使用很多次 patchVnode




4.请简述 Vue 中模板编译的过程


Vue 模板编译入口文件执行过程

在完整版 Vue 中,src/platforms/web/entry-runtime-with-compiler.js 里先保留 Vue 实例的 mount方法,然后重写该mount方法,然后重写该mount 方法,这个重写的方法就是完整版 Vue 中的模板编译器,其中在 $options 上挂载了模板编译后生成的 render 函数。

$options 上的 render 函数是由 compileToFunctions(template, options, vm) 这个函数生成,即将 template 转换成了 render 函数。所以这里就是完成了首次加载时对模板的编译。

这里梳理下生成 render 函数的相关函数的调用过程


第一步

调用 compileToFunctions(template, options, vm)

// src/platforms/web/entry-runtime-with-compiler.js
// 把 template 转换成 render 函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

第二步

compileToFunctions 是由 src/platforms/web/compiler/index.js 里的 createCompiler(baseOptions) 生成的。baseOptions 里是一些关于指令、模块、HTML标签相关的方法,这里不予关心。

所以 第一步compileToFunctions 是这里的 createCompiler 返回的函数。

// src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }

第三步

createCompiler 来自于 src/compiler/index.js,其中调用了 createCompilerCreator(function baseCompile (template, options)) 方法

所以 第二步 中的 createCompiler 来自于这里的 createCompilerCreator 返回的函数,createCompilerCreator 中传入 函数 baseCompile 作为参数

那么 第一步 中的 compileToFunctions 就是这里的 createCompilerCreator 返回的函数执行(即执行 createCompiler(baseOptions))后返回的函数

// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 把模板转换成 ast 抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options)
return {
ast,
// 渲染函数
render: code.render,
// 静态渲染函数,生成静态 VNode 树
staticRenderFns: code.staticRenderFns
}
})

第四步

createCompilerCreator 来自于 src/compiler/create-compiler.js

// src/compiler/create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptiOns= Object.create(baseOptions)
....
const compiled = baseCompile(template.trim(), finalOptions)
...
return compiled
}

return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}

可以看出这个函数返回了 createCompiler(baseOptions) 函数,则往上推可知 第二步 中的

// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)

其实就是执行的这里的 function createCompiler (baseOptions: CompilerOptions){...},这个 createCompiler 函数里返回了方法 compile、compileToFunctions

方法 compile 中执行了传入的函数参数 baseCompile,这个 baseCompile第三步 中传入的,其返回值为 ast、render、staticRenderFns

而方法 compileToFunctions 正是 第一步 中调用的 compileToFunctions(template, options, vm),其来自于 createCompileToFunctionFn(compile)


第五步

createCompileToFunctionFn 来自于 src/compiler/to-function.js

// src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
...

// 1. 读取缓存中的 CompiledFunctionResult 对象,如果有直接返回
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}

// 2. 把模板编译为编译对象(render, staticRenderFns),字符串形式的js代码
const compiled = compile(template, options)

// 3. 把字符串形式的js代码转换成js方法
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})

// 4. 缓存并返回res对象(render, staticRenderFns方法)
return (cache[key] = res)
}
}

createCompileToFunctionFn 返回函数 compileToFunctions,即 第四步createCompiler 函数返回的 compileToFunctions,所以是 第一步 中调用的 compileToFunctions 就是在执行这里的 compileToFunctions


总结过程


  • 执行 src/platforms/web/entry-runtime-with-compiler.js 中的 compileToFunctions(template, options, vm)
  • 执行 src/compiler/to-function.js 中的 compileToFunctionscompileToFunctions 中调用 compile(template, options)
  • 执行 src/compiler/create-compiler.js 中的 compilecompile 中调用 baseCompile(template.trim(), finalOptions)
  • 执行 src/compiler/index.js 传入 createCompilerCreator 中的函数参数 baseCompile(template, options),返回 ast、render、staticRenderFns
  • src/compiler/create-compiler.jsconst compiled = { ast、render、staticRenderFns },返回 compiled
  • src/compiler/to-function.js 中返回 res,即返回 render, staticRenderFns 方法
  • src/platforms/web/entry-runtime-with-compiler.js 获取 render、staticRenderFns


推荐阅读
  • uniapp开发H5解决跨域问题的两种代理方法
    本文介绍了uniapp开发H5解决跨域问题的两种代理方法,分别是在manifest.json文件和vue.config.js文件中设置代理。通过设置代理根域名和配置路径别名,可以实现H5页面的跨域访问。同时还介绍了如何开启内网穿透,让外网的人可以访问到本地调试的H5页面。 ... [详细]
  • 网络请求模块选择——axios框架的基本使用和封装
    本文介绍了选择网络请求模块axios的原因,以及axios框架的基本使用和封装方法。包括发送并发请求的演示,全局配置的设置,创建axios实例的方法,拦截器的使用,以及如何封装和请求响应劫持等内容。 ... [详细]
  • 在package.json中有如下两个对象:husky:{hooks:{pre-commit:lint-staged}},lint-staged:{src** ... [详细]
  • 本文主要对比了Proxy和Object.defineProperty两种对象属性操作方式的优劣,并介绍了它们各自的应用场景。Proxy具有直接监听对象和数组变化、多种拦截方法以及新标准的性能优势等特点,而Object.defineProperty则兼容性好,支持IE9,并且无法用polyfill磨平浏览器兼容性问题。根据具体需求和浏览器兼容性考虑,选择合适的方式进行对象属性操作。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • 如何提高PHP编程技能及推荐高级教程
    本文介绍了如何提高PHP编程技能的方法,推荐了一些高级教程。学习任何一种编程语言都需要长期的坚持和不懈的努力,本文提醒读者要有足够的耐心和时间投入。通过实践操作学习,可以更好地理解和掌握PHP语言的特异性,特别是单引号和双引号的用法。同时,本文也指出了只走马观花看整体而不深入学习的学习方式无法真正掌握这门语言,建议读者要从整体来考虑局部,培养大局观。最后,本文提醒读者完成一个像模像样的网站需要付出更多的努力和实践。 ... [详细]
  • 合并列值-合并为一列问题需求:createtabletab(Aint,Bint,Cint)inserttabselect1,2,3unionallsel ... [详细]
  • JavaScript和HTML之间的交互是经由过程事宜完成的。事宜:文档或浏览器窗口中发作的一些特定的交互霎时。能够运用侦听器(或处置惩罚递次来预订事宜),以便事宜发作时实行相应的 ... [详细]
  • React基础篇一 - JSX语法扩展与使用
    本文介绍了React基础篇一中的JSX语法扩展与使用。JSX是一种JavaScript的语法扩展,用于描述React中的用户界面。文章详细介绍了在JSX中使用表达式的方法,并给出了一个示例代码。最后,提到了JSX在编译后会被转化为普通的JavaScript对象。 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • Hibernate延迟加载深入分析-集合属性的延迟加载策略
    本文深入分析了Hibernate延迟加载的机制,特别是集合属性的延迟加载策略。通过延迟加载,可以降低系统的内存开销,提高Hibernate的运行性能。对于集合属性,推荐使用延迟加载策略,即在系统需要使用集合属性时才从数据库装载关联的数据,避免一次加载所有集合属性导致性能下降。 ... [详细]
  • jQuery如何判断一个元素是否被点击?
    本文介绍了使用jQuery判断一个元素是否被点击的方法,并通过示例进行了具体说明。注意要指定父级,否则会执行多次。 ... [详细]
  • Jquery 跨域问题
    为什么80%的码农都做不了架构师?JQuery1.2后getJSON方法支持跨域读取json数据,原理是利用一个叫做jsonp的概念。当然 ... [详细]
author-avatar
洪锐林
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有