Vue2.x原理剖析(二)の手書き1つの簡版Vue
16309 ワード
前言
前回の記事Vue2.x応答式原理剖析(一)では、データ応答式の原理を明らかにしましたが、今日は前回の実装を利用して、簡単なVueを作成してみましょう.
MVVMクラスの作成
TVueは作成時に2つのことをする必要があります.は、入力されたデータに対して応答式処理を行う. コンパイルテンプレート結果を でレンダリング
⚠️私たちは普段data属性値を使用するときになぜ直接thisを通過できるのか.xxxアクセスは、thisを通過する必要はありません.data.xxxは訪問しますか?
これは,Vueソースコードにエージェントを作り,vmインスタンスのdata属性値をvmインスタンスに直接エージェントしたためである.
ここでは、プロキシメソッドを実装するためにソースコードを学ぶこともできます.
observeメソッド実装
前の文章の共有を通じて、Vue 2.xにはJS言語の特性Objectが利用する.defineProperty()では、オブジェクト属性getter/setterを定義して属性へのアクセスをブロックします.observeメソッドの作成と機能を振り返ってみましょう.
WatcherとDepの作成
1. Watcher依存収集後depsに保存 が変動したときdepsはパブリッシャー にパブリッシャーとして通知する. watcherコールバックレンダリング
2. Depパブリッシャー、複数のオブザーバー を購読可能収集依存後1つまたは複数のwatcher 変動があるとwatcher に通知する
3.関係 Depは、watcherインスタンスの削除および通知更新 を含むWatcherのセットを管理する. Watcherは式を解析し、依存を収集し、数値が変化するとコールバック関数をトリガし、$watch APIおよび命令でよく使用される.各コンポーネントにも対応するWatcherがあり、数値の変化によってupdate関数がトリガーされ、 が再レンダリングされます.
4.defineReactiveメソッドの改造---Depインスタンスの作成、依存の収集&サブスクリプションの配布
テンプレートをコンパイル、$mount実装
1.$mount作成
2.$createElementと_update実装$createElementはただ一つのことをします:仮想dom($createElementは実際にrender関数に渡されるh) を返します. _update関数はdomの更新を担当し、vnodeをdom に変換します.
3. patch
patchはcreatePatchFunctionの戻り値であり、nodeOpsとmodulesを渡すのはwebプラットフォームが特に実現している.
patch実装
まず、ツリーレベルの比較を行います.3つの場合があります.削除を追加します. new VNodeが存在しない場合は削除します. old VNodeが存在しなければ増加する. は、diff実行更新について、属性更新、テキスト更新、サブノード更新の3つのタイプのアクションを含む2つのVNodeを比較しています. 新旧ノードにchildrenサブノードがある場合、サブノードに対してdiff操作を行い、**updateChildren を呼び出す.新しいノードにサブノードがあり、古いノードにサブノード**がない場合は、古いノードのテキスト内容を空にしてから、サブノードを追加します. 新しいノードにサブノードがなく、古いノードにサブノードがある場合、そのノードのすべてのサブノードが除去される. 新しい古いノードにサブノードがない場合、テキストの置換だけです.
4.updateChildrenは新旧の2つのVNodeのchildrenに対して最小の操作を出すは2サイクルを実行するのが伝統的な方法であり、vueではwebシーンの特徴に対して特別なアルゴリズム最適化 を行った.新旧の2組のVNodeノードの左右の頭と尾の両側に変数タグがあり、遍歴中にこれらの変数が中間に近づく.oldStartIdx>oldEndIdx、またはnewStartIdx>newEndIdxの場合に終了するループ の下には、遍歴ルールがあります. oldStartVnodeとnewStartVnodeまたはoldEndVnodeとnewEndVnodeがsameVnodeを満たす場合、このVNodeノードを直接patchVnodeにすればよく、これ以上巡回することなく サイクルを完了する. oldStartVnodeとnewEndVnodeがsameVnodeを満たす場合.説明oldStartVnodeはすでにoldEndVnodeの後ろに走っており、patchVnodeを行うと同時に実際のDOMノードをoldEndVnodeの後ろ に移動する必要がある. oldEndVnodeとnewStartVnodeがsameVnodeを満たす場合、oldEndVnodeがoldStartVnodeの前に走ったことを説明し、patchVnodeを行うと同時にoldEndVnode対応DOMをoldStartVnode対応DOMの前に移動する. 以上が一致しない場合、old VNodeでnewStartVnodeと同じノードを探し、patchVnodeが実行されている場合、elmToMoveをoldStartIdxに対応するDOMの前に移動します. newStartVnodeがold VNodeノードで一致するsameVnodeが見つからない可能性もあります.このときcreateElmを呼び出して新しいDOMノードを作成します. はこのサイクルで終了するが、残りのノードを処理する必要がある. 終了時oldStartIdx>oldEndIdx、この時点で古いVNodeノードは遍歴済みですが、新しいノードはまだありません.新しいVNodeノードは実際に古いVNodeノードよりも多く、残りのVNode対応のDOMを実際のDOMに挿入する必要があることを説明し、addVnodes(createElmインタフェースを一括呼び出します)を呼び出します. ただし、終了時にnewStartIdx>newEndIdxとなると、新しいVNodeノードは遍歴済みであるが、古いノードはまだ残っており、ドキュメントから削除するノードは を削除する必要がある.
⚠️元のアルゴリズムは比較的に複雑で、直接ソースコードを調べて調べることができて、以下私達は1つの最適化されていない硬い更新の操作を実現することができます
5.createElm再帰domツリーの作成
6.完全バージョンのTVueソース
まとめ
以上、データ応答と非同期一括更新domインフラストラクチャを実現する簡単なバージョンのTVueを作成しました.
もちろんVue 2.xの強さはそれだけではありません.残りはソースコードに答えを探しに行きましょう.
以下はVueのいくつかの重要な概念の簡単な紹介で、皆さんがこの文章の全体的な考え方をもっとよく理解するのに役立つかもしれません. Observerは、データにDep依存性を追加するために使用されます. Depは、dataの各オブジェクトにサブオブジェクトが含まれているオブジェクトであり、バインドされたデータが変更された場合、dep.notify()によってWatcherに通知される. CompileはHTMLコマンド解析器であり、各要素ノードのコマンドをスキャンして解析し、コマンドテンプレートに基づいてデータを置き換え、対応する更新関数をバインドします. WatcherはObserverとCompileを接続するブリッジであり、Compileが命令を解析すると対応するWatcherが作成され、updateメソッドがバインドされ、Depオブジェクトに追加されます.
拡張
Vueソースの学習テクニック取得Vueソースコード項目アドレス:https://github.com/vuejs/vue デバッグ環境構築 インストール依存:npm iインストールphantom.jsで を終了インストールrollup:npm i-g rollup devスクリプトを修正し、sourcemap、packageを追加します.json 開発コマンドを実行する:npm run devは前に作成したvueを導入する.js
用語の説明: runtime:実行時のみ、コンパイラ は含まれません. common:cjs仕様、webpack 1 用 esm:ESモジュール、webpack 2+ 用 umd:universal module definition、cjsとamdに対応、浏 に使用
前回の記事Vue2.x応答式原理剖析(一)では、データ応答式の原理を明らかにしましたが、今日は前回の実装を利用して、簡単なVueを作成してみましょう.
MVVMクラスの作成
// TVue.js
/**
* @desc: TVue MVVM , Vue
* @params {} options
*/
class TVue {
constructor (options) {
this.$options = options
this.$data = options.data
}
}
TVueは作成時に2つのことをする必要があります.
class TVue {
constructor (options) {
this.$options = options
this.$data = options.data
// 1.
observe(this.$data)
// 2.
if (options.el) {
this.$mount(options.el)
}
}
}
⚠️私たちは普段data属性値を使用するときになぜ直接thisを通過できるのか.xxxアクセスは、thisを通過する必要はありません.data.xxxは訪問しますか?
これは,Vueソースコードにエージェントを作り,vmインスタンスのdata属性値をvmインスタンスに直接エージェントしたためである.
ここでは、プロキシメソッドを実装するためにソースコードを学ぶこともできます.
function proxy (vm) {
Object.keys(vm.$data).forEach(key=>{
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(v) {
vm.$data[key] = v
}
})
})
}
// TVue :
class TVue {
constructor (options) {
this.$options = options
this.$data = options.data
// 1.
observe(this.$data)
// 1.5 $data
proxy(this)
// 2.
if (options.el) {
this.$mount(options.el)
}
}
}
observeメソッド実装
前の文章の共有を通じて、Vue 2.xにはJS言語の特性Objectが利用する.defineProperty()では、オブジェクト属性getter/setterを定義して属性へのアクセスをブロックします.observeメソッドの作成と機能を振り返ってみましょう.
function observe(obj) {
if (typeof obj !== 'object' || typeof === null) {
// null ,
return
}
// , Observer
new Observer(obj)
}
// Observer
// defineReactive getter/setter
// getter ,setter
class Observer {
constructor(options) {
if (Array.isArray(obj)) {
// todo: , Array.js
} else {
this.walk(options)
}
}
walk(obj) {
//
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]
})
}
}
function defineReactive (obj, key, val) {
//
observe(val)
Object.defineProperty(obj, key, {
get () {
// todo:
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
// ( test.foo={f1: 666})
observe(val)
// todo:
}
}
})
}
WatcherとDepの作成
1. Watcher
class Watch {
// expOrFn: Watcher
constructor (vm, expOrFn){
this.vm = vm
this.getter = expOrFn
//
this.get()
}
get() {
Dep.target = this
this.getter.call(this.vm)
Dep.target = null
}
update() {
// Dep
this.get()
}
}
2. Dep
class Dep {
// : key
constructor (){
//
this.deps = new Set()
}
addDep (watcher) {
this.deps.add(watcher)
}
notify() {
this.deps.forEach(watcher => watcher.update())
}
}
3.関係
4.defineReactiveメソッドの改造---Depインスタンスの作成、依存の収集&サブスクリプションの配布
function defineReactive (obj, key, val) {
// val ,
observe(val)
// Dep
const dep = new Dep()
Object.defineProperty(obj, key, {
get () {
//
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
// newVal ,
observe(val)
//
dep.notify()
}
}
})
}
テンプレートをコンパイル、$mount実装
1.$mount作成
function $mount (el){
this.$el = document.createElement(el)
// lifeCycleMixin _update renderMixin _render
const updateComponent = ()=> {
const { render } = this.$options
// , vnode
const vnode = render.call(this, this.$createElement)
this._update(vnode)
}
// watcher
new Watcher(this, updateComponent)
}
2.$createElementと_update実装
function $createElement(tag, props, children) {
return { tag, props, children}
}
function _update(vnode) {
const prevVnode = this._vnode
if(!prevVnode) {
//
this.__patch__(this.$el, vnode)
} else {
//
this.__patch__(prevVnode, vnode)
}
}
3. patch
patchはcreatePatchFunctionの戻り値であり、nodeOpsとmodulesを渡すのはwebプラットフォームが特に実現している.
patch実装
まず、ツリーレベルの比較を行います.3つの場合があります.削除を追加します.
function __patch__(oldVnode, Vnode) {
// oldVnode dom
if (oldVnode.nodeType) {
const parent = oldVnode.parentElement
const refElm = oldVnode.nextSibling
// dom dom,
const el = this.createElm(vnode)
parent.insertBefore(el, refElm)
//
parent.removeChild(oldVnode)
} else {
// dom
const el = vnode.el = oldVnode.el
//
if (oldVnode.tag === vnode.tag) {
const oldCh = oldVnode.children
const newCh = vnode.children
/**
* diff
* 1. string ( )
* 2. ( diff)
* 3. , string( dom )
* 4. string, ( )
*/
if (typeof newCh === 'string') {
// string
if (typeof oldCh === 'string') {
// string
if (newCh !== oldCh) {
el.textContent = newCh
}
} else {
// string dom
el.textContent = newCh
}
} else {
//
// 1. , ( , dom )
if (typeof oldCh === 'string') {
oldCh.innerHTML = ''
newCh.forEach(vnode => this.createElm(vnode))
} else {
// 2. ( diff )
this.updateChildren(el, oldCh, newCh)
}
}
} else {
//
}
}
// vnode
this._vnode = vnode
}
4.updateChildrenは新旧の2つのVNodeのchildrenに対して最小の操作を出す
⚠️元のアルゴリズムは比較的に複雑で、直接ソースコードを調べて調べることができて、以下私達は1つの最適化されていない硬い更新の操作を実現することができます
//
updateChildren (parentElm, odlCh, newCh) {
const len = Math.min(oldCh.length, newCh.length)
//
for(let i =0; i oldCh.length) {
newCh.slice(len).forEach(vnode=>{
const el = this.createElm(vnode)
parentElm.appendChild(el)
})
} else if(newCh.length < old.length){
parentElm.removeChild(vnode.el)
}
}
5.createElm再帰domツリーの作成
createElm(vnode) {
const el = document.createElement(vnode.tag)
// props
if(vnode.props) {
for(const key in vnode.props) {
el.setAttribute(key, vnode.props[key])
}
}
// children
if (vnode.children) {
//
if(typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
//
vnode.children.forEach(vnode=>{
const child = this.createElm(vnode)
el.appendChild(child)
})
}
vnode.el = el
return el
}
}
6.完全バージョンのTVueソース
function defineReactive(obj, key, val) {
// !
observe(val)
// Dep
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}: ${val}`)
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set ${key}: ${newVal}`)
val = newVal
//! ( test.foo={f1: 666})
observe(val)
dep.notify()
}
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
// * obj , Observer
new Observer(obj)
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(v) {
vm.$data[key] = v
}
})
})
}
class Observer {
constructor(options) {
if (Array.isArray(options)) {
// todo
} else {
this.walk(options)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
class TVue {
constructor(options) {
this.$options = options
this.$data = options.data
// ! 1.
observe(this.$data)
// ! 1.5 data JVue
proxy(this)
// ! 2.
// new Compile(options.el, this)
if (options.el) {
this.$mount(options.el)
}
}
$mount (el) {
//
this.$el = document.querySelector(el)
const updateComponent = () => {
//
const { render } = this.$options;
// dom
// const el = render.call(this);
// const parent = this.$el.parentElement;
// parent.insertBefore(el, this.$el.nextSibling);
// parent.removeChild(this.$el);
// this.$el = el;
// vnode
const vnode = render.call(this, this.$createElement)
this._update(vnode)
}
// Watcher
new Watcher(this, updateComponent)
}
$createElement (tag, props, children) {
return {
tag,
props,
children
}
}
_update (vnode) {
const prevVnode = this._vnode
if (!prevVnode) {
this.__patch__(this.$el, vnode)
} else {
this.__patch__(prevVnode, vnode)
}
}
__patch__ (oldVnode, vnode) {
// oldVnode dom
if (oldVnode.nodeType) {
const parent = oldVnode.parentElement
const refElm = oldVnode.nextSibling
// props
// children
const el = this.createElm(vnode)
parent.insertBefore(el, refElm)
parent.removeChild(oldVnode)
} else {
// update
// dom
const el = vnode.el = oldVnode.el
if (oldVnode.tag === vnode.tag) {
const oldCh = oldVnode.children
const newCh = vnode.children
/**
* diff
* 1. string ( )
* 2. ( diff)
* 3. , string( dom )
* 4. string, ( )
*/
if (typeof newCh === 'string') {
if(typeof oldCh === 'string') {
// string
if(newCh !== oldCh) {
el.textContent = newCh
}
} else {
el.textContent = newCh
}
} else {
// 1. , ( , dom )
if (typeof oldCh === 'string') {
//
oldCh.innerHTML = ''
newCh.forEach(vnode => this.createElm(vnode))
} else {
// 2.
this.updateChildren(el, oldCh, newCh)
}
}
}
}
this._vnode = vnode
}
// dom
createElm (vnode) {
const el = document.createElement(vnode.tag)
// props
if (vnode.props) {
for (const key in vnode.porps) {
el.setAttribute(key, vnode.props[key])
}
}
// children
if (vnode.children) {
//
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
//
vnode.children.forEach(vnode => {
const child = this.createElm(vnode)
el.appendChild(child)
})
}
}
// vnode dom
vnode.el = el
return el
}
//
updateChildren(parentElm, oldCh, newCh) {
const len = Math.min(oldCh.length, newCh.length)
//
for (let i = 0; i < len; i++) {
this.__patch__(oldCh[i], newCh[i])
}
// newCh ,
if (newCh.length > oldCh.length) {
newCh.slice(len).forEach(vnode => {
const el = this.createElm(vnode)
parentElm.appendChild(el)
})
} else if(newCh.length < oldCh.length){
oldCh.slice(len).forEach(vnode => {
parentElm.removeChild(vnode.el)
})
}
}
}
// ,
class Watcher {
constructor(vm, expOrFn) {
this.vm = vm;
this.getter = expOrFn;
//
this.get()
}
get () {
Dep.target = this;
this.getter.call(this.vm)
Dep.target = null
}
// Dep
update() {
this.get()
}
}
// : key
class Dep {
constructor() {
this.deps = new Set();
}
addDep(wather) {
this.deps.add(wather)
}
notify() {
this.deps.forEach(wather => wather.update())
}
}
まとめ
以上、データ応答と非同期一括更新domインフラストラクチャを実現する簡単なバージョンのTVueを作成しました.
もちろんVue 2.xの強さはそれだけではありません.残りはソースコードに答えを探しに行きましょう.
以下はVueのいくつかの重要な概念の簡単な紹介で、皆さんがこの文章の全体的な考え方をもっとよく理解するのに役立つかもしれません.
拡張
Vueソースの学習テクニック
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev"