什么是数据驱动
视图由数据驱动生成,意思就是只对数据修改,而不是直接操作DOM。
new Vue发生了什么
如何调试
可以通过vue-cli
新建一个项目,这里没有选择装最新的`vue-cli@3.0,而是安装了
vue-cli@2.9.6`
1 | npm install -g vue-cli@2.9.6 |
看一下项目根目录的package.json
文件的scripts
1 | "scripts": { |
也就是npm run dev
的时候,使用的打包配置文件是webpack.dev.conf.js
,而在这个配置文件中又merge
了webpack.base.conf
的配置。
resolve.alias
用于创建 import
或 require
的别名,来确保模块引入变得更简单,在这里的意思就是
1 | import Vue from 'vue'; |
所以在vue.esm.js
文件里的构造函数里打debugger
然后执行npm run dev
,就可以断点调试了。
我们的模板
1 | import Vue from 'vue' |
开始
一、入口
1 | function Vue (options) { |
执行_init
方法,传入option
参数
二、调用_init
方法
1 | Vue.prototype._init = function (options) { |
initState
方法
1 | function initState (vm) { |
initData
方法,这个方法判断了method
和props
是否包含data
对象里的键名,最后他们都会挂载到vm
实例上,所以不能重复。
1 | function initData (vm) { |
并且proxy
方法,这个方法做的事情就是为vm
实例上的属性,设置了属性描述符get
与set
。
1 | function proxy (target, sourceKey, key) { |
mergeOptions
合并配置信息- 初始化操作
- 最后
vm.$mount
,手动地挂载一个未挂载的实例。
实例挂载的实现
上面说了vm.$mount
方法,那看一下它做了什么
1 | Vue.prototype.$mount = function ( |
最后执行mount.call(this, el, hydrating)
方法
1 | function ( |
又走进mountComponent(this, el, hydrating)
1 | function mountComponent ( |
vm._update(vm._render(), hydrating);
方法走进去,一直往下走找到了下面这个方法insert
,在这里把内容插入body
里。
1 | function insert (parent, elm, ref$$1) { |
所以实例挂载主要做的事情就是
做一些基础判断,比如不允许绑在
body
和html
上等等vm.render
方法先生存虚拟节点(vnode),在实例化一个渲染watcher
,回调函数是updateComponent
之后
vm.update
更新DOM
vm._render
方法
上面vm
实例上的_render
方法,会生成一个虚拟节点(vnode)。
1 | Vue.prototype._render = function () { |
Virtual DOM
虚拟DOM,其实就是用个对象去描述DOM结构。目的是为了避免频繁操作DOM。而Virtual DOM 映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。
createElement方法
生成vnode的方法
1 | function createElement ( |
vm._update
方法
这个方法会在首次渲染和数据更新的时候调用
1 | Vue.prototype._update = function (vnode, hydrating) { |
这边走进vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
1 | ... |
其中有一段,通过 emptyNodeAt
方法把 oldVnode
转换成 VNode
对象,然后再调用 createElm
方法。走进createElm
1 | function createElm ( |
createElm
方法中的部分内容:
1 | //这里创建了一个占位符,我们这边是创建了一个p标签 |
上面代码中的createElement
方法
1 | function createElement$1 (tagName, vnode) { |
走进createChildren
方法,这个实际上是遍历子虚拟节点,递归调用 createElm
1 | function createChildren (vnode, children, insertedVnodeQueue) { |
这边再次进入createElm
方法
1 | vnode.elm = nodeOps.createTextNode(vnode.text); |
这里的nodeOps.createTextNode(vnode.text)
是通过document.createTextNode(text)
创建了一个文本节点,并且赋值给了 vnode.elm
,最后insert(parentElm, vnode.elm, refElm);
插入到父元素里。下面是insert
方法:
1 | function insert (parent, elm, ref$$1) { |
最后通过下面方法调用,将元素插入到body
下面
1 | insert(parentElm, vnode.elm, refElm); |
1 | function insert (parent, elm, ref$$1) { |
1 | function insertBefore (parentNode, newNode, referenceNode) { |
new Vue之后发生的事情流程图
每个阶段做的事情:
init:初始化阶段,为Vue添加原型方法与静态方法,将new Vue
传入的参数添加到实例上,并且判断是否有重复的键名。
$mount:做一些基础的判断,(比如判断替换模板不能是html与body之类)
compile:模板解析阶段
render:生成vnode的方法
vnode:生成的虚拟节点
patch:这个阶段创建占位符,并且遍历子节点(深度优先)添加到占位符里,最后添加到DOM中
DOM:生成DOM
补充
module
字段和main
字段
1 | "main": "dist/vue.runtime.common.js", |
在打包构建的时候:
在CommonJS 规范中,模块只能通过 exports
对象向外暴露属性。所有要暴露的方法、变量等都只能作为 exports
对象的一个属性出现。所以打包工具并不知道我们代码中最终会用到模块中的哪些方法。为了安全起见,整个模块的代码都被包含在了最终生成在 bundle 中。
在 ES6 中的 import
和 export
都是静态的。静态意味着一个模块要暴露或引入的所有方法在编译阶段就全部确定了,之后不能再改变。这样子打包的时候可以将没用到的模块方法剔除,这就是Tree Shaking
。
所以:
main
: 字段指向的应该是编译后生成的 ES5 版本的代码。
module
: 字段要指向的应该是一个基于 ES6 模块规范的使用ES5语法书写的模块。