vue 渲染流程
注意:以下代码都是关键代码,由本人手写并经过测试 ~ 并非直接down的源码。
使用方式
既然我们已经使用很多次vue了,那vue的使用方式大家一定不陌生。
假设我们在html中引入了打包好的vue.js文件,代码如下:在这个new Vue的构造函数中,我们传入了一个对象,对象的属性包括了data、el、mounted三个属性。今天只考虑这三个属性如何正确的渲染出来。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My-Vue</title>
</head>
<body>
<div id="app" class="app">hello<span class='12312'>{{a.name}}</span>{{b}}</div>
<script src="/dist/umd/vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
data(){return{a:{name:'张三'},b:[[1],0]}},
// template:"hello<span class='12312'>{{name}}</span>",
mounted(){
console.log('mounted')
}
})
console.log('1234444444',vm.$el.innerHTML )
setTimeout(()=>{
vm.a.name = '李四';
vm.b.push(5)
vm.b[0].push(2)
},1200)
</script>
</body>
</html>
从我们调用vue的方式来看,能得到一个关键的信息,也就是这个暴露出来的Vue,它是一个函数。否则不能用new来调用。
那这个vue函数是怎么样的呢?
且听我细细道来~
Vue 函数及原型方法
vue是一个函数当然也是对象,对象是有原型的,原型上有很多方法,当我们new一个实例的时候,这个实例就能够获取构造函数原型上的方法。这时候就像子类可以继承父类的方法一样,就可以使用vue原型上的方法。这与js原型链的机制息息相关~
在vue暴露出去之前,他会往他的原型上挂载很多方法,以供在需要的时候直接使用。那他都会挂在哪些方法呢?
比如说我们的初始化init方法、返回vdom的render方法,进行渲染的mount方法以及update更新DOM的方法等等。这里只列举了下文可能会涉及的一些方法。
事实上,在VUE的源码中,不同用途的函数会进行分门别类的挂载。
如下,与初始化相关的函数,由initMixin()来完成挂载。与生命周期相关的函数由lifecycleMixin来完成挂载,等等~
这些mixin函数的根本操作就是 Vue.prototype.XXX = function (){ // *****}
,也就是往Vue上挂载很多方法。
当我们把Vue函数暴露出去的时候,他的原型已经有了我们需要的全部方法。
// 定义Vue函数
function Vue(options) {
// 初始化操作
this._init(options);
}
debugger
// 给Vue原型上添加方法
initMixin(Vue)
renderMixin(Vue)
lifecycleMixin(Vue)
// 等等其他功能的函数
渲染流程1、 调用init() 初始化
这个时候,我们new Vue()
执行 this._init()
,这里的_init 其实就是在initMixin中进行挂载的。
initMixin函数的关键代码如下。
function initMixin(Vue){
// 初始化方式
Vue.prototype._init = function (options){
const vm = this
vm.$options = mergeOptions(vm.constructor.$options || {},options)
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// 如果用户传入了el属性,需要将页面渲染出来
vm.$options.el && vm.$mount(vm.$options.el)
}
}
构造函数,也就是new的时候调用_init,就是这里挂载到vue上的_init。
init做的三件事:(这里只简单介绍哈~)
1、合并options ,vue的原型继承策略并不能很好的满足我们,因为在很多情况下,我们不是当前对象找不到就使用继承属性。而是有策略在的,举一个例子,vue的生命周期事件,当前对象和原型链上都有,但是不是当前对象有就不去取原项链的方法了,他是要依次执行两个方法。所以这一步确保每一个options 都正确且合理。
2、各模块的初始化 。这里同样包括很多初始化,比如生命周期、渲染、数据等等初始化。我们今天很多不会涉及到。这里比较重要的一点就是数据的初始化。也就是 initState(),这一块我们会将data数据创建成响应式数据。这样我们在修改了数据之后,就可以看到dom自动更新啦。详细可以看:Vue的响应式原理
3、调用mount函数渲染。
渲染流程2 调用$mount() 挂载
$mount的函数在不同的打包方式下是不一样的。这里我们已编译版本的为例进行介绍。(编译版本就是我们写的时候要写template 运行时版本就是写render函数的那一种)。
Vue.prototype.$mount = function (el){
const vm = this;
const {render,template} = this.$options;
let myRender = render;
if(!render){
let myTemplate = template;
if(!template){
const dom = document.querySelector(el)
vm.$options.$el = dom;
if(dom){
myTemplate = dom.outerHTML
}
}
// template 转 render
myRender = compileToFunction(myTemplate)
this.$options.render = myRender;
}
// 渲染当前的组件
mountComponent(vm,el)
}
function mountComponent (
vm,
el
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
// 拿到render函数之后,进行beforemount
callHook(vm, 'beforeMount');
// HACK: 平台相关
var updateComponent;
updateComponent = function () {
// vm._render() 返回虚拟dom
// vm._update() 虚拟dom生成真实的dom
console.log('-------更新------')
vm._update(vm._render());
};
// 渲染watcher
new Watcher(vm, updateComponent, ()=>{}, null, true /* isRenderWatcher */);
// NOTE: 组件更新原理
// watcher 用来渲染的
// updateComponent 创建真实的dom
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
挂载dom的核心逻辑就是:
1、有render函数吗 ? 如果有直接使用 , 如果没有,找到template通过 vue 模板编译 转成 render函数。这个render函数会在下边执行_render的时候执行并返回dom对应的vdom;
2、有了render函数 ,调用组件渲染 。这个函数呢,会new一个watcher ,在new的当下执行 updateComponent 调用update更新一次dom,往后当data发生改变时继续执行update更新dom,这样就做到了响应式。所以当我们在后边改vm的数据的时候,就会更新dom.张三变成李四。
后续
这只是Vue的核心渲染流程。至于他是怎么响应式的,是怎么编译的,又是怎么进行的组件的渲染的,dom的更新策略是什么 等等
这些东西且听我下次讲解~